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

bbovenzi 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 da27a72a271 [v3-4-test] Fix task instance filtering for null start 
dates (#75321) (#75324)
da27a72a271 is described below

commit da27a72a271311bed917a980b0ef437c64aa2436
Author: github-actions[bot] 
<41898282+github-actions[bot]@users.noreply.github.com>
AuthorDate: Wed Oct 21 12:42:31 2026 -0400

    [v3-4-test] Fix task instance filtering for null start dates (#75321) 
(#75324)
    
    A task instance the executor reports as failed while still queued gets an
    end_date but no start_date. The nullable start_date lower bound kept 
matching
    it as if it had not started yet, so it showed up under any start_date_gte or
    start_date_gt filter. Same fix as #71654 for Dag runs.
    (cherry picked from commit b99f4a9b3f8067f27560df00ff507743ed52fd7d)
    
    Co-authored-by: Pierre Jeambrun <[email protected]>
---
 .../core_api/routes/public/task_instances.py       | 14 +++++++++--
 .../core_api/routes/public/test_task_instances.py  | 28 ++++++++++++++++++++++
 2 files changed, 40 insertions(+), 2 deletions(-)

diff --git 
a/airflow-core/src/airflow/api_fastapi/core_api/routes/public/task_instances.py 
b/airflow-core/src/airflow/api_fastapi/core_api/routes/public/task_instances.py
index 066e4501e36..351f2ec97bb 100644
--- 
a/airflow-core/src/airflow/api_fastapi/core_api/routes/public/task_instances.py
+++ 
b/airflow-core/src/airflow/api_fastapi/core_api/routes/public/task_instances.py
@@ -214,7 +214,12 @@ def get_mapped_task_instances(
     dag_bag: DagBagDep,
     run_after_range: Annotated[RangeFilter, 
Depends(datetime_range_filter_factory("run_after", TI))],
     logical_date_range: Annotated[RangeFilter, 
Depends(datetime_range_filter_factory("logical_date", TI))],
-    start_date_range: Annotated[RangeFilter, 
Depends(datetime_range_filter_factory("start_date", TI))],
+    start_date_range: Annotated[
+        RangeFilter,
+        Depends(
+            datetime_range_filter_factory("start_date", TI, 
null_lower_bound_clause=TI.end_date.is_(None))
+        ),
+    ],
     end_date_range: Annotated[RangeFilter, 
Depends(datetime_range_filter_factory("end_date", TI))],
     update_at_range: Annotated[RangeFilter, 
Depends(datetime_range_filter_factory("updated_at", TI))],
     duration_range: Annotated[RangeFilter, 
Depends(float_range_filter_factory("duration", TI))],
@@ -560,7 +565,12 @@ def get_task_instances(
     task_id: Annotated[FilterParam[str | None], 
Depends(filter_param_factory(TI.task_id, str | None))],
     run_after_range: Annotated[RangeFilter, 
Depends(datetime_range_filter_factory("run_after", TI))],
     logical_date_range: Annotated[RangeFilter, 
Depends(datetime_range_filter_factory("logical_date", TI))],
-    start_date_range: Annotated[RangeFilter, 
Depends(datetime_range_filter_factory("start_date", TI))],
+    start_date_range: Annotated[
+        RangeFilter,
+        Depends(
+            datetime_range_filter_factory("start_date", TI, 
null_lower_bound_clause=TI.end_date.is_(None))
+        ),
+    ],
     end_date_range: Annotated[RangeFilter, 
Depends(datetime_range_filter_factory("end_date", TI))],
     update_at_range: Annotated[RangeFilter, 
Depends(datetime_range_filter_factory("updated_at", TI))],
     duration_range: Annotated[RangeFilter, 
Depends(float_range_filter_factory("duration", TI))],
diff --git 
a/airflow-core/tests/unit/api_fastapi/core_api/routes/public/test_task_instances.py
 
b/airflow-core/tests/unit/api_fastapi/core_api/routes/public/test_task_instances.py
index 67d9a6751a3..ddb0776e8b8 100644
--- 
a/airflow-core/tests/unit/api_fastapi/core_api/routes/public/test_task_instances.py
+++ 
b/airflow-core/tests/unit/api_fastapi/core_api/routes/public/test_task_instances.py
@@ -1361,6 +1361,25 @@ class TestGetMappedTaskInstances:
         assert body["total_entries"] == 0
         assert body["task_instances"] == []
 
+    def test_start_date_lower_bound_excludes_finished_tis_with_null_start_date(
+        self, test_client, one_task_with_mapped_tis, session
+    ):
+        session.execute(
+            update(TaskInstance)
+            .where(TaskInstance.task_id == "task_2", TaskInstance.map_index == 
0)
+            .values(start_date=None, end_date=DEFAULT_DATETIME_2, 
state=TaskInstanceState.FAILED)
+        )
+        session.commit()
+
+        response = test_client.get(
+            
"/dags/mapped_tis/dagRuns/run_mapped_tis/taskInstances/task_2/listMapped",
+            params={"start_date_gte": DEFAULT_DATETIME_1},
+        )
+        assert response.status_code == 200
+        body = response.json()
+        assert body["total_entries"] == 2
+        assert [ti["map_index"] for ti in body["task_instances"]] == [1, 2]
+
     def test_with_logical_date(self, test_client, one_task_with_mapped_tis):
         response = test_client.get(
             
"/dags/mapped_tis/dagRuns/run_mapped_tis/taskInstances/task_2/listMapped",
@@ -1702,6 +1721,15 @@ class TestGetTaskInstances(TestTaskInstanceEndpoint):
                 8,
                 id="test start_date coalesce with null",
             ),
+            pytest.param(
+                [{"start_date": None, "end_date": DEFAULT_DATETIME_1}],
+                True,
+                
"/dags/example_python_operator/dagRuns/TEST_DAG_RUN_ID/taskInstances",
+                {"start_date_gte": DEFAULT_DATETIME_STR_1},
+                0,
+                7,
+                id="test start_date lower bound excludes finished with null 
start_date",
+            ),
             pytest.param(
                 [
                     {"pool": "test_pool_1"},

Reply via email to