diff --git a/airflow-core/docs/migrations-ref.rst b/airflow-core/docs/migrations-ref.rst index d2cfde623523f..957c32c663036 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.4.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/src/airflow/api_fastapi/common/parameters.py b/airflow-core/src/airflow/api_fastapi/common/parameters.py index 935b547c1a7cf..835d4e9eb0a7d 100644 --- a/airflow-core/src/airflow/api_fastapi/common/parameters.py +++ b/airflow-core/src/airflow/api_fastapi/common/parameters.py @@ -1578,6 +1578,23 @@ 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[list[str]], + Depends( + filter_param_factory( + AssetModel.uri, + 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 = ...``). Repeat the parameter (``?uri=a&uri=b``) to match " + "multiple assets." + ), + ) + ), +] 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..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 @@ -79,6 +79,20 @@ 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: + type: array + items: + type: string + description: 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. + title: Uri + description: 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. - 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_4_0_add_index_on_asset_uri.py b/airflow-core/src/airflow/migrations/versions/0124_3_4_0_add_index_on_asset_uri.py new file mode 100644 index 0000000000000..c166fe1f0d395 --- /dev/null +++ b/airflow-core/src/airflow/migrations/versions/0124_3_4_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.4.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..4354034e649fe 100644 --- a/airflow-core/src/airflow/models/asset.py +++ b/airflow-core/src/airflow/models/asset.py @@ -333,6 +333,7 @@ class AssetModel(Base): __tablename__ = "asset" __table_args__ = ( Index("idx_asset_name_uri_unique", name, uri, unique=True), + 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 69372a087a36b..d985d51d94d13 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 ff0174b77e946..642b3725354ff 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 = ...``). 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. @@ -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 1f8db278d9d3a..12ddda0fef566 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 = ...``). 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. @@ -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 c5ec74825be02..91fc704780729 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 = ...``). 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. @@ -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 90f75e1cfb359..9338c60f1556a 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 = ...``). 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. @@ -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 ebb30ffa68cd4..384df180433c6 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 = ...``). 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. @@ -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 a9943390b27c3..fb0fe94db983b 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 @@ -2773,6 +2773,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 = ...``). Repeat the parameter (``?uri=a&uri=b``) to match multiple assets. + */ + 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 f24f3636132ae..ce4ef036d20fd 100644 --- a/airflow-core/src/airflow/utils/db.py +++ b/airflow-core/src/airflow/utils/db.py @@ -117,6 +117,7 @@ class MappedClassProtocol(Protocol): "3.1.8": "509b94a1042d", "3.2.0": "1d6611b6ab7c", "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 3487fa28598a2..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 @@ -576,6 +576,14 @@ 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"}), + # 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()), ], ) @provide_session