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.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WgBfkm4rCmtd1T4vHWjEPx
This commit is contained in:
2026-10-15 10:32:33 +02:00
co-authored by Claude Opus 5.5
parent ffa044724b
commit b4d7ffb982
6 changed files with 104 additions and 25 deletions
+19 -2
View File
@@ -7,6 +7,7 @@ The pipeline half — what a hit restores and what a key is made of — is in
import base64
import csv
import json
import logging
import threading
from datetime import UTC, datetime, timedelta
@@ -41,6 +42,10 @@ def test_a_run_cache_finds_what_an_earlier_run_recorded(tmp_path):
reference = store.put([b"payload"], name="data.bin")
plain, with_artifact, collected = "k-plain", "k-artifact", "k-collected"
with Session(db_engine) as session:
for run_id in ("cache-1", "cache-2", "cache-3"):
session.add(
Run(id=run_id, flow="study", status="ok", created_at=datetime.now(UTC))
)
session.add(
RunNode(
run_id="cache-1",
@@ -891,7 +896,7 @@ def test_overview_is_not_read_as_a_run_id(client, superuser_token_headers):
# -----------------------------------------------------------------------------
# Deleting a run
#
# The route owns the four statements; what these guard is that it takes the
# The foreign keys cascade; what these guard is that deleting a run takes the
# children with it and refuses a run the driver is still writing to.
# -----------------------------------------------------------------------------
@@ -928,7 +933,7 @@ def deletable_run():
def test_deleting_a_run_takes_its_children_with_it(
client, superuser_token_headers, deletable_run
):
"""No foreign key cascades here, so the route has to do it itself."""
"""The foreign keys cascade, which needs them on for every connection."""
answer = client.delete(
f"{settings.API_V1_STR}/runs/{deletable_run}", headers=superuser_token_headers
)
@@ -943,6 +948,18 @@ def test_deleting_a_run_takes_its_children_with_it(
assert left == [], f"{table.__name__} rows outlived the run"
def test_a_node_finishing_after_its_run_was_deleted_is_dropped_quietly(caplog):
"""The constraint refuses its row, and that is a no-op rather than an error."""
service = RunService(controller=_OneFlow(FlowDef(name="gone")), queue=_Collect())
with caplog.at_level(logging.INFO, logger="fluksio.flow.runs"):
service._record_node("del-gone", NodeOutcome(node="gone.a", ok=True))
with Session(db_engine) as session:
assert session.get(RunNode, {"run_id": "del-gone", "node": "gone.a"}) is None
assert "is gone" in caplog.text
assert not [record for record in caplog.records if record.levelno >= logging.ERROR]
def test_deleting_a_run_that_is_not_there_is_a_404(client, superuser_token_headers):
answer = client.delete(
f"{settings.API_V1_STR}/runs/nope-1", headers=superuser_token_headers