Skip to content

Commit 1bea277

Browse files
committed
Keep Dolt checks and export events transaction-bound
1 parent aefd0c8 commit 1bea277

2 files changed

Lines changed: 166 additions & 8 deletions

File tree

‎src/openstatspec/sql/wide.py‎

Lines changed: 49 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -579,18 +579,18 @@ def _bound_catalog_transaction(
579579
try:
580580
with connection.begin():
581581
yield connection
582+
after = _capture_dolt_state(
583+
connection, profile_name=profile_name,
584+
audit_relations=audit_relations,
585+
)
586+
_require_dolt_success_identity(before, after, phase=phase)
582587
except Exception:
583588
after = _capture_dolt_state(
584589
connection, profile_name=profile_name,
585590
audit_relations=audit_relations,
586591
)
587592
_dolt_failure_boundary_evidence(before, after)
588593
raise
589-
after = _capture_dolt_state(
590-
connection, profile_name=profile_name,
591-
audit_relations=audit_relations,
592-
)
593-
_require_dolt_success_identity(before, after, phase=phase)
594594

595595

596596
def dolt_state_snapshot(
@@ -1417,6 +1417,13 @@ def record_export_operation(
14171417
"source_item": destination,
14181418
"details": operation_details,
14191419
},) if operation_details else ()),
1420+
{
1421+
"code": "export-operation-dataset-binding",
1422+
"detail": "Export operation dataset identity recorded for audit.",
1423+
"severity": "info",
1424+
"source_item": destination,
1425+
"details": {},
1426+
},
14201427
),
14211428
)
14221429
return operation_id
@@ -1435,6 +1442,30 @@ def _export_operation_row(
14351442
return row
14361443

14371444

1445+
def _export_operation_dataset_id(
1446+
connection: Any, normative: Any, operation_id: str,
1447+
) -> str:
1448+
dataset_ids = set(connection.execute(
1449+
select(normative.fidelity_event.c.dataset_id)
1450+
.where(normative.fidelity_event.c.operation_id == operation_id)
1451+
.where(normative.fidelity_event.c.dataset_id.is_not(None))
1452+
).scalars())
1453+
if len(dataset_ids) != 1:
1454+
raise UnsupportedOperationError(
1455+
"The export operation is not bound to exactly one dataset."
1456+
)
1457+
dataset_id = str(dataset_ids.pop())
1458+
if connection.execute(
1459+
select(normative.dataset.c.dataset_id).where(
1460+
normative.dataset.c.dataset_id == dataset_id
1461+
)
1462+
).scalar_one_or_none() is None:
1463+
raise UnsupportedOperationError(
1464+
"The export operation dataset no longer exists."
1465+
)
1466+
return dataset_id
1467+
1468+
14381469
def finish_export_operation(
14391470
*, database_url: str, operation_id: str,
14401471
dolt_conformance_source: Any | None = None,
@@ -1504,12 +1535,15 @@ def fail_export_operation(
15041535
raise UnsupportedOperationError(
15051536
"Only a started export operation can be failed."
15061537
)
1538+
dataset_id = _export_operation_dataset_id(
1539+
connection, normative, operation_id,
1540+
)
15071541
finish_normative_operation(
15081542
connection, normative, operation_id=operation_id, status="failed",
15091543
)
15101544
record_normative_fidelity_events(
15111545
connection, normative, operation_id=operation_id,
1512-
dataset_id=None, direction="export", events=(event,),
1546+
dataset_id=dataset_id, direction="export", events=(event,),
15131547
)
15141548

15151549

@@ -1529,9 +1563,12 @@ def record_export_backup_retained(
15291563
raise UnsupportedOperationError(
15301564
"A retained backup warning requires a succeeded export."
15311565
)
1566+
dataset_id = _export_operation_dataset_id(
1567+
connection, normative, operation_id,
1568+
)
15321569
record_normative_fidelity_events(
15331570
connection, normative, operation_id=operation_id,
1534-
dataset_id=None, direction="export", events=({
1571+
dataset_id=dataset_id, direction="export", events=({
15351572
"code": "backup_retained",
15361573
"detail": "A successful export retained its durable prior-file backup.",
15371574
"severity": "warning",
@@ -1578,6 +1615,7 @@ def record_export_cleanup_failure(
15781615
normative.operation.c.operation_id == operation_id
15791616
)
15801617
).mappings().one_or_none()
1618+
dataset_id = None
15811619
if row is None:
15821620
failed_at = datetime.now(UTC).replace(tzinfo=None)
15831621
record_normative_operation(
@@ -1590,12 +1628,15 @@ def record_export_cleanup_failure(
15901628
raise UnsupportedOperationError(
15911629
"Existing export operation cannot transition to cleanup failure."
15921630
)
1631+
dataset_id = _export_operation_dataset_id(
1632+
connection, normative, operation_id,
1633+
)
15931634
finish_normative_operation(
15941635
connection, normative, operation_id=operation_id, status="failed",
15951636
)
15961637
record_normative_fidelity_events(
15971638
connection, normative, operation_id=operation_id,
1598-
dataset_id=None, direction="export", events=(event,),
1639+
dataset_id=dataset_id, direction="export", events=(event,),
15991640
)
16001641
return operation_id
16011642

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
"""Regression tests for bound commit checks and export dataset linkage."""
2+
3+
import sqlite3
4+
5+
from sqlalchemy import create_engine
6+
7+
from openstatspec.sql import wide
8+
9+
10+
def _variables():
11+
return [{
12+
"ordinal": 1,
13+
"source_name": "name",
14+
"physical_name": "name",
15+
"storage_kind": "string",
16+
"string_width": 8,
17+
"label": "",
18+
"format": "A8",
19+
"measure": "nominal",
20+
"alignment": "left",
21+
"display_width": 8,
22+
"value_labels": "{}",
23+
"missing_ranges": "[]",
24+
}]
25+
26+
27+
def test_dolt_completion_identity_is_checked_before_transaction_commit(
28+
monkeypatch,
29+
):
30+
engine = create_engine("sqlite://")
31+
state = {
32+
"database": "catalog",
33+
"active_branch": "main",
34+
"head": "abc123",
35+
}
36+
transaction_states = []
37+
38+
def capture(connection, **_kwargs):
39+
transaction_states.append(connection.in_transaction())
40+
return state
41+
42+
monkeypatch.setattr(wide, "_capture_dolt_state", capture)
43+
with wide._bound_catalog_transaction(
44+
engine=engine,
45+
profile_name="dolt",
46+
active={
47+
"working_set_binding": {
48+
"database": "catalog",
49+
"active_branch": "main",
50+
},
51+
},
52+
audit_relations=set(),
53+
phase="test",
54+
):
55+
pass
56+
57+
assert transaction_states == [False, True]
58+
59+
60+
def test_export_lifecycle_events_remain_linked_to_the_dataset(tmp_path):
61+
path = tmp_path / "export-events.sqlite"
62+
database_url = f"sqlite:///{path}"
63+
wide.create_wide_dataset(
64+
database_url=database_url,
65+
dataset_id="sample",
66+
source_name="sample.sav",
67+
source_format="SAV",
68+
rows=[{"name": "ok"}],
69+
variables=_variables(),
70+
)
71+
72+
failed_id = wide.record_export_operation(
73+
database_url=database_url,
74+
dataset_id="sample",
75+
destination="failed.sav",
76+
allowed_fidelity_events=(),
77+
terminal=False,
78+
)
79+
wide.fail_export_operation(
80+
database_url=database_url,
81+
operation_id=failed_id,
82+
failure_details={"reason": "test"},
83+
)
84+
succeeded_id = wide.record_export_operation(
85+
database_url=database_url,
86+
dataset_id="sample",
87+
destination="succeeded.sav",
88+
allowed_fidelity_events=(),
89+
terminal=False,
90+
)
91+
wide.finish_export_operation(
92+
database_url=database_url,
93+
operation_id=succeeded_id,
94+
)
95+
wide.record_export_backup_retained(
96+
database_url=database_url,
97+
operation_id=succeeded_id,
98+
destination="succeeded.sav",
99+
backup="succeeded.sav.backup",
100+
cleanup_error=RuntimeError("test"),
101+
)
102+
103+
events = wide.read_fidelity_events(
104+
database_url=database_url,
105+
dataset_id="sample",
106+
)
107+
assert {event["code"] for event in events} == {
108+
"backup_retained",
109+
"export_failed",
110+
}
111+
connection = sqlite3.connect(path)
112+
assert connection.execute(
113+
"select count(*) from fidelity_event "
114+
"where operation_id in (?, ?) and dataset_id is null",
115+
(failed_id, succeeded_id),
116+
).fetchone() == (0,)
117+
connection.close()

0 commit comments

Comments
 (0)