This is an automated email from the ASF dual-hosted git repository.

potiuk pushed a commit to branch v3-4-test
in repository https://gitbox.apache.org/repos/asf/airflow.git


The following commit(s) were added to refs/heads/v3-4-test by this push:
     new 4c9f8f025af [v3-4-test] Keep queued connection tests from overwriting 
newer connections (#74837) (#74895)
4c9f8f025af is described below

commit 4c9f8f025afa058092062ca53b0ce1a1ac1c19dd
Author: Jarek Potiuk <[email protected]>
AuthorDate: Sat Oct 17 00:30:05 2026 +0100

    [v3-4-test] Keep queued connection tests from overwriting newer connections 
(#74837) (#74895)
    
    (cherry picked from commit 3ce8d152d0dd5424512cbb58a7a756586f79e51e)
---
 airflow-core/docs/migrations-ref.rst               |  4 +-
 airflow-core/newsfragments/74837.bugfix.rst        |  1 +
 .../core_api/routes/public/connections.py          |  4 +-
 .../execution_api/routes/connection_tests.py       |  7 ++-
 ..._add_commit_allows_update_to_connection_test.py | 51 ++++++++++++++++++++++
 airflow-core/src/airflow/models/connection_test.py | 26 ++++++++++-
 airflow-core/src/airflow/utils/db.py               |  2 +-
 .../core_api/routes/public/test_connections.py     | 21 +++++++++
 .../versions/head/test_connection_tests.py         | 33 ++++++++++++++
 .../tests/unit/models/test_connection_test.py      | 32 +++++++++++++-
 10 files changed, 174 insertions(+), 7 deletions(-)

diff --git a/airflow-core/docs/migrations-ref.rst 
b/airflow-core/docs/migrations-ref.rst
index 35315b7d044..44e2090370d 100644
--- a/airflow-core/docs/migrations-ref.rst
+++ b/airflow-core/docs/migrations-ref.rst
@@ -39,7 +39,9 @@ Here's the list of all the Database Migrations that are 
executed via when you ru
 
+-------------------------+------------------+-------------------+--------------------------------------------------------------+
 | Revision ID             | Revises ID       | Airflow Version   | Description 
                                                 |
 
+=========================+==================+===================+==============================================================+
-| ``a5d7b9c13e40`` (head) | ``c3e7a9182f64`` | ``3.4.0``         | Preserve 
exact task execution attribution in asset state.    |
+| ``9cffe90fa0c0`` (head) | ``a5d7b9c13e40`` | ``3.4.0``         | Add 
commit_allows_update to connection_test_request.         |
++-------------------------+------------------+-------------------+--------------------------------------------------------------+
+| ``a5d7b9c13e40``        | ``c3e7a9182f64`` | ``3.4.0``         | Preserve 
exact task execution attribution in asset state.    |
 
+-------------------------+------------------+-------------------+--------------------------------------------------------------+
 | ``c3e7a9182f64``        | ``7f8c9a2d410e`` | ``3.4.0``         | Attribute 
log and asset events to exact task tries.          |
 
+-------------------------+------------------+-------------------+--------------------------------------------------------------+
diff --git a/airflow-core/newsfragments/74837.bugfix.rst 
b/airflow-core/newsfragments/74837.bugfix.rst
new file mode 100644
index 00000000000..7c974ce06c9
--- /dev/null
+++ b/airflow-core/newsfragments/74837.bugfix.rst
@@ -0,0 +1 @@
+A queued connection test with ``commit_on_success`` for a connection that did 
not exist when the test was queued no longer overwrites a connection with the 
same ID created in the meantime. The test is reported as failed with a message 
explaining that the tested values were not saved.
diff --git 
a/airflow-core/src/airflow/api_fastapi/core_api/routes/public/connections.py 
b/airflow-core/src/airflow/api_fastapi/core_api/routes/public/connections.py
index d803b85b191..7fa0151bae8 100644
--- a/airflow-core/src/airflow/api_fastapi/core_api/routes/public/connections.py
+++ b/airflow-core/src/airflow/api_fastapi/core_api/routes/public/connections.py
@@ -452,7 +452,8 @@ def enqueue_connection_test(
     else:
         effective_team = test_body.team_name
 
-    auth_method: ResourceMethod = "PUT" if existing is not None and 
test_body.commit_on_success else "POST"
+    commit_allows_update = existing is not None and test_body.commit_on_success
+    auth_method: ResourceMethod = "PUT" if commit_allows_update else "POST"
     if not get_auth_manager().is_authorized_connection(
         method=auth_method,
         details=ConnectionDetails(conn_id=test_body.connection_id, 
team_name=effective_team),
@@ -473,6 +474,7 @@ def enqueue_connection_test(
         port=test_body.port,
         extra=test_body.extra,
         commit_on_success=test_body.commit_on_success,
+        commit_allows_update=commit_allows_update,
         executor=test_body.executor,
         queue=test_body.queue,
         team_name=effective_team,
diff --git 
a/airflow-core/src/airflow/api_fastapi/execution_api/routes/connection_tests.py 
b/airflow-core/src/airflow/api_fastapi/execution_api/routes/connection_tests.py
index 21654109478..892243e182e 100644
--- 
a/airflow-core/src/airflow/api_fastapi/execution_api/routes/connection_tests.py
+++ 
b/airflow-core/src/airflow/api_fastapi/execution_api/routes/connection_tests.py
@@ -137,4 +137,9 @@ def patch_connection_test(
     ct.result_message = body.result_message
 
     if body.state == ConnectionTestState.SUCCESS and ct.commit_on_success:
-        ct.commit_to_connection_table(session=session)
+        if not ct.commit_to_connection_table(session=session):
+            ct.state = ConnectionTestState.FAILED
+            ct.result_message = (
+                f"Connection test succeeded, but connection 
`{ct.connection_id}` was created by "
+                "another request after this test was queued, so the tested 
values were not saved."
+            )
diff --git 
a/airflow-core/src/airflow/migrations/versions/0147_3_4_0_add_commit_allows_update_to_connection_test.py
 
b/airflow-core/src/airflow/migrations/versions/0147_3_4_0_add_commit_allows_update_to_connection_test.py
new file mode 100644
index 00000000000..16ac18f9703
--- /dev/null
+++ 
b/airflow-core/src/airflow/migrations/versions/0147_3_4_0_add_commit_allows_update_to_connection_test.py
@@ -0,0 +1,51 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+"""
+Add commit_allows_update to connection_test_request.
+
+Revision ID: 9cffe90fa0c0
+Revises: a5d7b9c13e40
+Create Date: 2026-10-16 10:00:00.000000
+
+"""
+
+from __future__ import annotations
+
+import sqlalchemy as sa
+from alembic import op
+
+revision = "9cffe90fa0c0"
+down_revision = "a5d7b9c13e40"
+branch_labels = None
+depends_on = None
+airflow_version = "3.4.0"
+
+
+def upgrade():
+    """Add the commit_allows_update column to connection_test_request."""
+    with op.batch_alter_table("connection_test_request", schema=None) as 
batch_op:
+        batch_op.add_column(
+            sa.Column("commit_allows_update", sa.Boolean(), nullable=False, 
server_default="0")
+        )
+
+
+def downgrade():
+    """Remove the commit_allows_update column from connection_test_request."""
+    with op.batch_alter_table("connection_test_request", schema=None) as 
batch_op:
+        batch_op.drop_column("commit_allows_update")
diff --git a/airflow-core/src/airflow/models/connection_test.py 
b/airflow-core/src/airflow/models/connection_test.py
index f3d79a2a34c..e9c0a3ec9b9 100644
--- a/airflow-core/src/airflow/models/connection_test.py
+++ b/airflow-core/src/airflow/models/connection_test.py
@@ -110,6 +110,12 @@ class ConnectionTestRequest(Base, FernetFieldsMixin):
     commit_on_success: Mapped[bool] = mapped_column(
         Boolean, nullable=False, default=False, server_default="0"
     )
+    # Whether a successful commit may overwrite an existing connection. Set at 
enqueue time only
+    # when the connection already existed and the requester was authorized to 
edit it; otherwise
+    # the commit may only create the connection.
+    commit_allows_update: Mapped[bool] = mapped_column(
+        Boolean, nullable=False, default=False, server_default="0"
+    )
     is_encrypted: Mapped[bool] = mapped_column(
         Boolean, unique=False, default=False, nullable=False, 
server_default="0"
     )
@@ -140,6 +146,7 @@ class ConnectionTestRequest(Base, FernetFieldsMixin):
         port: int | None = None,
         extra: str | None = None,
         commit_on_success: bool = False,
+        commit_allows_update: bool = False,
         executor: str | None = None,
         queue: str | None = None,
         team_name: str | None = None,
@@ -155,6 +162,7 @@ class ConnectionTestRequest(Base, FernetFieldsMixin):
         self.port = port
         self.extra = extra
         self.commit_on_success = commit_on_success
+        self.commit_allows_update = commit_allows_update
         self.executor = executor
         self.queue = queue
         self.team_name = team_name
@@ -202,8 +210,15 @@ class ConnectionTestRequest(Base, FernetFieldsMixin):
             extra=self.extra,
         )
 
-    def commit_to_connection_table(self, *, session: Session) -> None:
-        """Upsert the tested connection into the real ``connection`` table."""
+    def commit_to_connection_table(self, *, session: Session) -> bool:
+        """
+        Write the tested connection into the real ``connection`` table.
+
+        An existing connection is only overwritten when 
``commit_allows_update`` is set, i.e. the
+        connection existed when the test was enqueued and the requester was 
allowed to edit it.
+
+        :return: True if the connection was created or updated, False if it 
was left unchanged.
+        """
         conn = 
session.scalar(select(Connection).filter_by(conn_id=self.connection_id))
         if conn is None:
             conn = Connection(
@@ -219,6 +234,12 @@ class ConnectionTestRequest(Base, FernetFieldsMixin):
             )
             session.add(conn)
             log.info("Created new connection from successful test", 
connection_id=self.connection_id)
+        elif not self.commit_allows_update:
+            log.warning(
+                "Not updating connection created after the test was enqueued",
+                connection_id=self.connection_id,
+            )
+            return False
         else:
             conn.conn_type = self.conn_type
             conn.host = self.host
@@ -228,3 +249,4 @@ class ConnectionTestRequest(Base, FernetFieldsMixin):
             conn.port = self.port
             conn.extra = self.extra
             log.info("Updated existing connection from successful test", 
connection_id=self.connection_id)
+        return True
diff --git a/airflow-core/src/airflow/utils/db.py 
b/airflow-core/src/airflow/utils/db.py
index 594bfae64c4..5f0f2d245cc 100644
--- a/airflow-core/src/airflow/utils/db.py
+++ b/airflow-core/src/airflow/utils/db.py
@@ -117,7 +117,7 @@ _REVISION_HEADS_MAP: dict[str, str] = {
     "3.1.8": "509b94a1042d",
     "3.2.0": "1d6611b6ab7c",
     "3.3.0": "d2f4e1b3c5a7",
-    "3.4.0": "a5d7b9c13e40",
+    "3.4.0": "9cffe90fa0c0",
 }
 
 # Prefix used to identify tables holding data moved during migration.
diff --git 
a/airflow-core/tests/unit/api_fastapi/core_api/routes/public/test_connections.py
 
b/airflow-core/tests/unit/api_fastapi/core_api/routes/public/test_connections.py
index 67c276b7300..f8735b498e1 100644
--- 
a/airflow-core/tests/unit/api_fastapi/core_api/routes/public/test_connections.py
+++ 
b/airflow-core/tests/unit/api_fastapi/core_api/routes/public/test_connections.py
@@ -1610,6 +1610,27 @@ class TestAsyncConnectionTest(TestConnectionEndpoint):
         assert ct is not None
         assert ct.commit_on_success is True
 
+    @mock.patch.dict(os.environ, {"AIRFLOW__CORE__TEST_CONNECTION": "Enabled"})
+    def test_post_commit_on_success_new_connection_is_create_only(self, 
test_client, session):
+        """A commit_on_success test for a connection that does not exist yet 
may only create it."""
+        body = {**self.TEST_REQUEST_BODY, "commit_on_success": True}
+        response = test_client.post("/connections/enqueue-test", json=body)
+        assert response.status_code == 202
+
+        ct = 
session.scalar(select(ConnectionTestRequest).filter_by(token=response.json()["token"]))
+        assert ct.commit_allows_update is False
+
+    @mock.patch.dict(os.environ, {"AIRFLOW__CORE__TEST_CONNECTION": "Enabled"})
+    def test_post_commit_on_success_existing_connection_allows_update(self, 
test_client, session):
+        """A commit_on_success test for an existing connection, authorized for 
edit, may update it."""
+        self.create_connection()
+        body = {**self.TEST_REQUEST_BODY, "commit_on_success": True}
+        response = test_client.post("/connections/enqueue-test", json=body)
+        assert response.status_code == 202
+
+        ct = 
session.scalar(select(ConnectionTestRequest).filter_by(token=response.json()["token"]))
+        assert ct.commit_allows_update is True
+
     
@mock.patch("airflow.api_fastapi.core_api.routes.public.connections.get_auth_manager",
 autospec=True)
     @mock.patch.dict(os.environ, {"AIRFLOW__CORE__TEST_CONNECTION": "Enabled"})
     def 
test_post_commit_on_success_existing_connection_requires_edit_permission(
diff --git 
a/airflow-core/tests/unit/api_fastapi/execution_api/versions/head/test_connection_tests.py
 
b/airflow-core/tests/unit/api_fastapi/execution_api/versions/head/test_connection_tests.py
index 6c699176589..a739cbcc62e 100644
--- 
a/airflow-core/tests/unit/api_fastapi/execution_api/versions/head/test_connection_tests.py
+++ 
b/airflow-core/tests/unit/api_fastapi/execution_api/versions/head/test_connection_tests.py
@@ -140,6 +140,7 @@ class TestPatchConnectionTestCommitOnSuccess:
             host="new-host.example.com",
             login="new_user",
             commit_on_success=True,
+            commit_allows_update=True,
         )
         ct.state = ConnectionTestState.RUNNING
         session.add(ct)
@@ -156,6 +157,38 @@ class TestPatchConnectionTestCommitOnSuccess:
         assert conn.conn_type == "postgres"
         assert conn.host == "new-host.example.com"
 
+    def test_success_with_create_only_commit_does_not_overwrite_existing(self, 
client, session):
+        """A create-only commit does not overwrite a connection created after 
the test was enqueued."""
+        ct = ConnectionTestRequest(
+            connection_id="raced_conn",
+            conn_type="postgres",
+            host="tested-host.example.com",
+            login="tested_user",
+            password="tested_secret",
+            commit_on_success=True,
+        )
+        ct.state = ConnectionTestState.RUNNING
+        session.add(ct)
+        session.add(Connection(conn_id="raced_conn", conn_type="http", 
host="other-host.example.com"))
+        session.commit()
+
+        response = client.patch(
+            f"/execution/connection-tests/{ct.id}",
+            json={"state": "success", "result_message": "Connection OK"},
+        )
+        assert response.status_code == 204
+
+        session.expire_all()
+        conn = 
session.scalar(select(Connection).filter_by(conn_id="raced_conn"))
+        assert conn.conn_type == "http"
+        assert conn.host == "other-host.example.com"
+        assert conn.login is None
+        assert conn.password is None
+
+        ct = session.get(ConnectionTestRequest, ct.id)
+        assert ct.state == ConnectionTestState.FAILED
+        assert "were not saved" in ct.result_message
+
     def test_success_without_commit_does_not_create(self, client, session):
         """PATCH with state=success but commit_on_success=False does not 
create a connection."""
         ct = ConnectionTestRequest(
diff --git a/airflow-core/tests/unit/models/test_connection_test.py 
b/airflow-core/tests/unit/models/test_connection_test.py
index 1c27ac998a8..3cce66c3c01 100644
--- a/airflow-core/tests/unit/models/test_connection_test.py
+++ b/airflow-core/tests/unit/models/test_connection_test.py
@@ -261,11 +261,12 @@ class TestCommitToConnectionTable:
             host="new-host.example.com",
             login="new_user",
             password="new_secret",
+            commit_allows_update=True,
         )
         session.add(ct)
         session.flush()
 
-        ct.commit_to_connection_table(session=session)
+        assert ct.commit_to_connection_table(session=session) is True
         session.flush()
         session.refresh(conn)
 
@@ -273,3 +274,32 @@ class TestCommitToConnectionTable:
         assert conn.host == "new-host.example.com"
         assert conn.login == "new_user"
         assert conn.password == "new_secret"
+
+    def 
test_does_not_update_existing_connection_without_commit_allows_update(self, 
session):
+        """A create-only commit leaves a connection that appeared after 
enqueue untouched."""
+        conn = Connection(conn_id="existing_conn", conn_type="http", 
host="old-host.example.com")
+        session.add(conn)
+        session.flush()
+
+        ct = ConnectionTestRequest(
+            connection_id="existing_conn",
+            conn_type="postgres",
+            host="new-host.example.com",
+            login="new_user",
+            password="new_secret",
+        )
+        session.add(ct)
+        session.flush()
+
+        assert ct.commit_to_connection_table(session=session) is False
+        session.flush()
+        session.refresh(conn)
+
+        assert conn.conn_type == "http"
+        assert conn.host == "old-host.example.com"
+        assert conn.login is None
+        assert conn.password is None
+
+    def test_commit_allows_update_defaults_to_false(self):
+        ct = ConnectionTestRequest(connection_id="test_conn", 
conn_type="postgres", commit_on_success=True)
+        assert ct.commit_allows_update is False

Reply via email to