Skip to content

Commit ecebe50

Browse files
committed
Reject foreign views and prune variable sets
1 parent b0b4b98 commit ecebe50

4 files changed

Lines changed: 112 additions & 10 deletions

File tree

‎src/openstatspec/sql/inplace_transform.py‎

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -316,6 +316,11 @@ def _delete_variable_metadata(
316316
) -> None:
317317
"""Delete one variable and every normative catalog row owned by it."""
318318
variable_id = str(variable["variable_id"])
319+
variable_set_ids = list(connection.execute(
320+
select(core.variable_set_member.c.variable_set_id).where(
321+
core.variable_set_member.c.variable_id == variable_id
322+
)
323+
).scalars())
319324
response_set_ids = list(connection.execute(
320325
select(core.multiple_response_member.c.multiple_response_set_id).where(
321326
core.multiple_response_member.c.variable_id == variable_id
@@ -332,6 +337,16 @@ def _delete_variable_metadata(
332337
connection.execute(delete(core.variable_set_member).where(
333338
core.variable_set_member.c.variable_id == variable_id
334339
))
340+
for variable_set_id in variable_set_ids:
341+
remaining_member = connection.execute(
342+
select(core.variable_set_member.c.variable_id).where(
343+
core.variable_set_member.c.variable_set_id == variable_set_id
344+
)
345+
).first()
346+
if remaining_member is None:
347+
connection.execute(delete(core.variable_set).where(
348+
core.variable_set.c.variable_set_id == variable_set_id
349+
))
335350
connection.execute(delete(core.multiple_response_member).where(
336351
core.multiple_response_member.c.variable_id == variable_id
337352
))

‎src/openstatspec/sql/wide.py‎

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -813,14 +813,18 @@ def create_wide_dataset(
813813
engine=engine, profile_name=profile.name, active=active_connection,
814814
audit_relations=audit_relations, phase="catalog initialization",
815815
) as catalog_connection:
816-
catalog_existed = inspect(catalog_connection).has_table(
817-
normative.catalog_identity.name
818-
)
819-
create_normative_catalog(catalog_connection, normative)
820-
if catalog_existed:
821-
require_verified_catalog(catalog_connection)
816+
inspector = inspect(catalog_connection)
817+
existing_tables = set(inspector.get_table_names())
818+
existing_views = set(inspector.get_view_names())
819+
if existing_tables or existing_views:
820+
if normative.catalog_identity.name not in existing_tables:
821+
raise UnsupportedOperationError(
822+
"The selected database catalog is foreign; "
823+
"import is not permitted."
824+
)
822825
else:
823-
_verify_normative_catalog(catalog_connection, normative)
826+
create_normative_catalog(catalog_connection, normative)
827+
require_verified_catalog(catalog_connection)
824828
operation_id = str(uuid4())
825829
normative_dataset_id = str(uuid4())
826830
fidelity_events = tuple(fidelity_events)
@@ -873,12 +877,13 @@ def create_wide_dataset(
873877
data_table_created = False
874878
try:
875879
with engine.begin() as setup:
876-
_verify_normative_catalog(setup, normative)
880+
require_verified_catalog(setup)
877881
namespace_owned = True
878882
with _bound_catalog_transaction(
879883
engine=engine, profile_name=profile.name, active=active_connection,
880884
audit_relations={*audit_relations, data_table.name}, phase="import",
881885
) as connection:
886+
require_verified_catalog(connection)
882887
if connection.execute(
883888
select(normative.dataset.c.dataset_id).where(or_(
884889
normative.dataset.c.dataset_name == dataset_id,

‎tests/test_atomic_import.py‎

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,9 @@ def inspect_with_concurrent_table(bind):
5050
actual = real_inspect(bind)
5151

5252
class Inspector:
53+
def __getattr__(self, name):
54+
return getattr(actual, name)
55+
5356
def has_table(self, table_name):
5457
nonlocal inspected_data_table
5558
if table_name == "data_race":
@@ -298,6 +301,39 @@ def fail_after_normative_write(*args, **kwargs):
298301
"select direction, severity, event_code, dataset_id from fidelity_event"
299302
).fetchall() == [("import", "error", "import_failed", None)]
300303

304+
def test_occupied_foreign_view_namespace_fails_without_modification(
305+
tmp_path,
306+
) -> None:
307+
database_path = tmp_path / "foreign-view.sqlite"
308+
database = f"sqlite:///{database_path}"
309+
connection = sqlite3.connect(database_path)
310+
connection.execute("create view foreign_view as select 1 as value")
311+
before_schema = connection.execute(
312+
"select name, type, sql from sqlite_master order by name"
313+
).fetchall()
314+
connection.close()
315+
variables = [{
316+
"ordinal": 1, "source_name": "score", "physical_name": "score",
317+
"storage_kind": "numeric", "string_width": None, "label": "",
318+
"format": "F8.0", "measure": "scale", "alignment": "right",
319+
"display_width": 8, "value_labels": "{}", "missing_ranges": "[]",
320+
}]
321+
322+
with pytest.raises(wide.UnsupportedOperationError, match="foreign"):
323+
create_wide_dataset(
324+
database_url=database, dataset_id="foreign-view",
325+
source_name="fixture.sav", source_format="SAV",
326+
rows=[{"score": 1.0}], variables=variables,
327+
)
328+
329+
connection = sqlite3.connect(database_path)
330+
assert connection.execute(
331+
"select name, type, sql from sqlite_master order by name"
332+
).fetchall() == before_schema
333+
assert connection.execute("select value from foreign_view").fetchall() == [(1,)]
334+
connection.close()
335+
336+
301337
def test_occupied_foreign_namespace_fails_without_modification(tmp_path) -> None:
302338
database_path = tmp_path / "foreign.sqlite"
303339
database = f"sqlite:///{database_path}"
@@ -315,7 +351,7 @@ def test_occupied_foreign_namespace_fails_without_modification(tmp_path) -> None
315351
"format": "A8", "measure": "nominal", "alignment": "left",
316352
"display_width": 8, "value_labels": "{}", "missing_ranges": "[]",
317353
}]
318-
with pytest.raises(RuntimeError, match="occupied"):
354+
with pytest.raises(RuntimeError, match="foreign"):
319355
create_wide_dataset(
320356
database_url=database, dataset_id="foreign", source_name="fixture.sav",
321357
source_format="SAV", rows=[{"name": "ok"}], variables=variables,

‎tests/test_inplace_transform.py‎

Lines changed: 47 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,9 @@
1616
from openstatspec.sql.profiles import (
1717
DOLT, SQLITE, TargetCapabilityExceededError,
1818
)
19-
from openstatspec.sql.wide import create_wide_dataset, validate_wide_dataset
19+
from openstatspec.sql.wide import (
20+
create_wide_dataset, read_wide_dataset, validate_wide_dataset,
21+
)
2022

2123

2224
def _variables() -> list[dict[str, object]]:
@@ -250,6 +252,50 @@ def test_delete_recanonicalizes_surviving_collision_columns(tmp_path) -> None:
250252
)["valid"] is True
251253

252254

255+
def test_delete_prunes_an_empty_spss_variable_set(catalog) -> None:
256+
url, path, dataset_id, _table_name = catalog
257+
connection = sqlite3.connect(path)
258+
variable_id = connection.execute(
259+
"select variable_id from variable where dataset_id = ?",
260+
(dataset_id,),
261+
).fetchone()[0]
262+
variable_set_id = "00000000-0000-0000-0000-000000000002"
263+
connection.execute(
264+
"insert into variable_set "
265+
"(variable_set_id, dataset_id, source_ordinal, set_name) "
266+
"values (?, ?, 1, 'scores')",
267+
(variable_set_id, dataset_id),
268+
)
269+
connection.execute(
270+
"insert into variable_set_member "
271+
"(variable_set_id, variable_id, source_ordinal) values (?, ?, 1)",
272+
(variable_set_id, variable_id),
273+
)
274+
connection.commit()
275+
connection.close()
276+
277+
openstatspec.apply_spss_in_place(
278+
database_url=url,
279+
dataset_id=dataset_id,
280+
source_text="COMPUTE other = score. DELETE VARIABLES score.",
281+
actor="test-agent",
282+
)
283+
284+
connection = sqlite3.connect(path)
285+
assert connection.execute(
286+
"select count(*) from variable_set where dataset_id = ?",
287+
(dataset_id,),
288+
).fetchone() == (0,)
289+
assert connection.execute(
290+
"select count(*) from variable_set_member"
291+
).fetchone() == (0,)
292+
connection.close()
293+
dataset, _variables, _rows = read_wide_dataset(
294+
database_url=url, dataset_id=dataset_id,
295+
)
296+
assert dataset["source_extensions"] == {}
297+
298+
253299
def test_delete_prunes_an_empty_multiple_response_set(catalog) -> None:
254300
url, path, dataset_id, _table_name = catalog
255301
connection = sqlite3.connect(path)

0 commit comments

Comments
 (0)