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

shahar1 pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/airflow.git


The following commit(s) were added to refs/heads/main by this push:
     new d07cb5f727d Fix airflowctl backfill create-dry-run printing one 
all-null row (#76224)
d07cb5f727d is described below

commit d07cb5f727d504218f9ce354e00534a1680ce096
Author: Y-C <[email protected]>
AuthorDate: Thu Nov 5 01:48:50 2026 +0800

    Fix airflowctl backfill create-dry-run printing one all-null row (#76224)
    
    The dry-run endpoint returns the runs a backfill would create, not a
    backfill. Parsing that payload as a backfill dropped every row, and the
    missing-field fallback for older servers turned the mismatch into a
    single all-null row plus a misleading "older Airflow line" warning.
---
 .../airflowctl_tests/test_airflowctl_commands.py   | 18 ++++++++++++++++
 airflow-ctl/src/airflowctl/api/operations.py       |  7 +++++--
 .../tests/airflow_ctl/api/test_operations.py       | 24 ++++++++++++++++++----
 .../ci/prek/check_airflowctl_command_coverage.py   |  1 -
 4 files changed, 43 insertions(+), 7 deletions(-)

diff --git 
a/airflow-ctl-tests/tests/airflowctl_tests/test_airflowctl_commands.py 
b/airflow-ctl-tests/tests/airflowctl_tests/test_airflowctl_commands.py
index fef33b01081..e7eda083307 100644
--- a/airflow-ctl-tests/tests/airflowctl_tests/test_airflowctl_commands.py
+++ b/airflow-ctl-tests/tests/airflowctl_tests/test_airflowctl_commands.py
@@ -16,6 +16,8 @@
 # under the License.
 from __future__ import annotations
 
+import json
+
 import pytest
 from packaging.version import Version
 
@@ -262,6 +264,22 @@ def test_airflowctl_commands_skip_keyring(
     )
 
 
+def test_backfill_create_dry_run_lists_the_runs_it_would_create(api_token, 
run_command, tmp_path):
+    # 2024 stays clear of the runs triggered above, which a dry run would 
leave out.
+    output = run_command(
+        command="backfill create-dry-run --dag-id example_bash_operator "
+        "--from-date 2024-01-01T00:00:00+00:00 --to-date 
2024-01-03T00:00:00+00:00 -o json",
+        env_vars={"AIRFLOW_HOME": str(tmp_path), "AIRFLOW_CLI_TOKEN": 
api_token},
+        skip_login=True,
+    )
+    # A server older than 3.2 omits the partition fields, and that warning 
precedes the JSON.
+    assert [row["logical_date"] for row in 
json.loads(output.splitlines()[-1])] == [
+        "2024-01-01T00:00:00Z",
+        "2024-01-02T00:00:00Z",
+        "2024-01-03T00:00:00Z",
+    ]
+
+
 @pytest.mark.parametrize("command", NO_AUTH_TEST_COMMANDS)
 def test_airflowctl_no_auth_commands(command: str, run_command, tmp_path):
     """Test airflowctl no-auth commands without login or persisted 
credentials."""
diff --git a/airflow-ctl/src/airflowctl/api/operations.py 
b/airflow-ctl/src/airflowctl/api/operations.py
index cec290720eb..342c61ee0ea 100644
--- a/airflow-ctl/src/airflowctl/api/operations.py
+++ b/airflow-ctl/src/airflowctl/api/operations.py
@@ -66,6 +66,7 @@ from airflowctl.api.datamodels.generated import (
     DAGVersionCollectionResponse,
     DagVersionResponse,
     DAGWarningCollectionResponse,
+    DryRunBackfillCollectionResponse,
     ImportErrorCollectionResponse,
     ImportErrorResponse,
     JobCollectionResponse,
@@ -490,13 +491,15 @@ class BackfillOperations(BaseOperations):
         )
         return validate_response(self.response.content, BackfillResponse)
 
-    def create_dry_run(self, backfill: BackfillPostBody) -> BackfillResponse | 
ServerResponseError:
+    def create_dry_run(
+        self, backfill: BackfillPostBody
+    ) -> DryRunBackfillCollectionResponse | ServerResponseError:
         """Create a dry run backfill."""
         self.response = self.client.post(
             "backfills/dry_run",
             json=backfill.model_dump(mode="json", exclude_none=True, 
exclude_defaults=True),
         )
-        return validate_response(self.response.content, BackfillResponse)
+        return validate_response(self.response.content, 
DryRunBackfillCollectionResponse)
 
     def get(self, backfill_id: str) -> BackfillResponse | ServerResponseError:
         """Get a backfill."""
diff --git a/airflow-ctl/tests/airflow_ctl/api/test_operations.py 
b/airflow-ctl/tests/airflow_ctl/api/test_operations.py
index 670e1876007..0a963bbd61b 100644
--- a/airflow-ctl/tests/airflow_ctl/api/test_operations.py
+++ b/airflow-ctl/tests/airflow_ctl/api/test_operations.py
@@ -80,6 +80,8 @@ from airflowctl.api.datamodels.generated import (
     DAGWarningCollectionResponse,
     DAGWarningResponse,
     DagWarningType,
+    DryRunBackfillCollectionResponse,
+    DryRunBackfillResponse,
     ImportErrorCollectionResponse,
     ImportErrorResponse,
     JobCollectionResponse,
@@ -772,15 +774,29 @@ class TestBackfillOperations:
         backfills=[backfill_response],
         total_entries=1,
     )
+    dry_run_response = DryRunBackfillCollectionResponse(
+        backfills=[
+            DryRunBackfillResponse(
+                logical_date=datetime.datetime(2025, 1, 1, 0, 0, 0),
+                partition_key=None,
+                partition_date=None,
+            )
+        ],
+        total_entries=1,
+    )
 
     @pytest.mark.parametrize(
-        ("operation", "path"), [("create", "backfills"), ("create_dry_run", 
"backfills/dry_run")]
+        ("operation", "path", "server_response"),
+        [
+            ("create", "backfills", backfill_response),
+            ("create_dry_run", "backfills/dry_run", dry_run_response),
+        ],
     )
     @pytest.mark.parametrize(
         ("request_fields", "expected_fields"),
         [({}, {}), ({"drain_dag": False}, {}), ({"drain_dag": True}, 
{"drain_dag": True})],
     )
-    def test_create(self, operation, path, request_fields, expected_fields):
+    def test_create(self, operation, path, server_response, request_fields, 
expected_fields):
         expected_body = {
             "dag_id": "dag_id",
             "from_date": "2024-12-31T23:59:59",
@@ -795,13 +811,13 @@ class TestBackfillOperations:
             assert request.url.path == f"/api/v2/{path}"
             assert request.headers.get("content-type", 
"").startswith("application/json")
             assert json.loads(request.content.decode()) == expected_body
-            return httpx.Response(200, 
json=json.loads(self.backfill_response.model_dump_json()))
+            return httpx.Response(200, 
json=json.loads(server_response.model_dump_json()))
 
         client = make_api_client(transport=httpx.MockTransport(handle_request))
         response = getattr(client.backfills, operation)(
             backfill=self.backfill_body.model_copy(update=request_fields)
         )
-        assert response == self.backfill_response
+        assert response == server_response
 
     def test_get(self):
         def handle_request(request: httpx.Request) -> httpx.Response:
diff --git a/scripts/ci/prek/check_airflowctl_command_coverage.py 
b/scripts/ci/prek/check_airflowctl_command_coverage.py
index 3e3be605f65..a0473158282 100755
--- a/scripts/ci/prek/check_airflowctl_command_coverage.py
+++ b/scripts/ci/prek/check_airflowctl_command_coverage.py
@@ -61,7 +61,6 @@ EXCLUDED_COMMANDS = {
     "assets materialize",
     "backfill cancel",
     "backfill create",
-    "backfill create-dry-run",
     "backfill get",
     "backfill pause",
     "backfill unpause",

Reply via email to