Skip to content

Commit 707bd1b

Browse files
committed
[pac] Add get_asset_check_partitions storage method
1 parent e2c8637 commit 707bd1b

6 files changed

Lines changed: 334 additions & 2 deletions

File tree

python_modules/dagster/dagster/_core/storage/asset_check_execution_record.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
from collections.abc import Iterable
33
from typing import NamedTuple, Optional, cast
44

5+
from dagster_shared.record import record
56
from dagster_shared.serdes import deserialize_value
67

78
import dagster._check as check
@@ -238,3 +239,30 @@ async def targets_latest_materialization(self, loading_context: LoadingContext)
238239
)
239240
else:
240241
check.failed(f"Unexpected check status {resolved_status}")
242+
243+
244+
@record
245+
class AssetCheckPartitionInfo:
246+
check_key: AssetCheckKey
247+
partition_key: Optional[str]
248+
# the status of the last execution of the check
249+
latest_execution_status: AssetCheckExecutionRecordStatus
250+
# the run id of the last planned event for the check
251+
latest_planned_run_id: str
252+
# the storage id of the last event (planned or evaluation) for the check
253+
latest_check_event_storage_id: int
254+
# the storage id of the last materialization for the asset / partition that this check targets
255+
# this is the latest overall materialization, independent of if there has been a check event
256+
# that targets it
257+
latest_materialization_storage_id: Optional[int]
258+
# the storage id of the materialization that the last execution of the check targeted
259+
latest_target_materialization_storage_id: Optional[int]
260+
261+
@property
262+
def is_current(self) -> bool:
263+
"""Returns True if the latest check execution targets the latest materialization event."""
264+
return (
265+
self.latest_materialization_storage_id is None
266+
or self.latest_materialization_storage_id
267+
== self.latest_target_materialization_storage_id
268+
)

