From 5d6cf9276b5fb9914ecaf957f18f99a280e3d7b4 Mon Sep 17 00:00:00 2001 From: seanmuth Date: Mon, 6 Jul 2026 13:40:05 -0500 Subject: [PATCH 1/2] Add exact-match uri filter to GET /assets endpoint Resolving a single asset by its full URI currently requires uri_pattern, which compiles to ILIKE '%...%' and cannot use a database index, so it degrades to a full table scan on large asset tables. This is the fast, index-backed exact-URI lookup that the Airflow 2 REST API exposed via GET /datasets/{uri} but that was not carried forward when the endpoint moved to FastAPI under AIP-84. The uri query parameter matches an asset by its exact URI using an equality comparison, backed by a new single-column index on asset.uri (the existing unique index leads with name and cannot serve uri-only lookups). Passing it as a query parameter rather than a path segment avoids the URI-encoding problems that path parameters have with the '/' and ':' characters in asset URIs. --- airflow-core/docs/migrations-ref.rst | 4 +- .../newsfragments/69489.improvement.rst | 1 + .../airflow/api_fastapi/common/parameters.py | 16 +++++++ .../openapi/v2-rest-api-generated.yaml | 16 +++++++ .../core_api/routes/public/assets.py | 12 ++++- .../0124_3_3_0_add_index_on_asset_uri.py | 47 +++++++++++++++++++ airflow-core/src/airflow/models/asset.py | 3 ++ .../airflow/ui/openapi-gen/queries/common.ts | 5 +- .../ui/openapi-gen/queries/ensureQueryData.ts | 6 ++- .../ui/openapi-gen/queries/prefetch.ts | 6 ++- .../airflow/ui/openapi-gen/queries/queries.ts | 6 ++- .../ui/openapi-gen/queries/suspense.ts | 6 ++- .../ui/openapi-gen/requests/services.gen.ts | 2 + .../ui/openapi-gen/requests/types.gen.ts | 4 ++ airflow-core/src/airflow/utils/db.py | 2 +- .../core_api/routes/public/test_assets.py | 6 +++ 16 files changed, 129 insertions(+), 13 deletions(-) create mode 100644 airflow-core/newsfragments/69489.improvement.rst create mode 100644 airflow-core/src/airflow/migrations/versions/0124_3_3_0_add_index_on_asset_uri.py diff --git a/airflow-core/docs/migrations-ref.rst b/airflow-core/docs/migrations-ref.rst index d2cfde623523f..63dbefbc4c33b 100644 --- a/airflow-core/docs/migrations-ref.rst +++ b/airflow-core/docs/migrations-ref.rst @@ -39,7 +39,9 @@ Here's the list of all the Database Migrations that are executed via when you ru +-------------------------+------------------+-------------------+--------------------------------------------------------------+ | Revision ID | Revises ID | Airflow Version | Description | +=========================+==================+===================+==============================================================+ -| ``d2f4e1b3c5a7`` (head) | ``9ff64e1c35d3`` | ``3.3.0`` | Add partition_date to asset_partition_dag_run. | +| ``c4e7a1f9b2d0`` (head) | ``d2f4e1b3c5a7`` | ``3.3.0`` | Add index on asset.uri. | ++-------------------------+------------------+-------------------+--------------------------------------------------------------+ +| ``d2f4e1b3c5a7`` | ``9ff64e1c35d3`` | ``3.3.0`` | Add partition_date to asset_partition_dag_run. | +-------------------------+------------------+-------------------+--------------------------------------------------------------+ | ``9ff64e1c35d3`` | ``dd5f3a8e2b91`` | ``3.3.0`` | Add indexes on dag_run.created_dag_version_id and | | | | | task_instance.dag_version_id. | diff --git a/airflow-core/newsfragments/69489.improvement.rst b/airflow-core/newsfragments/69489.improvement.rst new file mode 100644 index 0000000000000..8a0c98da505e8 --- /dev/null +++ b/airflow-core/newsfragments/69489.improvement.rst @@ -0,0 +1 @@ +Add an indexed exact-match ``uri`` query parameter to ``GET /api/v2/assets`` for fast single-asset lookup by URI (much faster than ``uri_pattern``, which uses an unindexed ``ILIKE '%...%'``). diff --git a/airflow-core/src/airflow/api_fastapi/common/parameters.py b/airflow-core/src/airflow/api_fastapi/common/parameters.py index 935b547c1a7cf..b4b19e7aedc32 100644 --- a/airflow-core/src/airflow/api_fastapi/common/parameters.py +++ b/airflow-core/src/airflow/api_fastapi/common/parameters.py @@ -1578,6 +1578,22 @@ def _transform_ti_states(states: list[str] | None) -> list[TaskInstanceState | N QueryUriPrefixPatternSearch = Annotated[ _PrefixSearchParam, Depends(prefix_search_param_factory(AssetModel.uri, "uri_prefix_pattern")) ] +QueryUriExactMatch = Annotated[ + FilterParam[str | None], + Depends( + filter_param_factory( + AssetModel.uri, + str | None, + filter_name="uri", + description=( + "Exact-match filter on the full asset URI. Compiles to an indexed equality " + "comparison (``uri = ...``), so it is far faster than ``uri_pattern`` (which uses " + "``ILIKE '%...%'`` and cannot use an index) for resolving a single asset by its " + "known URI." + ), + ) + ), +] QueryAssetAliasNamePatternSearch = Annotated[ _SearchParam, Depends(search_param_factory(AssetAliasModel.name, "name_pattern")) ] 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 571d9cbeb9d6a..e9761dc712687 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 @@ -79,6 +79,22 @@ paths: \ stays index-compatible under locale-aware collations \u2014 e.g. `test_`\ \ effectively matches items starting with `test`, and `s3://` matches items\ \ starting with `s3`." + - name: uri + in: query + required: false + schema: + anyOf: + - type: string + - type: 'null' + description: Exact-match filter on the full asset URI. Compiles to an indexed + equality comparison (``uri = ...``), so it is far faster than ``uri_pattern`` + (which uses ``ILIKE '%...%'`` and cannot use an index) for resolving a + single asset by its known URI. + title: Uri + description: Exact-match filter on the full asset URI. Compiles to an indexed + equality comparison (``uri = ...``), so it is far faster than ``uri_pattern`` + (which uses ``ILIKE '%...%'`` and cannot use an index) for resolving a single + asset by its known URI. - name: uri_pattern in: query required: false diff --git a/airflow-core/src/airflow/api_fastapi/core_api/routes/public/assets.py b/airflow-core/src/airflow/api_fastapi/core_api/routes/public/assets.py index a19f4c7da7797..6bb72aa5fdf18 100644 --- a/airflow-core/src/airflow/api_fastapi/core_api/routes/public/assets.py +++ b/airflow-core/src/airflow/api_fastapi/core_api/routes/public/assets.py @@ -41,6 +41,7 @@ QueryAssetNamePrefixPatternSearch, QueryLimit, QueryOffset, + QueryUriExactMatch, QueryUriPatternSearch, QueryUriPrefixPatternSearch, RangeFilter, @@ -139,6 +140,7 @@ def get_assets( offset: QueryOffset, name_pattern: QueryAssetNamePatternSearch, name_prefix_pattern: QueryAssetNamePrefixPatternSearch, + uri: QueryUriExactMatch, uri_pattern: QueryUriPatternSearch, uri_prefix_pattern: QueryUriPrefixPatternSearch, dag_ids: QueryAssetDagIdPatternSearch, @@ -184,7 +186,15 @@ def get_assets( assets_select, total_entries = paginated_select( statement=assets_select_statement, - filters=[only_active, name_pattern, name_prefix_pattern, uri_pattern, uri_prefix_pattern, dag_ids], + filters=[ + only_active, + name_pattern, + name_prefix_pattern, + uri, + uri_pattern, + uri_prefix_pattern, + dag_ids, + ], order_by=order_by, offset=offset, limit=limit, diff --git a/airflow-core/src/airflow/migrations/versions/0124_3_3_0_add_index_on_asset_uri.py b/airflow-core/src/airflow/migrations/versions/0124_3_3_0_add_index_on_asset_uri.py new file mode 100644 index 0000000000000..5e70771b52f46 --- /dev/null +++ b/airflow-core/src/airflow/migrations/versions/0124_3_3_0_add_index_on_asset_uri.py @@ -0,0 +1,47 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +""" +Add index on asset.uri. + +Revision ID: c4e7a1f9b2d0 +Revises: d2f4e1b3c5a7 +Create Date: 2026-07-06 00:00:00.000000 +""" + +from __future__ import annotations + +from alembic import op + +# revision identifiers, used by Alembic. +revision = "c4e7a1f9b2d0" +down_revision = "d2f4e1b3c5a7" +branch_labels = None +depends_on = None +airflow_version = "3.3.0" + + +def upgrade(): + """Apply Add index on asset.uri.""" + with op.batch_alter_table("asset", schema=None) as batch_op: + batch_op.create_index("idx_asset_uri", ["uri"], unique=False) + + +def downgrade(): + """Unapply Add index on asset.uri.""" + with op.batch_alter_table("asset", schema=None) as batch_op: + batch_op.drop_index("idx_asset_uri") diff --git a/airflow-core/src/airflow/models/asset.py b/airflow-core/src/airflow/models/asset.py index 60256f9cd8852..f32c102e541d7 100644 --- a/airflow-core/src/airflow/models/asset.py +++ b/airflow-core/src/airflow/models/asset.py @@ -333,6 +333,9 @@ class AssetModel(Base): __tablename__ = "asset" __table_args__ = ( Index("idx_asset_name_uri_unique", name, uri, unique=True), + # Single-column index so exact-match lookups by URI (GET /assets?uri=...) can use an + # index; the composite index above leads with ``name`` and cannot serve uri-only queries. + Index("idx_asset_uri", uri), {"sqlite_autoincrement": True}, # ensures PK values not reused ) 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 9b07c3c8649c4..026b2e08ad1d0 100644 --- a/airflow-core/src/airflow/ui/openapi-gen/queries/common.ts +++ b/airflow-core/src/airflow/ui/openapi-gen/queries/common.ts @@ -6,7 +6,7 @@ import { DagRunState, DagWarningType } from "../requests/types.gen"; export type AssetServiceGetAssetsDefaultResponse = Awaited>; export type AssetServiceGetAssetsQueryResult = UseQueryResult; export const useAssetServiceGetAssetsKey = "AssetServiceGetAssets"; -export const UseAssetServiceGetAssetsKeyFn = ({ dagIds, limit, namePattern, namePrefixPattern, offset, onlyActive, orderBy, uriPattern, uriPrefixPattern }: { +export const UseAssetServiceGetAssetsKeyFn = ({ dagIds, limit, namePattern, namePrefixPattern, offset, onlyActive, orderBy, uri, uriPattern, uriPrefixPattern }: { dagIds?: string[]; limit?: number; namePattern?: string; @@ -14,9 +14,10 @@ export const UseAssetServiceGetAssetsKeyFn = ({ dagIds, limit, namePattern, name offset?: number; onlyActive?: boolean; orderBy?: string[]; + uri?: string; uriPattern?: string; uriPrefixPattern?: string; -} = {}, queryKey?: Array) => [useAssetServiceGetAssetsKey, ...(queryKey ?? [{ dagIds, limit, namePattern, namePrefixPattern, offset, onlyActive, orderBy, uriPattern, uriPrefixPattern }])]; +} = {}, queryKey?: Array) => [useAssetServiceGetAssetsKey, ...(queryKey ?? [{ dagIds, limit, namePattern, namePrefixPattern, offset, onlyActive, orderBy, uri, uriPattern, uriPrefixPattern }])]; export type AssetServiceGetAssetAliasesDefaultResponse = Awaited>; export type AssetServiceGetAssetAliasesQueryResult = UseQueryResult; export const useAssetServiceGetAssetAliasesKey = "AssetServiceGetAssetAliases"; 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 501d61585034d..d3742d496c8c9 100644 --- a/airflow-core/src/airflow/ui/openapi-gen/queries/ensureQueryData.ts +++ b/airflow-core/src/airflow/ui/openapi-gen/queries/ensureQueryData.ts @@ -14,6 +14,7 @@ import * as Common from "./common"; * * **Performance note:** this full-match pattern is evaluated as ``ILIKE '%term%'`` and most of the time prevents the database from using B-tree indexes, which can be very slow on large tables. Prefer the equivalent ``name_prefix_pattern`` parameter when possible. * @param data.namePrefixPattern 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.uri Exact-match filter on the full asset URI. Compiles to an indexed equality comparison (``uri = ...``), so it is far faster than ``uri_pattern`` (which uses ``ILIKE '%...%'`` and cannot use an index) for resolving a single asset by its known URI. * @param data.uriPattern SQL LIKE expression — use `%` / `_` wildcards (e.g. `%customer_%`). Use the pipe `|` operator for OR logic (e.g. `dag1 | dag2`). Regular expressions are **not** supported. * * **Performance note:** this full-match pattern is evaluated as ``ILIKE '%term%'`` and most of the time prevents the database from using B-tree indexes, which can be very slow on large tables. Prefer the equivalent ``uri_prefix_pattern`` parameter when possible. @@ -24,7 +25,7 @@ import * as Common from "./common"; * @returns AssetCollectionResponse Successful Response * @throws ApiError */ -export const ensureUseAssetServiceGetAssetsData = (queryClient: QueryClient, { dagIds, limit, namePattern, namePrefixPattern, offset, onlyActive, orderBy, uriPattern, uriPrefixPattern }: { +export const ensureUseAssetServiceGetAssetsData = (queryClient: QueryClient, { dagIds, limit, namePattern, namePrefixPattern, offset, onlyActive, orderBy, uri, uriPattern, uriPrefixPattern }: { dagIds?: string[]; limit?: number; namePattern?: string; @@ -32,9 +33,10 @@ export const ensureUseAssetServiceGetAssetsData = (queryClient: QueryClient, { d offset?: number; onlyActive?: boolean; orderBy?: string[]; + uri?: string; uriPattern?: string; uriPrefixPattern?: string; -} = {}) => queryClient.ensureQueryData({ queryKey: Common.UseAssetServiceGetAssetsKeyFn({ dagIds, limit, namePattern, namePrefixPattern, offset, onlyActive, orderBy, uriPattern, uriPrefixPattern }), queryFn: () => AssetService.getAssets({ dagIds, limit, namePattern, namePrefixPattern, offset, onlyActive, orderBy, uriPattern, uriPrefixPattern }) }); +} = {}) => queryClient.ensureQueryData({ queryKey: Common.UseAssetServiceGetAssetsKeyFn({ dagIds, limit, namePattern, namePrefixPattern, offset, onlyActive, orderBy, uri, uriPattern, uriPrefixPattern }), queryFn: () => AssetService.getAssets({ dagIds, limit, namePattern, namePrefixPattern, offset, onlyActive, orderBy, uri, uriPattern, uriPrefixPattern }) }); /** * Get Asset Aliases * Get asset aliases. 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 d4ade79763f23..6e7f1e4d39a11 100644 --- a/airflow-core/src/airflow/ui/openapi-gen/queries/prefetch.ts +++ b/airflow-core/src/airflow/ui/openapi-gen/queries/prefetch.ts @@ -14,6 +14,7 @@ import * as Common from "./common"; * * **Performance note:** this full-match pattern is evaluated as ``ILIKE '%term%'`` and most of the time prevents the database from using B-tree indexes, which can be very slow on large tables. Prefer the equivalent ``name_prefix_pattern`` parameter when possible. * @param data.namePrefixPattern 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.uri Exact-match filter on the full asset URI. Compiles to an indexed equality comparison (``uri = ...``), so it is far faster than ``uri_pattern`` (which uses ``ILIKE '%...%'`` and cannot use an index) for resolving a single asset by its known URI. * @param data.uriPattern SQL LIKE expression — use `%` / `_` wildcards (e.g. `%customer_%`). Use the pipe `|` operator for OR logic (e.g. `dag1 | dag2`). Regular expressions are **not** supported. * * **Performance note:** this full-match pattern is evaluated as ``ILIKE '%term%'`` and most of the time prevents the database from using B-tree indexes, which can be very slow on large tables. Prefer the equivalent ``uri_prefix_pattern`` parameter when possible. @@ -24,7 +25,7 @@ import * as Common from "./common"; * @returns AssetCollectionResponse Successful Response * @throws ApiError */ -export const prefetchUseAssetServiceGetAssets = (queryClient: QueryClient, { dagIds, limit, namePattern, namePrefixPattern, offset, onlyActive, orderBy, uriPattern, uriPrefixPattern }: { +export const prefetchUseAssetServiceGetAssets = (queryClient: QueryClient, { dagIds, limit, namePattern, namePrefixPattern, offset, onlyActive, orderBy, uri, uriPattern, uriPrefixPattern }: { dagIds?: string[]; limit?: number; namePattern?: string; @@ -32,9 +33,10 @@ export const prefetchUseAssetServiceGetAssets = (queryClient: QueryClient, { dag offset?: number; onlyActive?: boolean; orderBy?: string[]; + uri?: string; uriPattern?: string; uriPrefixPattern?: string; -} = {}) => queryClient.prefetchQuery({ queryKey: Common.UseAssetServiceGetAssetsKeyFn({ dagIds, limit, namePattern, namePrefixPattern, offset, onlyActive, orderBy, uriPattern, uriPrefixPattern }), queryFn: () => AssetService.getAssets({ dagIds, limit, namePattern, namePrefixPattern, offset, onlyActive, orderBy, uriPattern, uriPrefixPattern }) }); +} = {}) => queryClient.prefetchQuery({ queryKey: Common.UseAssetServiceGetAssetsKeyFn({ dagIds, limit, namePattern, namePrefixPattern, offset, onlyActive, orderBy, uri, uriPattern, uriPrefixPattern }), queryFn: () => AssetService.getAssets({ dagIds, limit, namePattern, namePrefixPattern, offset, onlyActive, orderBy, uri, uriPattern, uriPrefixPattern }) }); /** * Get Asset Aliases * Get asset aliases. 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 a8cd6066357be..fd08d60716ba9 100644 --- a/airflow-core/src/airflow/ui/openapi-gen/queries/queries.ts +++ b/airflow-core/src/airflow/ui/openapi-gen/queries/queries.ts @@ -14,6 +14,7 @@ import * as Common from "./common"; * * **Performance note:** this full-match pattern is evaluated as ``ILIKE '%term%'`` and most of the time prevents the database from using B-tree indexes, which can be very slow on large tables. Prefer the equivalent ``name_prefix_pattern`` parameter when possible. * @param data.namePrefixPattern 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.uri Exact-match filter on the full asset URI. Compiles to an indexed equality comparison (``uri = ...``), so it is far faster than ``uri_pattern`` (which uses ``ILIKE '%...%'`` and cannot use an index) for resolving a single asset by its known URI. * @param data.uriPattern SQL LIKE expression — use `%` / `_` wildcards (e.g. `%customer_%`). Use the pipe `|` operator for OR logic (e.g. `dag1 | dag2`). Regular expressions are **not** supported. * * **Performance note:** this full-match pattern is evaluated as ``ILIKE '%term%'`` and most of the time prevents the database from using B-tree indexes, which can be very slow on large tables. Prefer the equivalent ``uri_prefix_pattern`` parameter when possible. @@ -24,7 +25,7 @@ import * as Common from "./common"; * @returns AssetCollectionResponse Successful Response * @throws ApiError */ -export const useAssetServiceGetAssets = = unknown[]>({ dagIds, limit, namePattern, namePrefixPattern, offset, onlyActive, orderBy, uriPattern, uriPrefixPattern }: { +export const useAssetServiceGetAssets = = unknown[]>({ dagIds, limit, namePattern, namePrefixPattern, offset, onlyActive, orderBy, uri, uriPattern, uriPrefixPattern }: { dagIds?: string[]; limit?: number; namePattern?: string; @@ -32,9 +33,10 @@ export const useAssetServiceGetAssets = , "queryKey" | "queryFn">) => useQuery({ queryKey: Common.UseAssetServiceGetAssetsKeyFn({ dagIds, limit, namePattern, namePrefixPattern, offset, onlyActive, orderBy, uriPattern, uriPrefixPattern }, queryKey), queryFn: () => AssetService.getAssets({ dagIds, limit, namePattern, namePrefixPattern, offset, onlyActive, orderBy, uriPattern, uriPrefixPattern }) as TData, ...options }); +} = {}, queryKey?: TQueryKey, options?: Omit, "queryKey" | "queryFn">) => useQuery({ queryKey: Common.UseAssetServiceGetAssetsKeyFn({ dagIds, limit, namePattern, namePrefixPattern, offset, onlyActive, orderBy, uri, uriPattern, uriPrefixPattern }, queryKey), queryFn: () => AssetService.getAssets({ dagIds, limit, namePattern, namePrefixPattern, offset, onlyActive, orderBy, uri, uriPattern, uriPrefixPattern }) as TData, ...options }); /** * Get Asset Aliases * Get asset aliases. 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 384cfb52b0167..a8360458c6968 100644 --- a/airflow-core/src/airflow/ui/openapi-gen/queries/suspense.ts +++ b/airflow-core/src/airflow/ui/openapi-gen/queries/suspense.ts @@ -14,6 +14,7 @@ import * as Common from "./common"; * * **Performance note:** this full-match pattern is evaluated as ``ILIKE '%term%'`` and most of the time prevents the database from using B-tree indexes, which can be very slow on large tables. Prefer the equivalent ``name_prefix_pattern`` parameter when possible. * @param data.namePrefixPattern 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.uri Exact-match filter on the full asset URI. Compiles to an indexed equality comparison (``uri = ...``), so it is far faster than ``uri_pattern`` (which uses ``ILIKE '%...%'`` and cannot use an index) for resolving a single asset by its known URI. * @param data.uriPattern SQL LIKE expression — use `%` / `_` wildcards (e.g. `%customer_%`). Use the pipe `|` operator for OR logic (e.g. `dag1 | dag2`). Regular expressions are **not** supported. * * **Performance note:** this full-match pattern is evaluated as ``ILIKE '%term%'`` and most of the time prevents the database from using B-tree indexes, which can be very slow on large tables. Prefer the equivalent ``uri_prefix_pattern`` parameter when possible. @@ -24,7 +25,7 @@ import * as Common from "./common"; * @returns AssetCollectionResponse Successful Response * @throws ApiError */ -export const useAssetServiceGetAssetsSuspense = = unknown[]>({ dagIds, limit, namePattern, namePrefixPattern, offset, onlyActive, orderBy, uriPattern, uriPrefixPattern }: { +export const useAssetServiceGetAssetsSuspense = = unknown[]>({ dagIds, limit, namePattern, namePrefixPattern, offset, onlyActive, orderBy, uri, uriPattern, uriPrefixPattern }: { dagIds?: string[]; limit?: number; namePattern?: string; @@ -32,9 +33,10 @@ export const useAssetServiceGetAssetsSuspense = , "queryKey" | "queryFn">) => useSuspenseQuery({ queryKey: Common.UseAssetServiceGetAssetsKeyFn({ dagIds, limit, namePattern, namePrefixPattern, offset, onlyActive, orderBy, uriPattern, uriPrefixPattern }, queryKey), queryFn: () => AssetService.getAssets({ dagIds, limit, namePattern, namePrefixPattern, offset, onlyActive, orderBy, uriPattern, uriPrefixPattern }) as TData, ...options }); +} = {}, queryKey?: TQueryKey, options?: Omit, "queryKey" | "queryFn">) => useSuspenseQuery({ queryKey: Common.UseAssetServiceGetAssetsKeyFn({ dagIds, limit, namePattern, namePrefixPattern, offset, onlyActive, orderBy, uri, uriPattern, uriPrefixPattern }, queryKey), queryFn: () => AssetService.getAssets({ dagIds, limit, namePattern, namePrefixPattern, offset, onlyActive, orderBy, uri, uriPattern, uriPrefixPattern }) as TData, ...options }); /** * Get Asset Aliases * Get asset aliases. 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 7f1229ddb9c05..30e75dba150b0 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 @@ -16,6 +16,7 @@ export class AssetService { * * **Performance note:** this full-match pattern is evaluated as ``ILIKE '%term%'`` and most of the time prevents the database from using B-tree indexes, which can be very slow on large tables. Prefer the equivalent ``name_prefix_pattern`` parameter when possible. * @param data.namePrefixPattern 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.uri Exact-match filter on the full asset URI. Compiles to an indexed equality comparison (``uri = ...``), so it is far faster than ``uri_pattern`` (which uses ``ILIKE '%...%'`` and cannot use an index) for resolving a single asset by its known URI. * @param data.uriPattern SQL LIKE expression — use `%` / `_` wildcards (e.g. `%customer_%`). Use the pipe `|` operator for OR logic (e.g. `dag1 | dag2`). Regular expressions are **not** supported. * * **Performance note:** this full-match pattern is evaluated as ``ILIKE '%term%'`` and most of the time prevents the database from using B-tree indexes, which can be very slow on large tables. Prefer the equivalent ``uri_prefix_pattern`` parameter when possible. @@ -35,6 +36,7 @@ export class AssetService { offset: data.offset, name_pattern: data.namePattern, name_prefix_pattern: data.namePrefixPattern, + uri: data.uri, uri_pattern: data.uriPattern, uri_prefix_pattern: data.uriPrefixPattern, dag_ids: data.dagIds, 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 2d8295b54b971..d79efefd644ae 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 @@ -2808,6 +2808,10 @@ export type GetAssetsData = { * Attributes to order by, multi criteria sort is supported. Prefix with `-` for descending order. Supported attributes: `id, name, uri, created_at, updated_at` */ orderBy?: Array<(string)>; + /** + * Exact-match filter on the full asset URI. Compiles to an indexed equality comparison (``uri = ...``), so it is far faster than ``uri_pattern`` (which uses ``ILIKE '%...%'`` and cannot use an index) for resolving a single asset by its known URI. + */ + uri?: string | null; /** * SQL LIKE expression — use `%` / `_` wildcards (e.g. `%customer_%`). Use the pipe `|` operator for OR logic (e.g. `dag1 | dag2`). Regular expressions are **not** supported. * diff --git a/airflow-core/src/airflow/utils/db.py b/airflow-core/src/airflow/utils/db.py index f24f3636132ae..337d6520ae65c 100644 --- a/airflow-core/src/airflow/utils/db.py +++ b/airflow-core/src/airflow/utils/db.py @@ -116,7 +116,7 @@ class MappedClassProtocol(Protocol): "3.1.0": "cc92b33c6709", "3.1.8": "509b94a1042d", "3.2.0": "1d6611b6ab7c", - "3.3.0": "d2f4e1b3c5a7", + "3.3.0": "c4e7a1f9b2d0", } # Prefix used to identify tables holding data moved during migration. diff --git a/airflow-core/tests/unit/api_fastapi/core_api/routes/public/test_assets.py b/airflow-core/tests/unit/api_fastapi/core_api/routes/public/test_assets.py index 3487fa28598a2..fd8941b4929f0 100644 --- a/airflow-core/tests/unit/api_fastapi/core_api/routes/public/test_assets.py +++ b/airflow-core/tests/unit/api_fastapi/core_api/routes/public/test_assets.py @@ -576,6 +576,12 @@ def test_filter_assets_by_name_pattern_works(self, test_client, params, expected "wasb://some_asset_bucket_/key", }, ), + # Exact-match ``uri`` filter: only the asset whose full URI matches is returned. + ({"uri": "s3://folder/key"}, {"s3://folder/key"}), + ({"uri": "gcp://bucket/key"}, {"gcp://bucket/key"}), + # A substring of an existing URI must NOT match (unlike uri_pattern). + ({"uri": "s3://folder"}, set()), + ({"uri": "does-not-exist://key"}, set()), ], ) @provide_session From 24b2e925251102f33110521da78a5d8e1f3edf19 Mon Sep 17 00:00:00 2001 From: seanmuth Date: Tue, 7 Jul 2026 09:55:21 -0500 Subject: [PATCH 2/2] Accept multiple uri values and drop the extra index note Address review feedback: let the exact-match uri filter accept repeated values (?uri=a&uri=b) so the list endpoint can resolve several assets in one call, target the migration at 3.4.0 (3.3.0 is already released), and drop the newsfragment since this is not a behaviour change that needs a user warning. --- airflow-core/docs/migrations-ref.rst | 2 +- airflow-core/newsfragments/69489.improvement.rst | 1 - .../src/airflow/api_fastapi/common/parameters.py | 11 ++++++----- .../core_api/openapi/v2-rest-api-generated.yaml | 16 +++++++--------- ...i.py => 0124_3_4_0_add_index_on_asset_uri.py} | 2 +- airflow-core/src/airflow/models/asset.py | 2 -- .../src/airflow/ui/openapi-gen/queries/common.ts | 2 +- .../ui/openapi-gen/queries/ensureQueryData.ts | 4 ++-- .../airflow/ui/openapi-gen/queries/prefetch.ts | 4 ++-- .../airflow/ui/openapi-gen/queries/queries.ts | 4 ++-- .../airflow/ui/openapi-gen/queries/suspense.ts | 4 ++-- .../ui/openapi-gen/requests/services.gen.ts | 2 +- .../airflow/ui/openapi-gen/requests/types.gen.ts | 4 ++-- airflow-core/src/airflow/utils/db.py | 3 ++- .../core_api/routes/public/test_assets.py | 2 ++ 15 files changed, 31 insertions(+), 32 deletions(-) delete mode 100644 airflow-core/newsfragments/69489.improvement.rst rename airflow-core/src/airflow/migrations/versions/{0124_3_3_0_add_index_on_asset_uri.py => 0124_3_4_0_add_index_on_asset_uri.py} (98%) diff --git a/airflow-core/docs/migrations-ref.rst b/airflow-core/docs/migrations-ref.rst index 63dbefbc4c33b..957c32c663036 100644 --- a/airflow-core/docs/migrations-ref.rst +++ b/airflow-core/docs/migrations-ref.rst @@ -39,7 +39,7 @@ Here's the list of all the Database Migrations that are executed via when you ru +-------------------------+------------------+-------------------+--------------------------------------------------------------+ | Revision ID | Revises ID | Airflow Version | Description | +=========================+==================+===================+==============================================================+ -| ``c4e7a1f9b2d0`` (head) | ``d2f4e1b3c5a7`` | ``3.3.0`` | Add index on asset.uri. | +| ``c4e7a1f9b2d0`` (head) | ``d2f4e1b3c5a7`` | ``3.4.0`` | Add index on asset.uri. | +-------------------------+------------------+-------------------+--------------------------------------------------------------+ | ``d2f4e1b3c5a7`` | ``9ff64e1c35d3`` | ``3.3.0`` | Add partition_date to asset_partition_dag_run. | +-------------------------+------------------+-------------------+--------------------------------------------------------------+ diff --git a/airflow-core/newsfragments/69489.improvement.rst b/airflow-core/newsfragments/69489.improvement.rst deleted file mode 100644 index 8a0c98da505e8..0000000000000 --- a/airflow-core/newsfragments/69489.improvement.rst +++ /dev/null @@ -1 +0,0 @@ -Add an indexed exact-match ``uri`` query parameter to ``GET /api/v2/assets`` for fast single-asset lookup by URI (much faster than ``uri_pattern``, which uses an unindexed ``ILIKE '%...%'``). diff --git a/airflow-core/src/airflow/api_fastapi/common/parameters.py b/airflow-core/src/airflow/api_fastapi/common/parameters.py index b4b19e7aedc32..835d4e9eb0a7d 100644 --- a/airflow-core/src/airflow/api_fastapi/common/parameters.py +++ b/airflow-core/src/airflow/api_fastapi/common/parameters.py @@ -1579,17 +1579,18 @@ def _transform_ti_states(states: list[str] | None) -> list[TaskInstanceState | N _PrefixSearchParam, Depends(prefix_search_param_factory(AssetModel.uri, "uri_prefix_pattern")) ] QueryUriExactMatch = Annotated[ - FilterParam[str | None], + FilterParam[list[str]], Depends( filter_param_factory( AssetModel.uri, - str | None, + list[str], + FilterOptionEnum.ANY_EQUAL, filter_name="uri", + default_factory=list, description=( "Exact-match filter on the full asset URI. Compiles to an indexed equality " - "comparison (``uri = ...``), so it is far faster than ``uri_pattern`` (which uses " - "``ILIKE '%...%'`` and cannot use an index) for resolving a single asset by its " - "known URI." + "comparison (``uri = ...``). Repeat the parameter (``?uri=a&uri=b``) to match " + "multiple assets." ), ) ), 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 e9761dc712687..025fb2191a38a 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 @@ -83,18 +83,16 @@ paths: in: query required: false schema: - anyOf: - - type: string - - type: 'null' + type: array + items: + type: string description: Exact-match filter on the full asset URI. Compiles to an indexed - equality comparison (``uri = ...``), so it is far faster than ``uri_pattern`` - (which uses ``ILIKE '%...%'`` and cannot use an index) for resolving a - single asset by its known URI. + equality comparison (``uri = ...``). Repeat the parameter (``?uri=a&uri=b``) + to match multiple assets. title: Uri description: Exact-match filter on the full asset URI. Compiles to an indexed - equality comparison (``uri = ...``), so it is far faster than ``uri_pattern`` - (which uses ``ILIKE '%...%'`` and cannot use an index) for resolving a single - asset by its known URI. + equality comparison (``uri = ...``). Repeat the parameter (``?uri=a&uri=b``) + to match multiple assets. - name: uri_pattern in: query required: false diff --git a/airflow-core/src/airflow/migrations/versions/0124_3_3_0_add_index_on_asset_uri.py b/airflow-core/src/airflow/migrations/versions/0124_3_4_0_add_index_on_asset_uri.py similarity index 98% rename from airflow-core/src/airflow/migrations/versions/0124_3_3_0_add_index_on_asset_uri.py rename to airflow-core/src/airflow/migrations/versions/0124_3_4_0_add_index_on_asset_uri.py index 5e70771b52f46..c166fe1f0d395 100644 --- a/airflow-core/src/airflow/migrations/versions/0124_3_3_0_add_index_on_asset_uri.py +++ b/airflow-core/src/airflow/migrations/versions/0124_3_4_0_add_index_on_asset_uri.py @@ -32,7 +32,7 @@ down_revision = "d2f4e1b3c5a7" branch_labels = None depends_on = None -airflow_version = "3.3.0" +airflow_version = "3.4.0" def upgrade(): diff --git a/airflow-core/src/airflow/models/asset.py b/airflow-core/src/airflow/models/asset.py index f32c102e541d7..4354034e649fe 100644 --- a/airflow-core/src/airflow/models/asset.py +++ b/airflow-core/src/airflow/models/asset.py @@ -333,8 +333,6 @@ class AssetModel(Base): __tablename__ = "asset" __table_args__ = ( Index("idx_asset_name_uri_unique", name, uri, unique=True), - # Single-column index so exact-match lookups by URI (GET /assets?uri=...) can use an - # index; the composite index above leads with ``name`` and cannot serve uri-only queries. Index("idx_asset_uri", uri), {"sqlite_autoincrement": True}, # ensures PK values not reused ) 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 026b2e08ad1d0..de6336502409b 100644 --- a/airflow-core/src/airflow/ui/openapi-gen/queries/common.ts +++ b/airflow-core/src/airflow/ui/openapi-gen/queries/common.ts @@ -14,7 +14,7 @@ export const UseAssetServiceGetAssetsKeyFn = ({ dagIds, limit, namePattern, name offset?: number; onlyActive?: boolean; orderBy?: string[]; - uri?: string; + uri?: string[]; uriPattern?: string; uriPrefixPattern?: string; } = {}, queryKey?: Array) => [useAssetServiceGetAssetsKey, ...(queryKey ?? [{ dagIds, limit, namePattern, namePrefixPattern, offset, onlyActive, orderBy, uri, uriPattern, uriPrefixPattern }])]; 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 d3742d496c8c9..0c8d7255b0e1d 100644 --- a/airflow-core/src/airflow/ui/openapi-gen/queries/ensureQueryData.ts +++ b/airflow-core/src/airflow/ui/openapi-gen/queries/ensureQueryData.ts @@ -14,7 +14,7 @@ import * as Common from "./common"; * * **Performance note:** this full-match pattern is evaluated as ``ILIKE '%term%'`` and most of the time prevents the database from using B-tree indexes, which can be very slow on large tables. Prefer the equivalent ``name_prefix_pattern`` parameter when possible. * @param data.namePrefixPattern 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.uri Exact-match filter on the full asset URI. Compiles to an indexed equality comparison (``uri = ...``), so it is far faster than ``uri_pattern`` (which uses ``ILIKE '%...%'`` and cannot use an index) for resolving a single asset by its known URI. +* @param data.uri Exact-match filter on the full asset URI. Compiles to an indexed equality comparison (``uri = ...``). Repeat the parameter (``?uri=a&uri=b``) to match multiple assets. * @param data.uriPattern SQL LIKE expression — use `%` / `_` wildcards (e.g. `%customer_%`). Use the pipe `|` operator for OR logic (e.g. `dag1 | dag2`). Regular expressions are **not** supported. * * **Performance note:** this full-match pattern is evaluated as ``ILIKE '%term%'`` and most of the time prevents the database from using B-tree indexes, which can be very slow on large tables. Prefer the equivalent ``uri_prefix_pattern`` parameter when possible. @@ -33,7 +33,7 @@ export const ensureUseAssetServiceGetAssetsData = (queryClient: QueryClient, { d offset?: number; onlyActive?: boolean; orderBy?: string[]; - uri?: string; + uri?: string[]; uriPattern?: string; uriPrefixPattern?: string; } = {}) => queryClient.ensureQueryData({ queryKey: Common.UseAssetServiceGetAssetsKeyFn({ dagIds, limit, namePattern, namePrefixPattern, offset, onlyActive, orderBy, uri, uriPattern, uriPrefixPattern }), queryFn: () => AssetService.getAssets({ dagIds, limit, namePattern, namePrefixPattern, offset, onlyActive, orderBy, uri, uriPattern, uriPrefixPattern }) }); 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 6e7f1e4d39a11..009da00ae1e8a 100644 --- a/airflow-core/src/airflow/ui/openapi-gen/queries/prefetch.ts +++ b/airflow-core/src/airflow/ui/openapi-gen/queries/prefetch.ts @@ -14,7 +14,7 @@ import * as Common from "./common"; * * **Performance note:** this full-match pattern is evaluated as ``ILIKE '%term%'`` and most of the time prevents the database from using B-tree indexes, which can be very slow on large tables. Prefer the equivalent ``name_prefix_pattern`` parameter when possible. * @param data.namePrefixPattern 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.uri Exact-match filter on the full asset URI. Compiles to an indexed equality comparison (``uri = ...``), so it is far faster than ``uri_pattern`` (which uses ``ILIKE '%...%'`` and cannot use an index) for resolving a single asset by its known URI. +* @param data.uri Exact-match filter on the full asset URI. Compiles to an indexed equality comparison (``uri = ...``). Repeat the parameter (``?uri=a&uri=b``) to match multiple assets. * @param data.uriPattern SQL LIKE expression — use `%` / `_` wildcards (e.g. `%customer_%`). Use the pipe `|` operator for OR logic (e.g. `dag1 | dag2`). Regular expressions are **not** supported. * * **Performance note:** this full-match pattern is evaluated as ``ILIKE '%term%'`` and most of the time prevents the database from using B-tree indexes, which can be very slow on large tables. Prefer the equivalent ``uri_prefix_pattern`` parameter when possible. @@ -33,7 +33,7 @@ export const prefetchUseAssetServiceGetAssets = (queryClient: QueryClient, { dag offset?: number; onlyActive?: boolean; orderBy?: string[]; - uri?: string; + uri?: string[]; uriPattern?: string; uriPrefixPattern?: string; } = {}) => queryClient.prefetchQuery({ queryKey: Common.UseAssetServiceGetAssetsKeyFn({ dagIds, limit, namePattern, namePrefixPattern, offset, onlyActive, orderBy, uri, uriPattern, uriPrefixPattern }), queryFn: () => AssetService.getAssets({ dagIds, limit, namePattern, namePrefixPattern, offset, onlyActive, orderBy, uri, uriPattern, uriPrefixPattern }) }); 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 fd08d60716ba9..14abec34c6f9b 100644 --- a/airflow-core/src/airflow/ui/openapi-gen/queries/queries.ts +++ b/airflow-core/src/airflow/ui/openapi-gen/queries/queries.ts @@ -14,7 +14,7 @@ import * as Common from "./common"; * * **Performance note:** this full-match pattern is evaluated as ``ILIKE '%term%'`` and most of the time prevents the database from using B-tree indexes, which can be very slow on large tables. Prefer the equivalent ``name_prefix_pattern`` parameter when possible. * @param data.namePrefixPattern 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.uri Exact-match filter on the full asset URI. Compiles to an indexed equality comparison (``uri = ...``), so it is far faster than ``uri_pattern`` (which uses ``ILIKE '%...%'`` and cannot use an index) for resolving a single asset by its known URI. +* @param data.uri Exact-match filter on the full asset URI. Compiles to an indexed equality comparison (``uri = ...``). Repeat the parameter (``?uri=a&uri=b``) to match multiple assets. * @param data.uriPattern SQL LIKE expression — use `%` / `_` wildcards (e.g. `%customer_%`). Use the pipe `|` operator for OR logic (e.g. `dag1 | dag2`). Regular expressions are **not** supported. * * **Performance note:** this full-match pattern is evaluated as ``ILIKE '%term%'`` and most of the time prevents the database from using B-tree indexes, which can be very slow on large tables. Prefer the equivalent ``uri_prefix_pattern`` parameter when possible. @@ -33,7 +33,7 @@ export const useAssetServiceGetAssets = , "queryKey" | "queryFn">) => useQuery({ queryKey: Common.UseAssetServiceGetAssetsKeyFn({ dagIds, limit, namePattern, namePrefixPattern, offset, onlyActive, orderBy, uri, uriPattern, uriPrefixPattern }, queryKey), queryFn: () => AssetService.getAssets({ dagIds, limit, namePattern, namePrefixPattern, offset, onlyActive, orderBy, uri, uriPattern, uriPrefixPattern }) as TData, ...options }); 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 a8360458c6968..0abb35c601ee6 100644 --- a/airflow-core/src/airflow/ui/openapi-gen/queries/suspense.ts +++ b/airflow-core/src/airflow/ui/openapi-gen/queries/suspense.ts @@ -14,7 +14,7 @@ import * as Common from "./common"; * * **Performance note:** this full-match pattern is evaluated as ``ILIKE '%term%'`` and most of the time prevents the database from using B-tree indexes, which can be very slow on large tables. Prefer the equivalent ``name_prefix_pattern`` parameter when possible. * @param data.namePrefixPattern 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.uri Exact-match filter on the full asset URI. Compiles to an indexed equality comparison (``uri = ...``), so it is far faster than ``uri_pattern`` (which uses ``ILIKE '%...%'`` and cannot use an index) for resolving a single asset by its known URI. +* @param data.uri Exact-match filter on the full asset URI. Compiles to an indexed equality comparison (``uri = ...``). Repeat the parameter (``?uri=a&uri=b``) to match multiple assets. * @param data.uriPattern SQL LIKE expression — use `%` / `_` wildcards (e.g. `%customer_%`). Use the pipe `|` operator for OR logic (e.g. `dag1 | dag2`). Regular expressions are **not** supported. * * **Performance note:** this full-match pattern is evaluated as ``ILIKE '%term%'`` and most of the time prevents the database from using B-tree indexes, which can be very slow on large tables. Prefer the equivalent ``uri_prefix_pattern`` parameter when possible. @@ -33,7 +33,7 @@ export const useAssetServiceGetAssetsSuspense = , "queryKey" | "queryFn">) => useSuspenseQuery({ queryKey: Common.UseAssetServiceGetAssetsKeyFn({ dagIds, limit, namePattern, namePrefixPattern, offset, onlyActive, orderBy, uri, uriPattern, uriPrefixPattern }, queryKey), queryFn: () => AssetService.getAssets({ dagIds, limit, namePattern, namePrefixPattern, offset, onlyActive, orderBy, uri, uriPattern, uriPrefixPattern }) as TData, ...options }); 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 30e75dba150b0..69cf96d4e7d50 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 @@ -16,7 +16,7 @@ export class AssetService { * * **Performance note:** this full-match pattern is evaluated as ``ILIKE '%term%'`` and most of the time prevents the database from using B-tree indexes, which can be very slow on large tables. Prefer the equivalent ``name_prefix_pattern`` parameter when possible. * @param data.namePrefixPattern 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.uri Exact-match filter on the full asset URI. Compiles to an indexed equality comparison (``uri = ...``), so it is far faster than ``uri_pattern`` (which uses ``ILIKE '%...%'`` and cannot use an index) for resolving a single asset by its known URI. + * @param data.uri Exact-match filter on the full asset URI. Compiles to an indexed equality comparison (``uri = ...``). Repeat the parameter (``?uri=a&uri=b``) to match multiple assets. * @param data.uriPattern SQL LIKE expression — use `%` / `_` wildcards (e.g. `%customer_%`). Use the pipe `|` operator for OR logic (e.g. `dag1 | dag2`). Regular expressions are **not** supported. * * **Performance note:** this full-match pattern is evaluated as ``ILIKE '%term%'`` and most of the time prevents the database from using B-tree indexes, which can be very slow on large tables. Prefer the equivalent ``uri_prefix_pattern`` parameter when possible. 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 d79efefd644ae..1fd2774ccf0c6 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 @@ -2809,9 +2809,9 @@ export type GetAssetsData = { */ orderBy?: Array<(string)>; /** - * Exact-match filter on the full asset URI. Compiles to an indexed equality comparison (``uri = ...``), so it is far faster than ``uri_pattern`` (which uses ``ILIKE '%...%'`` and cannot use an index) for resolving a single asset by its known URI. + * Exact-match filter on the full asset URI. Compiles to an indexed equality comparison (``uri = ...``). Repeat the parameter (``?uri=a&uri=b``) to match multiple assets. */ - uri?: string | null; + uri?: Array<(string)>; /** * SQL LIKE expression — use `%` / `_` wildcards (e.g. `%customer_%`). Use the pipe `|` operator for OR logic (e.g. `dag1 | dag2`). Regular expressions are **not** supported. * diff --git a/airflow-core/src/airflow/utils/db.py b/airflow-core/src/airflow/utils/db.py index 337d6520ae65c..ce4ef036d20fd 100644 --- a/airflow-core/src/airflow/utils/db.py +++ b/airflow-core/src/airflow/utils/db.py @@ -116,7 +116,8 @@ class MappedClassProtocol(Protocol): "3.1.0": "cc92b33c6709", "3.1.8": "509b94a1042d", "3.2.0": "1d6611b6ab7c", - "3.3.0": "c4e7a1f9b2d0", + "3.3.0": "d2f4e1b3c5a7", + "3.4.0": "c4e7a1f9b2d0", } # Prefix used to identify tables holding data moved during migration. diff --git a/airflow-core/tests/unit/api_fastapi/core_api/routes/public/test_assets.py b/airflow-core/tests/unit/api_fastapi/core_api/routes/public/test_assets.py index fd8941b4929f0..72023ccabb1d0 100644 --- a/airflow-core/tests/unit/api_fastapi/core_api/routes/public/test_assets.py +++ b/airflow-core/tests/unit/api_fastapi/core_api/routes/public/test_assets.py @@ -579,6 +579,8 @@ def test_filter_assets_by_name_pattern_works(self, test_client, params, expected # Exact-match ``uri`` filter: only the asset whose full URI matches is returned. ({"uri": "s3://folder/key"}, {"s3://folder/key"}), ({"uri": "gcp://bucket/key"}, {"gcp://bucket/key"}), + # Repeated ``uri`` params match any of the given URIs. + ({"uri": ["s3://folder/key", "gcp://bucket/key"]}, {"s3://folder/key", "gcp://bucket/key"}), # A substring of an existing URI must NOT match (unlike uri_pattern). ({"uri": "s3://folder"}, set()), ({"uri": "does-not-exist://key"}, set()),