Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 21 additions & 2 deletions airflow-core/src/airflow/api_fastapi/common/cursors.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,12 +44,31 @@ def _b64url_decode_padded(token: str) -> bytes:


def _nonstrict_bound(col: ColumnElement, value: Any, is_desc: bool) -> ColumnElement[bool]:
"""Inclusive range edge on the leading column at each nesting level (``>=`` / ``<=``)."""
"""
Inclusive range edge on the leading column at each nesting level (``>=`` / ``<=``).

When *value* is ``None`` the column is nullable and the cursor sits at a
NULL boundary. ``col IS NULL`` is used instead of ``col >= NULL`` (which
SQLAlchemy rejects and SQL evaluates as UNKNOWN).
"""
if value is None:
return col.is_(None)
return col <= value if is_desc else col >= value


def _strict_bound(col: ColumnElement, value: Any, is_desc: bool) -> ColumnElement[bool]:
"""Strict inequality for ``or_`` branches (``<`` / ``>``)."""
"""
Strict inequality for ``or_`` branches (``<`` / ``>``).

When *value* is ``None`` the cursor is at a NULL boundary. The only rows
that can be "strictly after" a NULL are non-NULL rows (regardless of
whether the database sorts NULLs first or last), so ``col IS NOT NULL`` is
used. When the surrounding ``_nonstrict_bound`` already constrains
``col IS NULL``, this branch evaluates to FALSE and the inner keyset
predicate takes over — which is the correct behaviour.
"""
if value is None:
return col.is_not(None)
return col < value if is_desc else col > value


Expand Down
26 changes: 23 additions & 3 deletions airflow-core/src/airflow/api_fastapi/common/parameters.py
Original file line number Diff line number Diff line change
Expand Up @@ -521,7 +521,10 @@ class SortParam(BaseParam[list[str]]):
MAX_SORT_PARAMS = 10

def __init__(
self, allowed_attrs: list[str], model: Base, to_replace: dict[str, str | Column] | None = None
self,
allowed_attrs: list[str],
model: Base,
to_replace: dict[str, str | Column | list[Column]] | None = None,
) -> None:
super().__init__()
self.allowed_attrs = allowed_attrs
Expand Down Expand Up @@ -560,6 +563,16 @@ def _resolve(self) -> list[tuple[str, ColumnElement, bool]]:
replacement = self.to_replace.get(lstriped_orderby, lstriped_orderby)
if isinstance(replacement, str):
lstriped_orderby = replacement
elif isinstance(replacement, list):
# Compound sort: expand the list into multiple sort entries.
# Each column's ORM key becomes its attr_name so that
# row_value() can read the corresponding attribute via
# getattr(row, attr_name) without further to_replace lookups.
is_desc = order_by_value.startswith("-")
for col in replacement:
col_attr_name = col.key
resolved.append((col_attr_name, col, is_desc))
continue
else:
column = replacement

Expand Down Expand Up @@ -610,7 +623,7 @@ def row_value(self, row: Any, name: str) -> Any:
replacement = self.to_replace.get(name)
if isinstance(replacement, str):
return getattr(row, replacement, None)
if replacement is not None:
if replacement is not None and not isinstance(replacement, list):
# TODO: Column-form ``to_replace`` (e.g. ``{"last_run_state": DagRun.state}``)
# isn't supported for cursor pagination — no endpoint that uses cursor
# pagination needs it today. When one does, decide how the row exposes the
Expand All @@ -622,6 +635,10 @@ def row_value(self, row: Any, name: str) -> Any:
f"``{name}``. Use a string alias in ``to_replace`` or sort by a primary-model "
f"attribute."
)
# List-form replacements are expanded in _resolve() into individual entries
# each using the column's own ORM key as attr_name, so ``name`` at this point
# is already a concrete model attribute (e.g. ``_rendered_map_index`` or
# ``map_index``) — fall through to the getattr below.
return getattr(row, name, None)

def get_primary_key_column(self) -> Column:
Expand All @@ -637,7 +654,10 @@ def depends(cls, *args: Any, **kwargs: Any) -> Self:
raise NotImplementedError("Use dynamic_depends, depends not implemented.")

def dynamic_depends(self, default: str | Sequence[str] | None = None) -> Callable:
to_replace_attrs = list(self.to_replace.keys()) if self.to_replace else []
# Include to_replace keys that are not already in allowed_attrs to avoid
# duplicate entries in the spec description.
allowed_set = set(self.allowed_attrs)
to_replace_attrs = [k for k in self.to_replace if k not in allowed_set] if self.to_replace else []