python_modules/dagster/dagster/_core/storage/event_log/base.py

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@
3636
from dagster._core.storage.asset_check_execution_record import (
3737
AssetCheckExecutionRecord,
3838
AssetCheckExecutionRecordStatus,
39+
AssetCheckPartitionInfo,
3940
)
4041
from dagster._core.storage.dagster_run import DagsterRunStatsSnapshot
4142
from dagster._core.storage.partition_status_cache import get_and_update_asset_status_cache_value
@@ -648,6 +649,16 @@ def get_latest_asset_check_execution_by_key(
648649
"""Get the latest executions for a list of asset checks."""
649650
pass
650651

652+
@abstractmethod
653+
def get_asset_check_partition_info(
654+
self,
655+
keys: Sequence[AssetCheckKey],
656+
after_storage_id: Optional[int] = None,
657+
partition_keys: Optional[Sequence[str]] = None,
658+
) -> Sequence[AssetCheckPartitionInfo]:
659+
"""Get asset check partition records with execution status and planned run info."""
660+
pass
661+
651662
@abstractmethod
652663
def fetch_materializations(
653664
self,
@@ -744,3 +755,23 @@ def get_pool_config(self) -> PoolConfig:
744755
# Base implementation of fetching pool config. To be overriden for remote storage
745756
# implementations where the local instance might not match the remote instance.
746757
return self._instance.get_concurrency_config().pool_config
758+
759+
def _get_latest_unpartitioned_materialization_storage_ids(
760+
self, keys: Sequence[AssetKey]
761+
) -> Mapping[AssetKey, int]:
762+
# Returns a mapping of asset key to the latest recorded materialization storage id for the asset,
763+
# ignoring partitioned assets. Used purely for the `get_asset_check_partition_info` method across
764+
# different storage implementations.
765+
asset_records = self.get_asset_records(keys)
766+
latest_unpartitioned_materialization_storage_ids = {}
767+
for asset_record in asset_records:
768+
if (
769+
asset_record.asset_entry.last_materialization_record is not None
770+
and asset_record.asset_entry.last_materialization_record.event_log_entry.get_dagster_event().partition
771+
is None
772+
):
773+
latest_unpartitioned_materialization_storage_ids[
774+
asset_record.asset_entry.asset_key
775+
] = asset_record.asset_entry.last_materialization_storage_id
776+
777+
return latest_unpartitioned_materialization_storage_ids

python_modules/dagster/dagster/_core/storage/event_log/sql_event_log.py

Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@
5959
COMPLETED_ASSET_CHECK_EXECUTION_RECORD_STATUSES,
6060
AssetCheckExecutionRecord,
6161
AssetCheckExecutionRecordStatus,
62+
AssetCheckPartitionInfo,
6263
)
6364
from dagster._core.storage.dagster_run import DagsterRunStatsSnapshot
6465
from dagster._core.storage.event_log.base import (
@@ -3008,6 +3009,7 @@ def _store_asset_check_evaluation_planned(
30083009
execution_status=AssetCheckExecutionRecordStatus.PLANNED.value,
30093010
evaluation_event=serialize_value(event),
30103011
evaluation_event_timestamp=self._event_insert_timestamp(event),
3012+
evaluation_event_storage_id=event_id,
30113013
)
30123014
for partition_key in partition_keys
30133015
]
@@ -3232,6 +3234,135 @@ def get_latest_asset_check_execution_by_key(
32323234
results[check_key] = AssetCheckExecutionRecord.from_db_row(row, key=check_key)
32333235
return results
32343236

3237+
def _get_asset_check_partition_info_for_key(
3238+
self,
3239+
check_key: AssetCheckKey,
3240+
after_storage_id: Optional[int],
3241+
partition_keys: Optional[Sequence[str]],
3242+
latest_unpartitioned_materialization_storage_ids: Mapping[AssetKey, int],
3243+
) -> Sequence[AssetCheckPartitionInfo]:
3244+
# Build the base filter conditions
3245+
filter_conditions = [
3246+
AssetCheckExecutionsTable.c.asset_key == check_key.asset_key.to_string(),
3247+
AssetCheckExecutionsTable.c.check_name == check_key.name,
3248+
# Historical records may have NULL in the evaluation_event_storage_id column for
3249+
# PLANNED events
3250+
AssetCheckExecutionsTable.c.evaluation_event_storage_id.isnot(None),
3251+
]
3252+
if partition_keys is not None:
3253+
filter_conditions.append(AssetCheckExecutionsTable.c.partition.in_(partition_keys))
3254+
3255+
# Subquery to find the max id for each partition
3256+
latest_check_ids_subquery = db_subquery(
3257+
db_select(
3258+
[
3259+
db.func.max(AssetCheckExecutionsTable.c.id).label("id"),
3260+
AssetCheckExecutionsTable.c.partition.label("partition"),
3261+
]
3262+
)
3263+
.where(db.and_(*filter_conditions))
3264+
.group_by(AssetCheckExecutionsTable.c.partition),
3265+
"latest_check_ids_subquery",
3266+
)
3267+
3268+
# Subquery to find the latest materialization storage id for each partition of the
3269+
# target asset. Note: we don't filter by after_storage_id here because we always want
3270+
# to return the latest materialization storage id, even if it's older than after_storage_id.
3271+
latest_materialization_ids_subquery = self._latest_event_ids_by_partition_subquery(
3272+
check_key.asset_key,
3273+
[DagsterEventType.ASSET_MATERIALIZATION],
3274+
asset_partitions=partition_keys,
3275+
)
3276+
3277+
# Main query to get all columns for the latest records, joined with latest
3278+
# materialization storage ids
3279+
query = db_select(
3280+
[
3281+
AssetCheckExecutionsTable.c.id,
3282+
AssetCheckExecutionsTable.c.partition,
3283+
AssetCheckExecutionsTable.c.execution_status,
3284+
AssetCheckExecutionsTable.c.evaluation_event_storage_id,
3285+
AssetCheckExecutionsTable.c.materialization_event_storage_id,
3286+
AssetCheckExecutionsTable.c.run_id,
3287+
latest_materialization_ids_subquery.c.id.label("latest_materialization_storage_id"),
3288+
]
3289+
).select_from(
3290+
AssetCheckExecutionsTable.join(
3291+
latest_check_ids_subquery,
3292+
AssetCheckExecutionsTable.c.id == latest_check_ids_subquery.c.id,
3293+
).join(
3294+
latest_materialization_ids_subquery,
3295+
AssetCheckExecutionsTable.c.partition
3296+
== latest_materialization_ids_subquery.c.partition,
3297+
isouter=True,
3298+
)
3299+
)
3300+
3301+
# these filters are applied to the main query rather than the individual subqueries to ensure
3302+
# we don't miss records that only have a new materialization or a new check execution but not both
3303+
if after_storage_id is not None:
3304+
query = query.where(
3305+
db.or_(
3306+
AssetCheckExecutionsTable.c.evaluation_event_storage_id > after_storage_id,
3307+
latest_materialization_ids_subquery.c.id > after_storage_id,
3308+
)
3309+
)
3310+
3311+
with self.index_connection() as conn:
3312+
rows = db_fetch_mappings(conn, query)
3313+
3314+
return [
3315+
AssetCheckPartitionInfo(
3316+
check_key=check_key,
3317+
partition_key=row["partition"],
3318+
latest_execution_status=AssetCheckExecutionRecordStatus(row["execution_status"]),
3319+
latest_target_materialization_storage_id=row["materialization_event_storage_id"],
3320+
latest_planned_run_id=row["run_id"],
3321+
latest_check_event_storage_id=row["evaluation_event_storage_id"],
3322+
latest_materialization_storage_id=max(
3323+
filter(
3324+
None,
3325+
[
3326+
row["latest_materialization_storage_id"],
3327+
latest_unpartitioned_materialization_storage_ids.get(
3328+
check_key.asset_key
3329+
),
3330+
],
3331+
),
3332+
default=None,
3333+
),
3334+
)
3335+
for row in rows
3336+
]
3337+
3338+
def get_asset_check_partition_info(
3339+
self,
3340+
keys: Sequence[AssetCheckKey],
3341+
after_storage_id: Optional[int] = None,
3342+
partition_keys: Optional[Sequence[str]] = None,
3343+
) -> Sequence[AssetCheckPartitionInfo]:
3344+
check.list_param(keys, "keys", of_type=AssetCheckKey)
3345+
check.opt_int_param(after_storage_id, "after_storage_id")
3346+
3347+
infos = []
3348+
latest_unpartitioned_materialization_storage_ids = (
3349+
self._get_latest_unpartitioned_materialization_storage_ids(
3350+
list(set(key.asset_key for key in keys))
3351+
)
3352+
)
3353+
# the inner query is not feasible to join in a single query because the latest materialization ids subquery,
3354+
# so for now we fetch the info for each key separately
3355+
for key in keys:
3356+
infos.extend(
3357+
self._get_asset_check_partition_info_for_key(
3358+
key,
3359+
after_storage_id,
3360+
partition_keys,
3361+
latest_unpartitioned_materialization_storage_ids,
3362+
)
3363+
)
3364+
return infos
3365+
32353366
@property
32363367
def supports_asset_checks(self): # pyright: ignore[reportIncompatibleMethodOverride]
32373368
return self.has_table(AssetCheckExecutionsTable.name)

python_modules/dagster/dagster/_core/storage/event_log/sqlite/sqlite_event_log.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -276,7 +276,12 @@ def store_event(self, event: EventLogEntry) -> None:
276276
self.store_asset_event_tags([event], [event_id])
277277

278278
if event.is_dagster_event and event.dagster_event_type in ASSET_CHECK_EVENTS:
279-
self.store_asset_check_event(event, None)
279+
# mirror the event in the cross-run index database
280+
with self.index_connection() as conn:
281+
result = conn.execute(insert_event_statement)
282+
event_id = result.inserted_primary_key[0]
283+
284+
self.store_asset_check_event(event, event_id)
280285

281286
if event.is_dagster_event and event.dagster_event_type in EVENT_TYPE_TO_PIPELINE_RUN_STATUS:
282287
# should mirror run status change events in the index shard

python_modules/dagster/dagster/_core/storage/legacy_storage.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
from dagster._core.storage.asset_check_execution_record import (
1717
AssetCheckExecutionRecord,
1818
AssetCheckExecutionRecordStatus,
19+
AssetCheckPartitionInfo,
1920
)
2021
from dagster._core.storage.base_storage import DagsterStorage
2122
from dagster._core.storage.event_log.base import (
@@ -765,6 +766,16 @@ def get_latest_asset_check_execution_by_key(
765766
check_keys, partition=partition
766767
)
767768

769+
def get_asset_check_partition_info(
770+
self,
771+
keys: Sequence["AssetCheckKey"],
772+
after_storage_id: Optional[int] = None,
773+
partition_keys: Optional[Sequence[str]] = None,
774+
) -> Sequence[AssetCheckPartitionInfo]:
775+
return self._storage.event_log_storage.get_asset_check_partition_info(
776+
keys=keys, after_storage_id=after_storage_id, partition_keys=partition_keys
777+
)
778+
768779

769780
class LegacyScheduleStorage(ScheduleStorage, ConfigurableClass):
770781
def __init__(self, storage: DagsterStorage, inst_data: Optional[ConfigurableClassData] = None):

0 commit comments

Comments
 (0)