Skip to content

Commit d271f37

Browse files
committed
Declare SQL target profiles and enforce limits
1 parent f871ed4 commit d271f37

4 files changed

Lines changed: 64 additions & 2 deletions

File tree

‎src/openstatspec/api.py‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,14 @@
66

77
from .core import CapabilityDeclaration
88
from .spss import export_dataset, import_dataset, inspect_source
9-
from .sql import validate_dataset
9+
from .sql import declared_profiles, validate_dataset
1010

1111

1212
def capabilities() -> Mapping[str, Any]:
1313
return CapabilityDeclaration(
1414
specification="OpenStatSpec strict wide-table SPSS profile (initial)",
1515
formats={"SAV": {"import": True, "export": True}, "ZSAV": {"import": True, "export": False}},
16-
database_profiles={"sqlite": {"import": True, "export": True}},
16+
database_profiles=declared_profiles(),
1717
).as_dict()
1818

1919

‎src/openstatspec/sql/__init__.py‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,13 @@
22

33
from typing import Any
44

5+
from .profiles import PROFILES
56
from .wide import validate_wide_dataset
67

78

9+
def declared_profiles() -> dict[str, dict[str, object]]:
10+
return {profile.name: profile.as_dict() for profile in PROFILES}
11+
12+
813
def validate_dataset(*, database_url: Any, dataset_id: str, **options: Any) -> dict[str, Any]:
914
return validate_wide_dataset(database_url=str(database_url), dataset_id=dataset_id)

‎src/openstatspec/sql/profiles.py‎

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
"""Dialect capability declarations for the strict wide-table contract.
2+
3+
A declaration is deliberately not a claim that a live target has been tested.
4+
Importers use this information for preflight checks before creating a dataset.
5+
"""
6+
7+
from dataclasses import dataclass
8+
from urllib.parse import urlparse
9+
10+
from ..core import UnsupportedOperationError
11+
12+
13+
@dataclass(frozen=True)
14+
class SqlProfile:
15+
name: str
16+
url_schemes: tuple[str, ...]
17+
max_physical_variables: int
18+
identifier_limit: int
19+
binary64_numeric: bool
20+
lossless_text: bool
21+
tested_reference: bool = False
22+
23+
def as_dict(self) -> dict[str, object]:
24+
return {
25+
"max_physical_variables": self.max_physical_variables,
26+
"identifier_limit": self.identifier_limit,
27+
"binary64_numeric": self.binary64_numeric,
28+
"lossless_text": self.lossless_text,
29+
"tested_reference": self.tested_reference,
30+
}
31+
32+
33+
SQLITE = SqlProfile("sqlite", ("sqlite",), 1_999, 255, True, True, True)
34+
POSTGRESQL = SqlProfile("postgresql", ("postgresql", "postgres"), 1_599, 63, True, True)
35+
MYSQL = SqlProfile("mysql", ("mysql", "mariadb"), 1_016, 64, True, True)
36+
PROFILES = (SQLITE, POSTGRESQL, MYSQL)
37+
38+
39+
def profile_for_url(database_url: str) -> SqlProfile:
40+
scheme = urlparse(database_url).scheme.split("+", 1)[0].lower()
41+
for profile in PROFILES:
42+
if scheme in profile.url_schemes:
43+
return profile
44+
raise UnsupportedOperationError(
45+
f"No OpenStatSpec SQL profile is declared for database URL scheme {scheme!r}."
46+
)
47+
48+
49+
def preflight(profile: SqlProfile, variable_count: int) -> None:
50+
if variable_count > profile.max_physical_variables:
51+
raise UnsupportedOperationError(
52+
f"Target capability exceeded: {profile.name} supports at most "
53+
f"{profile.max_physical_variables} source variables in one strict wide table."
54+
)

‎src/openstatspec/sql/wide.py‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
from typing import Any
66

77
from sqlalchemy import BigInteger, Column, Float, Integer, MetaData, String, Table, Text, create_engine, insert, select
8+
from .profiles import preflight, profile_for_url
89

910
_IDENTIFIER = re.compile(r"[^a-zA-Z0-9_]+")
1011

@@ -61,6 +62,8 @@ def create_wide_dataset(
6162
rows: Iterable[Mapping[str, Any]], variables: list[dict[str, Any]], file_label: str = "",
6263
source_encoding: str | None = None, documents: str = "[]",
6364
) -> dict[str, Any]:
65+
profile = profile_for_url(database_url)
66+
preflight(profile, len(variables))
6467
engine = create_engine(database_url)
6568
metadata = MetaData()
6669
datasets, variable_catalog = catalog(metadata)

0 commit comments

Comments
 (0)