Add exact-match uri filter to GET /assets endpoint#69489
Conversation
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.
d549e9b to
5416302
Compare
| | 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. | |
There was a problem hiding this comment.
Target version needs to be updated, 3.3.0 was just released, that would be for 3.4.0
| @@ -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 '%...%'``). | |||
There was a problem hiding this comment.
This needs to be removed. (I know doc mentions that user facing change should have a fragment, but that's only true for significant / behavior change that needs a user warning basically).
| 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." | ||
| ), | ||
| ) | ||
| ), | ||
| ] |
There was a problem hiding this comment.
I would remove the piece that compares to search params. They have their own doc explaining the performance pitfalls, this can stay about 'exact match'.
| 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." | |
| ), | |
| ) | |
| ), | |
| ] | |
| 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 = ...``). | |
| ), | |
| ) | |
| ), | |
| ] |
| 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 " |
There was a problem hiding this comment.
We could make this filter accept multiple values.
To allow ?uri=some_uri1&uri=some_uri2 to get both assets with those URI. (And it makes more sense to me for a list endpoint to accept multiple values, otherwise it's really close to a single GET endpoint and maybe we should instead have GET assets/{uri}, but lets not go there)
It's really a simple change, as an exemple for implementation:
QueryTIStateFilter = Annotated[
FilterParam[list[str]],
Depends(
filter_param_factory(
TaskInstance.state,
list[str],
FilterOptionEnum.ANY_EQUAL,
default_factory=list,
transform_callable=_transform_ti_states,
)
),
]|
|
||
| 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") |
There was a problem hiding this comment.
It's fine to have a 'filter/order_by' in the API that isn't backed by an index. Indeed it will be slow if the table grows out of control but does the table grows out of control in most common cases? I would avoid adding an index (costly) just for a single filter of a single endpoint.
I'm not sure here but I would say the table isn't huge most of the time. (millions of assets) and no index is probably fine for most? (the index probably wasn't there in 2.x as well)
If someone happen to have a huge asset table they can add their own index to improve performance following https://github.com/apache/airflow/blob/main/airflow-core/docs/howto/performance.rst
There was a problem hiding this comment.
I just checked, in 2.x there was an index indeed, so the table was worth indexing. Disregard my previous comment, this makes sense.
| # 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. |
There was a problem hiding this comment.
I would remove this, to not mention the API layer in ORM models.
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.
What
Adds a
uriquery parameter toGET /api/v2/assetsthat matches an asset by its exact URI, backed by a new single-column index onasset.uri.Why
Today, resolving a single asset when you already know its full URI (for example, discovering an
asset_idbefore posting to/assets/{asset_id}/eventsfor cross-deployment dependencies) requiresuri_pattern. That parameter compiles toILIKE '%...%', which cannot use a B-tree index and degrades to a full table scan on large asset tables — the API docs themselves warn about this.The Airflow 2 REST API offered a fast exact lookup via
GET /datasets/{uri}(laterGET /assets/{uri}), but it was not carried forward when the endpoint migrated to FastAPI under AIP-84 — the single-resource route becameGET /assets/{asset_id}(integer PK only). This PR restores the exact-URI lookup as a query parameter.Passing the URI as a query parameter rather than a path segment also avoids the URI-encoding problems path parameters have with the
/,:, and%2Fcharacters that asset URIs contain.What changed
uriexact-match filter onGET /assets(equality comparison via the existingfilter_param_factory).idx_asset_urionasset.uri(the existing unique index leads withnameand cannot serve uri-only lookups), with an Alembic migration.REVISION_HEADS_MAPand migration ref doc.uri_pattern).Was generative AI tooling used to co-author this PR?
Generated-by: Claude Code (Opus 4.8, 1M context) following the guidelines
Drafted-by: Claude Code (Opus 4.8, 1M context) (no human review before posting)