Run child tables cascade from run.id

Migration ccb21eacf5d5 deletes rows whose run is gone, then adds foreign keys
with ON DELETE CASCADE from run_node, run_metric and run_artifact to run.id.
Deleting runs is now the one statement. A node or metric written after its
run was deleted is dropped with an info line. Migrations run with foreign keys
off, since a batch rebuild of run would otherwise cascade into every child row.
This commit is contained in:
2026-10-15 10:32:33 +02:00
parent 44223ab168
commit 599e01b0bf
6 changed files with 104 additions and 25 deletions
+4 -7
View File
@@ -851,10 +851,10 @@ async def retry_run(run_id: str, request: Request, user: CurrentUser) -> Any:
def _drop_runs(session: Session, chosen: Any) -> None:
"""A selection of runs and everything hanging off them.
The run tables carry a plain string ``run_id`` and no foreign key, so
nothing cascades on its own and these four statements are the whole of it.
``flow_run``, ``metric_minute`` and ``engine_event`` stay — they are the
observability record and are pruned on their own window.
The run tables' foreign keys cascade, so deleting the runs takes their
nodes, metrics and artifact rows with them. ``flow_run``, ``metric_minute``
and ``engine_event`` stay — they are the observability record and are
pruned on their own window.
Two things this costs, both deliberate. ``RunNode.outputs`` *is* the stage
cache, so a later run loses hits these would have served. And a node
@@ -864,9 +864,6 @@ def _drop_runs(session: Session, chosen: Any) -> None:
keeps whatever a ``run_artifact`` row or a live message still names, so
dropping the rows is enough and the hourly sweep reclaims the blobs.
"""
session.execute(delete(RunNode).where(col(RunNode.run_id).in_(chosen)))
session.execute(delete(RunMetric).where(col(RunMetric.run_id).in_(chosen)))
session.execute(delete(RunArtifact).where(col(RunArtifact.run_id).in_(chosen)))
session.execute(delete(Run).where(col(Run.id).in_(chosen)))