This is an automated email from the ASF dual-hosted git repository.
pierrejeambrun pushed a commit to branch v3-3-test
in repository https://gitbox.apache.org/repos/asf/airflow.git
The following commit(s) were added to refs/heads/v3-3-test by this push:
new 2b68a6293dd Add API endpoint for backfill dag run entries (#67381)
(#70127)
2b68a6293dd is described below
commit 2b68a6293dd9d924edef414afb1e7d227879daf5
Author: Pierre Jeambrun <[email protected]>
AuthorDate: Mon Jul 20 16:42:10 2026 +0200
Add API endpoint for backfill dag run entries (#67381) (#70127)
* Add GET /backfills/{backfill_id}/dag_runs endpoint
Adds a new public API endpoint that returns the BackfillDagRun entries
for a given backfill with joined DagRun state. Users can see what
happened in a backfill: which dates ran, their states (queued, running,
success, failed), and which slots were skipped (with reason).
- BackfillDagRunResponse / BackfillDagRunCollectionResponse models
- LEFT OUTER JOIN via joinedload includes skipped slots (null dag_run_id)
- Pagination via limit/offset, default ordering by sort_ordinal
- 404 when backfill doesn't exist
- 8 unit tests covering happy path, skipped slots, 404, pagination,
empty backfill, and ordering contract
closes: #46250
* Align backfill dag run response identifiers
(cherry picked from commit 6f88f293037cd4934eec4d2317607853cd801e01)
Co-authored-by: Shivam Rastogi <[email protected]>
---
.../api_fastapi/core_api/datamodels/backfills.py | 22 +++
.../core_api/openapi/v2-rest-api-generated.yaml | 148 ++++++++++++++++++
.../core_api/routes/public/backfills.py | 39 +++++
.../src/airflow/ui/openapi-gen/queries/common.ts | 9 ++
.../ui/openapi-gen/queries/ensureQueryData.ts | 17 +++
.../src/airflow/ui/openapi-gen/queries/prefetch.ts | 17 +++
.../src/airflow/ui/openapi-gen/queries/queries.ts | 17 +++
.../src/airflow/ui/openapi-gen/queries/suspense.ts | 17 +++
.../airflow/ui/openapi-gen/requests/schemas.gen.ts | 102 +++++++++++++
.../ui/openapi-gen/requests/services.gen.ts | 34 ++++-
.../airflow/ui/openapi-gen/requests/types.gen.ts | 62 ++++++++
.../core_api/routes/public/test_backfills.py | 167 +++++++++++++++++++++
.../src/airflowctl/api/datamodels/generated.py | 25 +++
13 files changed, 675 insertions(+), 1 deletion(-)
diff --git
a/airflow-core/src/airflow/api_fastapi/core_api/datamodels/backfills.py
b/airflow-core/src/airflow/api_fastapi/core_api/datamodels/backfills.py
index 52538b37e16..df1ffaca014 100644
--- a/airflow-core/src/airflow/api_fastapi/core_api/datamodels/backfills.py
+++ b/airflow-core/src/airflow/api_fastapi/core_api/datamodels/backfills.py
@@ -24,6 +24,7 @@ from pydantic import AliasPath, Field, NonNegativeInt
from airflow.api_fastapi.core_api.base import BaseModel, StrictBaseModel
from airflow.models.backfill import ReprocessBehavior
+from airflow.utils.state import DagRunState
class BackfillPostBody(StrictBaseModel):
@@ -69,6 +70,27 @@ class BackfillCollectionResponse(BaseModel):
total_entries: int
+class BackfillDagRunResponse(BaseModel):
+ """Serializer for a single BackfillDagRun entry with joined DagRun
state."""
+
+ id: NonNegativeInt
+ backfill_id: NonNegativeInt
+ dag_id: str = Field(validation_alias=AliasPath("backfill", "dag_id"))
+ dag_run_id: str | None = Field(default=None,
validation_alias=AliasPath("dag_run", "run_id"))
+ logical_date: datetime | None
+ partition_key: str | None
+ sort_ordinal: int
+ exception_reason: str | None
+ dag_run_state: DagRunState | None = Field(default=None,
validation_alias=AliasPath("dag_run", "state"))
+
+
+class BackfillDagRunCollectionResponse(BaseModel):
+ """BackfillDagRun Collection serializer for responses."""
+
+ backfill_dag_runs: list[BackfillDagRunResponse]
+ total_entries: int
+
+
class DryRunBackfillResponse(BaseModel):
"""Backfill serializer for responses in dry-run mode."""
diff --git
a/airflow-core/src/airflow/api_fastapi/core_api/openapi/v2-rest-api-generated.yaml
b/airflow-core/src/airflow/api_fastapi/core_api/openapi/v2-rest-api-generated.yaml
index 58ac2e2840c..75caaf4f033 100644
---
a/airflow-core/src/airflow/api_fastapi/core_api/openapi/v2-rest-api-generated.yaml
+++
b/airflow-core/src/airflow/api_fastapi/core_api/openapi/v2-rest-api-generated.yaml
@@ -1221,6 +1221,85 @@ paths:
application/json:
schema:
$ref: '#/components/schemas/HTTPValidationError'
+ /api/v2/backfills/{backfill_id}/dag_runs:
+ get:
+ tags:
+ - Backfill
+ summary: List Backfill Dag Runs
+ description: List Dag runs associated with a backfill, including skipped
slots.
+ operationId: list_backfill_dag_runs
+ security:
+ - OAuth2PasswordBearer: []
+ - HTTPBearer: []
+ parameters:
+ - name: backfill_id
+ in: path
+ required: true
+ schema:
+ type: integer
+ minimum: 0
+ title: Backfill Id
+ - name: limit
+ in: query
+ required: false
+ schema:
+ type: integer
+ minimum: 0
+ default: 50
+ title: Limit
+ - name: offset
+ in: query
+ required: false
+ schema:
+ type: integer
+ minimum: 0
+ default: 0
+ title: Offset
+ - name: order_by
+ in: query
+ required: false
+ schema:
+ type: array
+ items:
+ type: string
+ description: 'Attributes to order by, multi criteria sort is
supported.
+ Prefix with `-` for descending order. Supported attributes: `id,
sort_ordinal`'
+ default:
+ - sort_ordinal
+ title: Order By
+ description: 'Attributes to order by, multi criteria sort is
supported. Prefix
+ with `-` for descending order. Supported attributes: `id,
sort_ordinal`'
+ responses:
+ '200':
+ description: Successful Response
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/BackfillDagRunCollectionResponse'
+ '401':
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/HTTPExceptionResponse'
+ description: Unauthorized
+ '403':
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/HTTPExceptionResponse'
+ description: Forbidden
+ '404':
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/HTTPExceptionResponse'
+ description: Not Found
+ '422':
+ description: Validation Error
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/HTTPValidationError'
/api/v2/backfills/{backfill_id}/pause:
put:
tags:
@@ -11861,6 +11940,75 @@ components:
- total_entries
title: BackfillCollectionResponse
description: Backfill Collection serializer for responses.
+ BackfillDagRunCollectionResponse:
+ properties:
+ backfill_dag_runs:
+ items:
+ $ref: '#/components/schemas/BackfillDagRunResponse'
+ type: array
+ title: Backfill Dag Runs
+ total_entries:
+ type: integer
+ title: Total Entries
+ type: object
+ required:
+ - backfill_dag_runs
+ - total_entries
+ title: BackfillDagRunCollectionResponse
+ description: BackfillDagRun Collection serializer for responses.
+ BackfillDagRunResponse:
+ properties:
+ id:
+ type: integer
+ minimum: 0.0
+ title: Id
+ backfill_id:
+ type: integer
+ minimum: 0.0
+ title: Backfill Id
+ dag_id:
+ type: string
+ title: Dag Id
+ dag_run_id:
+ anyOf:
+ - type: string
+ - type: 'null'
+ title: Dag Run Id
+ logical_date:
+ anyOf:
+ - type: string
+ format: date-time
+ - type: 'null'
+ title: Logical Date
+ partition_key:
+ anyOf:
+ - type: string
+ - type: 'null'
+ title: Partition Key
+ sort_ordinal:
+ type: integer
+ title: Sort Ordinal
+ exception_reason:
+ anyOf:
+ - type: string
+ - type: 'null'
+ title: Exception Reason
+ dag_run_state:
+ anyOf:
+ - $ref: '#/components/schemas/DagRunState'
+ - type: 'null'
+ type: object
+ required:
+ - id
+ - backfill_id
+ - dag_id
+ - logical_date
+ - partition_key
+ - sort_ordinal
+ - exception_reason
+ title: BackfillDagRunResponse
+ description: Serializer for a single BackfillDagRun entry with joined
DagRun
+ state.
BackfillPostBody:
properties:
dag_id:
diff --git
a/airflow-core/src/airflow/api_fastapi/core_api/routes/public/backfills.py
b/airflow-core/src/airflow/api_fastapi/core_api/routes/public/backfills.py
index c4971b883a1..62c43616c40 100644
--- a/airflow-core/src/airflow/api_fastapi/core_api/routes/public/backfills.py
+++ b/airflow-core/src/airflow/api_fastapi/core_api/routes/public/backfills.py
@@ -35,6 +35,7 @@ from airflow.api_fastapi.common.parameters import QueryLimit,
QueryOffset, SortP
from airflow.api_fastapi.common.router import AirflowRouter
from airflow.api_fastapi.core_api.datamodels.backfills import (
BackfillCollectionResponse,
+ BackfillDagRunCollectionResponse,
BackfillPostBody,
BackfillResponse,
DryRunBackfillCollectionResponse,
@@ -128,6 +129,44 @@ def get_backfill(
raise HTTPException(status.HTTP_404_NOT_FOUND, "Backfill not found")
+@backfills_router.get(
+ path="/{backfill_id}/dag_runs",
+ responses=create_openapi_http_exception_doc([status.HTTP_404_NOT_FOUND]),
+ dependencies=[
+ Depends(requires_access_backfill(method="GET")),
+ ],
+)
+def list_backfill_dag_runs(
+ backfill_id: NonNegativeInt,
+ limit: QueryLimit,
+ offset: QueryOffset,
+ order_by: Annotated[
+ SortParam,
+ Depends(SortParam(["id", "sort_ordinal"],
BackfillDagRun).dynamic_depends(default="sort_ordinal")),
+ ],
+ session: SessionDep,
+) -> BackfillDagRunCollectionResponse:
+ """List Dag runs associated with a backfill, including skipped slots."""
+ backfill = session.get(Backfill, backfill_id)
+ if not backfill:
+ raise HTTPException(status.HTTP_404_NOT_FOUND, f"Backfill with id
{backfill_id} not found")
+
+ select_stmt, total_entries = paginated_select(
+ statement=select(BackfillDagRun)
+ .where(BackfillDagRun.backfill_id == backfill_id)
+ # Load backfill for dag_id; dag_run may be null for skipped slots.
+ .options(joinedload(BackfillDagRun.backfill),
joinedload(BackfillDagRun.dag_run)),
+ order_by=order_by,
+ offset=offset,
+ limit=limit,
+ session=session,
+ )
+ return BackfillDagRunCollectionResponse(
+ backfill_dag_runs=list(session.scalars(select_stmt).unique()),
+ total_entries=total_entries,
+ )
+
+
@backfills_router.put(
path="/{backfill_id}/pause",
responses=create_openapi_http_exception_doc(
diff --git a/airflow-core/src/airflow/ui/openapi-gen/queries/common.ts
b/airflow-core/src/airflow/ui/openapi-gen/queries/common.ts
index b72cc90eb30..e48d6a8d38c 100644
--- a/airflow-core/src/airflow/ui/openapi-gen/queries/common.ts
+++ b/airflow-core/src/airflow/ui/openapi-gen/queries/common.ts
@@ -101,6 +101,15 @@ export const useBackfillServiceGetBackfillKey =
"BackfillServiceGetBackfill";
export const UseBackfillServiceGetBackfillKeyFn = ({ backfillId }: {
backfillId: number;
}, queryKey?: Array<unknown>) => [useBackfillServiceGetBackfillKey,
...(queryKey ?? [{ backfillId }])];
+export type BackfillServiceListBackfillDagRunsDefaultResponse =
Awaited<ReturnType<typeof BackfillService.listBackfillDagRuns>>;
+export type BackfillServiceListBackfillDagRunsQueryResult<TData =
BackfillServiceListBackfillDagRunsDefaultResponse, TError = unknown> =
UseQueryResult<TData, TError>;
+export const useBackfillServiceListBackfillDagRunsKey =
"BackfillServiceListBackfillDagRuns";
+export const UseBackfillServiceListBackfillDagRunsKeyFn = ({ backfillId,
limit, offset, orderBy }: {
+ backfillId: number;
+ limit?: number;
+ offset?: number;
+ orderBy?: string[];
+}, queryKey?: Array<unknown>) => [useBackfillServiceListBackfillDagRunsKey,
...(queryKey ?? [{ backfillId, limit, offset, orderBy }])];
export type BackfillServiceListBackfillsUiDefaultResponse =
Awaited<ReturnType<typeof BackfillService.listBackfillsUi>>;
export type BackfillServiceListBackfillsUiQueryResult<TData =
BackfillServiceListBackfillsUiDefaultResponse, TError = unknown> =
UseQueryResult<TData, TError>;
export const useBackfillServiceListBackfillsUiKey =
"BackfillServiceListBackfillsUi";
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 74a833b2ccf..4a73de88c2e 100644
--- a/airflow-core/src/airflow/ui/openapi-gen/queries/ensureQueryData.ts
+++ b/airflow-core/src/airflow/ui/openapi-gen/queries/ensureQueryData.ts
@@ -195,6 +195,23 @@ export const ensureUseBackfillServiceGetBackfillData =
(queryClient: QueryClient
backfillId: number;
}) => queryClient.ensureQueryData({ queryKey:
Common.UseBackfillServiceGetBackfillKeyFn({ backfillId }), queryFn: () =>
BackfillService.getBackfill({ backfillId }) });
/**
+* List Backfill Dag Runs
+* List Dag runs associated with a backfill, including skipped slots.
+* @param data The data for the request.
+* @param data.backfillId
+* @param data.limit
+* @param data.offset
+* @param data.orderBy Attributes to order by, multi criteria sort is
supported. Prefix with `-` for descending order. Supported attributes: `id,
sort_ordinal`
+* @returns BackfillDagRunCollectionResponse Successful Response
+* @throws ApiError
+*/
+export const ensureUseBackfillServiceListBackfillDagRunsData = (queryClient:
QueryClient, { backfillId, limit, offset, orderBy }: {
+ backfillId: number;
+ limit?: number;
+ offset?: number;
+ orderBy?: string[];
+}) => queryClient.ensureQueryData({ queryKey:
Common.UseBackfillServiceListBackfillDagRunsKeyFn({ backfillId, limit, offset,
orderBy }), queryFn: () => BackfillService.listBackfillDagRuns({ backfillId,
limit, offset, orderBy }) });
+/**
* List Backfills Ui
* @param data The data for the request.
* @param data.limit
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 9db58a9676c..49baa72e3de 100644
--- a/airflow-core/src/airflow/ui/openapi-gen/queries/prefetch.ts
+++ b/airflow-core/src/airflow/ui/openapi-gen/queries/prefetch.ts
@@ -195,6 +195,23 @@ export const prefetchUseBackfillServiceGetBackfill =
(queryClient: QueryClient,
backfillId: number;
}) => queryClient.prefetchQuery({ queryKey:
Common.UseBackfillServiceGetBackfillKeyFn({ backfillId }), queryFn: () =>
BackfillService.getBackfill({ backfillId }) });
/**
+* List Backfill Dag Runs
+* List Dag runs associated with a backfill, including skipped slots.
+* @param data The data for the request.
+* @param data.backfillId
+* @param data.limit
+* @param data.offset
+* @param data.orderBy Attributes to order by, multi criteria sort is
supported. Prefix with `-` for descending order. Supported attributes: `id,
sort_ordinal`
+* @returns BackfillDagRunCollectionResponse Successful Response
+* @throws ApiError
+*/
+export const prefetchUseBackfillServiceListBackfillDagRuns = (queryClient:
QueryClient, { backfillId, limit, offset, orderBy }: {
+ backfillId: number;
+ limit?: number;
+ offset?: number;
+ orderBy?: string[];
+}) => queryClient.prefetchQuery({ queryKey:
Common.UseBackfillServiceListBackfillDagRunsKeyFn({ backfillId, limit, offset,
orderBy }), queryFn: () => BackfillService.listBackfillDagRuns({ backfillId,
limit, offset, orderBy }) });
+/**
* List Backfills Ui
* @param data The data for the request.
* @param data.limit
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 c10c9635706..440f88ffa64 100644
--- a/airflow-core/src/airflow/ui/openapi-gen/queries/queries.ts
+++ b/airflow-core/src/airflow/ui/openapi-gen/queries/queries.ts
@@ -195,6 +195,23 @@ export const useBackfillServiceGetBackfill = <TData =
Common.BackfillServiceGetB
backfillId: number;
}, queryKey?: TQueryKey, options?: Omit<UseQueryOptions<TData, TError>,
"queryKey" | "queryFn">) => useQuery<TData, TError>({ queryKey:
Common.UseBackfillServiceGetBackfillKeyFn({ backfillId }, queryKey), queryFn:
() => BackfillService.getBackfill({ backfillId }) as TData, ...options });
/**
+* List Backfill Dag Runs
+* List Dag runs associated with a backfill, including skipped slots.
+* @param data The data for the request.
+* @param data.backfillId
+* @param data.limit
+* @param data.offset
+* @param data.orderBy Attributes to order by, multi criteria sort is
supported. Prefix with `-` for descending order. Supported attributes: `id,
sort_ordinal`
+* @returns BackfillDagRunCollectionResponse Successful Response
+* @throws ApiError
+*/
+export const useBackfillServiceListBackfillDagRuns = <TData =
Common.BackfillServiceListBackfillDagRunsDefaultResponse, TError = unknown,
TQueryKey extends Array<unknown> = unknown[]>({ backfillId, limit, offset,
orderBy }: {
+ backfillId: number;
+ limit?: number;
+ offset?: number;
+ orderBy?: string[];
+}, queryKey?: TQueryKey, options?: Omit<UseQueryOptions<TData, TError>,
"queryKey" | "queryFn">) => useQuery<TData, TError>({ queryKey:
Common.UseBackfillServiceListBackfillDagRunsKeyFn({ backfillId, limit, offset,
orderBy }, queryKey), queryFn: () => BackfillService.listBackfillDagRuns({
backfillId, limit, offset, orderBy }) as TData, ...options });
+/**
* List Backfills Ui
* @param data The data for the request.
* @param data.limit
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 8c4f6707176..e9e96431829 100644
--- a/airflow-core/src/airflow/ui/openapi-gen/queries/suspense.ts
+++ b/airflow-core/src/airflow/ui/openapi-gen/queries/suspense.ts
@@ -195,6 +195,23 @@ export const useBackfillServiceGetBackfillSuspense =
<TData = Common.BackfillSer
backfillId: number;
}, queryKey?: TQueryKey, options?: Omit<UseQueryOptions<TData, TError>,
"queryKey" | "queryFn">) => useSuspenseQuery<TData, TError>({ queryKey:
Common.UseBackfillServiceGetBackfillKeyFn({ backfillId }, queryKey), queryFn:
() => BackfillService.getBackfill({ backfillId }) as TData, ...options });
/**
+* List Backfill Dag Runs
+* List Dag runs associated with a backfill, including skipped slots.
+* @param data The data for the request.
+* @param data.backfillId
+* @param data.limit
+* @param data.offset
+* @param data.orderBy Attributes to order by, multi criteria sort is
supported. Prefix with `-` for descending order. Supported attributes: `id,
sort_ordinal`
+* @returns BackfillDagRunCollectionResponse Successful Response
+* @throws ApiError
+*/
+export const useBackfillServiceListBackfillDagRunsSuspense = <TData =
Common.BackfillServiceListBackfillDagRunsDefaultResponse, TError = unknown,
TQueryKey extends Array<unknown> = unknown[]>({ backfillId, limit, offset,
orderBy }: {
+ backfillId: number;
+ limit?: number;
+ offset?: number;
+ orderBy?: string[];
+}, queryKey?: TQueryKey, options?: Omit<UseQueryOptions<TData, TError>,
"queryKey" | "queryFn">) => useSuspenseQuery<TData, TError>({ queryKey:
Common.UseBackfillServiceListBackfillDagRunsKeyFn({ backfillId, limit, offset,
orderBy }, queryKey), queryFn: () => BackfillService.listBackfillDagRuns({
backfillId, limit, offset, orderBy }) as TData, ...options });
+/**
* List Backfills Ui
* @param data The data for the request.
* @param data.limit
diff --git a/airflow-core/src/airflow/ui/openapi-gen/requests/schemas.gen.ts
b/airflow-core/src/airflow/ui/openapi-gen/requests/schemas.gen.ts
index 16f14472c6a..58b49673193 100644
--- a/airflow-core/src/airflow/ui/openapi-gen/requests/schemas.gen.ts
+++ b/airflow-core/src/airflow/ui/openapi-gen/requests/schemas.gen.ts
@@ -619,6 +619,108 @@ export const $BackfillCollectionResponse = {
description: 'Backfill Collection serializer for responses.'
} as const;
+export const $BackfillDagRunCollectionResponse = {
+ properties: {
+ backfill_dag_runs: {
+ items: {
+ '$ref': '#/components/schemas/BackfillDagRunResponse'
+ },
+ type: 'array',
+ title: 'Backfill Dag Runs'
+ },
+ total_entries: {
+ type: 'integer',
+ title: 'Total Entries'
+ }
+ },
+ type: 'object',
+ required: ['backfill_dag_runs', 'total_entries'],
+ title: 'BackfillDagRunCollectionResponse',
+ description: 'BackfillDagRun Collection serializer for responses.'
+} as const;
+
+export const $BackfillDagRunResponse = {
+ properties: {
+ id: {
+ type: 'integer',
+ minimum: 0,
+ title: 'Id'
+ },
+ backfill_id: {
+ type: 'integer',
+ minimum: 0,
+ title: 'Backfill Id'
+ },
+ dag_id: {
+ type: 'string',
+ title: 'Dag Id'
+ },
+ dag_run_id: {
+ anyOf: [
+ {
+ type: 'string'
+ },
+ {
+ type: 'null'
+ }
+ ],
+ title: 'Dag Run Id'
+ },
+ logical_date: {
+ anyOf: [
+ {
+ type: 'string',
+ format: 'date-time'
+ },
+ {
+ type: 'null'
+ }
+ ],
+ title: 'Logical Date'
+ },
+ partition_key: {
+ anyOf: [
+ {
+ type: 'string'
+ },
+ {
+ type: 'null'
+ }
+ ],
+ title: 'Partition Key'
+ },
+ sort_ordinal: {
+ type: 'integer',
+ title: 'Sort Ordinal'
+ },
+ exception_reason: {
+ anyOf: [
+ {
+ type: 'string'
+ },
+ {
+ type: 'null'
+ }
+ ],
+ title: 'Exception Reason'
+ },
+ dag_run_state: {
+ anyOf: [
+ {
+ '$ref': '#/components/schemas/DagRunState'
+ },
+ {
+ type: 'null'
+ }
+ ]
+ }
+ },
+ type: 'object',
+ required: ['id', 'backfill_id', 'dag_id', 'logical_date', 'partition_key',
'sort_ordinal', 'exception_reason'],
+ title: 'BackfillDagRunResponse',
+ description: 'Serializer for a single BackfillDagRun entry with joined
DagRun state.'
+} as const;
+
export const $BackfillPostBody = {
properties: {
dag_id: {
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 810e9a9a0f5..b9346d8e459 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
@@ -3,7 +3,7 @@
import type { CancelablePromise } from './core/CancelablePromise';
import { OpenAPI } from './core/OpenAPI';
import { request as __request } from './core/request';
-import type { GetAssetsData, GetAssetsResponse, GetAssetAliasesData,
GetAssetAliasesResponse, GetAssetAliasData, GetAssetAliasResponse,
GetAssetEventsData, GetAssetEventsResponse, CreateAssetEventData,
CreateAssetEventResponse, MaterializeAssetData, MaterializeAssetResponse,
GetAssetQueuedEventsData, GetAssetQueuedEventsResponse,
DeleteAssetQueuedEventsData, DeleteAssetQueuedEventsResponse, GetAssetData,
GetAssetResponse, GetDagAssetQueuedEventsData, GetDagAssetQueuedEventsResponse,
Dele [...]
+import type { GetAssetsData, GetAssetsResponse, GetAssetAliasesData,
GetAssetAliasesResponse, GetAssetAliasData, GetAssetAliasResponse,
GetAssetEventsData, GetAssetEventsResponse, CreateAssetEventData,
CreateAssetEventResponse, MaterializeAssetData, MaterializeAssetResponse,
GetAssetQueuedEventsData, GetAssetQueuedEventsResponse,
DeleteAssetQueuedEventsData, DeleteAssetQueuedEventsResponse, GetAssetData,
GetAssetResponse, GetDagAssetQueuedEventsData, GetDagAssetQueuedEventsResponse,
Dele [...]
export class AssetService {
/**
@@ -506,6 +506,38 @@ export class BackfillService {
});
}
+ /**
+ * List Backfill Dag Runs
+ * List Dag runs associated with a backfill, including skipped slots.
+ * @param data The data for the request.
+ * @param data.backfillId
+ * @param data.limit
+ * @param data.offset
+ * @param data.orderBy Attributes to order by, multi criteria sort is
supported. Prefix with `-` for descending order. Supported attributes: `id,
sort_ordinal`
+ * @returns BackfillDagRunCollectionResponse Successful Response
+ * @throws ApiError
+ */
+ public static listBackfillDagRuns(data: ListBackfillDagRunsData):
CancelablePromise<ListBackfillDagRunsResponse> {
+ return __request(OpenAPI, {
+ method: 'GET',
+ url: '/api/v2/backfills/{backfill_id}/dag_runs',
+ path: {
+ backfill_id: data.backfillId
+ },
+ query: {
+ limit: data.limit,
+ offset: data.offset,
+ order_by: data.orderBy
+ },
+ errors: {
+ 401: 'Unauthorized',
+ 403: 'Forbidden',
+ 404: 'Not Found',
+ 422: 'Validation Error'
+ }
+ });
+ }
+
/**
* Pause Backfill
* @param data The data for the request.
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 6433c3bfcd6..c9527b1a58d 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
@@ -177,6 +177,29 @@ export type BackfillCollectionResponse = {
total_entries: number;
};
+/**
+ * BackfillDagRun Collection serializer for responses.
+ */
+export type BackfillDagRunCollectionResponse = {
+ backfill_dag_runs: Array<BackfillDagRunResponse>;
+ total_entries: number;
+};
+
+/**
+ * Serializer for a single BackfillDagRun entry with joined DagRun state.
+ */
+export type BackfillDagRunResponse = {
+ id: number;
+ backfill_id: number;
+ dag_id: string;
+ dag_run_id?: string | null;
+ logical_date: string | null;
+ partition_key: string | null;
+ sort_ordinal: number;
+ exception_reason: string | null;
+ dag_run_state?: DagRunState | null;
+};
+
/**
* Object used for create backfill request.
*/
@@ -2888,6 +2911,18 @@ export type GetBackfillData = {
export type GetBackfillResponse = BackfillResponse;
+export type ListBackfillDagRunsData = {
+ backfillId: number;
+ limit?: number;
+ offset?: number;
+ /**
+ * Attributes to order by, multi criteria sort is supported. Prefix with
`-` for descending order. Supported attributes: `id, sort_ordinal`
+ */
+ orderBy?: Array<(string)>;
+};
+
+export type ListBackfillDagRunsResponse = BackfillDagRunCollectionResponse;
+
export type PauseBackfillData = {
backfillId: number;
};
@@ -5087,6 +5122,33 @@ export type $OpenApiTs = {
};
};
};
+ '/api/v2/backfills/{backfill_id}/dag_runs': {
+ get: {
+ req: ListBackfillDagRunsData;
+ res: {
+ /**
+ * Successful Response
+ */
+ 200: BackfillDagRunCollectionResponse;
+ /**
+ * Unauthorized
+ */
+ 401: HTTPExceptionResponse;
+ /**
+ * Forbidden
+ */
+ 403: HTTPExceptionResponse;
+ /**
+ * Not Found
+ */
+ 404: HTTPExceptionResponse;
+ /**
+ * Validation Error
+ */
+ 422: HTTPValidationError;
+ };
+ };
+ };
'/api/v2/backfills/{backfill_id}/pause': {
put: {
req: PauseBackfillData;
diff --git
a/airflow-core/tests/unit/api_fastapi/core_api/routes/public/test_backfills.py
b/airflow-core/tests/unit/api_fastapi/core_api/routes/public/test_backfills.py
index d0fdd77e764..a8afb9612d6 100644
---
a/airflow-core/tests/unit/api_fastapi/core_api/routes/public/test_backfills.py
+++
b/airflow-core/tests/unit/api_fastapi/core_api/routes/public/test_backfills.py
@@ -215,6 +215,173 @@ class TestGetBackfill(TestBackfillEndpoint):
)
+class TestListBackfillDagRuns(TestBackfillEndpoint):
+ def test_list_backfill_dag_runs(self, test_client, session):
+ """Happy path: backfill with mixed dag run states."""
+ (dag,) = self._create_dag_models()
+ from_date = pendulum.parse("2024-01-01")
+ to_date = pendulum.parse("2024-01-03")
+ b = Backfill(dag_id=dag.dag_id, from_date=from_date, to_date=to_date)
+ session.add(b)
+ session.flush()
+
+ dr1 = DagRun(
+ dag_id=dag.dag_id,
+ run_id="backfill__2024-01-01",
+ logical_date=pendulum.parse("2024-01-01"),
+ state=DagRunState.SUCCESS,
+ run_type="scheduled",
+ )
+ dr2 = DagRun(
+ dag_id=dag.dag_id,
+ run_id="backfill__2024-01-02",
+ logical_date=pendulum.parse("2024-01-02"),
+ state=DagRunState.FAILED,
+ run_type="scheduled",
+ )
+ session.add_all([dr1, dr2])
+ session.flush()
+
+ bdr1 = BackfillDagRun(
+ backfill_id=b.id, dag_run_id=dr1.id,
logical_date=pendulum.parse("2024-01-01"), sort_ordinal=1
+ )
+ bdr2 = BackfillDagRun(
+ backfill_id=b.id, dag_run_id=dr2.id,
logical_date=pendulum.parse("2024-01-02"), sort_ordinal=2
+ )
+ session.add_all([bdr1, bdr2])
+ session.commit()
+
+ response = test_client.get(f"/backfills/{b.id}/dag_runs")
+ assert response.status_code == 200
+ data = response.json()
+ assert data["total_entries"] == 2
+ runs = data["backfill_dag_runs"]
+ assert len(runs) == 2
+ assert runs[0]["sort_ordinal"] == 1
+ assert runs[0]["dag_id"] == dag.dag_id
+ assert runs[0]["dag_run_id"] == "backfill__2024-01-01"
+ assert runs[0]["dag_run_state"] == "success"
+ assert runs[1]["sort_ordinal"] == 2
+ assert runs[1]["dag_id"] == dag.dag_id
+ assert runs[1]["dag_run_id"] == "backfill__2024-01-02"
+ assert runs[1]["dag_run_state"] == "failed"
+
+ def test_list_backfill_dag_runs_with_skipped_slots(self, test_client,
session):
+ """Slots skipped due to existing runs have null dag_run_id and
exception_reason set."""
+ (dag,) = self._create_dag_models()
+ b = Backfill(
+ dag_id=dag.dag_id, from_date=pendulum.parse("2024-01-01"),
to_date=pendulum.parse("2024-01-02")
+ )
+ session.add(b)
+ session.flush()
+
+ bdr = BackfillDagRun(
+ backfill_id=b.id,
+ dag_run_id=None,
+ logical_date=pendulum.parse("2024-01-01"),
+ sort_ordinal=1,
+ exception_reason="already exists",
+ )
+ session.add(bdr)
+ session.commit()
+
+ response = test_client.get(f"/backfills/{b.id}/dag_runs")
+ assert response.status_code == 200
+ data = response.json()
+ assert data["total_entries"] == 1
+ run = data["backfill_dag_runs"][0]
+ assert run["dag_id"] == dag.dag_id
+ assert run["dag_run_id"] is None
+ assert run["dag_run_state"] is None
+ assert run["exception_reason"] == "already exists"
+
+ def test_list_backfill_dag_runs_not_found(self, test_client):
+ """Non-existent backfill returns 404."""
+ response = test_client.get("/backfills/999999/dag_runs")
+ assert response.status_code == 404
+
+ def test_list_backfill_dag_runs_pagination(self, test_client, session):
+ """Limit and offset work correctly."""
+ (dag,) = self._create_dag_models()
+ b = Backfill(
+ dag_id=dag.dag_id, from_date=pendulum.parse("2024-01-01"),
to_date=pendulum.parse("2024-01-05")
+ )
+ session.add(b)
+ session.flush()
+
+ for i in range(1, 4):
+ session.add(
+ BackfillDagRun(
+ backfill_id=b.id,
+ dag_run_id=None,
+ logical_date=pendulum.parse(f"2024-01-0{i}"),
+ sort_ordinal=i,
+ exception_reason="already exists",
+ )
+ )
+ session.commit()
+
+ response =
test_client.get(f"/backfills/{b.id}/dag_runs?limit=2&offset=0")
+ assert response.status_code == 200
+ data = response.json()
+ assert data["total_entries"] == 3
+ assert len(data["backfill_dag_runs"]) == 2
+
+ response =
test_client.get(f"/backfills/{b.id}/dag_runs?limit=2&offset=2")
+ assert response.status_code == 200
+ data = response.json()
+ assert len(data["backfill_dag_runs"]) == 1
+
+ def test_list_backfill_dag_runs_empty(self, test_client, session):
+ """Backfill with no dag runs returns empty list."""
+ (dag,) = self._create_dag_models()
+ b = Backfill(
+ dag_id=dag.dag_id, from_date=pendulum.parse("2024-01-01"),
to_date=pendulum.parse("2024-01-02")
+ )
+ session.add(b)
+ session.commit()
+
+ response = test_client.get(f"/backfills/{b.id}/dag_runs")
+ assert response.status_code == 200
+ data = response.json()
+ assert data["total_entries"] == 0
+ assert data["backfill_dag_runs"] == []
+
+ @pytest.mark.parametrize(
+ ("order_by", "expected_first_ordinal"),
+ [
+ ("sort_ordinal", 1),
+ ("-sort_ordinal", 3),
+ ("id", 1),
+ ],
+ )
+ def test_list_backfill_dag_runs_ordering(self, order_by,
expected_first_ordinal, test_client, session):
+ """Verify sort contract for allowed order_by values."""
+ (dag,) = self._create_dag_models()
+ b = Backfill(
+ dag_id=dag.dag_id, from_date=pendulum.parse("2024-01-01"),
to_date=pendulum.parse("2024-01-03")
+ )
+ session.add(b)
+ session.flush()
+
+ for i in range(1, 4):
+ session.add(
+ BackfillDagRun(
+ backfill_id=b.id,
+ dag_run_id=None,
+ logical_date=pendulum.parse(f"2024-01-0{i}"),
+ sort_ordinal=i,
+ exception_reason="already exists",
+ )
+ )
+ session.commit()
+
+ response =
test_client.get(f"/backfills/{b.id}/dag_runs?order_by={order_by}")
+ assert response.status_code == 200
+ data = response.json()
+ assert data["backfill_dag_runs"][0]["sort_ordinal"] ==
expected_first_ordinal
+
+
class TestCreateBackfill(TestBackfillEndpoint):
@pytest.mark.parametrize(
("repro_act", "repro_exp"),
diff --git a/airflow-ctl/src/airflowctl/api/datamodels/generated.py
b/airflow-ctl/src/airflowctl/api/datamodels/generated.py
index 8804d2040ea..83bd18eb942 100644
--- a/airflow-ctl/src/airflowctl/api/datamodels/generated.py
+++ b/airflow-ctl/src/airflowctl/api/datamodels/generated.py
@@ -1365,6 +1365,22 @@ class AssetStateStoreResponse(BaseModel):
last_updated_by: AssetStateStoreLastUpdatedBy | None = None
+class BackfillDagRunResponse(BaseModel):
+ """
+ Serializer for a single BackfillDagRun entry with joined DagRun state.
+ """
+
+ id: Annotated[int, Field(ge=0, title="Id")]
+ backfill_id: Annotated[int, Field(ge=0, title="Backfill Id")]
+ dag_id: Annotated[str, Field(title="Dag Id")]
+ dag_run_id: Annotated[str | None, Field(title="Dag Run Id")] = None
+ logical_date: Annotated[datetime | None, Field(title="Logical Date")] =
None
+ partition_key: Annotated[str | None, Field(title="Partition Key")] = None
+ sort_ordinal: Annotated[int, Field(title="Sort Ordinal")]
+ exception_reason: Annotated[str | None, Field(title="Exception Reason")] =
None
+ dag_run_state: DagRunState | None = None
+
+
class BackfillPostBody(BaseModel):
"""
Object used for create backfill request.
@@ -2251,6 +2267,15 @@ class BackfillCollectionResponse(BaseModel):
total_entries: Annotated[int, Field(title="Total Entries")]
+class BackfillDagRunCollectionResponse(BaseModel):
+ """
+ BackfillDagRun Collection serializer for responses.
+ """
+
+ backfill_dag_runs: Annotated[list[BackfillDagRunResponse],
Field(title="Backfill Dag Runs")]
+ total_entries: Annotated[int, Field(title="Total Entries")]
+
+
class BulkBodyConnectionBody(BaseModel):
model_config = ConfigDict(
extra="forbid",