Skip to content

Commit abdbd91

Browse files
Merge pull request #28 from OpenStatSpec/audit/workflow-sql-calls
perf: avoid discarded core relation hash during workflow registration
2 parents 40466ea + 5eed10e commit abdbd91

2 files changed

Lines changed: 87 additions & 13 deletions

File tree

‎src/openstatspec/sql/workflow.py‎

Lines changed: 37 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1170,18 +1170,31 @@ def _validate_declared_semantics(
11701170
return row_semantics
11711171

11721172

1173+
def _relation_snapshot_statement(
1174+
connection: Any, *, relation_schema: str | None, relation_name: str,
1175+
variables: Sequence[Mapping[str, Any]], ordinal_name: str,
1176+
validate_only: bool = False,
1177+
) -> Any:
1178+
quote = connection.dialect.identifier_preparer.quote
1179+
relation = _quote_relation(connection, relation_schema, relation_name)
1180+
alias = quote("__oss_snapshot_relation")
1181+
projection = ", ".join(
1182+
f"{alias}.{quote(str(item['physical_name']))}" for item in variables
1183+
)
1184+
ordinal = f"{alias}.{quote(ordinal_name)}"
1185+
suffix = " WHERE 1 = 0" if validate_only else f" ORDER BY {ordinal}"
1186+
return text(f"SELECT {ordinal}, {projection} FROM {relation} AS {alias}{suffix}")
1187+
1188+
11731189
def _relation_snapshot_hash(
11741190
connection: Any, *, relation_schema: str | None, relation_name: str,
11751191
variables: Sequence[Mapping[str, Any]], ordinal_name: str,
11761192
schema_hash: str,
11771193
) -> str:
11781194
"""Hash the normative typed relation-snapshot envelope without buffering."""
1179-
quote = connection.dialect.identifier_preparer.quote
1180-
relation = _quote_relation(connection, relation_schema, relation_name)
1181-
projection = ", ".join(quote(str(item["physical_name"])) for item in variables)
1182-
statement = text(
1183-
f"SELECT {quote(ordinal_name)}, {projection} FROM {relation} "
1184-
f"ORDER BY {quote(ordinal_name)}"
1195+
statement = _relation_snapshot_statement(
1196+
connection, relation_schema=relation_schema, relation_name=relation_name,
1197+
variables=variables, ordinal_name=ordinal_name,
11851198
)
11861199

11871200
def typed(value: Any, kind: str) -> dict[str, str]:
@@ -1217,7 +1230,10 @@ def typed(value: Any, kind: str) -> dict[str, str]:
12171230
return digest.hexdigest()
12181231

12191232

1220-
def _parent_snapshot(connection: Any, parent_kind: str, parent_dataset_id: str) -> dict[str, Any]:
1233+
def _parent_snapshot(
1234+
connection: Any, parent_kind: str, parent_dataset_id: str,
1235+
*, include_relation_hash: bool = True,
1236+
) -> dict[str, Any]:
12211237
parent_dataset_id = _uuid(parent_dataset_id, "parent_dataset_id")
12221238
core = core_catalog(MetaData())
12231239
workflow = workflow_catalog(MetaData())
@@ -1282,14 +1298,19 @@ def _parent_snapshot(connection: Any, parent_kind: str, parent_dataset_id: str)
12821298
for row in converted
12831299
]))
12841300
)
1285-
dataset_hash = (
1286-
verified_derived_hash
1287-
if parent_kind == "derived"
1288-
else _relation_snapshot_hash(
1301+
if parent_kind == "derived":
1302+
dataset_hash = verified_derived_hash
1303+
elif include_relation_hash:
1304+
dataset_hash = _relation_snapshot_hash(
12891305
connection, relation_schema=relation_schema, relation_name=relation_name,
12901306
variables=converted, ordinal_name="__case_ordinal", schema_hash=schema_hash,
12911307
)
1292-
)
1308+
else:
1309+
connection.execute(_relation_snapshot_statement(
1310+
connection, relation_schema=relation_schema, relation_name=relation_name,
1311+
variables=converted, ordinal_name="__case_ordinal", validate_only=True,
1312+
)).first()
1313+
dataset_hash = None
12931314
weight_source_name = next((
12941315
row["source_name"] for row in converted
12951316
if row["variable_id"] == weight_variable_id
@@ -1417,7 +1438,10 @@ def register_transformation(
14171438
tables = workflow_catalog(MetaData())
14181439
with engine.begin() as connection:
14191440
_assert_sqlite_server_version(connection, server_version_constraint)
1420-
parent = _parent_snapshot(connection, parent_kind, parent_dataset_id)
1441+
parent = _parent_snapshot(
1442+
connection, parent_kind, parent_dataset_id,
1443+
include_relation_hash=False,
1444+
)
14211445
parent_columns = {item["source_name"] for item in parent["variables"]}
14221446
declared_sources = {
14231447
lineage["parent_column"]

‎tests/test_sql_workflow.py‎

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -250,6 +250,56 @@ def test_parameterized_view_is_rejected(catalog):
250250
assert caught.value.code == "output_mode_not_supported"
251251

252252

253+
def test_register_core_does_not_hash_case_rows(catalog, monkeypatch):
254+
url, _path, parent_id = catalog
255+
256+
def fail_if_called(*_args, **_kwargs):
257+
raise AssertionError("core registration should not hash case rows")
258+
259+
monkeypatch.setattr(workflow, "_relation_snapshot_hash", fail_if_called)
260+
registered = openstatspec.register_sql_transformation(
261+
database_url=url, parent_dataset_id=parent_id,
262+
query_sql="SELECT score, grp FROM parent ORDER BY score ASC NULLS LAST",
263+
columns=_columns(), transformation_name="schema_only_registration",
264+
)
265+
266+
assert registered["version_number"] == 1
267+
268+
269+
def test_register_core_rejects_missing_physical_relation(catalog):
270+
url, path, parent_id = catalog
271+
with sqlite3.connect(path) as connection:
272+
connection.execute('DROP TABLE "data_source"')
273+
274+
with pytest.raises(OperationalError):
275+
openstatspec.register_sql_transformation(
276+
database_url=url, parent_dataset_id=parent_id,
277+
query_sql="SELECT score, grp FROM parent ORDER BY score ASC NULLS LAST",
278+
columns=_columns(), transformation_name="missing_relation_registration",
279+
)
280+
281+
with sqlite3.connect(path) as connection:
282+
assert connection.execute(
283+
"SELECT name FROM sqlite_master "
284+
"WHERE type = 'table' AND name = 'transformation_version'"
285+
).fetchone() is None
286+
287+
288+
def test_register_core_rejects_missing_physical_column(catalog):
289+
url, path, parent_id = catalog
290+
with sqlite3.connect(path) as connection:
291+
connection.execute(
292+
'ALTER TABLE "data_source" RENAME COLUMN "score" TO "missing_score"'
293+
)
294+
295+
with pytest.raises(OperationalError):
296+
openstatspec.register_sql_transformation(
297+
database_url=url, parent_dataset_id=parent_id,
298+
query_sql="SELECT score, grp FROM parent ORDER BY score ASC NULLS LAST",
299+
columns=_columns(), transformation_name="missing_column_registration",
300+
)
301+
302+
253303
def test_failed_run_is_audited_without_derived_or_physical_output(catalog):
254304
url, path, parent_id = catalog
255305
registered = openstatspec.register_sql_transformation(

0 commit comments

Comments
 (0)