Skip to content

Commit e98ef83

Browse files
committed
Validate catalog storage and SQLite URI persistence
1 parent 0713ca7 commit e98ef83

2 files changed

Lines changed: 55 additions & 5 deletions

File tree

‎src/openstatspec/sql/wide.py‎

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1364,24 +1364,26 @@ def _catalog_variable_bijection_state(
13641364
legacy_rows = [
13651365
(
13661366
row["dataset_id"], row["ordinal"], row["source_name"],
1367-
row["physical_name"],
1367+
row["physical_name"], row["storage_kind"],
13681368
)
13691369
for row in connection.execute(select(
13701370
variables.c.dataset_id, variables.c.ordinal,
13711371
variables.c.source_name, variables.c.physical_name,
1372+
variables.c.storage_kind,
13721373
)).mappings()
13731374
]
13741375
normative_rows = [
13751376
(
13761377
row["dataset_name"], row["source_ordinal"], row["source_name"],
1377-
row["physical_name"],
1378+
row["physical_name"], row["storage_kind"],
13781379
)
13791380
for row in connection.execute(
13801381
select(
13811382
normative.dataset.c.dataset_name,
13821383
normative.variable.c.source_ordinal,
13831384
normative.variable.c.source_name,
13841385
normative.variable.c.physical_name,
1386+
normative.variable.c.storage_kind,
13851387
).join(
13861388
normative.variable,
13871389
normative.variable.c.dataset_id == normative.dataset.c.dataset_id,
@@ -1394,7 +1396,8 @@ def _catalog_variable_bijection_state(
13941396
or not isinstance(ordinal, int) or ordinal < 1
13951397
or not isinstance(source_name, str) or not source_name.strip()
13961398
or not isinstance(physical_name, str) or not physical_name.strip()
1397-
for dataset_name, ordinal, source_name, physical_name in rows
1399+
or storage_kind not in {"numeric", "string"}
1400+
for dataset_name, ordinal, source_name, physical_name, storage_kind in rows
13981401
):
13991402
return "unverified"
14001403
if len(set(rows)) != len(rows):
@@ -1675,9 +1678,15 @@ def initialize_wide_catalog(
16751678
"""Install or explicitly migrate a dedicated catalog after server preflight."""
16761679
validate_connection_url(database_url)
16771680
parsed_url = make_url(database_url)
1681+
sqlite_database = parsed_url.database or ""
1682+
sqlite_mode = str(parsed_url.query.get("mode", "")).lower()
16781683
if (
16791684
parsed_url.get_backend_name() == "sqlite"
1680-
and parsed_url.database in {None, "", ":memory:"}
1685+
and (
1686+
sqlite_database in {"", ":memory:"}
1687+
or sqlite_database.lower() == "file::memory:"
1688+
or sqlite_mode == "memory"
1689+
)
16811690
):
16821691
raise UnsupportedOperationError(
16831692
"Catalog initialization requires a persistent SQLite database URL."

‎tests/test_catalog_lifecycle.py‎

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -328,6 +328,34 @@ def test_duplicate_import_failure_preserves_existing_dataset(tmp_path):
328328

329329

330330

331+
def test_variable_bijection_rejects_storage_kind_mismatch(tmp_path):
332+
path = tmp_path / "variable-storage-mismatch.sqlite"
333+
database = f"sqlite:///{path}"
334+
openstatspec.initialize_catalog(database_url=database)
335+
variables = _variables()
336+
variables[0].update({
337+
"storage_kind": "numeric",
338+
"string_width": None,
339+
})
340+
create_wide_dataset(
341+
database_url=database,
342+
dataset_id="sample",
343+
source_name="fixture.sav",
344+
source_format="SAV",
345+
rows=[{"name": 1.0}],
346+
variables=variables,
347+
)
348+
connection = sqlite3.connect(path)
349+
connection.execute(
350+
"update variable set storage_kind = 'string' where source_name = 'name'"
351+
)
352+
connection.commit()
353+
connection.close()
354+
355+
with pytest.raises(UnsupportedOperationError, match="catalog is unverified"):
356+
read_wide_dataset(database_url=database, dataset_id="sample")
357+
358+
331359
def test_missing_additive_column_requires_explicit_migration_without_audit_mutation(
332360
tmp_path,
333361
):
@@ -433,14 +461,27 @@ def test_reflected_mysql_integer_display_width_is_not_semantic():
433461
inspector, mysql.VARCHAR(length=255),
434462
) == "VARCHAR(255)"
435463

436-
@pytest.mark.parametrize("database_url", ["sqlite://", "sqlite:///:memory:"])
464+
@pytest.mark.parametrize("database_url", [
465+
"sqlite://",
466+
"sqlite:///:memory:",
467+
"sqlite:///file:memdb1?mode=memory&cache=shared&uri=true",
468+
"sqlite:///file::memory:?cache=shared&uri=true",
469+
])
437470
def test_catalog_initialization_rejects_ephemeral_sqlite_url(database_url):
438471
with pytest.raises(
439472
UnsupportedOperationError,
440473
match="requires a persistent SQLite database URL",
441474
):
442475
openstatspec.initialize_catalog(database_url=database_url)
443476

477+
def test_catalog_initialization_allows_file_backed_sqlite_uri(tmp_path):
478+
path = tmp_path / "uri-catalog.sqlite"
479+
database = f"sqlite:///file:{path}?mode=rwc&uri=true"
480+
481+
assert openstatspec.initialize_catalog(database_url=database)["catalog"] == "verified"
482+
assert path.is_file()
483+
484+
444485
def test_import_fidelity_reader_excludes_export_operational_events(tmp_path):
445486
path = tmp_path / "fidelity-directions.sqlite"
446487
database = f"sqlite:///{path}"

0 commit comments

Comments
 (0)