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

pierrejeambrun 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 a990ce976e7 Allow filtering the Dags list by failed and success runs 
in any run state filter (#69875)
a990ce976e7 is described below

commit a990ce976e7ddba115e3d864d18a84ff58b7e7ed
Author: Yuseok Jo <[email protected]>
AuthorDate: Thu Jul 23 17:51:56 2026 +0900

    Allow filtering the Dags list by failed and success runs in any run state 
filter (#69875)
---
 .../src/airflow/api_fastapi/common/parameters.py   | 19 +++++++---------
 .../api_fastapi/core_api/openapi/_private_ui.yaml  |  6 ++----
 .../ui/openapi-gen/queries/ensureQueryData.ts      |  2 +-
 .../src/airflow/ui/openapi-gen/queries/prefetch.ts |  2 +-
 .../src/airflow/ui/openapi-gen/queries/queries.ts  |  2 +-
 .../src/airflow/ui/openapi-gen/queries/suspense.ts |  2 +-
 .../ui/openapi-gen/requests/services.gen.ts        |  2 +-
 .../airflow/ui/openapi-gen/requests/types.gen.ts   |  2 +-
 .../src/pages/DagsList/DagsFilters/DagsFilters.tsx |  8 +++----
 .../api_fastapi/core_api/routes/ui/test_dags.py    | 25 +++++++++++-----------
 10 files changed, 32 insertions(+), 38 deletions(-)

diff --git a/airflow-core/src/airflow/api_fastapi/common/parameters.py 
b/airflow-core/src/airflow/api_fastapi/common/parameters.py
index e4c7b1bfd5d..18445acfb04 100644
--- a/airflow-core/src/airflow/api_fastapi/common/parameters.py
+++ b/airflow-core/src/airflow/api_fastapi/common/parameters.py
@@ -1438,29 +1438,26 @@ QueryPendingActionsFilter = 
Annotated[_PendingActionsFilter, Depends(_PendingAct
 class _AnyDagRunStateFilter(BaseParam[DagRunState | None]):
     """Filter Dags that have any DagRun in the given state, not only the 
latest one."""
 
-    # Only these states have a partial index on dag_run; others would force a 
full table scan.
-    SUPPORTED_STATES = (DagRunState.QUEUED, DagRunState.RUNNING)
-
     def to_orm(self, select: Select) -> Select:
         if self.value is None and self.skip_none:
             return select
 
-        run_subquery = sql_select(DagRun.dag_id).where(DagRun.state == 
self.value).distinct()
-        return select.where(DagModel.dag_id.in_(run_subquery))
+        # EXISTS resolves each Dag via the (dag_id, state) index instead of 
scanning every run in the state.
+        has_run_in_state = (
+            sql_select(DagRun.dag_id)
+            .where(DagRun.dag_id == DagModel.dag_id, DagRun.state == 
self.value)
+            .exists()
+        )
+        return select.where(has_run_in_state)
 
     @classmethod
     def depends(
         cls,
         dag_run_state: DagRunState | None = Query(
             None,
-            description="Filter Dags that have any DagRun in the given state. 
Only ``queued`` and ``running`` are supported.",
+            description="Filter Dags that have any DagRun in the given state.",
         ),
     ) -> _AnyDagRunStateFilter:
-        if dag_run_state is not None and dag_run_state not in 
cls.SUPPORTED_STATES:
-            raise HTTPException(
-                status.HTTP_400_BAD_REQUEST,
-                detail=f"dag_run_state only supports {[state.value for state 
in cls.SUPPORTED_STATES]}.",
-            )
         return cls().set_value(dag_run_state)
 
 
diff --git 
a/airflow-core/src/airflow/api_fastapi/core_api/openapi/_private_ui.yaml 
b/airflow-core/src/airflow/api_fastapi/core_api/openapi/_private_ui.yaml
index e34789c3524..6dc04dde73b 100644
--- a/airflow-core/src/airflow/api_fastapi/core_api/openapi/_private_ui.yaml
+++ b/airflow-core/src/airflow/api_fastapi/core_api/openapi/_private_ui.yaml
@@ -488,11 +488,9 @@ paths:
           anyOf:
           - $ref: '#/components/schemas/DagRunState'
           - type: 'null'
-          description: Filter Dags that have any DagRun in the given state. 
Only ``queued``
-            and ``running`` are supported.
+          description: Filter Dags that have any DagRun in the given state.
           title: Dag Run State
-        description: Filter Dags that have any DagRun in the given state. Only 
``queued``
-          and ``running`` are supported.
+        description: Filter Dags that have any DagRun in the given state.
       - name: bundle_name
         in: query
         required: false
diff --git a/airflow-core/src/airflow/ui/openapi-gen/queries/ensureQueryData.ts 
b/airflow-core/src/airflow/ui/openapi-gen/queries/ensureQueryData.ts
index 8fbebba6f50..5222629f74e 100644
--- a/airflow-core/src/airflow/ui/openapi-gen/queries/ensureQueryData.ts
+++ b/airflow-core/src/airflow/ui/openapi-gen/queries/ensureQueryData.ts
@@ -689,7 +689,7 @@ export const ensureUseDagServiceGetDagTagsData = 
(queryClient: QueryClient, { li
 * @param data.paused
 * @param data.hasImportErrors Filter Dags by having import errors. Only Dags 
that have been successfully loaded before will be returned.
 * @param data.lastDagRunState
-* @param data.dagRunState Filter Dags that have any DagRun in the given state. 
Only ``queued`` and ``running`` are supported.
+* @param data.dagRunState Filter Dags that have any DagRun in the given state.
 * @param data.bundleName
 * @param data.bundleVersion
 * @param data.orderBy Attributes to order by, multi criteria sort is 
supported. Prefix with `-` for descending order. Supported attributes: `dag_id, 
dag_display_name, next_dagrun, state, start_date, last_run_state, 
last_run_start_date, last_run_run_after`
diff --git a/airflow-core/src/airflow/ui/openapi-gen/queries/prefetch.ts 
b/airflow-core/src/airflow/ui/openapi-gen/queries/prefetch.ts
index e6937375f25..d093905c34d 100644
--- a/airflow-core/src/airflow/ui/openapi-gen/queries/prefetch.ts
+++ b/airflow-core/src/airflow/ui/openapi-gen/queries/prefetch.ts
@@ -689,7 +689,7 @@ export const prefetchUseDagServiceGetDagTags = 
(queryClient: QueryClient, { limi
 * @param data.paused
 * @param data.hasImportErrors Filter Dags by having import errors. Only Dags 
that have been successfully loaded before will be returned.
 * @param data.lastDagRunState
-* @param data.dagRunState Filter Dags that have any DagRun in the given state. 
Only ``queued`` and ``running`` are supported.
+* @param data.dagRunState Filter Dags that have any DagRun in the given state.
 * @param data.bundleName
 * @param data.bundleVersion
 * @param data.orderBy Attributes to order by, multi criteria sort is 
supported. Prefix with `-` for descending order. Supported attributes: `dag_id, 
dag_display_name, next_dagrun, state, start_date, last_run_state, 
last_run_start_date, last_run_run_after`
diff --git a/airflow-core/src/airflow/ui/openapi-gen/queries/queries.ts 
b/airflow-core/src/airflow/ui/openapi-gen/queries/queries.ts
index c586eabf179..ee361bd5541 100644
--- a/airflow-core/src/airflow/ui/openapi-gen/queries/queries.ts
+++ b/airflow-core/src/airflow/ui/openapi-gen/queries/queries.ts
@@ -689,7 +689,7 @@ export const useDagServiceGetDagTags = <TData = 
Common.DagServiceGetDagTagsDefau
 * @param data.paused
 * @param data.hasImportErrors Filter Dags by having import errors. Only Dags 
that have been successfully loaded before will be returned.
 * @param data.lastDagRunState
-* @param data.dagRunState Filter Dags that have any DagRun in the given state. 
Only ``queued`` and ``running`` are supported.
+* @param data.dagRunState Filter Dags that have any DagRun in the given state.
 * @param data.bundleName
 * @param data.bundleVersion
 * @param data.orderBy Attributes to order by, multi criteria sort is 
supported. Prefix with `-` for descending order. Supported attributes: `dag_id, 
dag_display_name, next_dagrun, state, start_date, last_run_state, 
last_run_start_date, last_run_run_after`
diff --git a/airflow-core/src/airflow/ui/openapi-gen/queries/suspense.ts 
b/airflow-core/src/airflow/ui/openapi-gen/queries/suspense.ts
index 3bc869e2b7d..9095d363573 100644
--- a/airflow-core/src/airflow/ui/openapi-gen/queries/suspense.ts
+++ b/airflow-core/src/airflow/ui/openapi-gen/queries/suspense.ts
@@ -689,7 +689,7 @@ export const useDagServiceGetDagTagsSuspense = <TData = 
Common.DagServiceGetDagT
 * @param data.paused
 * @param data.hasImportErrors Filter Dags by having import errors. Only Dags 
that have been successfully loaded before will be returned.
 * @param data.lastDagRunState
-* @param data.dagRunState Filter Dags that have any DagRun in the given state. 
Only ``queued`` and ``running`` are supported.
+* @param data.dagRunState Filter Dags that have any DagRun in the given state.
 * @param data.bundleName
 * @param data.bundleVersion
 * @param data.orderBy Attributes to order by, multi criteria sort is 
supported. Prefix with `-` for descending order. Supported attributes: `dag_id, 
dag_display_name, next_dagrun, state, start_date, last_run_state, 
last_run_start_date, last_run_run_after`
diff --git a/airflow-core/src/airflow/ui/openapi-gen/requests/services.gen.ts 
b/airflow-core/src/airflow/ui/openapi-gen/requests/services.gen.ts
index 3ae4565069c..71a43d7b6a9 100644
--- a/airflow-core/src/airflow/ui/openapi-gen/requests/services.gen.ts
+++ b/airflow-core/src/airflow/ui/openapi-gen/requests/services.gen.ts
@@ -1964,7 +1964,7 @@ export class DagService {
      * @param data.paused
      * @param data.hasImportErrors Filter Dags by having import errors. Only 
Dags that have been successfully loaded before will be returned.
      * @param data.lastDagRunState
-     * @param data.dagRunState Filter Dags that have any DagRun in the given 
state. Only ``queued`` and ``running`` are supported.
+     * @param data.dagRunState Filter Dags that have any DagRun in the given 
state.
      * @param data.bundleName
      * @param data.bundleVersion
      * @param data.orderBy Attributes to order by, multi criteria sort is 
supported. Prefix with `-` for descending order. Supported attributes: `dag_id, 
dag_display_name, next_dagrun, state, start_date, last_run_state, 
last_run_start_date, last_run_run_after`
diff --git a/airflow-core/src/airflow/ui/openapi-gen/requests/types.gen.ts 
b/airflow-core/src/airflow/ui/openapi-gen/requests/types.gen.ts
index 38bc1662831..60a6eb9cfb4 100644
--- a/airflow-core/src/airflow/ui/openapi-gen/requests/types.gen.ts
+++ b/airflow-core/src/airflow/ui/openapi-gen/requests/types.gen.ts
@@ -3542,7 +3542,7 @@ export type GetDagsUiData = {
     dagIds?: Array<(string)> | null;
     dagRunsLimit?: number;
     /**
-     * Filter Dags that have any DagRun in the given state. Only ``queued`` 
and ``running`` are supported.
+     * Filter Dags that have any DagRun in the given state.
      */
     dagRunState?: DagRunState | null;
     excludeStale?: boolean;
diff --git 
a/airflow-core/src/airflow/ui/src/pages/DagsList/DagsFilters/DagsFilters.tsx 
b/airflow-core/src/airflow/ui/src/pages/DagsList/DagsFilters/DagsFilters.tsx
index 74300bf28a3..424dbb1b615 100644
--- a/airflow-core/src/airflow/ui/src/pages/DagsList/DagsFilters/DagsFilters.tsx
+++ b/airflow-core/src/airflow/ui/src/pages/DagsList/DagsFilters/DagsFilters.tsx
@@ -47,9 +47,7 @@ const {
 
 type BooleanFilterValue = "all" | "false" | "true";
 
-const lastRunStates = ["failed", "queued", "running", "success"] as const;
-// Mirrors the backend limit (see _AnyDagRunStateFilter): only running/queued 
are indexed on dag_run.
-const anyRunStates = ["queued", "running"] as const;
+const runStates = ["failed", "queued", "running", "success"] as const;
 const booleanFilterValues: ReadonlyArray<BooleanFilterValue> = ["all", "true", 
"false"];
 
 const toBooleanFilterValue = (
@@ -174,14 +172,14 @@ export const DagsFilters = () => {
         dataTestId="dags-last-run-state-filter"
         label={translate("filters.lastRunState")}
         onChange={handleStateChange}
-        states={lastRunStates}
+        states={runStates}
         value={state ?? undefined}
       />
       <RunStateSelect
         dataTestId="dags-any-run-state-filter"
         label={translate("filters.anyRunState")}
         onChange={handleActiveRunChange}
-        states={anyRunStates}
+        states={runStates}
         value={activeRunState ?? undefined}
       />
       <RequiredActionFilter needsReview={needsReview === "true"} 
onToggle={handleNeedsReviewToggle} />
diff --git 
a/airflow-core/tests/unit/api_fastapi/core_api/routes/ui/test_dags.py 
b/airflow-core/tests/unit/api_fastapi/core_api/routes/ui/test_dags.py
index 1cd4a70de7d..837e7805c69 100644
--- a/airflow-core/tests/unit/api_fastapi/core_api/routes/ui/test_dags.py
+++ b/airflow-core/tests/unit/api_fastapi/core_api/routes/ui/test_dags.py
@@ -136,30 +136,31 @@ class TestGetDagRuns(TestPublicDagEndpoint):
                 previous_run_after = dag_run["run_after"]
 
     @pytest.mark.usefixtures("configure_git_connection_for_dag_bundle")
-    def test_dag_run_state_matches_any_run_not_only_latest(self, test_client, 
session):
-        # Backwards backfill: an older run is still running while the latest 
run already finished.
+    @pytest.mark.parametrize("state", ["queued", "running", "failed", 
"success"])
+    def test_dag_run_state_matches_any_run_not_only_latest(self, test_client, 
session, state):
+        # Give DAG1 an older run in the probed state while its latest run ends 
in a
+        # different state, so a latest-run filter misses it but an any-run 
filter finds it
+        # (e.g. failure history hidden behind a green latest run).
         older_run = session.scalar(
             select(DagRun).where(DagRun.dag_id == DAG1_ID, DagRun.run_id == 
"run_id_1")
         )
-        older_run.state = DagRunState.RUNNING
+        older_run.state = DagRunState(state)
+        latest_run = session.scalar(
+            select(DagRun).where(DagRun.dag_id == DAG1_ID, DagRun.run_id == 
"run_id_5")
+        )
+        latest_run.state = DagRunState.FAILED if state == "success" else 
DagRunState.SUCCESS
         session.commit()
 
-        # last_dag_run_state only looks at the latest run, which is not running
-        last_state = test_client.get("/dags", params={"last_dag_run_state": 
"running"})
+        # last_dag_run_state only looks at the latest run, which is in a 
different state
+        last_state = test_client.get("/dags", params={"last_dag_run_state": 
state, "dag_ids": [DAG1_ID]})
         assert last_state.status_code == 200
         assert [dag["dag_id"] for dag in last_state.json()["dags"]] == []
 
         # dag_run_state matches a Dag that has any run in the state
-        any_state = test_client.get("/dags", params={"dag_run_state": 
"running"})
+        any_state = test_client.get("/dags", params={"dag_run_state": state, 
"dag_ids": [DAG1_ID]})
         assert any_state.status_code == 200
         assert [dag["dag_id"] for dag in any_state.json()["dags"]] == [DAG1_ID]
 
-    @pytest.mark.parametrize("unsupported_state", ["success", "failed"])
-    def test_dag_run_state_rejects_unsupported_states(self, test_client, 
unsupported_state):
-        # Only running/queued have a partial index; other states would force a 
full table scan.
-        response = test_client.get("/dags", params={"dag_run_state": 
unsupported_state})
-        assert response.status_code == 400
-
     @pytest.fixture
     def setup_hitl_data(self, create_task_instance: TaskInstance, session: 
Session):
         """Setup HITL test data for parametrized tests."""

Reply via email to