openstatspec 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,260 @@
1
+ """Machine-readable SQL and specification capability declarations."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import re
6
+ from dataclasses import replace
7
+ from pathlib import Path
8
+ from typing import Any
9
+
10
+ from sqlalchemy import MetaData, create_engine, text
11
+ from sqlalchemy.engine import make_url
12
+
13
+ from .normative import catalog
14
+ from .profiles import MYSQL, POSTGRESQL, SQLITE, SqlProfile
15
+ from .profiles import profile_for_url
16
+ from ..core import UnsupportedOperationError
17
+
18
+ SPECIFICATION_COMMIT = "6b9d1fc38f2f083c0ac5cf1c64874a6d07b95045"
19
+ SPECIFICATION_RELEASE: str | None = None
20
+
21
+ SERVER_POLICIES = {
22
+ "sqlite": {
23
+ "claimed": ["SQLite >=3.24.0 <4.0.0"],
24
+ "ci": ["active GitHub runner SQLite version"],
25
+ },
26
+ "mysql": {
27
+ "claimed": ["MySQL 8.4.x", "MySQL 9.7.x"],
28
+ "ci": ["MySQL 8.4.x", "MySQL 9.7.x"],
29
+ },
30
+ "mariadb": {
31
+ "claimed": ["MariaDB 11.4.x", "MariaDB 11.8.x", "MariaDB 12.3.x"],
32
+ "ci": ["MariaDB 11.4.x", "MariaDB 11.8.x", "MariaDB 12.3.x"],
33
+ },
34
+ "postgresql": {
35
+ "claimed": ["PostgreSQL 17.x", "PostgreSQL 18.x"],
36
+ "ci": ["PostgreSQL 17.x", "PostgreSQL 18.x"],
37
+ },
38
+ }
39
+
40
+
41
+ def profile_declarations(database_url: str | None = None) -> dict[str, dict[str, Any]]:
42
+ """Declare every profile and, optionally, one active connection."""
43
+ active = active_connection(database_url) if database_url else None
44
+ return {
45
+ "sqlite": _profile("sqlite", SQLITE, active),
46
+ "mysql": _profile("mysql", MYSQL, active),
47
+ "mariadb": _profile("mariadb", MYSQL, active),
48
+ "postgresql": _profile("postgresql", POSTGRESQL, active),
49
+ }
50
+
51
+
52
+ def active_connection(database_url: str) -> dict[str, Any]:
53
+ engine = create_engine(database_url)
54
+ configured = make_url(database_url)
55
+ with engine.connect() as connection:
56
+ dialect = connection.dialect.name
57
+ if dialect == "sqlite":
58
+ profile_name = "sqlite"
59
+ raw_version = str(connection.execute(text("select sqlite_version()")).scalar_one())
60
+ identity_source = "select sqlite_version()"
61
+ compile_options = {
62
+ name: int(value)
63
+ for option in connection.exec_driver_sql("pragma compile_options").scalars()
64
+ if "=" in str(option)
65
+ for name, value in [str(option).split("=", 1)]
66
+ if value.isdigit()
67
+ }
68
+ observed = {"compile_options": compile_options}
69
+ elif dialect == "postgresql":
70
+ profile_name = "postgresql"
71
+ raw_version = str(connection.execute(text("show server_version")).scalar_one())
72
+ identity_source = "SHOW server_version"
73
+ observed = {}
74
+ elif dialect in {"mysql", "mariadb"}:
75
+ raw_version = str(connection.execute(text("select @@version")).scalar_one())
76
+ comment = str(connection.execute(text("select @@version_comment")).scalar_one())
77
+ profile_name = "mariadb" if "mariadb" in (raw_version + " " + comment).lower() else "mysql"
78
+ identity_source = "SELECT @@version, @@version_comment"
79
+ observed = {
80
+ "max_allowed_packet": int(
81
+ connection.execute(text("select @@max_allowed_packet")).scalar_one()
82
+ )
83
+ }
84
+ else: # pragma: no cover - validate_connection_url rejects this first
85
+ raise ValueError(f"Unsupported active SQL dialect {dialect!r}.")
86
+ return {
87
+ "profile": profile_name,
88
+ "server_version": _normalized_version(raw_version),
89
+ "raw_server_version": raw_version,
90
+ "identity_source": identity_source,
91
+ "claimed_supported": server_version_supported(profile_name, raw_version),
92
+ "matched_claim": _matched_claim(profile_name, raw_version),
93
+ "catalog_binding": catalog_binding(database_url),
94
+ "observed": observed,
95
+ }
96
+
97
+
98
+ def effective_profile(database_url: str) -> tuple[SqlProfile, dict[str, Any]]:
99
+ """Resolve and enforce the profile used by import preflight."""
100
+ configured = profile_for_url(database_url)
101
+ active = active_connection(database_url)
102
+ if configured is not MYSQL and active["profile"] != configured.name:
103
+ raise UnsupportedOperationError("The active SQL server does not match the configured profile.")
104
+ if configured is MYSQL and active["profile"] not in {"mysql", "mariadb"}:
105
+ raise UnsupportedOperationError("The active SQL server is not MySQL or MariaDB.")
106
+ if not active["claimed_supported"]:
107
+ raise UnsupportedOperationError(
108
+ f"Active {active['profile']} server version {active['server_version']} is not claimed supported."
109
+ )
110
+ declaration = _profile(active["profile"], configured, active)
111
+ limits = declaration["effective_limits"]
112
+ assert limits is not None
113
+ return replace(
114
+ configured,
115
+ name=active["profile"],
116
+ max_physical_variables=int(limits["maximum_source_variables"]),
117
+ max_text_value_bytes=int(limits["maximum_value_bytes"]),
118
+ max_row_bytes=int(limits["maximum_row_bytes"]),
119
+ ), active
120
+
121
+
122
+ def catalog_binding(database_url: str) -> dict[str, Any]:
123
+ url = make_url(database_url)
124
+ database = url.database or ""
125
+ if url.get_backend_name() == "sqlite":
126
+ namespace = str(Path(database).resolve()) if database and database != ":memory:" else database
127
+ mode = "dedicated_database_file"
128
+ else:
129
+ namespace = database
130
+ mode = "dedicated_database"
131
+ logical = catalog(MetaData())
132
+ return {
133
+ "mode": mode,
134
+ "namespace": namespace,
135
+ "identity_marker": "catalog_identity",
136
+ "contract_id": "openstatspec-strict-wide-table-v1",
137
+ "schema_version": 1,
138
+ "required_isolation": "exclusive OpenStatSpec catalog connection",
139
+ "logical_to_physical_relations": {
140
+ table.name: table.name for table in logical.all()
141
+ },
142
+ }
143
+
144
+
145
+ def _profile(
146
+ name: str, profile: SqlProfile, active: dict[str, Any] | None,
147
+ ) -> dict[str, Any]:
148
+ identifier = {
149
+ "value": profile.identifier_limit,
150
+ "unit": "characters" if name in {"mysql", "mariadb"} else "bytes",
151
+ "source": (
152
+ "MySQL/MariaDB native identifier limit" if name in {"mysql", "mariadb"}
153
+ else "PostgreSQL NAMEDATALEN minus one native byte limit" if name == "postgresql"
154
+ else "OpenStatSpec profile boundary; SQLite has no fixed native identifier limit"
155
+ ),
156
+ "repertoire": "generated ASCII [a-z0-9_] identifiers",
157
+ }
158
+ theoretical = {
159
+ "maximum_physical_columns": profile.max_physical_variables + 1,
160
+ "maximum_source_variables": profile.max_physical_variables,
161
+ "identifier_limit": identifier,
162
+ "maximum_value_bytes": profile.max_text_value_bytes,
163
+ "maximum_row_bytes": profile.max_row_bytes,
164
+ }
165
+ effective = None
166
+ status = "not_connected"
167
+ if active and active["profile"] == name:
168
+ effective = dict(theoretical)
169
+ sources = {
170
+ "maximum_source_variables": "profile theoretical engine ceiling",
171
+ "maximum_physical_columns": "profile theoretical engine ceiling",
172
+ "identifier_limit": identifier["source"],
173
+ "maximum_value_bytes": "profile theoretical engine ceiling",
174
+ "maximum_row_bytes": "profile theoretical engine ceiling",
175
+ }
176
+ observed = active["observed"]
177
+ if name == "sqlite":
178
+ options = observed["compile_options"]
179
+ if "MAX_COLUMN" in options:
180
+ effective["maximum_physical_columns"] = min(
181
+ theoretical["maximum_physical_columns"], options["MAX_COLUMN"],
182
+ )
183
+ effective["maximum_source_variables"] = max(
184
+ 0, effective["maximum_physical_columns"] - 1,
185
+ )
186
+ sources["maximum_physical_columns"] = "active PRAGMA compile_options MAX_COLUMN"
187
+ sources["maximum_source_variables"] = "active MAX_COLUMN minus technical ordinal"
188
+ if "MAX_LENGTH" in options:
189
+ effective["maximum_value_bytes"] = min(
190
+ theoretical["maximum_value_bytes"], options["MAX_LENGTH"],
191
+ )
192
+ effective["maximum_row_bytes"] = min(
193
+ theoretical["maximum_row_bytes"], options["MAX_LENGTH"],
194
+ )
195
+ sources["maximum_value_bytes"] = "active PRAGMA compile_options MAX_LENGTH"
196
+ sources["maximum_row_bytes"] = "active PRAGMA compile_options MAX_LENGTH"
197
+ status = "active_connection_mixed"
198
+ elif name in {"mysql", "mariadb"}:
199
+ packet = int(observed["max_allowed_packet"])
200
+ payload = max(0, (packet - 131_072) // 2)
201
+ effective["maximum_value_bytes"] = min(
202
+ theoretical["maximum_value_bytes"], payload,
203
+ )
204
+ effective["maximum_statement_bytes"] = payload
205
+ sources["maximum_value_bytes"] = "active @@max_allowed_packet worst-case payload"
206
+ sources["maximum_statement_bytes"] = "active @@max_allowed_packet worst-case payload"
207
+ status = "active_connection_mixed"
208
+ else:
209
+ status = "profile_theoretical_fallback"
210
+ effective["sources"] = sources
211
+ policy = SERVER_POLICIES[name]
212
+ return {
213
+ "driver": "psycopg" if name == "postgresql" else "PyMySQL" if name in {"mysql", "mariadb"} else "sqlite3",
214
+ "claimed_server_versions": policy["claimed"],
215
+ "ci_tested_server_versions": policy["ci"],
216
+ "theoretical_limits": theoretical,
217
+ "effective_limits": effective,
218
+ "effective_limits_status": status,
219
+ "numeric_type": "DOUBLE PRECISION" if name == "postgresql" else "DOUBLE" if name in {"mysql", "mariadb"} else "REAL",
220
+ "text_type": "LONGTEXT" if name in {"mysql", "mariadb"} else "TEXT",
221
+ "ddl_atomic": name not in {"mysql", "mariadb"},
222
+ "failure_cleanup": "compensating_cleanup" if name in {"mysql", "mariadb"} else "transaction_rollback",
223
+ "physical_table_mapping": "dataset.physical_table_schema + dataset.physical_table_name",
224
+ "identifier_policy": "deterministic ASCII mapping; source name remains authoritative",
225
+ }
226
+
227
+
228
+ def server_version_supported(profile: str, raw_version: str) -> bool:
229
+ version = _version_tuple(raw_version)
230
+ if profile == "sqlite":
231
+ return (3, 24) <= version[:2] < (4, 0)
232
+ allowed = {
233
+ "postgresql": {(17,), (18,)},
234
+ "mysql": {(8, 4), (9, 7)},
235
+ "mariadb": {(11, 4), (11, 8), (12, 3)},
236
+ }[profile]
237
+ width = len(next(iter(allowed)))
238
+ return version[:width] in allowed
239
+
240
+
241
+ def _matched_claim(profile: str, raw_version: str) -> str | None:
242
+ if not server_version_supported(profile, raw_version):
243
+ return None
244
+ version = _version_tuple(raw_version)
245
+ for claim in SERVER_POLICIES[profile]["claimed"]:
246
+ numbers = _version_tuple(claim)
247
+ if profile == "sqlite" or version[:2] == numbers[:2] or version[:1] == numbers[:1]:
248
+ return claim
249
+ return SERVER_POLICIES[profile]["claimed"][0]
250
+
251
+
252
+ def _version_tuple(raw_version: str) -> tuple[int, ...]:
253
+ match = re.search(r"(\d+)(?:\.(\d+))?(?:\.(\d+))?", raw_version)
254
+ if not match:
255
+ return ()
256
+ return tuple(int(part) for part in match.groups(default="0"))
257
+
258
+
259
+ def _normalized_version(raw_version: str) -> str:
260
+ return ".".join(str(part) for part in _version_tuple(raw_version))