|
| 1 | +"""SQLite reference SQL profile for the strict OpenStatSpec wide-table contract.""" |
| 2 | + |
| 3 | +import re |
| 4 | +from collections.abc import Iterable, Mapping |
| 5 | +from typing import Any |
| 6 | + |
| 7 | +from sqlalchemy import BigInteger, Column, Float, Integer, MetaData, String, Table, Text, create_engine, insert, select |
| 8 | + |
| 9 | +_IDENTIFIER = re.compile(r"[^a-zA-Z0-9_]+") |
| 10 | + |
| 11 | + |
| 12 | +def catalog(metadata: MetaData) -> tuple[Table, Table]: |
| 13 | + datasets = Table( |
| 14 | + "dataset_catalog", metadata, |
| 15 | + Column("dataset_id", String(255), primary_key=True), |
| 16 | + Column("data_table", String(255), nullable=False, unique=True), |
| 17 | + Column("source_format", String(16), nullable=False), |
| 18 | + Column("source_name", Text, nullable=False), |
| 19 | + Column("case_count", BigInteger, nullable=False), |
| 20 | + Column("file_label", Text, nullable=False, default=""), |
| 21 | + ) |
| 22 | + variables = Table( |
| 23 | + "variable_catalog", metadata, |
| 24 | + Column("dataset_id", String(255), primary_key=True), |
| 25 | + Column("ordinal", Integer, primary_key=True), |
| 26 | + Column("source_name", String(255), nullable=False), |
| 27 | + Column("physical_name", String(255), nullable=False), |
| 28 | + Column("storage_kind", String(16), nullable=False), |
| 29 | + Column("string_width", Integer), |
| 30 | + Column("label", Text, nullable=False, default=""), |
| 31 | + Column("format", String(64)), |
| 32 | + Column("measure", String(32)), |
| 33 | + Column("display_width", Integer), |
| 34 | + Column("value_labels", Text, nullable=False, default="{}"), |
| 35 | + Column("missing_ranges", Text, nullable=False, default="[]"), |
| 36 | + ) |
| 37 | + return datasets, variables |
| 38 | + |
| 39 | + |
| 40 | +def physical_name(source_name: str, used: set[str]) -> str: |
| 41 | + stem = _IDENTIFIER.sub("_", source_name).strip("_").lower() or "variable" |
| 42 | + stem = stem[:54] |
| 43 | + candidate, suffix = stem, 2 |
| 44 | + while candidate.lower() in used or candidate.startswith("__"): |
| 45 | + candidate = f"{stem[:50]}_{suffix}" |
| 46 | + suffix += 1 |
| 47 | + used.add(candidate.lower()) |
| 48 | + return candidate |
| 49 | + |
| 50 | + |
| 51 | +def data_table_name(dataset_id: str) -> str: |
| 52 | + stem = _IDENTIFIER.sub("_", dataset_id).strip("_").lower() or "dataset" |
| 53 | + return f"data_{stem[:48]}" |
| 54 | + |
| 55 | + |
| 56 | +def create_wide_dataset( |
| 57 | + *, database_url: str, dataset_id: str, source_name: str, source_format: str, |
| 58 | + rows: Iterable[Mapping[str, Any]], variables: list[dict[str, Any]], file_label: str = "", |
| 59 | +) -> dict[str, Any]: |
| 60 | + engine = create_engine(database_url) |
| 61 | + metadata = MetaData() |
| 62 | + datasets, variable_catalog = catalog(metadata) |
| 63 | + data_table = Table( |
| 64 | + data_table_name(dataset_id), metadata, |
| 65 | + Column("__case_ordinal", BigInteger, primary_key=True, nullable=False), |
| 66 | + *(Column(item["physical_name"], Float() if item["storage_kind"] == "numeric" else Text(), |
| 67 | + nullable=item["storage_kind"] == "numeric") for item in variables), |
| 68 | + ) |
| 69 | + with engine.begin() as connection: |
| 70 | + metadata.create_all(connection, tables=[datasets, variable_catalog]) |
| 71 | + if connection.execute(select(datasets.c.dataset_id).where(datasets.c.dataset_id == dataset_id)).first(): |
| 72 | + raise ValueError(f"Dataset {dataset_id!r} already exists; imports never overwrite a dataset.") |
| 73 | + data_table.create(connection) |
| 74 | + materialized = [{"__case_ordinal": ordinal, **row} for ordinal, row in enumerate(rows, start=1)] |
| 75 | + connection.execute(insert(datasets).values( |
| 76 | + dataset_id=dataset_id, data_table=data_table.name, source_format=source_format, |
| 77 | + source_name=source_name, case_count=len(materialized), file_label=file_label, |
| 78 | + )) |
| 79 | + connection.execute(insert(variable_catalog), [dict(dataset_id=dataset_id, **item) for item in variables]) |
| 80 | + if materialized: |
| 81 | + connection.execute(insert(data_table), materialized) |
| 82 | + return {"dataset_id": dataset_id, "data_table": data_table.name, "case_count": len(materialized)} |
| 83 | + |
| 84 | + |
| 85 | +def read_wide_dataset(*, database_url: str, dataset_id: str) -> tuple[dict[str, Any], list[dict[str, Any]], list[dict[str, Any]]]: |
| 86 | + engine = create_engine(database_url) |
| 87 | + metadata = MetaData() |
| 88 | + datasets, variable_catalog = catalog(metadata) |
| 89 | + with engine.connect() as connection: |
| 90 | + dataset = connection.execute(select(datasets).where(datasets.c.dataset_id == dataset_id)).mappings().one() |
| 91 | + data_table = Table(dataset["data_table"], MetaData(), autoload_with=connection) |
| 92 | + variables = connection.execute( |
| 93 | + select(variable_catalog).where(variable_catalog.c.dataset_id == dataset_id).order_by(variable_catalog.c.ordinal) |
| 94 | + ).mappings().all() |
| 95 | + rows = connection.execute(select(data_table).order_by(data_table.c.__case_ordinal)).mappings().all() |
| 96 | + return dict(dataset), [dict(item) for item in variables], [dict(item) for item in rows] |
| 97 | + |
| 98 | + |
| 99 | +def validate_wide_dataset(*, database_url: str, dataset_id: str) -> dict[str, Any]: |
| 100 | + dataset, variables, rows = read_wide_dataset(database_url=database_url, dataset_id=dataset_id) |
| 101 | + if not variables: |
| 102 | + raise ValueError("A conforming dataset needs at least one source variable.") |
| 103 | + if [row["__case_ordinal"] for row in rows] != list(range(1, len(rows) + 1)): |
| 104 | + raise ValueError("Case ordinals are not contiguous source order.") |
| 105 | + return {"dataset_id": dataset["dataset_id"], "valid": True, "case_count": len(rows), "variable_count": len(variables)} |
| 106 | + |
0 commit comments