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

hussein-awala pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/airflow.git


The following commit(s) were added to refs/heads/main by this push:
     new 40311ffe4d7 Add partition_key and partition_key_regexp_pattern filters 
for asset events (#64610)
40311ffe4d7 is described below

commit 40311ffe4d7fb19da91115959233dc218b3dbb15
Author: Hussein Awala <[email protected]>
AuthorDate: Mon Jul 20 15:57:32 2026 +0200

    Add partition_key and partition_key_regexp_pattern filters for asset events 
(#64610)
    
    * Add partition_key and partition_key_pattern filters for asset events
    
    Add two new query parameters to the asset events API endpoints:
    
    - partition_key: exact-match filter leveraging the new composite
      B-tree index on (asset_id, partition_key)
    - partition_key_pattern: regex-based filter using database-native
      regexp_match for flexible pattern matching
    
    Includes Core API, Execution API, Task SDK (InletEventsAccessor),
    client, documentation, migration for the composite index, and tests.
    
    Made-with: Cursor
    
    * Regenerate supervisor schema snapshot for partition_key filters
    
    * Add ReDoS safeguards for partition_key_pattern regex filter
    
    The pattern is evaluated by the database's regex engine, so a pathological
    pattern could consume DB CPU (ReDoS). Mitigations:
    
    - Cap the pattern length (MAX_REGEX_PATTERN_LENGTH) in both the Core and
      Execution API validators, returning 400 for over-long patterns.
    - Bound the regex query with a transaction-local statement_timeout on
      PostgreSQL (apply_regex_query_timeout); no-op elsewhere. MySQL bounds
      regex evaluation via its built-in regexp_time_limit; SQLite has no
      server-side regex and MariaDB is unsupported.
    - Document the safeguards in assets.rst.
    
    * Gate regex query filters behind a config flag with configurable timeout
    
    Add two [api] configs (Airflow 3.4.0):
    
    - enable_regexp_query_filters (bool, default False): the 
partition_key_pattern
      filter passes a user-supplied regex to the database engine, which is a 
ReDoS
      vector, so it is disabled by default and must be explicitly enabled. 
Exact-match
      partition_key filtering is unaffected.
    - regexp_query_timeout (int seconds, default 30): the primary runtime 
mitigation,
      enforced as a PostgreSQL transaction-local statement_timeout; 0 disables 
it.
    
    When the flag is off, partition_key_pattern requests return HTTP 400 on 
both the
    Core and Execution APIs. Config descriptions document the security 
rationale.
    Adds tests for the disabled path and the timeout helper.
    
    * Revert pattern length cap for regex filters
    
    Drop MAX_REGEX_PATTERN_LENGTH; the opt-in flag and the PostgreSQL
    statement_timeout are the meaningful ReDoS safeguards, and a length cap
    does not stop short catastrophic patterns while risking false rejections
    of legitimate ones.
    
    * Fix CI: enable regex flag for by-alias tests, regenerate TS SDK schema
    
    - Add the enable_regexp_query_filters conf_vars fixture to
      TestGetAssetEventByAssetAliasPartitionKey (was missed), fixing the 4
      by-alias regex tests that now hit the default-off flag; add a
      disabled->400 test for the alias endpoint too.
    - Regenerate ts-sdk/src/generated/supervisor.ts for the partition_key /
      partition_key_pattern comms fields (check-ts-sdk-supervisor-schema).
    
    * Address review feedback on partition key regexp filter
    
    - Rename the public/SDK parameter partition_key_pattern ->
      partition_key_regexp_pattern to distinguish regexp filtering from the
      substring "*_pattern" filters on other APIs.
    - Move the enable_regexp_query_filters gate into _RegexParam construction
      and the regex compile check into depends_regex, so a regexp filter can
      never be instantiated without the setting being enabled.
    - Drop the mutually-exclusive 400 between partition_key and
      partition_key_regexp_pattern (both now combine with AND) on the Core and
      Execution APIs and in the Task SDK client/accessor.
    - Reuse the shared QueryAssetEventPartitionKeyFilter/Regex params in the
      Execution API asset-event routes instead of duplicating the query params
      and validation.
    - Keep backend implementation details out of the public parameter
      description; simplify the config docs; revert an unrelated limit check.
    - Regenerate OpenAPI specs, UI TS clients, and the supervisor schema
      snapshots.
    
    * Collapse regexp config into one setting and scope the query timeout
    
    Address further review feedback:
    
    - Replace the enable_regexp_query_filters + regexp_query_timeout pair with a
      single [api] regexp_query_timeout: 0 (default) disables regexp filtering
      entirely, any positive value enables it and bounds the query runtime. This
      removes the "enabled but unbounded" combination.
    - Make apply_regex_query_timeout a context manager that sets a 
transaction-local
      statement_timeout on enter and resets it on exit, so the bound is scoped 
to
      the regexp query and does not leak to other statements in the request's
      transaction. Both asset-event routes apply it automatically around query
      execution instead of relying on a manual call in the view.
    
    * Regenerate UI query client for partition_key_regexp_pattern rename
    
    The hand-applied rename missed the type-block declarations and JSDoc in the
    generated query hooks (they were not on lines matching the AssetEvents hook
    name). Regenerate with `pnpm codegen` so the UI client matches the OpenAPI
    spec, fixing the "Compile / format / lint UI" static check.
    
    * Fix provide_session positional static check in partition-key tests
    
    The check-no-new-provide-session-positional hook flagged the partition-key
    regexp tests. The nine test methods never used the injected session, so drop
    their @provide_session decorator and unused session parameter; make session
    keyword-only in the _create_partition_key_test_data helper that does use it.
    
    * Address review: MySQL query timeout, float config, doc link, test tidy-up
    
    - Enforce regexp_query_timeout on MySQL too (session max_execution_time),
      not just PostgreSQL statement_timeout; apply_regex_query_timeout now
      branches by dialect and is a no-op on SQLite.
    - Make [api] regexp_query_timeout a float so fractional seconds (e.g. 0.5)
      are allowed; read it via conf.getfloat.
    - Link the config option in assets.rst via :ref: and note the MySQL bound.
    - Tests: create partition-key rows via an autouse fixture instead of a
      per-test helper call, fold the exact-match cases into parametrized tests
      (core + execution API), and cover the MySQL/float timeout paths.
    
    * Restore previous db timeout instead of clearing it
    
    Address review nit: apply_regex_query_timeout now captures the current
    statement_timeout (PostgreSQL) / max_execution_time (MySQL) before 
overriding
    it and restores that value on exit, instead of resetting to 0. This 
preserves
    a server- or role-level global timeout rather than clobbering it.
    
    * Apply regexp query timeout automatically via the filter dependency
    
    Address review feedback: instead of relying on each view to wrap its query 
in
    apply_regex_query_timeout (easy to forget -> ReDoS), the regexp filter's
    dependency (regex_param_factory) now applies the timeout to the request's
    session itself. Any endpoint using QueryAssetEventPartitionKeyRegex is 
bounded
    automatically. The dependency is function-scoped so it can depend on the
    session it bounds; the previous timeout value is restored on teardown. 
Removes
    the manual wrappers from the core and execution asset-event routes and adds
    tests for the dependency behavior.
---
 .../docs/authoring-and-scheduling/assets.rst       |  53 +++-
 airflow-core/docs/migrations-ref.rst               |   4 +-
 .../src/airflow/api_fastapi/common/parameters.py   |  84 ++++-
 .../core_api/openapi/v2-rest-api-generated.yaml    |  20 ++
 .../api_fastapi/core_api/routes/public/assets.py   |  12 +-
 .../execution_api/routes/asset_events.py           |  27 +-
 .../src/airflow/config_templates/config.yml        |  23 ++
 ...0127_3_4_0_add_idx_asset_event_partition_key.py |  51 +++
 airflow-core/src/airflow/models/asset.py           |   1 +
 .../src/airflow/ui/openapi-gen/queries/common.ts   |   6 +-
 .../ui/openapi-gen/queries/ensureQueryData.ts      |   8 +-
 .../src/airflow/ui/openapi-gen/queries/prefetch.ts |   8 +-
 .../src/airflow/ui/openapi-gen/queries/queries.ts  |   8 +-
 .../src/airflow/ui/openapi-gen/queries/suspense.ts |   8 +-
 .../ui/openapi-gen/requests/services.gen.ts        |   4 +
 .../airflow/ui/openapi-gen/requests/types.gen.ts   |   5 +
 airflow-core/src/airflow/utils/db.py               |   2 +-
 airflow-core/src/airflow/utils/sqlalchemy.py       |  52 +++
 .../unit/api_fastapi/common/test_parameters.py     |  53 ++++
 .../core_api/routes/public/test_assets.py          | 156 +++++++++
 .../versions/head/test_asset_events.py             | 351 +++++++++++++++++++++
 airflow-core/tests/unit/utils/test_sqlalchemy.py   |  63 ++++
 task-sdk/src/airflow/sdk/api/client.py             |   6 +
 task-sdk/src/airflow/sdk/execution_time/comms.py   |   4 +
 task-sdk/src/airflow/sdk/execution_time/context.py |  18 ++
 .../airflow/sdk/execution_time/schema/schema.json  |  48 +++
 .../src/airflow/sdk/execution_time/supervisor.py   |   4 +
 task-sdk/tests/task_sdk/api/test_client.py         |  89 ++++++
 .../task_sdk/execution_time/test_supervisor.py     | 104 ++++++
 ts-sdk/src/generated/supervisor.ts                 |  12 +-
 30 files changed, 1263 insertions(+), 21 deletions(-)

diff --git a/airflow-core/docs/authoring-and-scheduling/assets.rst 
b/airflow-core/docs/authoring-and-scheduling/assets.rst
index 2a7990d233b..3bd84a054d0 100644
--- a/airflow-core/docs/authoring-and-scheduling/assets.rst
+++ b/airflow-core/docs/authoring-and-scheduling/assets.rst
@@ -247,7 +247,19 @@ Inlet asset events can be read with the ``inlet_events`` 
accessor in the executi
 
 Each value in the ``inlet_events`` mapping is a sequence-like object that 
orders past events of a given asset by ``timestamp``, earliest to latest. It 
supports most of Python's list interface, so you can use ``[-1]`` to access the 
last event, ``[-2:]`` for the last two, etc. The accessor is lazy and only hits 
the database when you access items inside it.
 
-The accessor also supports chaining methods to filter events before fetching 
them. For example, to retrieve only events where specific ``extra`` keys match 
given values:
+The accessor also supports chaining methods to filter events before fetching 
them. For example, to retrieve only events matching a specific partition key 
regular expression:
+
+.. code-block:: python
+
+    @task(inlets=[regional_sales])
+    def process_us_sales(*, inlet_events):
+        us_events = 
inlet_events[regional_sales].partition_key_regexp_pattern(r"^us\|")
+        for event in us_events:
+            print(event.extra, event.partition_key)
+
+For an exact partition key match, use ``.partition_key(value)`` instead. 
Regexp filtering is opt-in: it is enabled only by setting :ref:`[api] 
regexp_query_timeout <config:api__regexp_query_timeout>` to a positive number 
of seconds, which also bounds the query runtime; see the config for the 
security trade-off.
+
+You can also filter events by their ``extra`` key-value pairs:
 
 .. code-block:: python
 
@@ -975,6 +987,45 @@ When a runtime run emits exactly one partition key, the 
producing
 these events the same way as timetable-produced partitions, through
 ``PartitionedAssetTimetable``.
 
+You can also query asset events filtered by partition key using the REST API.
+Two parameters are available:
+
+- ``partition_key`` for **exact match** — uses the B-tree index for fast 
lookups:
+
+.. code-block:: bash
+
+    curl -G "http://<airflow-host>/api/v2/assets/events" \
+      --data-urlencode "partition_key=us|2026-03-10"
+
+- ``partition_key_regexp_pattern`` for **regular-expression filtering**:
+
+.. code-block:: bash
+
+    curl -G "http://<airflow-host>/api/v2/assets/events" \
+      --data-urlencode "partition_key_regexp_pattern=^us"
+
+Both parameters can be combined; the conditions are applied with AND logic.
+
+.. note::
+
+    ``partition_key_regexp_pattern`` is evaluated by the database's own 
regular-expression engine,
+    which is a Regular expression Denial of Service (ReDoS) surface. For that 
reason it is **disabled
+    by default**: it is enabled only by setting :ref:`[api] 
regexp_query_timeout <config:api__regexp_query_timeout>`
+    to a positive number of seconds (fractional values allowed), which 
simultaneously bounds the query
+    runtime (enforced as a ``statement_timeout`` on PostgreSQL and as 
``max_execution_time`` on MySQL).
+    Prefer the exact-match ``partition_key`` (which uses the B-tree index and 
is always enabled)
+    whenever a full key is known.
+
+The same filters are available in the ``InletEventsAccessor``:
+
+.. code-block:: python
+
+    # Exact match
+    events = 
inlet_events[Asset("my_asset")].partition_key("us|2026-03-10").limit(1)
+
+    # Regular-expression pattern
+    events = 
inlet_events[Asset("my_asset")].partition_key_regexp_pattern(r"^us\|2026-03-").limit(10)
+
 Fan-out mappers
 ~~~~~~~~~~~~~~~
 
diff --git a/airflow-core/docs/migrations-ref.rst 
b/airflow-core/docs/migrations-ref.rst
index 3dd6e96a7d0..de64f8123f0 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 
                                                 |
 
+=========================+==================+===================+==============================================================+
-| ``c4e7a1f9b2d0`` (head) | ``436dc127462c`` | ``3.4.0``         | Add index 
on asset.uri.                                      |
+| ``7a98f1b7dbd3`` (head) | ``c4e7a1f9b2d0`` | ``3.4.0``         | Add index 
on asset_event (asset_id, partition_key).          |
++-------------------------+------------------+-------------------+--------------------------------------------------------------+
+| ``c4e7a1f9b2d0``        | ``436dc127462c`` | ``3.4.0``         | Add index 
on asset.uri.                                      |
 
+-------------------------+------------------+-------------------+--------------------------------------------------------------+
 | ``436dc127462c``        | ``5a5d3253e946`` | ``3.4.0``         | Drop 
span_status column.                                     |
 
+-------------------------+------------------+-------------------+--------------------------------------------------------------+
diff --git a/airflow-core/src/airflow/api_fastapi/common/parameters.py 
b/airflow-core/src/airflow/api_fastapi/common/parameters.py
index 060d9478a39..31471d9e26a 100644
--- a/airflow-core/src/airflow/api_fastapi/common/parameters.py
+++ b/airflow-core/src/airflow/api_fastapi/common/parameters.py
@@ -17,8 +17,9 @@
 
 from __future__ import annotations
 
+import re
 from abc import ABC, abstractmethod
-from collections.abc import Callable, Iterable, Sequence
+from collections.abc import Callable, Generator, Iterable, Sequence
 from datetime import datetime
 from enum import Enum
 from typing import (
@@ -41,6 +42,7 @@ from sqlalchemy.inspection import inspect
 from sqlalchemy.sql.functions import FunctionElement
 
 from airflow._shared.timezones import timezone
+from airflow.api_fastapi.common.db.common import SessionDep
 from airflow.api_fastapi.compat import HTTP_422_UNPROCESSABLE_CONTENT
 from airflow.api_fastapi.core_api.base import OrmClause
 from airflow.api_fastapi.core_api.security import GetUserDep
@@ -68,7 +70,7 @@ from airflow.models.taskinstance import TaskInstance
 from airflow.models.variable import Variable
 from airflow.models.xcom import XComModel
 from airflow.typing_compat import Self
-from airflow.utils.sqlalchemy import JsonContains
+from airflow.utils.sqlalchemy import JsonContains, apply_regex_query_timeout
 from airflow.utils.state import DagRunState, TaskInstanceState
 from airflow.utils.types import DagRunType
 
@@ -516,6 +518,75 @@ def search_param_factory(
     return depends_search
 
 
+class _RegexParam(BaseParam[str]):
+    """
+    Filter using database-level regex matching (regexp_match).
+
+    The pattern is handed to the database's own regex engine (via SQLAlchemy's
+    ``regexp_match``), so this filter is gated behind the ``[api] 
regexp_query_timeout``
+    setting to contain the ReDoS attack surface: it cannot be instantiated 
with a value
+    unless a positive timeout is configured (which both enables the feature 
and bounds it).
+
+    Use :func:`regex_param_factory` to build the FastAPI dependency for this 
filter. That
+    dependency also applies 
:func:`airflow.utils.sqlalchemy.apply_regex_query_timeout` to the
+    request's session, so the query runtime is bounded automatically and 
callers never need to
+    remember to do it in the view.
+    """
+
+    def __init__(self, attribute: ColumnElement, value: str | None = None, 
skip_none: bool = True) -> None:
+        super().__init__(value=value, skip_none=skip_none)
+        self.attribute: ColumnElement = attribute
+        if value is not None and conf.getfloat("api", "regexp_query_timeout") 
<= 0:
+            raise HTTPException(
+                status_code=status.HTTP_400_BAD_REQUEST,
+                detail="Regexp query filters are disabled. "
+                "Set [api] regexp_query_timeout to a positive number of 
seconds to enable them.",
+            )
+
+    def to_orm(self, select: Select) -> Select:
+        if self.value is None and self.skip_none:
+            return select
+        return select.where(self.attribute.regexp_match(self.value))
+
+    @classmethod
+    def depends(cls, *args: Any, **kwargs: Any) -> Self:
+        raise NotImplementedError("Use regex_param_factory instead, depends is 
not implemented.")
+
+
+_DEFAULT_REGEX_DESCRIPTION = "Filter results by matching this regular 
expression against the field value."
+
+
+def regex_param_factory(
+    attribute: ColumnElement,
+    pattern_name: str,
+    skip_none: bool = True,
+    description: str = _DEFAULT_REGEX_DESCRIPTION,
+) -> Callable[..., Generator[_RegexParam, None, None]]:
+    def depends_regex(
+        session: SessionDep,
+        value: str | None = Query(alias=pattern_name, default=None, 
description=description),
+    ) -> Generator[_RegexParam, None, None]:
+        if value is not None:
+            try:
+                re.compile(value)
+            except re.error as e:
+                raise HTTPException(
+                    status_code=status.HTTP_400_BAD_REQUEST,
+                    detail=f"Invalid regular expression: {e}",
+                )
+        # ``__init__`` rejects the request (400) when a pattern is supplied 
while the feature is off.
+        param = _RegexParam(attribute, value, skip_none)
+        if value is None:
+            yield param
+            return
+        # Bound the query runtime for the whole request so a view can never 
forget to do it; the
+        # previous timeout is restored on teardown (after the response) before 
the session closes.
+        with apply_regex_query_timeout(session):
+            yield param
+
+    return depends_regex
+
+
 def prefix_search_param_factory(
     attribute: ColumnElement,
     prefix_pattern_name: str,
@@ -1687,6 +1758,15 @@ QueryAssetAliasNamePatternSearch = Annotated[
 QueryAssetAliasNamePrefixPatternSearch = Annotated[
     _PrefixSearchParam, 
Depends(prefix_search_param_factory(AssetAliasModel.name, 
"name_prefix_pattern"))
 ]
+QueryAssetEventPartitionKeyFilter = Annotated[
+    FilterParam[str | None],
+    Depends(filter_param_factory(AssetEvent.partition_key, str | None, 
filter_name="partition_key")),
+]
+QueryAssetEventPartitionKeyRegex = Annotated[
+    _RegexParam,
+    # ``function`` scope so the dependency can depend on the (function-scoped) 
session it bounds.
+    Depends(regex_param_factory(AssetEvent.partition_key, 
"partition_key_regexp_pattern"), scope="function"),
+]
 QueryAssetDagIdPatternSearch = Annotated[
     _DagIdAssetReferenceFilter, Depends(_DagIdAssetReferenceFilter.depends)
 ]
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 46860d9d1cb..4d6be859a13 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
@@ -447,6 +447,26 @@ paths:
           - type: integer
           - type: 'null'
           title: Source Map Index
+      - name: partition_key
+        in: query
+        required: false
+        schema:
+          anyOf:
+          - type: string
+          - type: 'null'
+          title: Partition Key
+      - name: partition_key_regexp_pattern
+        in: query
+        required: false
+        schema:
+          anyOf:
+          - type: string
+          - type: 'null'
+          description: Filter results by matching this regular expression 
against
+            the field value.
+          title: Partition Key Regexp Pattern
+        description: Filter results by matching this regular expression 
against the
+          field value.
       - name: name_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 424938af86d..434ab8f1fdd 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
@@ -38,6 +38,8 @@ from airflow.api_fastapi.common.parameters import (
     QueryAssetAliasNamePrefixPatternSearch,
     QueryAssetDagIdPatternSearch,
     QueryAssetEventExtraFilter,
+    QueryAssetEventPartitionKeyFilter,
+    QueryAssetEventPartitionKeyRegex,
     QueryAssetNamePatternSearch,
     QueryAssetNamePrefixPatternSearch,
     QueryLimit,
@@ -331,6 +333,8 @@ def get_asset_events(
     source_map_index: Annotated[
         FilterParam[int | None], 
Depends(filter_param_factory(AssetEvent.source_map_index, int | None))
     ],
+    partition_key: QueryAssetEventPartitionKeyFilter,
+    partition_key_regexp_pattern: QueryAssetEventPartitionKeyRegex,
     name_pattern: QueryAssetNamePatternSearch,
     name_prefix_pattern: QueryAssetNamePrefixPatternSearch,
     extra_filter: QueryAssetEventExtraFilter,
@@ -338,6 +342,8 @@ def get_asset_events(
     session: SessionDep,
 ) -> AssetEventCollectionResponse:
     """Get asset events."""
+    # The regexp partition-key filter bounds the query runtime automatically 
(its dependency applies
+    # ``apply_regex_query_timeout`` to this request's session), so no explicit 
wrapping is needed here.
     base_statement = select(AssetEvent)
     if name_pattern.value or name_prefix_pattern.value:
         base_statement = base_statement.join(AssetModel, AssetEvent.asset_id 
== AssetModel.id)
@@ -350,6 +356,8 @@ def get_asset_events(
             source_task_id,
             source_run_id,
             source_map_index,
+            partition_key,
+            partition_key_regexp_pattern,
             name_pattern,
             name_prefix_pattern,
             extra_filter,
@@ -364,7 +372,9 @@ def get_asset_events(
     assets_event_select = assets_event_select.options(
         subqueryload(AssetEvent.created_dagruns), joinedload(AssetEvent.asset)
     )
-    assets_events = session.scalars(assets_event_select)
+    # Materialize here (not lazily during response serialization) so the 
regexp query runs while the
+    # dependency-applied timeout is still active.
+    assets_events = session.scalars(assets_event_select).all()
 
     return AssetEventCollectionResponse(
         asset_events=assets_events,
diff --git 
a/airflow-core/src/airflow/api_fastapi/execution_api/routes/asset_events.py 
b/airflow-core/src/airflow/api_fastapi/execution_api/routes/asset_events.py
index b2a98a9de60..4fd73b30672 100644
--- a/airflow-core/src/airflow/api_fastapi/execution_api/routes/asset_events.py
+++ b/airflow-core/src/airflow/api_fastapi/execution_api/routes/asset_events.py
@@ -23,6 +23,11 @@ from fastapi import APIRouter, HTTPException, Query, status
 from sqlalchemy import and_, select
 
 from airflow.api_fastapi.common.db.common import SessionDep
+from airflow.api_fastapi.common.parameters import (
+    BaseParam,
+    QueryAssetEventPartitionKeyFilter,
+    QueryAssetEventPartitionKeyRegex,
+)
 from airflow.api_fastapi.common.types import UtcDateTime
 from airflow.api_fastapi.execution_api.datamodels.asset import AssetResponse
 from airflow.api_fastapi.execution_api.datamodels.asset_event import (
@@ -41,13 +46,23 @@ router = APIRouter(
 
 
 def _get_asset_events_through_sql_clauses(
-    *, join_clause, where_clause, session: SessionDep, ascending: bool = True, 
limit: int | None = None
+    *,
+    join_clause,
+    where_clause,
+    session: SessionDep,
+    ascending: bool = True,
+    limit: int | None = None,
+    filters: list[BaseParam] | None = None,
 ) -> AssetEventsResponse:
     order_by_clause = AssetEvent.timestamp.asc() if ascending else 
AssetEvent.timestamp.desc()
     asset_events_query = 
select(AssetEvent).join(join_clause).where(where_clause).order_by(order_by_clause)
-    if limit is not None:
+    for filter_ in filters or []:
+        asset_events_query = filter_.to_orm(asset_events_query)
+    if limit:
         asset_events_query = asset_events_query.limit(limit)
-    asset_events = session.scalars(asset_events_query)
+    # A regexp partition-key filter bounds the query runtime automatically 
(its dependency applies
+    # apply_regex_query_timeout to this request's session), so no explicit 
wrapping is needed here.
+    asset_events = session.scalars(asset_events_query).all()
     return AssetEventsResponse.model_validate(
         {
             "asset_events": [
@@ -96,6 +111,8 @@ def get_asset_event_by_asset_name_uri(
     name: Annotated[str | None, Query(description="The name of the Asset")],
     uri: Annotated[str | None, Query(description="The URI of the Asset")],
     session: SessionDep,
+    partition_key: QueryAssetEventPartitionKeyFilter,
+    partition_key_regexp_pattern: QueryAssetEventPartitionKeyRegex,
     after: Annotated[UtcDateTime | None, Query(description="The start of the 
time range")] = None,
     before: Annotated[UtcDateTime | None, Query(description="The end of the 
time range")] = None,
     ascending: Annotated[bool, Query(description="Whether to sort results in 
ascending order")] = True,
@@ -136,6 +153,7 @@ def get_asset_event_by_asset_name_uri(
         session=session,
         ascending=ascending,
         limit=limit,
+        filters=[partition_key, partition_key_regexp_pattern],
     )
 
 
@@ -143,6 +161,8 @@ def get_asset_event_by_asset_name_uri(
 def get_asset_event_by_asset_alias(
     name: Annotated[str, Query(description="The name of the Asset Alias")],
     session: SessionDep,
+    partition_key: QueryAssetEventPartitionKeyFilter,
+    partition_key_regexp_pattern: QueryAssetEventPartitionKeyRegex,
     after: Annotated[UtcDateTime | None, Query(description="The start of the 
time range")] = None,
     before: Annotated[UtcDateTime | None, Query(description="The end of the 
time range")] = None,
     ascending: Annotated[bool, Query(description="Whether to sort results in 
ascending order")] = True,
@@ -169,4 +189,5 @@ def get_asset_event_by_asset_alias(
         session=session,
         ascending=ascending,
         limit=limit,
+        filters=[partition_key, partition_key_regexp_pattern],
     )
diff --git a/airflow-core/src/airflow/config_templates/config.yml 
b/airflow-core/src/airflow/config_templates/config.yml
index af11f9fe701..7a65cedb2da 100644
--- a/airflow-core/src/airflow/config_templates/config.yml
+++ b/airflow-core/src/airflow/config_templates/config.yml
@@ -1635,6 +1635,29 @@ api:
       type: boolean
       example: ~
       default: "True"
+    regexp_query_timeout:
+      description: |
+        Timeout, in seconds, for regular-expression based query filters in the 
API. Fractional
+        values are allowed (e.g. ``0.5``). This single value both enables the 
feature and bounds its
+        runtime: a value of ``0`` (the default) disables regexp filtering 
entirely, and any positive
+        value enables it while capping how long such a query may run.
+
+        When enabled, it allows additional filtering features that require 
database-side regexp
+        matching, such as the ``partition_key_regexp_pattern`` filter on ``GET 
/assets/events``
+        and on the Execution API asset-event endpoints. It is disabled by 
default for security
+        reasons: a user-supplied regular expression is passed to the 
database's own regex engine,
+        which is a Regular expression Denial of Service (ReDoS) vector: a 
crafted pattern can
+        force the engine into pathological backtracking and consume database 
backend CPU.
+
+        The timeout is the primary runtime mitigation for that risk: it is 
enforced as a
+        transaction-local ``statement_timeout`` on PostgreSQL and as the 
session ``max_execution_time``
+        on MySQL, so a pathological pattern is aborted instead of pinning a 
database backend. Keep it
+        as low as your legitimate queries allow. Coupling the on/off switch 
with the timeout
+        intentionally makes it impossible to enable regexp filtering without a 
runtime bound in place.
+      version_added: 3.4.0
+      type: float
+      example: ~
+      default: "0"
     secret_key:
       description: |
         Secret key used to run your api server. It should be as random as 
possible. However, when running
diff --git 
a/airflow-core/src/airflow/migrations/versions/0127_3_4_0_add_idx_asset_event_partition_key.py
 
b/airflow-core/src/airflow/migrations/versions/0127_3_4_0_add_idx_asset_event_partition_key.py
new file mode 100644
index 00000000000..d8763debbb9
--- /dev/null
+++ 
b/airflow-core/src/airflow/migrations/versions/0127_3_4_0_add_idx_asset_event_partition_key.py
@@ -0,0 +1,51 @@
+#
+# 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_event (asset_id, partition_key).
+
+Revision ID: 7a98f1b7dbd3
+Revises: c4e7a1f9b2d0
+Create Date: 2026-04-01 00:00:00.000000
+
+"""
+
+from __future__ import annotations
+
+from alembic import op
+
+# revision identifiers, used by Alembic.
+revision = "7a98f1b7dbd3"
+down_revision = "c4e7a1f9b2d0"
+branch_labels = None
+depends_on = None
+airflow_version = "3.4.0"
+
+
+def upgrade():
+    """Apply Add index on asset_event (asset_id, partition_key)."""
+    op.create_index(
+        "idx_asset_event_asset_id_partition_key",
+        "asset_event",
+        ["asset_id", "partition_key"],
+    )
+
+
+def downgrade():
+    """Unapply Add index on asset_event (asset_id, partition_key)."""
+    op.drop_index("idx_asset_event_asset_id_partition_key", 
table_name="asset_event")
diff --git a/airflow-core/src/airflow/models/asset.py 
b/airflow-core/src/airflow/models/asset.py
index 0788611d8a0..7aafddb3b94 100644
--- a/airflow-core/src/airflow/models/asset.py
+++ b/airflow-core/src/airflow/models/asset.py
@@ -828,6 +828,7 @@ class AssetEvent(Base):
     __tablename__ = "asset_event"
     __table_args__ = (
         Index("idx_asset_id_timestamp", asset_id, timestamp),
+        Index("idx_asset_event_asset_id_partition_key", asset_id, 
partition_key),
         {"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 8ecba7c6df5..fa4853f6050 100644
--- a/airflow-core/src/airflow/ui/openapi-gen/queries/common.ts
+++ b/airflow-core/src/airflow/ui/openapi-gen/queries/common.ts
@@ -37,7 +37,7 @@ export const UseAssetServiceGetAssetAliasKeyFn = ({ 
assetAliasId }: {
 export type AssetServiceGetAssetEventsDefaultResponse = 
Awaited<ReturnType<typeof AssetService.getAssetEvents>>;
 export type AssetServiceGetAssetEventsQueryResult<TData = 
AssetServiceGetAssetEventsDefaultResponse, TError = unknown> = 
UseQueryResult<TData, TError>;
 export const useAssetServiceGetAssetEventsKey = "AssetServiceGetAssetEvents";
-export const UseAssetServiceGetAssetEventsKeyFn = ({ assetId, extra, limit, 
namePattern, namePrefixPattern, offset, orderBy, sourceDagId, sourceMapIndex, 
sourceRunId, sourceTaskId, timestampGt, timestampGte, timestampLt, timestampLte 
}: {
+export const UseAssetServiceGetAssetEventsKeyFn = ({ assetId, extra, limit, 
namePattern, namePrefixPattern, offset, orderBy, partitionKey, 
partitionKeyRegexpPattern, sourceDagId, sourceMapIndex, sourceRunId, 
sourceTaskId, timestampGt, timestampGte, timestampLt, timestampLte }: {
   assetId?: number;
   extra?: string[];
   limit?: number;
@@ -45,6 +45,8 @@ export const UseAssetServiceGetAssetEventsKeyFn = ({ assetId, 
extra, limit, name
   namePrefixPattern?: string;
   offset?: number;
   orderBy?: string[];
+  partitionKey?: string;
+  partitionKeyRegexpPattern?: string;
   sourceDagId?: string;
   sourceMapIndex?: number;
   sourceRunId?: string;
@@ -53,7 +55,7 @@ export const UseAssetServiceGetAssetEventsKeyFn = ({ assetId, 
extra, limit, name
   timestampGte?: string;
   timestampLt?: string;
   timestampLte?: string;
-} = {}, queryKey?: Array<unknown>) => [useAssetServiceGetAssetEventsKey, 
...(queryKey ?? [{ assetId, extra, limit, namePattern, namePrefixPattern, 
offset, orderBy, sourceDagId, sourceMapIndex, sourceRunId, sourceTaskId, 
timestampGt, timestampGte, timestampLt, timestampLte }])];
+} = {}, queryKey?: Array<unknown>) => [useAssetServiceGetAssetEventsKey, 
...(queryKey ?? [{ assetId, extra, limit, namePattern, namePrefixPattern, 
offset, orderBy, partitionKey, partitionKeyRegexpPattern, sourceDagId, 
sourceMapIndex, sourceRunId, sourceTaskId, timestampGt, timestampGte, 
timestampLt, timestampLte }])];
 export type AssetServiceGetAssetQueuedEventsDefaultResponse = 
Awaited<ReturnType<typeof AssetService.getAssetQueuedEvents>>;
 export type AssetServiceGetAssetQueuedEventsQueryResult<TData = 
AssetServiceGetAssetQueuedEventsDefaultResponse, TError = unknown> = 
UseQueryResult<TData, TError>;
 export const useAssetServiceGetAssetQueuedEventsKey = 
"AssetServiceGetAssetQueuedEvents";
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 1664d510fc5..f83894f1bef 100644
--- a/airflow-core/src/airflow/ui/openapi-gen/queries/ensureQueryData.ts
+++ b/airflow-core/src/airflow/ui/openapi-gen/queries/ensureQueryData.ts
@@ -81,6 +81,8 @@ export const ensureUseAssetServiceGetAssetAliasData = 
(queryClient: QueryClient,
 * @param data.sourceTaskId
 * @param data.sourceRunId
 * @param data.sourceMapIndex
+* @param data.partitionKey
+* @param data.partitionKeyRegexpPattern Filter results by matching this 
regular expression against the field value.
 * @param data.namePattern 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 
``name_prefix_pattern`` parameter when possible.
@@ -93,7 +95,7 @@ export const ensureUseAssetServiceGetAssetAliasData = 
(queryClient: QueryClient,
 * @returns AssetEventCollectionResponse Successful Response
 * @throws ApiError
 */
-export const ensureUseAssetServiceGetAssetEventsData = (queryClient: 
QueryClient, { assetId, extra, limit, namePattern, namePrefixPattern, offset, 
orderBy, sourceDagId, sourceMapIndex, sourceRunId, sourceTaskId, timestampGt, 
timestampGte, timestampLt, timestampLte }: {
+export const ensureUseAssetServiceGetAssetEventsData = (queryClient: 
QueryClient, { assetId, extra, limit, namePattern, namePrefixPattern, offset, 
orderBy, partitionKey, partitionKeyRegexpPattern, sourceDagId, sourceMapIndex, 
sourceRunId, sourceTaskId, timestampGt, timestampGte, timestampLt, timestampLte 
}: {
   assetId?: number;
   extra?: string[];
   limit?: number;
@@ -101,6 +103,8 @@ export const ensureUseAssetServiceGetAssetEventsData = 
(queryClient: QueryClient
   namePrefixPattern?: string;
   offset?: number;
   orderBy?: string[];
+  partitionKey?: string;
+  partitionKeyRegexpPattern?: string;
   sourceDagId?: string;
   sourceMapIndex?: number;
   sourceRunId?: string;
@@ -109,7 +113,7 @@ export const ensureUseAssetServiceGetAssetEventsData = 
(queryClient: QueryClient
   timestampGte?: string;
   timestampLt?: string;
   timestampLte?: string;
-} = {}) => queryClient.ensureQueryData({ queryKey: 
Common.UseAssetServiceGetAssetEventsKeyFn({ assetId, extra, limit, namePattern, 
namePrefixPattern, offset, orderBy, sourceDagId, sourceMapIndex, sourceRunId, 
sourceTaskId, timestampGt, timestampGte, timestampLt, timestampLte }), queryFn: 
() => AssetService.getAssetEvents({ assetId, extra, limit, namePattern, 
namePrefixPattern, offset, orderBy, sourceDagId, sourceMapIndex, sourceRunId, 
sourceTaskId, timestampGt, timestampGte, timestampLt, [...]
+} = {}) => queryClient.ensureQueryData({ queryKey: 
Common.UseAssetServiceGetAssetEventsKeyFn({ assetId, extra, limit, namePattern, 
namePrefixPattern, offset, orderBy, partitionKey, partitionKeyRegexpPattern, 
sourceDagId, sourceMapIndex, sourceRunId, sourceTaskId, timestampGt, 
timestampGte, timestampLt, timestampLte }), queryFn: () => 
AssetService.getAssetEvents({ assetId, extra, limit, namePattern, 
namePrefixPattern, offset, orderBy, partitionKey, partitionKeyRegexpPattern, 
sourceDagId,  [...]
 /**
 * Get Asset Queued Events
 * Get queued asset events for an asset.
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 d0cecdeb3d5..1b4b2d46aac 100644
--- a/airflow-core/src/airflow/ui/openapi-gen/queries/prefetch.ts
+++ b/airflow-core/src/airflow/ui/openapi-gen/queries/prefetch.ts
@@ -81,6 +81,8 @@ export const prefetchUseAssetServiceGetAssetAlias = 
(queryClient: QueryClient, {
 * @param data.sourceTaskId
 * @param data.sourceRunId
 * @param data.sourceMapIndex
+* @param data.partitionKey
+* @param data.partitionKeyRegexpPattern Filter results by matching this 
regular expression against the field value.
 * @param data.namePattern 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 
``name_prefix_pattern`` parameter when possible.
@@ -93,7 +95,7 @@ export const prefetchUseAssetServiceGetAssetAlias = 
(queryClient: QueryClient, {
 * @returns AssetEventCollectionResponse Successful Response
 * @throws ApiError
 */
-export const prefetchUseAssetServiceGetAssetEvents = (queryClient: 
QueryClient, { assetId, extra, limit, namePattern, namePrefixPattern, offset, 
orderBy, sourceDagId, sourceMapIndex, sourceRunId, sourceTaskId, timestampGt, 
timestampGte, timestampLt, timestampLte }: {
+export const prefetchUseAssetServiceGetAssetEvents = (queryClient: 
QueryClient, { assetId, extra, limit, namePattern, namePrefixPattern, offset, 
orderBy, partitionKey, partitionKeyRegexpPattern, sourceDagId, sourceMapIndex, 
sourceRunId, sourceTaskId, timestampGt, timestampGte, timestampLt, timestampLte 
}: {
   assetId?: number;
   extra?: string[];
   limit?: number;
@@ -101,6 +103,8 @@ export const prefetchUseAssetServiceGetAssetEvents = 
(queryClient: QueryClient,
   namePrefixPattern?: string;
   offset?: number;
   orderBy?: string[];
+  partitionKey?: string;
+  partitionKeyRegexpPattern?: string;
   sourceDagId?: string;
   sourceMapIndex?: number;
   sourceRunId?: string;
@@ -109,7 +113,7 @@ export const prefetchUseAssetServiceGetAssetEvents = 
(queryClient: QueryClient,
   timestampGte?: string;
   timestampLt?: string;
   timestampLte?: string;
-} = {}) => queryClient.prefetchQuery({ queryKey: 
Common.UseAssetServiceGetAssetEventsKeyFn({ assetId, extra, limit, namePattern, 
namePrefixPattern, offset, orderBy, sourceDagId, sourceMapIndex, sourceRunId, 
sourceTaskId, timestampGt, timestampGte, timestampLt, timestampLte }), queryFn: 
() => AssetService.getAssetEvents({ assetId, extra, limit, namePattern, 
namePrefixPattern, offset, orderBy, sourceDagId, sourceMapIndex, sourceRunId, 
sourceTaskId, timestampGt, timestampGte, timestampLt, t [...]
+} = {}) => queryClient.prefetchQuery({ queryKey: 
Common.UseAssetServiceGetAssetEventsKeyFn({ assetId, extra, limit, namePattern, 
namePrefixPattern, offset, orderBy, partitionKey, partitionKeyRegexpPattern, 
sourceDagId, sourceMapIndex, sourceRunId, sourceTaskId, timestampGt, 
timestampGte, timestampLt, timestampLte }), queryFn: () => 
AssetService.getAssetEvents({ assetId, extra, limit, namePattern, 
namePrefixPattern, offset, orderBy, partitionKey, partitionKeyRegexpPattern, 
sourceDagId, so [...]
 /**
 * Get Asset Queued Events
 * Get queued asset events for an asset.
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 c859fb7260a..3d334e51a96 100644
--- a/airflow-core/src/airflow/ui/openapi-gen/queries/queries.ts
+++ b/airflow-core/src/airflow/ui/openapi-gen/queries/queries.ts
@@ -81,6 +81,8 @@ export const useAssetServiceGetAssetAlias = <TData = 
Common.AssetServiceGetAsset
 * @param data.sourceTaskId
 * @param data.sourceRunId
 * @param data.sourceMapIndex
+* @param data.partitionKey
+* @param data.partitionKeyRegexpPattern Filter results by matching this 
regular expression against the field value.
 * @param data.namePattern 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 
``name_prefix_pattern`` parameter when possible.
@@ -93,7 +95,7 @@ export const useAssetServiceGetAssetAlias = <TData = 
Common.AssetServiceGetAsset
 * @returns AssetEventCollectionResponse Successful Response
 * @throws ApiError
 */
-export const useAssetServiceGetAssetEvents = <TData = 
Common.AssetServiceGetAssetEventsDefaultResponse, TError = unknown, TQueryKey 
extends Array<unknown> = unknown[]>({ assetId, extra, limit, namePattern, 
namePrefixPattern, offset, orderBy, sourceDagId, sourceMapIndex, sourceRunId, 
sourceTaskId, timestampGt, timestampGte, timestampLt, timestampLte }: {
+export const useAssetServiceGetAssetEvents = <TData = 
Common.AssetServiceGetAssetEventsDefaultResponse, TError = unknown, TQueryKey 
extends Array<unknown> = unknown[]>({ assetId, extra, limit, namePattern, 
namePrefixPattern, offset, orderBy, partitionKey, partitionKeyRegexpPattern, 
sourceDagId, sourceMapIndex, sourceRunId, sourceTaskId, timestampGt, 
timestampGte, timestampLt, timestampLte }: {
   assetId?: number;
   extra?: string[];
   limit?: number;
@@ -101,6 +103,8 @@ export const useAssetServiceGetAssetEvents = <TData = 
Common.AssetServiceGetAsse
   namePrefixPattern?: string;
   offset?: number;
   orderBy?: string[];
+  partitionKey?: string;
+  partitionKeyRegexpPattern?: string;
   sourceDagId?: string;
   sourceMapIndex?: number;
   sourceRunId?: string;
@@ -109,7 +113,7 @@ export const useAssetServiceGetAssetEvents = <TData = 
Common.AssetServiceGetAsse
   timestampGte?: string;
   timestampLt?: string;
   timestampLte?: string;
-} = {}, queryKey?: TQueryKey, options?: Omit<UseQueryOptions<TData, TError>, 
"queryKey" | "queryFn">) => useQuery<TData, TError>({ queryKey: 
Common.UseAssetServiceGetAssetEventsKeyFn({ assetId, extra, limit, namePattern, 
namePrefixPattern, offset, orderBy, sourceDagId, sourceMapIndex, sourceRunId, 
sourceTaskId, timestampGt, timestampGte, timestampLt, timestampLte }, 
queryKey), queryFn: () => AssetService.getAssetEvents({ assetId, extra, limit, 
namePattern, namePrefixPattern, offset, orde [...]
+} = {}, queryKey?: TQueryKey, options?: Omit<UseQueryOptions<TData, TError>, 
"queryKey" | "queryFn">) => useQuery<TData, TError>({ queryKey: 
Common.UseAssetServiceGetAssetEventsKeyFn({ assetId, extra, limit, namePattern, 
namePrefixPattern, offset, orderBy, partitionKey, partitionKeyRegexpPattern, 
sourceDagId, sourceMapIndex, sourceRunId, sourceTaskId, timestampGt, 
timestampGte, timestampLt, timestampLte }, queryKey), queryFn: () => 
AssetService.getAssetEvents({ assetId, extra, limit, nam [...]
 /**
 * Get Asset Queued Events
 * Get queued asset events for an asset.
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 4ca06b9e87c..a83ac5cb0ae 100644
--- a/airflow-core/src/airflow/ui/openapi-gen/queries/suspense.ts
+++ b/airflow-core/src/airflow/ui/openapi-gen/queries/suspense.ts
@@ -81,6 +81,8 @@ export const useAssetServiceGetAssetAliasSuspense = <TData = 
Common.AssetService
 * @param data.sourceTaskId
 * @param data.sourceRunId
 * @param data.sourceMapIndex
+* @param data.partitionKey
+* @param data.partitionKeyRegexpPattern Filter results by matching this 
regular expression against the field value.
 * @param data.namePattern 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 
``name_prefix_pattern`` parameter when possible.
@@ -93,7 +95,7 @@ export const useAssetServiceGetAssetAliasSuspense = <TData = 
Common.AssetService
 * @returns AssetEventCollectionResponse Successful Response
 * @throws ApiError
 */
-export const useAssetServiceGetAssetEventsSuspense = <TData = 
Common.AssetServiceGetAssetEventsDefaultResponse, TError = unknown, TQueryKey 
extends Array<unknown> = unknown[]>({ assetId, extra, limit, namePattern, 
namePrefixPattern, offset, orderBy, sourceDagId, sourceMapIndex, sourceRunId, 
sourceTaskId, timestampGt, timestampGte, timestampLt, timestampLte }: {
+export const useAssetServiceGetAssetEventsSuspense = <TData = 
Common.AssetServiceGetAssetEventsDefaultResponse, TError = unknown, TQueryKey 
extends Array<unknown> = unknown[]>({ assetId, extra, limit, namePattern, 
namePrefixPattern, offset, orderBy, partitionKey, partitionKeyRegexpPattern, 
sourceDagId, sourceMapIndex, sourceRunId, sourceTaskId, timestampGt, 
timestampGte, timestampLt, timestampLte }: {
   assetId?: number;
   extra?: string[];
   limit?: number;
@@ -101,6 +103,8 @@ export const useAssetServiceGetAssetEventsSuspense = <TData 
= Common.AssetServic
   namePrefixPattern?: string;
   offset?: number;
   orderBy?: string[];
+  partitionKey?: string;
+  partitionKeyRegexpPattern?: string;
   sourceDagId?: string;
   sourceMapIndex?: number;
   sourceRunId?: string;
@@ -109,7 +113,7 @@ export const useAssetServiceGetAssetEventsSuspense = <TData 
= Common.AssetServic
   timestampGte?: string;
   timestampLt?: string;
   timestampLte?: string;
-} = {}, queryKey?: TQueryKey, options?: Omit<UseQueryOptions<TData, TError>, 
"queryKey" | "queryFn">) => useSuspenseQuery<TData, TError>({ queryKey: 
Common.UseAssetServiceGetAssetEventsKeyFn({ assetId, extra, limit, namePattern, 
namePrefixPattern, offset, orderBy, sourceDagId, sourceMapIndex, sourceRunId, 
sourceTaskId, timestampGt, timestampGte, timestampLt, timestampLte }, 
queryKey), queryFn: () => AssetService.getAssetEvents({ assetId, extra, limit, 
namePattern, namePrefixPattern, offs [...]
+} = {}, queryKey?: TQueryKey, options?: Omit<UseQueryOptions<TData, TError>, 
"queryKey" | "queryFn">) => useSuspenseQuery<TData, TError>({ queryKey: 
Common.UseAssetServiceGetAssetEventsKeyFn({ assetId, extra, limit, namePattern, 
namePrefixPattern, offset, orderBy, partitionKey, partitionKeyRegexpPattern, 
sourceDagId, sourceMapIndex, sourceRunId, sourceTaskId, timestampGt, 
timestampGte, timestampLt, timestampLte }, queryKey), queryFn: () => 
AssetService.getAssetEvents({ assetId, extra, li [...]
 /**
 * Get Asset Queued Events
 * Get queued asset events for an asset.
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 7008d465426..0d225056ac2 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
@@ -122,6 +122,8 @@ export class AssetService {
      * @param data.sourceTaskId
      * @param data.sourceRunId
      * @param data.sourceMapIndex
+     * @param data.partitionKey
+     * @param data.partitionKeyRegexpPattern Filter results by matching this 
regular expression against the field value.
      * @param data.namePattern 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 
``name_prefix_pattern`` parameter when possible.
@@ -147,6 +149,8 @@ export class AssetService {
                 source_task_id: data.sourceTaskId,
                 source_run_id: data.sourceRunId,
                 source_map_index: data.sourceMapIndex,
+                partition_key: data.partitionKey,
+                partition_key_regexp_pattern: data.partitionKeyRegexpPattern,
                 name_pattern: data.namePattern,
                 name_prefix_pattern: data.namePrefixPattern,
                 extra: data.extra,
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 7e8597168b8..f43c5708a08 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
@@ -2904,6 +2904,11 @@ export type GetAssetEventsData = {
      * Attributes to order by, multi criteria sort is supported. Prefix with 
`-` for descending order. Supported attributes: `source_task_id, source_dag_id, 
source_run_id, source_map_index, timestamp`
      */
     orderBy?: Array<(string)>;
+    partitionKey?: string | null;
+    /**
+     * Filter results by matching this regular expression against the field 
value.
+     */
+    partitionKeyRegexpPattern?: string | null;
     sourceDagId?: string | null;
     sourceMapIndex?: number | null;
     sourceRunId?: string | null;
diff --git a/airflow-core/src/airflow/utils/db.py 
b/airflow-core/src/airflow/utils/db.py
index ce4ef036d20..1076715ee71 100644
--- a/airflow-core/src/airflow/utils/db.py
+++ b/airflow-core/src/airflow/utils/db.py
@@ -117,7 +117,7 @@ _REVISION_HEADS_MAP: dict[str, str] = {
     "3.1.8": "509b94a1042d",
     "3.2.0": "1d6611b6ab7c",
     "3.3.0": "d2f4e1b3c5a7",
-    "3.4.0": "c4e7a1f9b2d0",
+    "3.4.0": "7a98f1b7dbd3",
 }
 
 # Prefix used to identify tables holding data moved during migration.
diff --git a/airflow-core/src/airflow/utils/sqlalchemy.py 
b/airflow-core/src/airflow/utils/sqlalchemy.py
index 7ca788ffe12..0064c0ec3c8 100644
--- a/airflow-core/src/airflow/utils/sqlalchemy.py
+++ b/airflow-core/src/airflow/utils/sqlalchemy.py
@@ -62,6 +62,58 @@ def get_dialect_name(session: Session) -> str | None:
     return getattr(bind.dialect, "name", None)
 
 
[email protected]
+def apply_regex_query_timeout(session: Session) -> Generator[None, None, None]:
+    """
+    Bound the runtime of a user-supplied regex filter, scoped to the wrapped 
query.
+
+    Reads the ``[api] regexp_query_timeout`` config (in seconds, fractional 
values allowed) and sets
+    a database-side timeout for the duration of the ``with`` block, then 
restores the previous value
+    so it does not affect any other statement running later in the same 
transaction/session:
+
+    * PostgreSQL: transaction-local ``statement_timeout`` (via 
``set_config(..., is_local=True)``).
+    * MySQL: session ``max_execution_time`` (applies to read-only ``SELECT`` 
statements).
+
+    The previous value is captured and restored (rather than reset to ``0``) 
so a server- or
+    role-level global timeout is preserved instead of being cleared. This is a 
ReDoS safeguard: a
+    malicious pattern is aborted instead of pinning a database backend. No-op 
on other backends
+    (e.g. SQLite) and when the configured timeout is ``0`` (which also means 
regexp filtering is
+    disabled).
+    """
+    timeout_seconds = conf.getfloat("api", "regexp_query_timeout")
+    if timeout_seconds <= 0:
+        yield
+        return
+    # ``timeout_ms`` is derived from the (operator-controlled) config, not 
from user input.
+    timeout_ms = int(timeout_seconds * 1000)
+    dialect_name = get_dialect_name(session)
+    if dialect_name == "postgresql":
+        previous = session.execute(text("SELECT 
current_setting('statement_timeout')")).scalar()
+        session.execute(
+            text("SELECT set_config('statement_timeout', :timeout, 
true)").bindparams(timeout=str(timeout_ms))
+        )
+        try:
+            yield
+        finally:
+            # Restore the previous value (e.g. a global statement_timeout) 
instead of clearing it, so
+            # the bound only applies to the regexp query and not later 
statements in the transaction.
+            session.execute(
+                text("SELECT set_config('statement_timeout', :timeout, 
true)").bindparams(
+                    timeout=str(previous) if previous is not None else "0"
+                )
+            )
+    elif dialect_name == "mysql":
+        previous = session.execute(text("SELECT 
@@SESSION.max_execution_time")).scalar()
+        session.execute(text(f"SET SESSION max_execution_time = {timeout_ms}"))
+        try:
+            yield
+        finally:
+            # Restore the previous value so a global max_execution_time is 
preserved.
+            session.execute(text(f"SET SESSION max_execution_time = 
{int(previous or 0)}"))
+    else:
+        yield
+
+
 def build_upsert_stmt(
     dialect: str | None,
     model: Any,
diff --git a/airflow-core/tests/unit/api_fastapi/common/test_parameters.py 
b/airflow-core/tests/unit/api_fastapi/common/test_parameters.py
index 4d2f473f4f8..68df51a3f72 100644
--- a/airflow-core/tests/unit/api_fastapi/common/test_parameters.py
+++ b/airflow-core/tests/unit/api_fastapi/common/test_parameters.py
@@ -21,6 +21,7 @@ import re
 from datetime import datetime, timezone
 from types import SimpleNamespace
 from typing import Annotated
+from unittest import mock
 
 import pytest
 from fastapi import Depends, FastAPI, HTTPException
@@ -41,11 +42,15 @@ from airflow.api_fastapi.common.parameters import (
     _TaskDisplayNamePrefixPatternParam,
     datetime_range_filter_factory,
     filter_param_factory,
+    regex_param_factory,
 )
 from airflow.models import DagModel, DagRun, Log
+from airflow.models.asset import AssetEvent
 from airflow.models.errors import ParseImportError
 from airflow.models.taskinstance import TaskInstance
 
+from tests_common.test_utils.config import conf_vars
+
 
 class TestFilterParam:
     def test_filter_param_factory_description(self):
@@ -537,3 +542,51 @@ class TestDatetimeRangeFilterFactory:
         rf = _make_datetime_filter("start_date", upper_bound_lte=bound)
         sql = _compile(rf.to_orm(select(TaskInstance)))
         assert "coalesce" not in sql
+
+
+class TestRegexParamFactory:
+    """The regexp filter dependency must apply the query timeout itself 
(callers can't forget)."""
+
+    @staticmethod
+    def _depends():
+        return regex_param_factory(AssetEvent.partition_key, 
"partition_key_regexp_pattern")
+
+    def test_dependency_applies_timeout_when_pattern_provided(self):
+        session = mock.MagicMock()
+        with conf_vars({("api", "regexp_query_timeout"): "30"}):
+            with 
mock.patch("airflow.api_fastapi.common.parameters.apply_regex_query_timeout") 
as apply_mock:
+                gen = self._depends()(session=session, value="^us")
+                param = next(gen)
+                apply_mock.assert_called_once_with(session)
+                assert param.value == "^us"
+                with pytest.raises(StopIteration):
+                    next(gen)
+
+    def test_dependency_skips_timeout_without_pattern(self):
+        session = mock.MagicMock()
+        with conf_vars({("api", "regexp_query_timeout"): "30"}):
+            with 
mock.patch("airflow.api_fastapi.common.parameters.apply_regex_query_timeout") 
as apply_mock:
+                gen = self._depends()(session=session, value=None)
+                param = next(gen)
+                apply_mock.assert_not_called()
+                assert param.value is None
+                with pytest.raises(StopIteration):
+                    next(gen)
+
+    def test_dependency_rejects_pattern_when_disabled(self):
+        session = mock.MagicMock()
+        with conf_vars({("api", "regexp_query_timeout"): "0"}):
+            gen = self._depends()(session=session, value="^us")
+            with pytest.raises(HTTPException) as exc:
+                next(gen)
+        assert exc.value.status_code == 400
+        assert "disabled" in exc.value.detail
+
+    def test_dependency_rejects_invalid_regex(self):
+        session = mock.MagicMock()
+        with conf_vars({("api", "regexp_query_timeout"): "30"}):
+            gen = self._depends()(session=session, value="[invalid(")
+            with pytest.raises(HTTPException) as exc:
+                next(gen)
+        assert exc.value.status_code == 400
+        assert "Invalid regular expression" in exc.value.detail
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 d5c56d133ad..835a8ccb6dc 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
@@ -1087,6 +1087,162 @@ class TestGetAssetEvents(TestAssets):
         }
 
 
+class TestGetAssetEventsPartitionKeyRegex(TestAssets):
+    """Tests for partition_key_regexp_pattern regex filter on GET 
/assets/events.
+
+    Patterns are written to work consistently across PostgreSQL (~),
+    MySQL (REGEXP), and SQLite (re.match), including both anchored and
+    unanchored expressions where appropriate.
+    """
+
+    @pytest.fixture(autouse=True)
+    def _enable_regexp_query_filters(self):
+        with conf_vars({("api", "regexp_query_timeout"): "30"}):
+            yield
+
+    @pytest.fixture(autouse=True)
+    def _create_partition_key_test_data(self, setup, session):
+        _create_assets(session=session)
+        events = [
+            AssetEvent(
+                asset_id=1,
+                extra={},
+                source_task_id="t",
+                source_dag_id="d",
+                source_run_id="r1",
+                partition_key="2024-01-01",
+                timestamp=DEFAULT_DATE,
+            ),
+            AssetEvent(
+                asset_id=2,
+                extra={},
+                source_task_id="t",
+                source_dag_id="d",
+                source_run_id="r2",
+                partition_key="2024-01-02",
+                timestamp=DEFAULT_DATE,
+            ),
+            AssetEvent(
+                asset_id=1,
+                extra={},
+                source_task_id="t",
+                source_dag_id="d",
+                source_run_id="r3",
+                partition_key="us|2024-01-01",
+                timestamp=DEFAULT_DATE,
+            ),
+            AssetEvent(
+                asset_id=2,
+                extra={},
+                source_task_id="t",
+                source_dag_id="d",
+                source_run_id="r4",
+                partition_key="eu|2024-01-01",
+                timestamp=DEFAULT_DATE,
+            ),
+            AssetEvent(
+                asset_id=1,
+                extra={},
+                source_task_id="t",
+                source_dag_id="d",
+                source_run_id="r5",
+                partition_key="apac|2024-03-20",
+                timestamp=DEFAULT_DATE,
+            ),
+            AssetEvent(
+                asset_id=1,
+                extra={},
+                source_task_id="t",
+                source_dag_id="d",
+                source_run_id="r6",
+                partition_key=None,
+                timestamp=DEFAULT_DATE,
+            ),
+        ]
+        session.add_all(events)
+        session.commit()
+
+    @pytest.mark.parametrize(
+        ("partition_key_regexp_pattern", "expected_count"),
+        [
+            ("^2024-01-01$", 1),
+            ("^2024-01-", 2),
+            ("^us\\|", 1),
+            (".*\\|2024-01-01$", 2),
+            ("^(us|eu)\\|", 2),
+            ("^nonexistent", 0),
+        ],
+    )
+    def test_partition_key_regexp_pattern_filtering(
+        self, test_client, partition_key_regexp_pattern, expected_count
+    ):
+        response = test_client.get(
+            "/assets/events", params={"partition_key_regexp_pattern": 
partition_key_regexp_pattern}
+        )
+        assert response.status_code == 200
+        assert response.json()["total_entries"] == expected_count
+
+    @pytest.mark.parametrize(
+        ("params", "expected_count"),
+        [
+            ({"partition_key_regexp_pattern": "^us\\|", "asset_id": "1"}, 1),
+            ({"partition_key_regexp_pattern": "^us\\|", "asset_id": "2"}, 0),
+            ({"partition_key_regexp_pattern": ".*\\|2024-01-01$", 
"source_dag_id": "d"}, 2),
+            ({"partition_key_regexp_pattern": ".*\\|2024-01-01$", 
"source_dag_id": "other"}, 0),
+        ],
+    )
+    def test_partition_key_regexp_pattern_combined_filters(self, test_client, 
params, expected_count):
+        response = test_client.get("/assets/events", params=params)
+        assert response.status_code == 200
+        assert response.json()["total_entries"] == expected_count
+
+    def test_partition_key_regexp_pattern_invalid_regex_returns_400(self, 
test_client):
+        response = test_client.get(
+            "/assets/events", params={"partition_key_regexp_pattern": 
"[invalid(regex"}
+        )
+        assert response.status_code == 400
+        assert "Invalid regular expression" in response.json()["detail"]
+
+    def test_partition_key_regexp_pattern_disabled_returns_400(self, 
test_client):
+        with conf_vars({("api", "regexp_query_timeout"): "0"}):
+            response = test_client.get("/assets/events", 
params={"partition_key_regexp_pattern": "^2024-"})
+        assert response.status_code == 400
+        assert "disabled" in response.json()["detail"]
+
+    def test_exact_match_works_when_regex_disabled(self, test_client):
+        with conf_vars({("api", "regexp_query_timeout"): "0"}):
+            response = test_client.get("/assets/events", 
params={"partition_key": "2024-01-01"})
+        assert response.status_code == 200
+        assert response.json()["total_entries"] == 1
+
+    def test_partition_key_exact_match_via_regex(self, test_client):
+        response = test_client.get("/assets/events", 
params={"partition_key_regexp_pattern": "^2024-01-01$"})
+        assert response.status_code == 200
+        assert response.json()["total_entries"] == 1
+
+    @pytest.mark.parametrize(
+        ("partition_key", "expected_count"),
+        [
+            ("2024-01-01", 1),
+            ("us|2024-01-01", 1),
+            ("nonexistent", 0),
+        ],
+    )
+    def test_partition_key_exact_match(self, test_client, partition_key, 
expected_count):
+        response = test_client.get("/assets/events", params={"partition_key": 
partition_key})
+        assert response.status_code == 200
+        assert response.json()["total_entries"] == expected_count
+
+    def test_partition_key_and_pattern_combined(self, test_client):
+        # Both filters are allowed and combine with AND: a disjoint pair 
yields no results.
+        response = test_client.get(
+            "/assets/events",
+            params={"partition_key": "2024-01-01", 
"partition_key_regexp_pattern": "^2025-"},
+        )
+        assert response.status_code == 200
+        assert response.json()["total_entries"] == 0
+
+
 class TestGetAssetEventsExtraFilter(TestAssets):
     @pytest.fixture
     def _setup(self, session):
diff --git 
a/airflow-core/tests/unit/api_fastapi/execution_api/versions/head/test_asset_events.py
 
b/airflow-core/tests/unit/api_fastapi/execution_api/versions/head/test_asset_events.py
index 6300265472a..da1e3705daf 100644
--- 
a/airflow-core/tests/unit/api_fastapi/execution_api/versions/head/test_asset_events.py
+++ 
b/airflow-core/tests/unit/api_fastapi/execution_api/versions/head/test_asset_events.py
@@ -24,6 +24,8 @@ import pytest
 from airflow._shared.timezones import timezone
 from airflow.models.asset import AssetActive, AssetAliasModel, AssetEvent, 
AssetModel
 
+from tests_common.test_utils.config import conf_vars
+
 DEFAULT_DATE = timezone.parse("2021-01-01T00:00:00")
 
 pytestmark = pytest.mark.db_test
@@ -523,6 +525,355 @@ class TestGetAssetEventByAssetAlias:
         }
 
 
+class TestGetAssetEventByAssetPartitionKey:
+    """Tests for partition_key_regexp_pattern regex filter on execution API.
+
+    Patterns are written to work consistently across PostgreSQL (~),
+    MySQL (REGEXP), and SQLite (re.match). They are not necessarily
+    ^-anchored; some cases use explicit prefixes such as ``.*`` to
+    achieve SQLite ``re.match``-compatible behavior.
+    """
+
+    @pytest.fixture(autouse=True)
+    def _enable_regexp_query_filters(self):
+        with conf_vars({("api", "regexp_query_timeout"): "30"}):
+            yield
+
+    @pytest.fixture
+    def test_partitioned_events(self, session, test_asset):
+        def make_timestamp(day):
+            return datetime(2021, 1, day, tzinfo=timezone.utc)
+
+        common = {
+            "asset_id": 1,
+            "extra": {"foo": "bar"},
+            "source_dag_id": "foo",
+            "source_task_id": "bar",
+            "source_run_id": "custom",
+            "source_map_index": -1,
+        }
+
+        events = [
+            AssetEvent(id=10, timestamp=make_timestamp(1), 
partition_key="2024-01-01", **common),
+            AssetEvent(id=11, timestamp=make_timestamp(2), 
partition_key="2024-01-02", **common),
+            AssetEvent(id=12, timestamp=make_timestamp(3), 
partition_key="us|2024-01-01", **common),
+            AssetEvent(id=13, timestamp=make_timestamp(4), 
partition_key="eu|2024-01-01", **common),
+            AssetEvent(id=14, timestamp=make_timestamp(5), 
partition_key="apac|2024-03-20", **common),
+            AssetEvent(id=15, timestamp=make_timestamp(6), partition_key=None, 
**common),
+        ]
+        session.add_all(events)
+        session.commit()
+        yield events
+
+        for event in events:
+            session.delete(event)
+        session.commit()
+
+    @pytest.mark.usefixtures("test_asset", "test_partitioned_events")
+    def test_get_by_asset_with_exact_partition_key(self, client):
+        response = client.get(
+            "/execution/asset-events/by-asset",
+            params={
+                "name": "test_get_asset_by_name",
+                "uri": None,
+                "partition_key_regexp_pattern": "^2024-01-01$",
+            },
+        )
+        assert response.status_code == 200
+        data = response.json()
+        assert len(data["asset_events"]) == 1
+        assert data["asset_events"][0]["partition_key"] == "2024-01-01"
+
+    @pytest.mark.usefixtures("test_asset", "test_partitioned_events")
+    def test_get_by_asset_with_prefix_partition_key(self, client):
+        response = client.get(
+            "/execution/asset-events/by-asset",
+            params={
+                "name": "test_get_asset_by_name",
+                "uri": None,
+                "partition_key_regexp_pattern": "^2024-01-",
+            },
+        )
+        assert response.status_code == 200
+        data = response.json()
+        assert len(data["asset_events"]) == 2
+
+    @pytest.mark.usefixtures("test_asset", "test_partitioned_events")
+    def test_get_by_asset_with_composite_partition_key_multiple_regions(self, 
client):
+        response = client.get(
+            "/execution/asset-events/by-asset",
+            params={
+                "name": "test_get_asset_by_name",
+                "uri": None,
+                "partition_key_regexp_pattern": r"^(us|eu)\|2024-01-.*",
+            },
+        )
+        assert response.status_code == 200
+        data = response.json()
+        assert len(data["asset_events"]) == 2
+        keys = {e["partition_key"] for e in data["asset_events"]}
+        assert keys == {"us|2024-01-01", "eu|2024-01-01"}
+
+    @pytest.mark.usefixtures("test_asset", "test_partitioned_events")
+    def 
test_get_by_asset_with_composite_partition_key_date_across_regions(self, 
client):
+        response = client.get(
+            "/execution/asset-events/by-asset",
+            params={
+                "name": "test_get_asset_by_name",
+                "uri": None,
+                "partition_key_regexp_pattern": r".*\|2024-01-01$",
+            },
+        )
+        assert response.status_code == 200
+        data = response.json()
+        assert len(data["asset_events"]) == 2
+        keys = {e["partition_key"] for e in data["asset_events"]}
+        assert keys == {"us|2024-01-01", "eu|2024-01-01"}
+
+    @pytest.mark.usefixtures("test_asset", "test_partitioned_events")
+    def test_get_by_asset_with_partition_key_no_match(self, client):
+        response = client.get(
+            "/execution/asset-events/by-asset",
+            params={
+                "name": "test_get_asset_by_name",
+                "uri": None,
+                "partition_key_regexp_pattern": "^2025-",
+            },
+        )
+        assert response.status_code == 200
+        assert len(response.json()["asset_events"]) == 0
+
+    @pytest.mark.usefixtures("test_asset", "test_partitioned_events")
+    def test_get_by_asset_without_partition_key_returns_all(self, client):
+        response = client.get(
+            "/execution/asset-events/by-asset",
+            params={"name": "test_get_asset_by_name", "uri": None},
+        )
+        assert response.status_code == 200
+        assert len(response.json()["asset_events"]) == 6
+
+    @pytest.mark.usefixtures("test_asset", "test_partitioned_events")
+    def test_get_by_asset_with_partition_key_and_time_range(self, client):
+        response = client.get(
+            "/execution/asset-events/by-asset",
+            params={
+                "name": "test_get_asset_by_name",
+                "uri": None,
+                "partition_key_regexp_pattern": "^2024-01-",
+                "after": "2021-01-01T12:00:00Z",
+            },
+        )
+        assert response.status_code == 200
+        data = response.json()
+        assert len(data["asset_events"]) == 1
+        assert data["asset_events"][0]["partition_key"] == "2024-01-02"
+
+    @pytest.mark.usefixtures("test_asset", "test_partitioned_events")
+    def test_get_by_asset_with_invalid_regex_returns_400(self, client):
+        response = client.get(
+            "/execution/asset-events/by-asset",
+            params={
+                "name": "test_get_asset_by_name",
+                "uri": None,
+                "partition_key_regexp_pattern": "[invalid(regex",
+            },
+        )
+        assert response.status_code == 400
+        assert "Invalid regular expression" in response.json()["detail"]
+
+    def test_get_by_asset_with_pattern_disabled_returns_400(self, client):
+        with conf_vars({("api", "regexp_query_timeout"): "0"}):
+            response = client.get(
+                "/execution/asset-events/by-asset",
+                params={
+                    "name": "test_get_asset_by_name",
+                    "uri": None,
+                    "partition_key_regexp_pattern": "^2021-",
+                },
+            )
+        assert response.status_code == 400
+        assert "disabled" in response.json()["detail"]
+
+    @pytest.mark.usefixtures("test_asset", "test_partitioned_events")
+    @pytest.mark.parametrize(
+        ("partition_key", "expected_keys"),
+        [
+            ("2024-01-01", ["2024-01-01"]),
+            ("us|2024-01-01", ["us|2024-01-01"]),
+            ("nonexistent", []),
+        ],
+    )
+    def test_get_by_asset_exact_partition_key(self, client, partition_key, 
expected_keys):
+        response = client.get(
+            "/execution/asset-events/by-asset",
+            params={
+                "name": "test_get_asset_by_name",
+                "uri": None,
+                "partition_key": partition_key,
+            },
+        )
+        assert response.status_code == 200
+        keys = [event["partition_key"] for event in 
response.json()["asset_events"]]
+        assert keys == expected_keys
+
+    @pytest.mark.usefixtures("test_asset", "test_partitioned_events")
+    def test_get_by_asset_partition_key_and_pattern_combined(self, client):
+        # Both filters are allowed and combine with AND: a disjoint pair 
yields no results.
+        response = client.get(
+            "/execution/asset-events/by-asset",
+            params={
+                "name": "test_get_asset_by_name",
+                "uri": None,
+                "partition_key": "2024-01-01",
+                "partition_key_regexp_pattern": "^2025-",
+            },
+        )
+        assert response.status_code == 200
+        assert len(response.json()["asset_events"]) == 0
+
+
+class TestGetAssetEventByAssetAliasPartitionKey:
+    """Tests for partition_key_regexp_pattern regex filter on by-asset-alias 
endpoint.
+
+    All patterns use ^-anchored regex so they work consistently across
+    PostgreSQL (~), MySQL (REGEXP), and SQLite (re.match).
+    """
+
+    @pytest.fixture(autouse=True)
+    def _enable_regexp_query_filters(self):
+        with conf_vars({("api", "regexp_query_timeout"): "30"}):
+            yield
+
+    @pytest.fixture
+    def test_partitioned_alias_events(self, session, test_asset):
+        def make_timestamp(day):
+            return datetime(2021, 1, day, tzinfo=timezone.utc)
+
+        common = {
+            "asset_id": 1,
+            "extra": {"foo": "bar"},
+            "source_dag_id": "foo",
+            "source_task_id": "bar",
+            "source_run_id": "custom",
+            "source_map_index": -1,
+        }
+
+        events = [
+            AssetEvent(id=20, timestamp=make_timestamp(1), 
partition_key="us|2024-01-01", **common),
+            AssetEvent(id=21, timestamp=make_timestamp(2), 
partition_key="eu|2024-01-01", **common),
+            AssetEvent(id=22, timestamp=make_timestamp(3), partition_key=None, 
**common),
+        ]
+        session.add_all(events)
+        session.commit()
+
+        alias = AssetAliasModel(id=10, name="partitioned_alias")
+        alias.asset_events = events
+        alias.assets.append(test_asset)
+        session.add(alias)
+        session.commit()
+
+        yield events, alias
+
+        session.delete(alias)
+        for event in events:
+            session.delete(event)
+        session.commit()
+
+    @pytest.mark.usefixtures("test_asset", "test_partitioned_alias_events")
+    def test_get_by_alias_with_partition_key_region(self, client):
+        response = client.get(
+            "/execution/asset-events/by-asset-alias",
+            params={"name": "partitioned_alias", 
"partition_key_regexp_pattern": r"^us\|.*"},
+        )
+        assert response.status_code == 200
+        data = response.json()
+        assert len(data["asset_events"]) == 1
+        assert data["asset_events"][0]["partition_key"] == "us|2024-01-01"
+
+    @pytest.mark.usefixtures("test_asset", "test_partitioned_alias_events")
+    def test_get_by_alias_with_partition_key_all_regions(self, client):
+        response = client.get(
+            "/execution/asset-events/by-asset-alias",
+            params={"name": "partitioned_alias", 
"partition_key_regexp_pattern": r".*\|2024-01-01$"},
+        )
+        assert response.status_code == 200
+        assert len(response.json()["asset_events"]) == 2
+
+    @pytest.mark.usefixtures("test_asset", "test_partitioned_alias_events")
+    def test_get_by_alias_without_partition_key_returns_all(self, client):
+        response = client.get(
+            "/execution/asset-events/by-asset-alias",
+            params={"name": "partitioned_alias"},
+        )
+        assert response.status_code == 200
+        assert len(response.json()["asset_events"]) == 3
+
+    @pytest.mark.usefixtures("test_asset", "test_partitioned_alias_events")
+    def test_get_by_alias_with_invalid_regex_returns_400(self, client):
+        response = client.get(
+            "/execution/asset-events/by-asset-alias",
+            params={"name": "partitioned_alias", 
"partition_key_regexp_pattern": "[invalid(regex"},
+        )
+        assert response.status_code == 400
+        assert "Invalid regular expression" in response.json()["detail"]
+
+    def test_get_by_alias_with_pattern_disabled_returns_400(self, client):
+        with conf_vars({("api", "regexp_query_timeout"): "0"}):
+            response = client.get(
+                "/execution/asset-events/by-asset-alias",
+                params={"name": "partitioned_alias", 
"partition_key_regexp_pattern": "^us"},
+            )
+        assert response.status_code == 400
+        assert "disabled" in response.json()["detail"]
+
+    @pytest.mark.usefixtures("test_asset", "test_partitioned_alias_events")
+    def test_get_by_alias_with_exact_partition_key_regex(self, client):
+        response = client.get(
+            "/execution/asset-events/by-asset-alias",
+            params={"name": "partitioned_alias", 
"partition_key_regexp_pattern": r"^us\|2024-01-01$"},
+        )
+        assert response.status_code == 200
+        data = response.json()
+        assert len(data["asset_events"]) == 1
+        assert data["asset_events"][0]["partition_key"] == "us|2024-01-01"
+
+    @pytest.mark.usefixtures("test_asset", "test_partitioned_alias_events")
+    def test_get_by_alias_exact_partition_key(self, client):
+        response = client.get(
+            "/execution/asset-events/by-asset-alias",
+            params={"name": "partitioned_alias", "partition_key": 
"us|2024-01-01"},
+        )
+        assert response.status_code == 200
+        data = response.json()
+        assert len(data["asset_events"]) == 1
+        assert data["asset_events"][0]["partition_key"] == "us|2024-01-01"
+
+    @pytest.mark.usefixtures("test_asset", "test_partitioned_alias_events")
+    def test_get_by_alias_exact_partition_key_no_match(self, client):
+        response = client.get(
+            "/execution/asset-events/by-asset-alias",
+            params={"name": "partitioned_alias", "partition_key": 
"nonexistent"},
+        )
+        assert response.status_code == 200
+        assert len(response.json()["asset_events"]) == 0
+
+    @pytest.mark.usefixtures("test_asset", "test_partitioned_alias_events")
+    def test_get_by_alias_partition_key_and_pattern_combined(self, client):
+        # Both filters are allowed and combine with AND.
+        response = client.get(
+            "/execution/asset-events/by-asset-alias",
+            params={
+                "name": "partitioned_alias",
+                "partition_key": "us|2024-01-01",
+                "partition_key_regexp_pattern": r"^us\|",
+            },
+        )
+        assert response.status_code == 200
+        data = response.json()
+        assert len(data["asset_events"]) == 1
+        assert data["asset_events"][0]["partition_key"] == "us|2024-01-01"
+
+
 class TestGetAssetEventByAssetExtraFilter:
     @pytest.fixture
     def test_events_with_extra(self, session):
diff --git a/airflow-core/tests/unit/utils/test_sqlalchemy.py 
b/airflow-core/tests/unit/utils/test_sqlalchemy.py
index 1ef7b42c587..8dbcd413130 100644
--- a/airflow-core/tests/unit/utils/test_sqlalchemy.py
+++ b/airflow-core/tests/unit/utils/test_sqlalchemy.py
@@ -35,6 +35,7 @@ from airflow.serialization.serialized_objects import 
BaseSerialization
 from airflow.settings import Session
 from airflow.utils.sqlalchemy import (
     ExecutorConfigType,
+    apply_regex_query_timeout,
     ensure_pod_is_valid_after_unpickling,
     get_dialect_name,
     prohibit_commit,
@@ -43,6 +44,7 @@ from airflow.utils.sqlalchemy import (
 from airflow.utils.state import State
 from airflow.utils.types import DagRunTriggeredByType, DagRunType
 
+from tests_common.test_utils.config import conf_vars
 from tests_common.test_utils.dag import sync_dag_to_db
 
 pytestmark = pytest.mark.db_test
@@ -380,3 +382,64 @@ class TestExecutorConfigType:
         pickle.dumps(fixed_pod)
         assert fixed_pod.local_vars_configuration.refresh_api_key_hook is None
         assert 
fixed_pod.spec.containers[0].local_vars_configuration.refresh_api_key_hook is 
None
+
+
+class TestApplyRegexQueryTimeout:
+    @staticmethod
+    def _mock_session(dialect_name):
+        session = mock.MagicMock()
+        session.get_bind.return_value.dialect.name = dialect_name
+        return session
+
+    def test_sets_and_restores_statement_timeout_on_postgresql(self):
+        session = self._mock_session("postgresql")
+        # Pre-existing (e.g. global) statement_timeout captured before we 
override it.
+        session.execute.return_value.scalar.return_value = "10s"
+        with conf_vars({("api", "regexp_query_timeout"): "5"}):
+            with apply_regex_query_timeout(session):
+                # Capture-then-set on enter.
+                assert session.execute.call_count == 2
+                set_stmt = session.execute.call_args_list[1].args[0]
+                # 5 seconds -> 5000 ms, passed as a bound parameter (no SQL 
injection).
+                assert set_stmt.compile().params == {"timeout": "5000"}
+        # Restored on exit to the previous value (not reset to 0), so a global 
timeout is preserved.
+        assert session.execute.call_count == 3
+        restore_stmt = session.execute.call_args_list[2].args[0]
+        assert restore_stmt.compile().params == {"timeout": "10s"}
+
+    def test_sets_and_restores_max_execution_time_on_mysql(self):
+        session = self._mock_session("mysql")
+        # Pre-existing (e.g. global) max_execution_time captured before we 
override it.
+        session.execute.return_value.scalar.return_value = 1000
+        with conf_vars({("api", "regexp_query_timeout"): "5"}):
+            with apply_regex_query_timeout(session):
+                # Capture-then-set on enter (5 seconds -> 5000 ms).
+                assert session.execute.call_count == 2
+                assert "max_execution_time = 5000" in 
str(session.execute.call_args.args[0])
+        # Restored on exit to the previous value, so a global limit is 
preserved.
+        assert session.execute.call_count == 3
+        assert "max_execution_time = 1000" in 
str(session.execute.call_args.args[0])
+
+    def test_fractional_seconds_are_converted_to_milliseconds(self):
+        session = self._mock_session("postgresql")
+        session.execute.return_value.scalar.return_value = "0"
+        with conf_vars({("api", "regexp_query_timeout"): "0.5"}):
+            with apply_regex_query_timeout(session):
+                # Set is the second call (after capturing the previous value).
+                set_stmt = session.execute.call_args_list[1].args[0]
+                # 0.5 seconds -> 500 ms.
+                assert set_stmt.compile().params == {"timeout": "500"}
+
+    def test_noop_on_sqlite(self):
+        session = self._mock_session("sqlite")
+        with conf_vars({("api", "regexp_query_timeout"): "5"}):
+            with apply_regex_query_timeout(session):
+                pass
+        session.execute.assert_not_called()
+
+    def test_noop_when_timeout_disabled(self):
+        session = self._mock_session("postgresql")
+        with conf_vars({("api", "regexp_query_timeout"): "0"}):
+            with apply_regex_query_timeout(session):
+                pass
+        session.execute.assert_not_called()
diff --git a/task-sdk/src/airflow/sdk/api/client.py 
b/task-sdk/src/airflow/sdk/api/client.py
index 74a68005371..7e681c11149 100644
--- a/task-sdk/src/airflow/sdk/api/client.py
+++ b/task-sdk/src/airflow/sdk/api/client.py
@@ -858,6 +858,8 @@ class AssetEventOperations:
         before: datetime | None = None,
         ascending: bool = True,
         limit: int | None = None,
+        partition_key: str | None = None,
+        partition_key_regexp_pattern: str | None = None,
         extra: dict[str, str] | None = None,
     ) -> AssetEventsResponse:
         """Get Asset event from the API server."""
@@ -869,6 +871,10 @@ class AssetEventOperations:
         common_params["ascending"] = ascending
         if limit is not None:
             common_params["limit"] = limit
+        if partition_key is not None:
+            common_params["partition_key"] = partition_key
+        if partition_key_regexp_pattern is not None:
+            common_params["partition_key_regexp_pattern"] = 
partition_key_regexp_pattern
         extra_params: list[tuple[str, str]] = []
         if extra:
             extra_params = [("extra", f"{k}={v}") for k, v in extra.items()]
diff --git a/task-sdk/src/airflow/sdk/execution_time/comms.py 
b/task-sdk/src/airflow/sdk/execution_time/comms.py
index 6e9e49b2676..5c4ae14dff1 100644
--- a/task-sdk/src/airflow/sdk/execution_time/comms.py
+++ b/task-sdk/src/airflow/sdk/execution_time/comms.py
@@ -1142,6 +1142,8 @@ class GetAssetEventByAsset(BaseModel):
     before: AwareDatetime | None = None
     limit: int | None = None
     ascending: bool = True
+    partition_key: str | None = None
+    partition_key_regexp_pattern: str | None = None
     extra: dict[str, str] | None = None
     type: Literal["GetAssetEventByAsset"] = "GetAssetEventByAsset"
 
@@ -1152,6 +1154,8 @@ class GetAssetEventByAssetAlias(BaseModel):
     before: AwareDatetime | None = None
     limit: int | None = None
     ascending: bool = True
+    partition_key: str | None = None
+    partition_key_regexp_pattern: str | None = None
     extra: dict[str, str] | None = None
     type: Literal["GetAssetEventByAssetAlias"] = "GetAssetEventByAssetAlias"
 
diff --git a/task-sdk/src/airflow/sdk/execution_time/context.py 
b/task-sdk/src/airflow/sdk/execution_time/context.py
index 2828525a7bb..2c2364a49b2 100644
--- a/task-sdk/src/airflow/sdk/execution_time/context.py
+++ b/task-sdk/src/airflow/sdk/execution_time/context.py
@@ -1114,6 +1114,8 @@ class InletEventsAccessor(Sequence["AssetEventResult"]):
     _before: str | datetime | None
     _ascending: bool
     _limit: int | None
+    _partition_key: str | None
+    _partition_key_regexp_pattern: str | None
     _extra: dict[str, str]
     _asset_name: str | None
     _asset_uri: str | None
@@ -1129,6 +1131,8 @@ class InletEventsAccessor(Sequence["AssetEventResult"]):
         self._before = None
         self._ascending = True
         self._limit = None
+        self._partition_key = None
+        self._partition_key_regexp_pattern = None
         self._extra: dict[str, str] = {}
 
     def after(self, after: str) -> Self:
@@ -1151,6 +1155,18 @@ class InletEventsAccessor(Sequence["AssetEventResult"]):
         self._reset_cache()
         return self
 
+    def partition_key(self, key: str) -> Self:
+        """Filter by exact partition key match."""
+        self._partition_key = key
+        self._reset_cache()
+        return self
+
+    def partition_key_regexp_pattern(self, pattern: str) -> Self:
+        """Filter by partition key regexp pattern."""
+        self._partition_key_regexp_pattern = pattern
+        self._reset_cache()
+        return self
+
     def extra(self, key: str, value: str) -> Self:
         self._extra[key] = value
         self._reset_cache()
@@ -1165,6 +1181,8 @@ class InletEventsAccessor(Sequence["AssetEventResult"]):
             "before": self._before,
             "ascending": self._ascending,
             "limit": self._limit,
+            "partition_key": self._partition_key,
+            "partition_key_regexp_pattern": self._partition_key_regexp_pattern,
             "extra": self._extra or None,
         }
 
diff --git a/task-sdk/src/airflow/sdk/execution_time/schema/schema.json 
b/task-sdk/src/airflow/sdk/execution_time/schema/schema.json
index 01e3c8970b5..0ec8fe4e49a 100644
--- a/task-sdk/src/airflow/sdk/execution_time/schema/schema.json
+++ b/task-sdk/src/airflow/sdk/execution_time/schema/schema.json
@@ -1872,6 +1872,30 @@
           "title": "Ascending",
           "type": "boolean"
         },
+        "partition_key": {
+          "anyOf": [
+            {
+              "type": "string"
+            },
+            {
+              "type": "null"
+            }
+          ],
+          "default": null,
+          "title": "Partition Key"
+        },
+        "partition_key_regexp_pattern": {
+          "anyOf": [
+            {
+              "type": "string"
+            },
+            {
+              "type": "null"
+            }
+          ],
+          "default": null,
+          "title": "Partition Key Regexp Pattern"
+        },
         "extra": {
           "anyOf": [
             {
@@ -1950,6 +1974,30 @@
           "title": "Ascending",
           "type": "boolean"
         },
+        "partition_key": {
+          "anyOf": [
+            {
+              "type": "string"
+            },
+            {
+              "type": "null"
+            }
+          ],
+          "default": null,
+          "title": "Partition Key"
+        },
+        "partition_key_regexp_pattern": {
+          "anyOf": [
+            {
+              "type": "string"
+            },
+            {
+              "type": "null"
+            }
+          ],
+          "default": null,
+          "title": "Partition Key Regexp Pattern"
+        },
         "extra": {
           "anyOf": [
             {
diff --git a/task-sdk/src/airflow/sdk/execution_time/supervisor.py 
b/task-sdk/src/airflow/sdk/execution_time/supervisor.py
index 447f7c7594d..87311f02da7 100644
--- a/task-sdk/src/airflow/sdk/execution_time/supervisor.py
+++ b/task-sdk/src/airflow/sdk/execution_time/supervisor.py
@@ -1774,6 +1774,8 @@ class ActivitySubprocess(WatchedSubprocess):
                 before=msg.before,
                 ascending=msg.ascending,
                 limit=msg.limit,
+                partition_key=msg.partition_key,
+                partition_key_regexp_pattern=msg.partition_key_regexp_pattern,
                 extra=msg.extra,
             )
             asset_event_result = 
AssetEventsResult.from_asset_events_response(asset_event_resp)
@@ -1786,6 +1788,8 @@ class ActivitySubprocess(WatchedSubprocess):
                 before=msg.before,
                 ascending=msg.ascending,
                 limit=msg.limit,
+                partition_key=msg.partition_key,
+                partition_key_regexp_pattern=msg.partition_key_regexp_pattern,
                 extra=msg.extra,
             )
             asset_event_result = 
AssetEventsResult.from_asset_events_response(asset_event_resp)
diff --git a/task-sdk/tests/task_sdk/api/test_client.py 
b/task-sdk/tests/task_sdk/api/test_client.py
index 205b12bd4f2..0f9b8130e26 100644
--- a/task-sdk/tests/task_sdk/api/test_client.py
+++ b/task-sdk/tests/task_sdk/api/test_client.py
@@ -1216,6 +1216,95 @@ class TestAssetEventOperations:
         assert result.asset_events[0].asset.name == "this_asset"
         assert result.asset_events[0].asset.uri == "s3://bucket/key"
 
+    def test_partition_key_exact_match_param_passed(self):
+        def handle_request(request: httpx.Request) -> httpx.Response:
+            params = request.url.params
+            assert params.get("partition_key") == "2024-01-01"
+            assert "partition_key_regexp_pattern" not in params
+            return httpx.Response(
+                status_code=200,
+                json={
+                    "asset_events": [
+                        {
+                            "id": 1,
+                            "asset": {
+                                "name": "this_asset",
+                                "uri": "s3://bucket/key",
+                                "group": "asset",
+                            },
+                            "created_dagruns": [],
+                            "timestamp": "2023-01-01T00:00:00Z",
+                            "partition_key": "2024-01-01",
+                        }
+                    ]
+                },
+            )
+
+        client = make_client(httpx.MockTransport(handle_request))
+        result = client.asset_events.get(name="this_asset", 
partition_key="2024-01-01")
+
+        assert isinstance(result, AssetEventsResponse)
+        assert len(result.asset_events) == 1
+
+    def test_partition_key_regexp_pattern_param_passed(self):
+        def handle_request(request: httpx.Request) -> httpx.Response:
+            params = request.url.params
+            assert params.get("partition_key_regexp_pattern") == "^2024-01-"
+            assert "partition_key" not in params
+            return httpx.Response(
+                status_code=200,
+                json={"asset_events": []},
+            )
+
+        client = make_client(httpx.MockTransport(handle_request))
+        result = client.asset_events.get(name="this_asset", 
partition_key_regexp_pattern="^2024-01-")
+
+        assert isinstance(result, AssetEventsResponse)
+        assert len(result.asset_events) == 0
+
+    def test_partition_key_regexp_pattern_with_other_params(self):
+        def handle_request(request: httpx.Request) -> httpx.Response:
+            params = request.url.params
+            assert params.get("partition_key_regexp_pattern") == 
r"^us\|2024-.*"
+            assert "partition_key" not in params
+            assert params.get("after") == "2023-06-01T00:00:00+00:00"
+            assert params.get("limit") == "5"
+            assert params.get("ascending") == "false"
+            return httpx.Response(
+                status_code=200,
+                json={"asset_events": []},
+            )
+
+        client = make_client(httpx.MockTransport(handle_request))
+        result = client.asset_events.get(
+            name="this_asset",
+            partition_key_regexp_pattern=r"^us\|2024-.*",
+            after=datetime(2023, 6, 1, tzinfo=dt_timezone.utc),
+            limit=5,
+            ascending=False,
+        )
+
+        assert isinstance(result, AssetEventsResponse)
+        assert len(result.asset_events) == 0
+
+    def test_partition_key_with_alias(self):
+        def handle_request(request: httpx.Request) -> httpx.Response:
+            assert request.url.path == "/asset-events/by-asset-alias"
+            params = request.url.params
+            assert params.get("name") == "my_alias"
+            assert params.get("partition_key") == "us-east|2024-01-01"
+            assert "partition_key_regexp_pattern" not in params
+            return httpx.Response(
+                status_code=200,
+                json={"asset_events": []},
+            )
+
+        client = make_client(httpx.MockTransport(handle_request))
+        result = client.asset_events.get(alias_name="my_alias", 
partition_key="us-east|2024-01-01")
+
+        assert isinstance(result, AssetEventsResponse)
+        assert len(result.asset_events) == 0
+
     def test_extra_dict_param_passed(self):
         def handle_request(request: httpx.Request) -> httpx.Response:
             params = request.url.params
diff --git a/task-sdk/tests/task_sdk/execution_time/test_supervisor.py 
b/task-sdk/tests/task_sdk/execution_time/test_supervisor.py
index 870b2e6deed..c39ceb92b0e 100644
--- a/task-sdk/tests/task_sdk/execution_time/test_supervisor.py
+++ b/task-sdk/tests/task_sdk/execution_time/test_supervisor.py
@@ -1940,6 +1940,8 @@ REQUEST_TEST_CASES = [
                 "before": None,
                 "limit": None,
                 "ascending": True,
+                "partition_key": None,
+                "partition_key_regexp_pattern": None,
                 "extra": None,
             },
             response=AssetEventsResult(
@@ -1984,6 +1986,8 @@ REQUEST_TEST_CASES = [
                 "before": timezone.parse("2024-10-15T12:00:00Z"),
                 "limit": 5,
                 "ascending": False,
+                "partition_key": None,
+                "partition_key_regexp_pattern": None,
                 "extra": None,
             },
             response=AssetEventsResult(
@@ -2021,6 +2025,8 @@ REQUEST_TEST_CASES = [
                 "before": None,
                 "limit": None,
                 "ascending": True,
+                "partition_key": None,
+                "partition_key_regexp_pattern": None,
                 "extra": None,
             },
             response=AssetEventsResult(
@@ -2065,6 +2071,8 @@ REQUEST_TEST_CASES = [
                 "before": timezone.parse("2024-10-15T12:00:00Z"),
                 "limit": 5,
                 "ascending": False,
+                "partition_key": None,
+                "partition_key_regexp_pattern": None,
                 "extra": None,
             },
             response=AssetEventsResult(
@@ -2102,6 +2110,8 @@ REQUEST_TEST_CASES = [
                 "before": None,
                 "limit": None,
                 "ascending": True,
+                "partition_key": None,
+                "partition_key_regexp_pattern": None,
                 "extra": None,
             },
             response=AssetEventsResult(
@@ -2146,6 +2156,8 @@ REQUEST_TEST_CASES = [
                 "before": timezone.parse("2024-10-15T12:00:00Z"),
                 "limit": 5,
                 "ascending": False,
+                "partition_key": None,
+                "partition_key_regexp_pattern": None,
                 "extra": None,
             },
             response=AssetEventsResult(
@@ -2182,6 +2194,8 @@ REQUEST_TEST_CASES = [
                 "before": None,
                 "limit": None,
                 "ascending": True,
+                "partition_key": None,
+                "partition_key_regexp_pattern": None,
                 "extra": None,
             },
             response=AssetEventsResult(
@@ -2224,6 +2238,8 @@ REQUEST_TEST_CASES = [
                 "before": timezone.parse("2024-10-15T12:00:00Z"),
                 "limit": 5,
                 "ascending": False,
+                "partition_key": None,
+                "partition_key_regexp_pattern": None,
                 "extra": None,
             },
             response=AssetEventsResult(
@@ -2239,6 +2255,49 @@ REQUEST_TEST_CASES = [
         ),
         test_id="get_asset_events_by_asset_alias_with_filters",
     ),
+    RequestTestCase(
+        message=GetAssetEventByAsset(
+            uri="s3://bucket/obj",
+            name="test",
+            partition_key="us|2024-01-15",
+        ),
+        expected_body={
+            "asset_events": [
+                {
+                    "id": 1,
+                    "timestamp": timezone.parse("2024-10-31T12:00:00Z"),
+                    "asset": {"name": "asset", "uri": "s3://bucket/obj", 
"group": "asset"},
+                    "created_dagruns": [],
+                }
+            ],
+            "type": "AssetEventsResult",
+        },
+        client_mock=ClientMock(
+            method_path="asset_events.get",
+            kwargs={
+                "uri": "s3://bucket/obj",
+                "name": "test",
+                "after": None,
+                "before": None,
+                "limit": None,
+                "ascending": True,
+                "partition_key": "us|2024-01-15",
+                "partition_key_regexp_pattern": None,
+                "extra": None,
+            },
+            response=AssetEventsResult(
+                asset_events=[
+                    AssetEventResponse(
+                        id=1,
+                        asset=AssetResponse(name="asset", 
uri="s3://bucket/obj", group="asset"),
+                        created_dagruns=[],
+                        timestamp=timezone.parse("2024-10-31T12:00:00Z"),
+                    )
+                ]
+            ),
+        ),
+        test_id="get_asset_events_by_name_with_partition_key",
+    ),
     RequestTestCase(
         message=GetAssetEventByAsset(
             uri="s3://bucket/obj",
@@ -2265,6 +2324,8 @@ REQUEST_TEST_CASES = [
                 "before": None,
                 "limit": None,
                 "ascending": True,
+                "partition_key": None,
+                "partition_key_regexp_pattern": None,
                 "extra": {"region": "us"},
             },
             response=AssetEventsResult(
@@ -2280,6 +2341,47 @@ REQUEST_TEST_CASES = [
         ),
         test_id="get_asset_events_with_extra_filter",
     ),
+    RequestTestCase(
+        message=GetAssetEventByAssetAlias(
+            alias_name="test_alias",
+            partition_key="eu|2024-03",
+        ),
+        expected_body={
+            "asset_events": [
+                {
+                    "id": 1,
+                    "timestamp": timezone.parse("2024-10-31T12:00:00Z"),
+                    "asset": {"name": "asset", "uri": "s3://bucket/obj", 
"group": "asset"},
+                    "created_dagruns": [],
+                }
+            ],
+            "type": "AssetEventsResult",
+        },
+        client_mock=ClientMock(
+            method_path="asset_events.get",
+            kwargs={
+                "alias_name": "test_alias",
+                "after": None,
+                "before": None,
+                "limit": None,
+                "ascending": True,
+                "partition_key": "eu|2024-03",
+                "partition_key_regexp_pattern": None,
+                "extra": None,
+            },
+            response=AssetEventsResult(
+                asset_events=[
+                    AssetEventResponse(
+                        id=1,
+                        asset=AssetResponse(name="asset", 
uri="s3://bucket/obj", group="asset"),
+                        created_dagruns=[],
+                        timestamp=timezone.parse("2024-10-31T12:00:00Z"),
+                    )
+                ]
+            ),
+        ),
+        test_id="get_asset_events_by_alias_with_partition_key",
+    ),
     RequestTestCase(
         message=GetAssetEventByAssetAlias(
             alias_name="test_alias",
@@ -2304,6 +2406,8 @@ REQUEST_TEST_CASES = [
                 "before": None,
                 "limit": None,
                 "ascending": True,
+                "partition_key": None,
+                "partition_key_regexp_pattern": None,
                 "extra": {"env": "prod"},
             },
             response=AssetEventsResult(
diff --git a/ts-sdk/src/generated/supervisor.ts 
b/ts-sdk/src/generated/supervisor.ts
index 4766c0e8a79..ab2632831ab 100644
--- a/ts-sdk/src/generated/supervisor.ts
+++ b/ts-sdk/src/generated/supervisor.ts
@@ -372,6 +372,8 @@ export type After = string | null;
 export type Before = string | null;
 export type Limit = number | null;
 export type Ascending = boolean;
+export type PartitionKey5 = string | null;
+export type PartitionKeyRegexpPattern = string | null;
 export type Extra7 = {
   [k: string]: string;
 } | null;
@@ -381,6 +383,8 @@ export type After1 = string | null;
 export type Before1 = string | null;
 export type Limit1 = number | null;
 export type Ascending1 = boolean;
+export type PartitionKey6 = string | null;
+export type PartitionKeyRegexpPattern1 = string | null;
 export type Extra8 = {
   [k: string]: string;
 } | null;
@@ -589,7 +593,7 @@ export type Conf2 = {
   [k: string]: unknown;
 } | null;
 export type ResetDagRun = boolean | null;
-export type PartitionKey5 = string | null;
+export type PartitionKey7 = string | null;
 export type Note2 = string | null;
 export type DagId22 = string;
 export type DagRunId = string;
@@ -1223,6 +1227,8 @@ export interface GetAssetEventByAsset {
   before?: Before;
   limit?: Limit;
   ascending?: Ascending;
+  partition_key?: PartitionKey5;
+  partition_key_regexp_pattern?: PartitionKeyRegexpPattern;
   extra?: Extra7;
   type?: Type29;
 }
@@ -1236,6 +1242,8 @@ export interface GetAssetEventByAssetAlias {
   before?: Before1;
   limit?: Limit1;
   ascending?: Ascending1;
+  partition_key?: PartitionKey6;
+  partition_key_regexp_pattern?: PartitionKeyRegexpPattern1;
   extra?: Extra8;
   type?: Type30;
 }
@@ -1803,7 +1811,7 @@ export interface TriggerDagRun {
   run_after?: RunAfter2;
   conf?: Conf2;
   reset_dag_run?: ResetDagRun;
-  partition_key?: PartitionKey5;
+  partition_key?: PartitionKey7;
   note?: Note2;
   dag_id: DagId22;
   run_id: DagRunId;

Reply via email to