all_attrs = self.allowed_attrs + to_replace_attrs

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6963,16 +6963,14 @@ paths:
description: 'Attributes to order by, multi criteria sort is supported.
Prefix with `-` for descending order. Supported attributes: `id, state,
duration, start_date, end_date, map_index, try_number, logical_date, run_after,
data_interval_start, data_interval_end, rendered_map_index, operator,
run_after, logical_date, data_interval_start, data_interval_end`'
data_interval_start, data_interval_end, rendered_map_index, operator`'
default:
- map_index
title: Order By
description: 'Attributes to order by, multi criteria sort is supported. Prefix
with `-` for descending order. Supported attributes: `id, state, duration,
start_date, end_date, map_index, try_number, logical_date, run_after, data_interval_start,
data_interval_end, rendered_map_index, operator, run_after, logical_date,
data_interval_start, data_interval_end`'
data_interval_end, rendered_map_index, operator`'
responses:
'200':
description: Successful Response
Expand Down Expand Up @@ -8125,16 +8123,14 @@ paths:
description: 'Attributes to order by, multi criteria sort is supported.
Prefix with `-` for descending order. Supported attributes: `id, state,
duration, start_date, end_date, map_index, try_number, logical_date, run_after,
data_interval_start, data_interval_end, rendered_map_index, operator,
logical_date, run_after, data_interval_start, data_interval_end`'
data_interval_start, data_interval_end, rendered_map_index, operator`'
default:
- map_index
title: Order By
description: 'Attributes to order by, multi criteria sort is supported. Prefix
with `-` for descending order. Supported attributes: `id, state, duration,
start_date, end_date, map_index, try_number, logical_date, run_after, data_interval_start,
data_interval_end, rendered_map_index, operator, logical_date, run_after,
data_interval_start, data_interval_end`'
data_interval_end, rendered_map_index, operator`'
responses:
'200':
description: Successful Response
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,13 @@ def get_mapped_task_instances(
"logical_date": DagRun.logical_date,
"data_interval_start": DagRun.data_interval_start,
"data_interval_end": DagRun.data_interval_end,
# Compound sort: when _rendered_map_index is NULL (no map_index_template),
# all primary values tie and the integer map_index is the effective key,
# giving correct numeric ordering (0, 1, 2, 10…) rather than lexicographic
# ("0", "1", "10", "2"…). When _rendered_map_index is set (map_index_template
# used), TIs are ordered by their human-readable label first, then by
# map_index for identical labels.
"rendered_map_index": [TI._rendered_map_index, TI.map_index],
},
).dynamic_depends(default="map_index")
),
Expand Down Expand Up @@ -502,6 +509,8 @@ def get_task_instances(
"run_after": DagRun.run_after,
"data_interval_start": DagRun.data_interval_start,
"data_interval_end": DagRun.data_interval_end,
# Compound sort: see the listMapped endpoint comment for rationale.
"rendered_map_index": [TI._rendered_map_index, TI.map_index],
},
).dynamic_depends(default="map_index")
),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -871,7 +871,7 @@ export const ensureUseTaskInstanceServiceGetTaskInstanceData = (queryClient: Que
* @param data.renderedMapIndexPrefixPattern Prefix match — returns items whose value starts with the given string (case-sensitive, index-friendly). Use the pipe `|` operator for OR logic (e.g. `dag1|dag2`). Use `~` to match all. Wildcard characters (`%`, `_`) are treated as literal characters. Trailing non-alphanumeric characters in the prefix are stripped before matching so the range scan stays index-compatible under locale-aware collations — e.g. `test_` effectively matches items starting with `test`, and `s3://` matches items starting with `s3`.
* @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, state, duration, start_date, end_date, map_index, try_number, logical_date, run_after, data_interval_start, data_interval_end, rendered_map_index, operator, run_after, logical_date, data_interval_start, data_interval_end`
* @param data.orderBy Attributes to order by, multi criteria sort is supported. Prefix with `-` for descending order. Supported attributes: `id, state, duration, start_date, end_date, map_index, try_number, logical_date, run_after, data_interval_start, data_interval_end, rendered_map_index, operator`
* @returns TaskInstanceCollectionResponse Successful Response
* @throws ApiError
*/
Expand Down Expand Up @@ -1090,7 +1090,7 @@ export const ensureUseTaskInstanceServiceGetMappedTaskInstanceData = (queryClien
* @param data.renderedMapIndexPrefixPattern Prefix match — returns items whose value starts with the given string (case-sensitive, index-friendly). Use the pipe `|` operator for OR logic (e.g. `dag1|dag2`). Use `~` to match all. Wildcard characters (`%`, `_`) are treated as literal characters. Trailing non-alphanumeric characters in the prefix are stripped before matching so the range scan stays index-compatible under locale-aware collations — e.g. `test_` effectively matches items starting with `test`, and `s3://` matches items starting with `s3`.
* @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, state, duration, start_date, end_date, map_index, try_number, logical_date, run_after, data_interval_start, data_interval_end, rendered_map_index, operator, logical_date, run_after, data_interval_start, data_interval_end`
* @param data.orderBy Attributes to order by, multi criteria sort is supported. Prefix with `-` for descending order. Supported attributes: `id, state, duration, start_date, end_date, map_index, try_number, logical_date, run_after, data_interval_start, data_interval_end, rendered_map_index, operator`
* @returns TaskInstanceCollectionResponse Successful Response
* @throws ApiError
*/
Expand Down
4 changes: 2 additions & 2 deletions airflow-core/src/airflow/ui/openapi-gen/queries/prefetch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -871,7 +871,7 @@ export const prefetchUseTaskInstanceServiceGetTaskInstance = (queryClient: Query
* @param data.renderedMapIndexPrefixPattern Prefix match — returns items whose value starts with the given string (case-sensitive, index-friendly). Use the pipe `|` operator for OR logic (e.g. `dag1|dag2`). Use `~` to match all. Wildcard characters (`%`, `_`) are treated as literal characters. Trailing non-alphanumeric characters in the prefix are stripped before matching so the range scan stays index-compatible under locale-aware collations — e.g. `test_` effectively matches items starting with `test`, and `s3://` matches items starting with `s3`.
* @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, state, duration, start_date, end_date, map_index, try_number, logical_date, run_after, data_interval_start, data_interval_end, rendered_map_index, operator, run_after, logical_date, data_interval_start, data_interval_end`
* @param data.orderBy Attributes to order by, multi criteria sort is supported. Prefix with `-` for descending order. Supported attributes: `id, state, duration, start_date, end_date, map_index, try_number, logical_date, run_after, data_interval_start, data_interval_end, rendered_map_index, operator`
* @returns TaskInstanceCollectionResponse Successful Response
* @throws ApiError
*/
Expand Down Expand Up @@ -1090,7 +1090,7 @@ export const prefetchUseTaskInstanceServiceGetMappedTaskInstance = (queryClient:
* @param data.renderedMapIndexPrefixPattern Prefix match — returns items whose value starts with the given string (case-sensitive, index-friendly). Use the pipe `|` operator for OR logic (e.g. `dag1|dag2`). Use `~` to match all. Wildcard characters (`%`, `_`) are treated as literal characters. Trailing non-alphanumeric characters in the prefix are stripped before matching so the range scan stays index-compatible under locale-aware collations — e.g. `test_` effectively matches items starting with `test`, and `s3://` matches items starting with `s3`.
* @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, state, duration, start_date, end_date, map_index, try_number, logical_date, run_after, data_interval_start, data_interval_end, rendered_map_index, operator, logical_date, run_after, data_interval_start, data_interval_end`
* @param data.orderBy Attributes to order by, multi criteria sort is supported. Prefix with `-` for descending order. Supported attributes: `id, state, duration, start_date, end_date, map_index, try_number, logical_date, run_after, data_interval_start, data_interval_end, rendered_map_index, operator`
* @returns TaskInstanceCollectionResponse Successful Response
* @throws ApiError
*/
Expand Down
4 changes: 2 additions & 2 deletions airflow-core/src/airflow/ui/openapi-gen/queries/queries.ts
Original file line number Diff line number Diff line change
Expand Up @@ -871,7 +871,7 @@ export const useTaskInstanceServiceGetTaskInstance = <TData = Common.TaskInstanc
* @param data.renderedMapIndexPrefixPattern Prefix match — returns items whose value starts with the given string (case-sensitive, index-friendly). Use the pipe `|` operator for OR logic (e.g. `dag1|dag2`). Use `~` to match all. Wildcard characters (`%`, `_`) are treated as literal characters. Trailing non-alphanumeric characters in the prefix are stripped before matching so the range scan stays index-compatible under locale-aware collations — e.g. `test_` effectively matches items starting with `test`, and `s3://` matches items starting with `s3`.
* @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, state, duration, start_date, end_date, map_index, try_number, logical_date, run_after, data_interval_start, data_interval_end, rendered_map_index, operator, run_after, logical_date, data_interval_start, data_interval_end`
* @param data.orderBy Attributes to order by, multi criteria sort is supported. Prefix with `-` for descending order. Supported attributes: `id, state, duration, start_date, end_date, map_index, try_number, logical_date, run_after, data_interval_start, data_interval_end, rendered_map_index, operator`
* @returns TaskInstanceCollectionResponse Successful Response
* @throws ApiError
*/
Expand Down Expand Up @@ -1090,7 +1090,7 @@ export const useTaskInstanceServiceGetMappedTaskInstance = <TData = Common.TaskI
* @param data.renderedMapIndexPrefixPattern Prefix match — returns items whose value starts with the given string (case-sensitive, index-friendly). Use the pipe `|` operator for OR logic (e.g. `dag1|dag2`). Use `~` to match all. Wildcard characters (`%`, `_`) are treated as literal characters. Trailing non-alphanumeric characters in the prefix are stripped before matching so the range scan stays index-compatible under locale-aware collations — e.g. `test_` effectively matches items starting with `test`, and `s3://` matches items starting with `s3`.
* @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, state, duration, start_date, end_date, map_index, try_number, logical_date, run_after, data_interval_start, data_interval_end, rendered_map_index, operator, logical_date, run_after, data_interval_start, data_interval_end`
* @param data.orderBy Attributes to order by, multi criteria sort is supported. Prefix with `-` for descending order. Supported attributes: `id, state, duration, start_date, end_date, map_index, try_number, logical_date, run_after, data_interval_start, data_interval_end, rendered_map_index, operator`
* @returns TaskInstanceCollectionResponse Successful Response
* @throws ApiError
*/
Expand Down
4 changes: 2 additions & 2 deletions airflow-core/src/airflow/ui/openapi-gen/queries/suspense.ts
Original file line number Diff line number Diff line change
Expand Up @@ -871,7 +871,7 @@ export const useTaskInstanceServiceGetTaskInstanceSuspense = <TData = Common.Tas
* @param data.renderedMapIndexPrefixPattern Prefix match — returns items whose value starts with the given string (case-sensitive, index-friendly). Use the pipe `|` operator for OR logic (e.g. `dag1|dag2`). Use `~` to match all. Wildcard characters (`%`, `_`) are treated as literal characters. Trailing non-alphanumeric characters in the prefix are stripped before matching so the range scan stays index-compatible under locale-aware collations — e.g. `test_` effectively matches items starting with `test`, and `s3://` matches items starting with `s3`.
* @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, state, duration, start_date, end_date, map_index, try_number, logical_date, run_after, data_interval_start, data_interval_end, rendered_map_index, operator, run_after, logical_date, data_interval_start, data_interval_end`
* @param data.orderBy Attributes to order by, multi criteria sort is supported. Prefix with `-` for descending order. Supported attributes: `id, state, duration, start_date, end_date, map_index, try_number, logical_date, run_after, data_interval_start, data_interval_end, rendered_map_index, operator`
* @returns TaskInstanceCollectionResponse Successful Response
* @throws ApiError
*/
Expand Down Expand Up @@ -1090,7 +1090,7 @@ export const useTaskInstanceServiceGetMappedTaskInstanceSuspense = <TData = Comm
* @param data.renderedMapIndexPrefixPattern Prefix match — returns items whose value starts with the given string (case-sensitive, index-friendly). Use the pipe `|` operator for OR logic (e.g. `dag1|dag2`). Use `~` to match all. Wildcard characters (`%`, `_`) are treated as literal characters. Trailing non-alphanumeric characters in the prefix are stripped before matching so the range scan stays index-compatible under locale-aware collations — e.g. `test_` effectively matches items starting with `test`, and `s3://` matches items starting with `s3`.
* @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, state, duration, start_date, end_date, map_index, try_number, logical_date, run_after, data_interval_start, data_interval_end, rendered_map_index, operator, logical_date, run_after, data_interval_start, data_interval_end`
* @param data.orderBy Attributes to order by, multi criteria sort is supported. Prefix with `-` for descending order. Supported attributes: `id, state, duration, start_date, end_date, map_index, try_number, logical_date, run_after, data_interval_start, data_interval_end, rendered_map_index, operator`
* @returns TaskInstanceCollectionResponse Successful Response
* @throws ApiError
*/
Expand Down
Loading
Loading