Skip to content

Commit 44746c6

Browse files
committed
Store multiple response sets in catalog tables
1 parent bd59c75 commit 44746c6

3 files changed

Lines changed: 39 additions & 2 deletions

File tree

‎src/openstatspec/sql/wide.py‎

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
"""SQLite reference SQL profile for the strict OpenStatSpec wide-table contract."""
22

3+
import json
34
import re
45
from collections.abc import Iterable, Mapping
56
from typing import Any
@@ -46,6 +47,35 @@ def catalog(metadata: MetaData) -> tuple[Table, Table]:
4647
return datasets, variables
4748

4849

50+
51+
def multiple_response_set_catalog(metadata: MetaData) -> Table:
52+
return Table(
53+
"multiple_response_set_catalog", metadata,
54+
Column("dataset_id", String(255), primary_key=True),
55+
Column("set_name", String(255), primary_key=True),
56+
Column("member_ordinal", Integer, primary_key=True),
57+
Column("kind", String(16)), Column("label", Text),
58+
Column("counted_value", Text), Column("variable_name", String(255)),
59+
Column("definition", Text, nullable=False),
60+
)
61+
62+
def multiple_response_set_rows(dataset_id: str, definitions: str) -> list[dict[str, Any]]:
63+
sets = json.loads(definitions)
64+
rows = []
65+
for set_name, definition in sets.items():
66+
members = definition.get("variable_list", definition.get("variables", []))
67+
if isinstance(members, str):
68+
members = members.split()
69+
for ordinal, variable_name in enumerate(members or [None], start=1):
70+
rows.append({
71+
"dataset_id": dataset_id, "set_name": set_name, "member_ordinal": ordinal,
72+
"kind": definition.get("set_type", definition.get("type")),
73+
"label": definition.get("label"),
74+
"counted_value": str(definition.get("counted_value", definition.get("countedvalue", ""))),
75+
"variable_name": variable_name, "definition": json.dumps(definition, default=str, sort_keys=True),
76+
})
77+
return rows
78+
4979
def physical_name(source_name: str, used: set[str]) -> str:
5080
stem = _IDENTIFIER.sub("_", source_name).strip("_").lower() or "variable"
5181
stem = stem[:54]
@@ -76,14 +106,15 @@ def create_wide_dataset(
76106
engine = create_engine(database_url)
77107
metadata = MetaData()
78108
datasets, variable_catalog = catalog(metadata)
109+
multiple_response_catalog = multiple_response_set_catalog(metadata)
79110
data_table = Table(
80111
data_table_name(dataset_id), metadata,
81112
Column("__case_ordinal", BigInteger, primary_key=True, nullable=False),
82113
*(Column(item["physical_name"], Float() if item["storage_kind"] == "numeric" else Text(),
83114
nullable=item["storage_kind"] == "numeric") for item in variables),
84115
)
85116
with engine.begin() as connection:
86-
metadata.create_all(connection, tables=[datasets, variable_catalog])
117+
metadata.create_all(connection, tables=[datasets, variable_catalog, multiple_response_catalog])
87118
if connection.execute(select(datasets.c.dataset_id).where(datasets.c.dataset_id == dataset_id)).first():
88119
raise ValueError(f"Dataset {dataset_id!r} already exists; imports never overwrite a dataset.")
89120
data_table.create(connection)
@@ -98,6 +129,9 @@ def create_wide_dataset(
98129
multiple_response_sets=multiple_response_sets,
99130
))
100131
connection.execute(insert(variable_catalog), [dict(dataset_id=dataset_id, **item) for item in variables])
132+
mrset_rows = multiple_response_set_rows(dataset_id, multiple_response_sets)
133+
if mrset_rows:
134+
connection.execute(insert(multiple_response_catalog), mrset_rows)
101135
if materialized:
102136
connection.execute(insert(data_table), materialized)
103137
return {"dataset_id": dataset_id, "data_table": data_table.name, "case_count": len(materialized)}

‎tests/test_loss_reports.py‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,9 @@ def test_export_reports_unwritable_spss_dictionary_features(tmp_path) -> None:
2222
result = export_sav(
2323
database_url=database, dataset_id="dictionary-gap", destination=tmp_path / "output.sav"
2424
)
25+
import sqlite3
26+
connection = sqlite3.connect(tmp_path / "dataset.sqlite")
27+
assert connection.execute("select set_name, member_ordinal, variable_name from multiple_response_set_catalog").fetchall() == [("set1", 1, None)]
2528
assert {event["code"] for event in result["loss_report"]} == {
2629
"multiple-response-sets-not-exported", "variable-alignment-not-exported"
2730
}

‎tests/test_sav_sqlite.py‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ def test_sav_round_trip_uses_one_wide_table_and_catalog(tmp_path) -> None:
3232

3333
connection = sqlite3.connect(database_path)
3434
table_names = [row[0] for row in connection.execute("select name from sqlite_master where type = \"table\" order by name")]
35-
assert table_names == ["data_tiny", "dataset_catalog", "variable_catalog"]
35+
assert table_names == ["data_tiny", "dataset_catalog", "multiple_response_set_catalog", "variable_catalog"]
3636
assert connection.execute("select __case_ordinal, age, name from data_tiny order by __case_ordinal").fetchall() == [
3737
(1, 34.0, "Ada"), (2, None, "")
3838
]

0 commit comments

Comments
 (0)