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

bbovenzi 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 8e36d6d6efd Show whether a calendar day's run was a backfill (#72782)
8e36d6d6efd is described below

commit 8e36d6d6efdb1811570e5915deed404635171ccf
Author: Software Developer <[email protected]>
AuthorDate: Wed Sep 30 16:53:54 2026 +0200

    Show whether a calendar day's run was a backfill (#72782)
    
    * show backfill state on calendar view
    
    * addressing PR remarks.
    
    * addressing PR remarks.
---
 .../api_fastapi/core_api/datamodels/ui/calendar.py |   1 +
 .../api_fastapi/core_api/openapi/_private_ui.yaml  |   4 +
 .../api_fastapi/core_api/services/ui/calendar.py   |  10 +-
 .../airflow/ui/openapi-gen/requests/schemas.gen.ts |   5 +
 .../airflow/ui/openapi-gen/requests/types.gen.ts   |   1 +
 .../src/airflow/ui/public/i18n/locales/en/dag.json |   1 +
 .../ui/src/pages/Dag/Calendar/CalendarCell.tsx     |  31 ++-
 .../ui/src/pages/Dag/Calendar/CalendarLegend.tsx   |  11 +
 .../ui/src/pages/Dag/Calendar/CalendarTooltip.tsx  |  14 +-
 .../src/pages/Dag/Calendar/HourlyCalendarView.tsx  |  10 +-
 .../src/pages/Dag/Calendar/calendarUtils.test.ts   |  22 ++
 .../ui/src/pages/Dag/Calendar/calendarUtils.ts     |   4 +
 .../src/airflow/ui/src/pages/Dag/Calendar/types.ts |   1 +
 .../core_api/routes/ui/test_calendar.py            | 239 ++++++++++++++++++---
 14 files changed, 319 insertions(+), 35 deletions(-)

diff --git 
a/airflow-core/src/airflow/api_fastapi/core_api/datamodels/ui/calendar.py 
b/airflow-core/src/airflow/api_fastapi/core_api/datamodels/ui/calendar.py
index 09f5062a497..4dd75711b7f 100644
--- a/airflow-core/src/airflow/api_fastapi/core_api/datamodels/ui/calendar.py
+++ b/airflow-core/src/airflow/api_fastapi/core_api/datamodels/ui/calendar.py
@@ -35,6 +35,7 @@ class CalendarTimeRangeResponse(BaseModel):
         "planned",
     ]
     count: int
+    is_backfill: bool = False
 
 
 class CalendarTimeRangeCollectionResponse(BaseModel):
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 66032afd609..3c96ebf967f 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
@@ -2851,6 +2851,10 @@ components:
         count:
           type: integer
           title: Count
+        is_backfill:
+          type: boolean
+          title: Is Backfill
+          default: false
       type: object
       required:
       - date
diff --git 
a/airflow-core/src/airflow/api_fastapi/core_api/services/ui/calendar.py 
b/airflow-core/src/airflow/api_fastapi/core_api/services/ui/calendar.py
index c6853cafb83..13d6170b92b 100644
--- a/airflow-core/src/airflow/api_fastapi/core_api/services/ui/calendar.py
+++ b/airflow-core/src/airflow/api_fastapi/core_api/services/ui/calendar.py
@@ -40,6 +40,7 @@ from airflow.serialization.definitions.dag import 
SerializedDAG
 from airflow.timetables._cron import CronMixin
 from airflow.timetables.base import DagRunInfo, DataInterval, TimeRestriction
 from airflow.utils.sqlalchemy import get_dialect_name
+from airflow.utils.types import DagRunType
 
 log = structlog.get_logger(logger_name=__name__)
 
@@ -101,10 +102,16 @@ class CalendarService:
         effective_date = sa.func.coalesce(DagRun.partition_date, 
DagRun.logical_date)
         time_expression = self._get_time_truncation_expression(effective_date, 
granularity, dialect)
 
+        is_backfill_expression = sa.case(
+            (DagRun.run_type == DagRunType.BACKFILL_JOB, sa.literal(True)),
+            else_=sa.literal(False),
+        )
+
         select_stmt = (
             sa.select(
                 time_expression.label("datetime"),
                 DagRun.state,
+                is_backfill_expression.label("is_backfill"),
                 
sa.func.max(DagRun.data_interval_start).label("data_interval_start"),
                 
sa.func.max(DagRun.data_interval_end).label("data_interval_end"),
                 sa.func.max(DagRun.run_after).label("run_after"),
@@ -112,7 +119,7 @@ class CalendarService:
                 sa.func.count("*").label("count"),
             )
             .where(DagRun.dag_id == dag_id)
-            .group_by(time_expression, DagRun.state)
+            .group_by(time_expression, DagRun.state, is_backfill_expression)
             .order_by(time_expression.asc())
         )
 
