Skip to content

Commit b52a0c6

Browse files
committed
Bind cleanup and preflight schema edits
1 parent beaf6ae commit b52a0c6

5 files changed

Lines changed: 127 additions & 9 deletions

File tree

‎src/openstatspec/sql/inplace_transform.py‎

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
from .capabilities import effective_profile
2828
from .dolt_conformance import DoltConformanceSource
2929
from .normative import catalog as core_catalog
30+
from .profiles import SqlProfile, preflight
3031
from .wide import physical_name, require_verified_catalog
3132
from .workflow import TransformationError
3233

@@ -435,6 +436,7 @@ def _apply_plan_on_connection(
435436
submission: InPlacePlanSubmission,
436437
actor: str,
437438
database_profile: str,
439+
target_profile: SqlProfile,
438440
allow_schema_change: bool,
439441
allow_delete_variable: bool,
440442
dolt_branch: str | None,
@@ -451,6 +453,22 @@ def _apply_plan_on_connection(
451453
table_name = _physical_table_name(dataset)
452454
plan = submission.plan
453455
bound = bind_transformation_plan(plan, schema)
456+
output_used_physical = {"__case_ordinal"}
457+
output_variables = [
458+
{
459+
"ordinal": source_ordinal,
460+
"source_name": variable.name,
461+
"physical_name": physical_name(
462+
variable.name, output_used_physical,
463+
),
464+
"storage_kind": variable.storage_kind,
465+
"string_width": variable.declared_string_width,
466+
}
467+
for source_ordinal, variable in enumerate(
468+
bound.output_schema.variables, start=1,
469+
)
470+
]
471+
preflight(target_profile, output_variables)
454472
output_by_name = {
455473
variable.name.casefold(): variable
456474
for variable in bound.output_schema.variables
@@ -963,6 +981,7 @@ def _run_in_place_submission(
963981
submission=submission,
964982
actor=actor,
965983
database_profile=profile.name,
984+
target_profile=profile,
966985
allow_schema_change=profile.name in {"sqlite", "postgresql"},
967986
allow_delete_variable=allow_delete_variable,
968987
dolt_branch=branch,

‎src/openstatspec/sql/wide.py‎

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -949,7 +949,12 @@ def create_wide_dataset(
949949
except Exception as error:
950950
if namespace_owned:
951951
try:
952-
with engine.begin() as cleanup:
952+
with _bound_catalog_transaction(
953+
engine=engine, profile_name=profile.name,
954+
active=active_connection,
955+
audit_relations={*audit_relations, data_table.name},
956+
phase="import cleanup",
957+
) as cleanup:
953958
create_normative_catalog(cleanup, normative)
954959
if cleanup.execute(
955960
select(normative.dataset.c.dataset_id).where(

‎tests/test_atomic_import.py‎

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import sqlite3
2+
from contextlib import contextmanager
23
from dataclasses import replace
34

45
import pytest
@@ -194,6 +195,55 @@ def test_declared_string_width_preflight_is_atomic_and_diagnostic(tmp_path, monk
194195
assert '"reason": "declared_string_width_limit"' in details
195196
assert '"string_width": 4' in details
196197

198+
def test_dolt_import_cleanup_uses_bound_catalog_transaction(
199+
tmp_path, monkeypatch,
200+
) -> None:
201+
database_path = tmp_path / "dolt-cleanup.sqlite"
202+
database = f"sqlite:///{database_path}"
203+
active = {
204+
"working_set_binding": {
205+
"database": "test",
206+
"active_branch": "main",
207+
},
208+
}
209+
monkeypatch.setattr(wide, "effective_profile", lambda _url: (DOLT, active))
210+
monkeypatch.setattr(
211+
wide, "_capture_dolt_state", lambda *_args, **_kwargs: None,
212+
)
213+
phases = []
214+
real_bound = wide._bound_catalog_transaction
215+
216+
@contextmanager
217+
def capture_bound(**kwargs):
218+
phases.append(kwargs["phase"])
219+
with real_bound(**kwargs) as connection:
220+
yield connection
221+
222+
monkeypatch.setattr(wide, "_bound_catalog_transaction", capture_bound)
223+
real_store = wide.store_normative_dataset
224+
225+
def fail_after_normative_write(*args, **kwargs):
226+
real_store(*args, **kwargs)
227+
raise RuntimeError("fault after Dolt normative write")
228+
229+
monkeypatch.setattr(wide, "store_normative_dataset", fail_after_normative_write)
230+
variables = [{
231+
"ordinal": 1, "source_name": "score", "physical_name": "score",
232+
"storage_kind": "numeric", "string_width": None, "label": "",
233+
"format": "F8.0", "measure": "scale", "alignment": "right",
234+
"display_width": 8, "value_labels": "{}", "missing_ranges": "[]",
235+
}]
236+
237+
with pytest.raises(RuntimeError, match="fault after Dolt normative write"):
238+
create_wide_dataset(
239+
database_url=database, dataset_id="dolt-cleanup",
240+
source_name="fixture.sav", source_format="SAV",
241+
rows=[{"score": 1.0}], variables=variables,
242+
)
243+
244+
assert phases[-2:] == ["import", "import cleanup"]
245+
246+
197247
def test_nonatomic_failure_after_normative_write_cleans_both_catalogs_and_data(
198248
tmp_path, monkeypatch,
199249
) -> None:

‎tests/test_conditional_inplace_transform.py‎

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,11 @@
22

33
import sqlite3
44

5-
from types import SimpleNamespace
65
import pytest
76

87
import openstatspec
98
import openstatspec.sql.inplace_transform as inplace_transform
9+
from openstatspec.sql.profiles import DOLT
1010
from openstatspec.sql.wide import create_wide_dataset
1111

1212

@@ -254,7 +254,7 @@ def test_dolt_mock_applies_exact_program_to_preexisting_target_without_schema_dd
254254
monkeypatch.setattr(
255255
inplace_transform,
256256
"effective_profile",
257-
lambda _url, **_kwargs: (SimpleNamespace(name="dolt"), {}),
257+
lambda _url, **_kwargs: (DOLT, {}),
258258
)
259259
states = iter([
260260
("main", "abc123", 0),
@@ -291,7 +291,7 @@ def test_dolt_mock_rejects_create_target_before_schema_mutation(
291291
monkeypatch.setattr(
292292
inplace_transform,
293293
"effective_profile",
294-
lambda _url, **_kwargs: (SimpleNamespace(name="dolt"), {}),
294+
lambda _url, **_kwargs: (DOLT, {}),
295295
)
296296
monkeypatch.setattr(
297297
inplace_transform,
@@ -334,7 +334,7 @@ def test_dolt_mock_rechecks_clean_state_after_dataset_lock(
334334
monkeypatch.setattr(
335335
inplace_transform,
336336
"effective_profile",
337-
lambda _url, **_kwargs: (SimpleNamespace(name="dolt"), {}),
337+
lambda _url, **_kwargs: (DOLT, {}),
338338
)
339339
states = iter([
340340
("main", "abc123", 0),

‎tests/test_inplace_transform.py‎

Lines changed: 48 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
from __future__ import annotations
22

33
import sqlite3
4+
from dataclasses import replace
45
from types import SimpleNamespace
56

67
import pytest
@@ -12,6 +13,9 @@
1213
InPlacePlanSubmission,
1314
_apply_plan_on_connection,
1415
)
16+
from openstatspec.sql.profiles import (
17+
DOLT, SQLITE, TargetCapabilityExceededError,
18+
)
1519
from openstatspec.sql.wide import create_wide_dataset, validate_wide_dataset
1620

1721

@@ -102,6 +106,7 @@ def test_plan_applies_to_same_dataset_and_physical_table_without_copy(
102106
),
103107
actor="test-agent",
104108
database_profile="sqlite",
109+
target_profile=inplace_transform.effective_profile(url)[0],
105110
allow_schema_change=True,
106111
allow_delete_variable=True,
107112
dolt_branch=None,
@@ -318,6 +323,44 @@ def test_public_generic_plan_apply_accepts_object_and_mapping(
318323
)
319324

320325

326+
def test_generic_string_width_is_rejected_before_ddl(
327+
catalog, monkeypatch,
328+
) -> None:
329+
url, path, dataset_id, table_name = catalog
330+
monkeypatch.setattr(
331+
inplace_transform,
332+
"effective_profile",
333+
lambda _url, **_kwargs: (
334+
replace(SQLITE, max_text_value_bytes=3),
335+
{"server_version": "3.35.0"},
336+
),
337+
)
338+
plan = openstatspec.TransformationPlan((
339+
openstatspec.CreateVariableOperation("note", "string", 4),
340+
))
341+
342+
with pytest.raises(TargetCapabilityExceededError, match="permits 3"):
343+
openstatspec.apply_transformation_plan_in_place(
344+
database_url=url,
345+
dataset_id=dataset_id,
346+
plan=plan,
347+
actor="test-agent",
348+
)
349+
350+
connection = sqlite3.connect(path)
351+
assert [
352+
row[1] for row in connection.execute(f'PRAGMA table_info("{table_name}")')
353+
] == ["__case_ordinal", "score"]
354+
assert connection.execute(
355+
"select source_name from variable where dataset_id = ?",
356+
(dataset_id,),
357+
).fetchall() == [("score",)]
358+
assert connection.execute(
359+
"select count(*) from transformation_apply"
360+
).fetchone() == (0,)
361+
connection.close()
362+
363+
321364
def test_generic_plan_is_bound_to_live_schema_before_mutation(catalog) -> None:
322365
url, path, dataset_id, table_name = catalog
323366
plan = openstatspec.TransformationPlan((
@@ -481,7 +524,7 @@ def test_delete_is_rejected_when_drop_column_is_unavailable(
481524
inplace_transform,
482525
"effective_profile",
483526
lambda _url, **_kwargs: (
484-
SimpleNamespace(name="sqlite"),
527+
SQLITE,
485528
{"server_version": "3.34.0"},
486529
),
487530
)
@@ -521,6 +564,7 @@ def test_nontransactional_ddl_profile_rejects_create_before_mutation(
521564
),
522565
actor="test-agent",
523566
database_profile="mysql",
567+
target_profile=inplace_transform.effective_profile(url)[0],
524568
allow_schema_change=False,
525569
allow_delete_variable=True,
526570
dolt_branch=None,
@@ -546,7 +590,7 @@ def test_public_apply_binds_expected_dolt_branch_and_head(
546590
monkeypatch.setattr(
547591
inplace_transform,
548592
"effective_profile",
549-
lambda _url, **_kwargs: (SimpleNamespace(name="dolt"), {}),
593+
lambda _url, **_kwargs: (DOLT, {}),
550594
)
551595
states = iter([
552596
("feature/recode", "abc123", 0),
@@ -577,7 +621,7 @@ def test_public_apply_rejects_dirty_dolt_working_set_before_mutation(
577621
monkeypatch.setattr(
578622
inplace_transform,
579623
"effective_profile",
580-
lambda _url, **_kwargs: (SimpleNamespace(name="dolt"), {}),
624+
lambda _url, **_kwargs: (DOLT, {}),
581625
)
582626
monkeypatch.setattr(
583627
inplace_transform,
@@ -613,7 +657,7 @@ def test_public_apply_rejects_dolt_context_mismatch_before_mutation(
613657
monkeypatch.setattr(
614658
inplace_transform,
615659
"effective_profile",
616-
lambda _url, **_kwargs: (SimpleNamespace(name="dolt"), {}),
660+
lambda _url, **_kwargs: (DOLT, {}),
617661
)
618662
monkeypatch.setattr(
619663
inplace_transform, "_dolt_state", lambda _connection: state

0 commit comments

Comments
 (0)