@@ -125,6 +132,7 @@ class CalendarService:
                 date=ds.datetime,
                 state=ds.state,
                 count=int(ds._mapping["count"]),
+                is_backfill=bool(ds._mapping["is_backfill"]),
             )
             for ds in dag_states
         ]
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 729d8a5ad80..d5943c54077 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
@@ -9854,6 +9854,11 @@ export const $CalendarTimeRangeResponse = {
         count: {
             type: 'integer',
             title: 'Count'
+        },
+        is_backfill: {
+            type: 'boolean',
+            title: 'Is Backfill',
+            default: false
         }
     },
     type: 'object',
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 4017f33daee..441d1e1eb55 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
@@ -2517,6 +2517,7 @@ export type CalendarTimeRangeResponse = {
     date: string;
     state: 'queued' | 'running' | 'success' | 'failed' | 'planned';
     count: number;
+    is_backfill?: boolean;
 };
 
 export type state = 'queued' | 'running' | 'success' | 'failed' | 'planned';
diff --git a/airflow-core/src/airflow/ui/public/i18n/locales/en/dag.json 
b/airflow-core/src/airflow/ui/public/i18n/locales/en/dag.json
index feb8c81c88e..62ecc2d5c24 100644
--- a/airflow-core/src/airflow/ui/public/i18n/locales/en/dag.json
+++ b/airflow-core/src/airflow/ui/public/i18n/locales/en/dag.json
@@ -6,6 +6,7 @@
     "title": "Dependencies Blocking Task From Getting Scheduled"
   },
   "calendar": {
+    "backfill": "Backfill",
     "daily": "Daily",
     "hourly": "Hourly",
     "legend": {
diff --git 
a/airflow-core/src/airflow/ui/src/pages/Dag/Calendar/CalendarCell.tsx 
b/airflow-core/src/airflow/ui/src/pages/Dag/Calendar/CalendarCell.tsx
index 1244e20b907..73d3df9b6b5 100644
--- a/airflow-core/src/airflow/ui/src/pages/Dag/Calendar/CalendarCell.tsx
+++ b/airflow-core/src/airflow/ui/src/pages/Dag/Calendar/CalendarCell.tsx
@@ -21,6 +21,8 @@ import { FiAlertTriangle, FiClock } from "react-icons/fi";
 
 import { RouterLink, Tooltip } from "src/system-components";
 
+import { RunTypeIcon } from "src/components/RunTypeIcon";
+
 import { CalendarTooltip } from "./CalendarTooltip";
 import type { CalendarCellData, CalendarColorMode } from "./types";
 
@@ -63,7 +65,11 @@ export const CalendarCell = ({
   const runStates = cellData
     ? Object.entries(cellData.counts)
         .filter(
-          ([key, value]) => key !== "total" && value > 0 && (viewMode === 
"failed" ? key === "failed" : true),
+          ([key, value]) =>
+            key !== "total" &&
+            key !== "backfill" &&
+            value > 0 &&
+            (viewMode === "failed" ? key === "failed" : true),
         )
         .map(([key]) => key)
     : [];
@@ -95,6 +101,27 @@ export const CalendarCell = ({
     </Box>
   ) : undefined;
 
+  const hasBackfill = (cellData?.counts.backfill ?? 0) > 0;
+  const backfillIndicator = hasBackfill ? (
+    <Box
+      alignItems="center"
+      color="white"
+      data-testid="backfill-indicator"
+      display="flex"
+      filter="drop-shadow(0 0 1px rgba(0, 0, 0, 0.7))"
+      fontSize="9px"
+      height="100%"
+      justifyContent="center"
+      left="0"
+      lineHeight={1}
+      position="absolute"
+      top="0"
+      width="100%"
+    >
+      <RunTypeIcon runType="backfill" />
+    </Box>
+  ) : undefined;
+
   const cellBox = isMixedState ? (
     <Box
       _hover={hasData ? { transform: "scale(1.1)" } : {}}
@@ -125,6 +152,7 @@ export const CalendarCell = ({
         width="100%"
       />
       {deadlineIndicator}
+      {backfillIndicator}
     </Box>
   ) : (
     <Box
@@ -142,6 +170,7 @@ export const CalendarCell = ({
       width="14px"
     >
       {deadlineIndicator}
+      {backfillIndicator}
     </Box>
   );
 
diff --git 
a/airflow-core/src/airflow/ui/src/pages/Dag/Calendar/CalendarLegend.tsx 
b/airflow-core/src/airflow/ui/src/pages/Dag/Calendar/CalendarLegend.tsx
index c1a6f0d4b01..7c546be4370 100644
--- a/airflow-core/src/airflow/ui/src/pages/Dag/Calendar/CalendarLegend.tsx
+++ b/airflow-core/src/airflow/ui/src/pages/Dag/Calendar/CalendarLegend.tsx
@@ -22,6 +22,8 @@ import { FiAlertTriangle, FiClock } from "react-icons/fi";
 
 import { Tooltip } from "src/system-components";
 
+import { RunTypeIcon } from "src/components/RunTypeIcon";
+
 import { LegendIcon } from "./LegendIcon";
 import { PLANNED_COLOR } from "./calendarUtils";
 import type { CalendarScale, CalendarColorMode } from "./types";
@@ -135,6 +137,15 @@ export const CalendarLegend = ({ hasDeadlines = false, 
scale, vertical = false,
               {translate("dag:calendar.legend.mixed")}
             </Text>
           </HStack>
+
+          <HStack gap={2}>
+            <Box fontSize="14px" lineHeight={1}>
+              <RunTypeIcon runType="backfill" />
+            </Box>
+            <Text color="fg.muted" fontSize="xs">
+              {translate("dag:calendar.backfill")}
+            </Text>
+          </HStack>
         </HStack>
       </Box>
 
diff --git 
a/airflow-core/src/airflow/ui/src/pages/Dag/Calendar/CalendarTooltip.tsx 
b/airflow-core/src/airflow/ui/src/pages/Dag/Calendar/CalendarTooltip.tsx
index 1d026032655..8ca36b11109 100644
--- a/airflow-core/src/airflow/ui/src/pages/Dag/Calendar/CalendarTooltip.tsx
+++ b/airflow-core/src/airflow/ui/src/pages/Dag/Calendar/CalendarTooltip.tsx
@@ -20,6 +20,8 @@ import { Box, HStack, Text, VStack } from "@chakra-ui/react";
 import { useTranslation } from "react-i18next";
 import { FiAlertTriangle, FiClock } from "react-icons/fi";
 
+import { RunTypeIcon } from "src/components/RunTypeIcon";
+
 import type { CalendarCellData, CalendarColorMode } from "./types";
 
 const SQUARE_SIZE = "12px";
@@ -57,7 +59,7 @@ export const CalendarTooltip = ({ cellData, viewMode = 
"total" }: Props) => {
   // In failed mode, only show failed runs; in total mode, show all non-zero 
states
   const states = Object.entries(counts)
     .filter(([key, value]) => {
-      if (key === "total") {
+      if (key === "total" || key === "backfill") {
         return false;
       }
       if (value === 0) {
@@ -97,6 +99,16 @@ export const CalendarTooltip = ({ cellData, viewMode = 
"total" }: Props) => {
               </Text>
             </HStack>
           ))}
+          {counts.backfill > 0 && (
+            <HStack data-testid="calendar-tooltip-backfill" gap={3}>
+              <Box fontSize={SQUARE_SIZE} lineHeight={1}>
+                <RunTypeIcon runType="backfill" />
+              </Box>
+              <Text fontSize="xs">
+                {counts.backfill} {translate("dag:calendar.backfill")}
+              </Text>
+            </HStack>
+          )}
         </VStack>
       ) : null}
       {hasDeadlines ? (
diff --git 
a/airflow-core/src/airflow/ui/src/pages/Dag/Calendar/HourlyCalendarView.tsx 
b/airflow-core/src/airflow/ui/src/pages/Dag/Calendar/HourlyCalendarView.tsx
index 5f584591b7a..503f9c531e6 100644
--- a/airflow-core/src/airflow/ui/src/pages/Dag/Calendar/HourlyCalendarView.tsx
+++ b/airflow-core/src/airflow/ui/src/pages/Dag/Calendar/HourlyCalendarView.tsx
@@ -175,7 +175,15 @@ export const HourlyCalendarView = ({
                 const hourData = day.hours.find((hourItem) => hourItem.hour 
=== hour);
 
                 if (!hourData) {
-                  const emptyCounts = { failed: 0, planned: 0, queued: 0, 
running: 0, success: 0, total: 0 };
+                  const emptyCounts = {
+                    backfill: 0,
+                    failed: 0,
+                    planned: 0,
+                    queued: 0,
+                    running: 0,
+                    success: 0,
+                    total: 0,
+                  };
                   const emptyData = {
                     counts: emptyCounts,
                     date: `${dayjs(day.day).format("MMM DD")}, 
${hour.toString().padStart(2, "0")}:00`,
diff --git 
a/airflow-core/src/airflow/ui/src/pages/Dag/Calendar/calendarUtils.test.ts 
b/airflow-core/src/airflow/ui/src/pages/Dag/Calendar/calendarUtils.test.ts
index 738ebf698cc..fd73589dced 100644
--- a/airflow-core/src/airflow/ui/src/pages/Dag/Calendar/calendarUtils.test.ts
+++ b/airflow-core/src/airflow/ui/src/pages/Dag/Calendar/calendarUtils.test.ts
@@ -35,6 +35,7 @@ const DEFAULT_FAILED_COLOR = { _dark: "red.700", _light: 
"red.400" };
 const DEFAULT_RUNNING_COLOR = { _dark: "cyan.700", _light: "cyan.400" };
 
 const EMPTY_COUNTS: RunCounts = {
+  backfill: 0,
   failed: 0,
   planned: 0,
   queued: 0,
@@ -50,9 +51,15 @@ const run = (
 ): CalendarTimeRangeResponse => ({
   count,
   date,
+  is_backfill: false,
   state,
 });
 
+const backfillRun = (
+  state: CalendarTimeRangeResponse["state"],
+  count: number,
+): CalendarTimeRangeResponse => ({ ...run(state, count), is_backfill: true });
+
 describe("calculateRunCounts", () => {
   it("counts each calendar state and includes all states in total", () => {
     expect(
@@ -64,6 +71,7 @@ describe("calculateRunCounts", () => {
         run("planned", 5),
       ]),
     ).toEqual({
+      backfill: 0,
       failed: 1,
       planned: 5,
       queued: 4,
@@ -72,6 +80,20 @@ describe("calculateRunCounts", () => {
       total: 15,
     });
   });
+
+  it("counts backfill runs separately from their state", () => {
+    expect(
+      calculateRunCounts([backfillRun("success", 1), backfillRun("failed", 2), 
run("success", 3)]),
+    ).toEqual({
+      backfill: 3,
+      failed: 2,
+      planned: 0,
+      queued: 0,
+      running: 0,
+      success: 4,
+      total: 6,
+    });
+  });
 });
 
 describe("calculateDataBounds", () => {
diff --git 
a/airflow-core/src/airflow/ui/src/pages/Dag/Calendar/calendarUtils.ts 
b/airflow-core/src/airflow/ui/src/pages/Dag/Calendar/calendarUtils.ts
index 58a956c28f7..05acf1080ee 100644
--- a/airflow-core/src/airflow/ui/src/pages/Dag/Calendar/calendarUtils.ts
+++ b/airflow-core/src/airflow/ui/src/pages/Dag/Calendar/calendarUtils.ts
@@ -129,6 +129,7 @@ export const buildDeadlineDateMap = (
 
 export const calculateRunCounts = (runs: Array<CalendarTimeRangeResponse>): 
RunCounts => {
   const counts: { [K in keyof RunCounts]: number } = {
+    backfill: 0,
     failed: 0,
     planned: 0,
     queued: 0,
@@ -143,6 +144,9 @@ export const calculateRunCounts = (runs: 
Array<CalendarTimeRangeResponse>): RunC
     if (state in counts) {
       counts[state] += count;
     }
+    if (run.is_backfill === true) {
+      counts.backfill += count;
+    }
     counts.total += count;
   });
 
diff --git a/airflow-core/src/airflow/ui/src/pages/Dag/Calendar/types.ts 
b/airflow-core/src/airflow/ui/src/pages/Dag/Calendar/types.ts
index 755ca5ead23..8476ac8895f 100644
--- a/airflow-core/src/airflow/ui/src/pages/Dag/Calendar/types.ts
+++ b/airflow-core/src/airflow/ui/src/pages/Dag/Calendar/types.ts
@@ -21,6 +21,7 @@ import type { CalendarTimeRangeResponse } from 
"openapi/requests/types.gen";
 export type DagRunState = "failed" | "planned" | "queued" | "running" | 
"success";
 
 export type RunCounts = {
+  backfill: number;
   failed: number;
   planned: number;
   queued: number;
diff --git 
a/airflow-core/tests/unit/api_fastapi/core_api/routes/ui/test_calendar.py 
b/airflow-core/tests/unit/api_fastapi/core_api/routes/ui/test_calendar.py
index 95d8da70b2e..dccec5832b9 100644
--- a/airflow-core/tests/unit/api_fastapi/core_api/routes/ui/test_calendar.py
+++ b/airflow-core/tests/unit/api_fastapi/core_api/routes/ui/test_calendar.py
@@ -81,11 +81,31 @@ class TestCalendar:
                 {
                     "total_entries": 5,
                     "dag_runs": [
-                        {"date": "2025-01-01T00:00:00Z", "state": "failed", 
"count": 1},
-                        {"date": "2025-01-01T00:00:00Z", "state": "success", 
"count": 1},
-                        {"date": "2025-01-02T00:00:00Z", "state": "running", 
"count": 1},
-                        {"date": "2025-01-02T00:00:00Z", "state": "planned", 
"count": 1},
-                        {"date": "2025-01-03T00:00:00Z", "state": "planned", 
"count": 2},
+                        {"date": "2025-01-01T00:00:00Z", "state": "failed", 
"count": 1, "is_backfill": False},
+                        {
+                            "date": "2025-01-01T00:00:00Z",
+                            "state": "success",
+                            "count": 1,
+                            "is_backfill": False,
+                        },
+                        {
+                            "date": "2025-01-02T00:00:00Z",
+                            "state": "running",
+                            "count": 1,
+                            "is_backfill": False,
+                        },
+                        {
+                            "date": "2025-01-02T00:00:00Z",
+                            "state": "planned",
+                            "count": 1,
+                            "is_backfill": False,
+                        },
+                        {
+                            "date": "2025-01-03T00:00:00Z",
+                            "state": "planned",
+                            "count": 2,
+                            "is_backfill": False,
+                        },
                     ],
                 },
             ),
@@ -94,8 +114,13 @@ class TestCalendar:
                 {
                     "total_entries": 2,
                     "dag_runs": [
-                        {"date": "2025-01-01T00:00:00Z", "state": "failed", 
"count": 1},
-                        {"date": "2025-01-01T00:00:00Z", "state": "success", 
"count": 1},
+                        {"date": "2025-01-01T00:00:00Z", "state": "failed", 
"count": 1, "is_backfill": False},
+                        {
+                            "date": "2025-01-01T00:00:00Z",
+                            "state": "success",
+                            "count": 1,
+                            "is_backfill": False,
+                        },
                     ],
                 },
             ),
@@ -104,8 +129,18 @@ class TestCalendar:
                 {
                     "total_entries": 2,
                     "dag_runs": [
-                        {"date": "2025-01-02T00:00:00Z", "state": "running", 
"count": 1},
-                        {"date": "2025-01-02T00:00:00Z", "state": "planned", 
"count": 1},
+                        {
+                            "date": "2025-01-02T00:00:00Z",
+                            "state": "running",
+                            "count": 1,
+                            "is_backfill": False,
+                        },
+                        {
+                            "date": "2025-01-02T00:00:00Z",
+                            "state": "planned",
+                            "count": 1,
+                            "is_backfill": False,
+                        },
                     ],
                 },
             ),
@@ -128,12 +163,37 @@ class TestCalendar:
                 {
                     "total_entries": 6,
                     "dag_runs": [
-                        {"date": "2025-01-01T00:00:00Z", "state": "failed", 
"count": 1},
-                        {"date": "2025-01-01T01:00:00Z", "state": "success", 
"count": 1},
-                        {"date": "2025-01-02T00:00:00Z", "state": "running", 
"count": 1},
-                        {"date": "2025-01-02T01:00:00Z", "state": "planned", 
"count": 1},
-                        {"date": "2025-01-03T00:00:00Z", "state": "planned", 
"count": 1},
-                        {"date": "2025-01-03T01:00:00Z", "state": "planned", 
"count": 1},
+                        {"date": "2025-01-01T00:00:00Z", "state": "failed", 
"count": 1, "is_backfill": False},
+                        {
+                            "date": "2025-01-01T01:00:00Z",
+                            "state": "success",
+                            "count": 1,
+                            "is_backfill": False,
+                        },
+                        {
+                            "date": "2025-01-02T00:00:00Z",
+                            "state": "running",
+                            "count": 1,
+                            "is_backfill": False,
+                        },
+                        {
+                            "date": "2025-01-02T01:00:00Z",
+                            "state": "planned",
+                            "count": 1,
+                            "is_backfill": False,
+                        },
+                        {
+                            "date": "2025-01-03T00:00:00Z",
+                            "state": "planned",
+                            "count": 1,
+                            "is_backfill": False,
+                        },
+                        {
+                            "date": "2025-01-03T01:00:00Z",
+                            "state": "planned",
+                            "count": 1,
+                            "is_backfill": False,
+                        },
                     ],
                 },
             ),
@@ -146,8 +206,18 @@ class TestCalendar:
                 {
                     "total_entries": 2,
                     "dag_runs": [
-                        {"date": "2025-01-02T00:00:00Z", "state": "running", 
"count": 1},
-                        {"date": "2025-01-02T01:00:00Z", "state": "planned", 
"count": 1},
+                        {
+                            "date": "2025-01-02T00:00:00Z",
+                            "state": "running",
+                            "count": 1,
+                            "is_backfill": False,
+                        },
+                        {
+                            "date": "2025-01-02T01:00:00Z",
+                            "state": "planned",
+                            "count": 1,
+                            "is_backfill": False,
+                        },
                     ],
                 },
             ),
@@ -175,8 +245,18 @@ class TestCalendar:
                 {
                     "total_entries": 2,
                     "dag_runs": [
-                        {"date": "2025-01-02T00:00:00Z", "state": "running", 
"count": 1},
-                        {"date": "2025-01-02T01:00:00Z", "state": "planned", 
"count": 1},
+                        {
+                            "date": "2025-01-02T00:00:00Z",
+                            "state": "running",
+                            "count": 1,
+                            "is_backfill": False,
+                        },
+                        {
+                            "date": "2025-01-02T01:00:00Z",
+                            "state": "planned",
+                            "count": 1,
+                            "is_backfill": False,
+                        },
                     ],
                 },
             ),
@@ -373,10 +453,25 @@ class TestPartitionedCalendar:
                 {
                     "total_entries": 4,
                     "dag_runs": [
-                        {"date": "2025-01-01T00:00:00Z", "state": "success", 
"count": 1},
-                        {"date": "2025-01-02T00:00:00Z", "state": "failed", 
"count": 1},
-                        {"date": "2025-01-03T00:00:00Z", "state": "success", 
"count": 1},
-                        {"date": "2025-01-04T00:00:00Z", "state": "running", 
"count": 1},
+                        {
+                            "date": "2025-01-01T00:00:00Z",
+                            "state": "success",
+                            "count": 1,
+                            "is_backfill": False,
+                        },
+                        {"date": "2025-01-02T00:00:00Z", "state": "failed", 
"count": 1, "is_backfill": False},
+                        {
+                            "date": "2025-01-03T00:00:00Z",
+                            "state": "success",
+                            "count": 1,
+                            "is_backfill": False,
+                        },
+                        {
+                            "date": "2025-01-04T00:00:00Z",
+                            "state": "running",
+                            "count": 1,
+                            "is_backfill": False,
+                        },
                     ],
                 },
             ),
@@ -385,8 +480,13 @@ class TestPartitionedCalendar:
                 {
                     "total_entries": 2,
                     "dag_runs": [
-                        {"date": "2025-01-02T00:00:00Z", "state": "failed", 
"count": 1},
-                        {"date": "2025-01-03T00:00:00Z", "state": "success", 
"count": 1},
+                        {"date": "2025-01-02T00:00:00Z", "state": "failed", 
"count": 1, "is_backfill": False},
+                        {
+                            "date": "2025-01-03T00:00:00Z",
+                            "state": "success",
+                            "count": 1,
+                            "is_backfill": False,
+                        },
                     ],
                 },
             ),
@@ -395,7 +495,12 @@ class TestPartitionedCalendar:
                 {
                     "total_entries": 1,
                     "dag_runs": [
-                        {"date": "2025-01-04T00:00:00Z", "state": "running", 
"count": 1},
+                        {
+                            "date": "2025-01-04T00:00:00Z",
+                            "state": "running",
+                            "count": 1,
+                            "is_backfill": False,
+                        },
                     ],
                 },
             ),
@@ -416,10 +521,25 @@ class TestPartitionedCalendar:
                 {
                     "total_entries": 4,
                     "dag_runs": [
-                        {"date": "2025-01-01T00:00:00Z", "state": "success", 
"count": 1},
-                        {"date": "2025-01-02T00:00:00Z", "state": "failed", 
"count": 1},
-                        {"date": "2025-01-03T00:00:00Z", "state": "success", 
"count": 1},
-                        {"date": "2025-01-04T00:00:00Z", "state": "running", 
"count": 1},
+                        {
+                            "date": "2025-01-01T00:00:00Z",
+                            "state": "success",
+                            "count": 1,
+                            "is_backfill": False,
+                        },
+                        {"date": "2025-01-02T00:00:00Z", "state": "failed", 
"count": 1, "is_backfill": False},
+                        {
+                            "date": "2025-01-03T00:00:00Z",
+                            "state": "success",
+                            "count": 1,
+                            "is_backfill": False,
+                        },
+                        {
+                            "date": "2025-01-04T00:00:00Z",
+                            "state": "running",
+                            "count": 1,
+                            "is_backfill": False,
+                        },
                     ],
                 },
             ),
@@ -432,7 +552,12 @@ class TestPartitionedCalendar:
                 {
                     "total_entries": 1,
                     "dag_runs": [
-                        {"date": "2025-01-01T00:00:00Z", "state": "success", 
"count": 1},
+                        {
+                            "date": "2025-01-01T00:00:00Z",
+                            "state": "success",
+                            "count": 1,
+                            "is_backfill": False,
+                        },
                     ],
                 },
             ),
@@ -484,6 +609,58 @@ class TestCalendarPlannedRunsCap:
         assert sum(r["count"] for r in planned) == 
CalendarService.MAX_PLANNED_RUNS
 
 
+class TestCalendarBackfill:
+    """Historical runs must expose whether they came from a backfill."""
+
+    DAG_NAME = "test_backfill_calendar_dag"
+
+    @pytest.fixture(autouse=True)
+    @provide_session
+    def setup_dag_runs(self, dag_maker, *, session: Session = NEW_SESSION) -> 
None:
+        clear_db_runs()
+        clear_db_dags()
+        with dag_maker(
+            self.DAG_NAME,
+            schedule="0 0,1 * * *",
+            start_date=datetime(2025, 1, 1),
+            end_date=datetime(2025, 1, 1, 1),
+            catchup=True,
+            serialized=True,
+            session=session,
+        ):
+            EmptyOperator(task_id="test_task1")
+        dag_maker.create_dagrun(
+            run_id="scheduled_run",
+            state=DagRunState.SUCCESS,
+            run_type=DagRunType.SCHEDULED,
+            logical_date=datetime(2025, 1, 1),
+        )
+        dag_maker.create_dagrun(
+            run_id="backfill_run",
+            state=DagRunState.SUCCESS,
+            run_type=DagRunType.BACKFILL_JOB,
+            logical_date=datetime(2025, 1, 1, 1),
+        )
+        dag_maker.sync_dagbag_to_db()
+        session.commit()
+
+    def teardown_method(self) -> None:
+        clear_db_runs()
+        clear_db_dags()
+
+    def test_backfill_and_scheduled_runs_split_by_is_backfill(self, 
test_client):
+        response = test_client.get(f"/calendar/{self.DAG_NAME}")
+        assert response.status_code == 200
+        historical = sorted(
+            (r for r in response.json()["dag_runs"] if r["state"] != 
"planned"),
+            key=lambda r: r["is_backfill"],
+        )
+        assert historical == [
+            {"date": "2025-01-01T00:00:00Z", "state": "success", "count": 1, 
"is_backfill": False},
+            {"date": "2025-01-01T00:00:00Z", "state": "success", "count": 1, 
"is_backfill": True},
+        ]
+
+
 _CALLBACK_PATH = 
"tests.unit.api_fastapi.core_api.routes.ui.test_calendar._noop_callback"
 
 

Reply via email to