sqlbenchdag 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.
- infrastructure/debug_aws.py +40 -0
- scripts/dev/backfill_queries.py +44 -0
- scripts/dev/timestamp_capsule.py +49 -0
- scripts/dev/upgrade_capsule.py +63 -0
- scripts/dev/verify_capsule.py +79 -0
- scripts/dev/verify_portability.py +103 -0
- scripts/tools/_plotlib.py +43 -0
- scripts/tools/analyze_scaling.py +39 -0
- scripts/tools/demo_breach_detection.py +61 -0
- scripts/tools/extract_results.py +57 -0
- scripts/tools/find_kink.py +104 -0
- scripts/tools/gen_experiment_catalog.py +103 -0
- scripts/tools/load_parquet_to_local.py +98 -0
- scripts/tools/plot_execution_modes.py +82 -0
- scripts/tools/plot_scaling.py +91 -0
- scripts/tools/plot_selectivity.py +139 -0
- scripts/tools/plot_threads.py +95 -0
- scripts/tools/regenerate_dashboard.py +58 -0
- scripts/tools/verify_doc_claims.py +88 -0
- sql_benchmarks/__init__.py +3 -0
- sql_benchmarks/api/__init__.py +0 -0
- sql_benchmarks/api/app.py +30 -0
- sql_benchmarks/api/data/__init__.py +0 -0
- sql_benchmarks/api/data/catalog_reader.py +73 -0
- sql_benchmarks/api/data/reader.py +125 -0
- sql_benchmarks/api/logic/__init__.py +0 -0
- sql_benchmarks/api/logic/comparator.py +44 -0
- sql_benchmarks/api/logic/ranker.py +127 -0
- sql_benchmarks/api/models/__init__.py +0 -0
- sql_benchmarks/api/models/catalog.py +22 -0
- sql_benchmarks/api/models/experiments.py +20 -0
- sql_benchmarks/api/models/recommend.py +11 -0
- sql_benchmarks/api/models/results.py +66 -0
- sql_benchmarks/api/routers/__init__.py +0 -0
- sql_benchmarks/api/routers/catalog.py +19 -0
- sql_benchmarks/api/routers/experiments.py +79 -0
- sql_benchmarks/api/routers/recommend.py +22 -0
- sql_benchmarks/api/routers/results.py +51 -0
- sql_benchmarks/assets/__init__.py +4 -0
- sql_benchmarks/assets/benchmark_factory.py +222 -0
- sql_benchmarks/assets/data_factory.py +54 -0
- sql_benchmarks/assets/data_quality.py +91 -0
- sql_benchmarks/assets/ingestion_factory.py +88 -0
- sql_benchmarks/assets/maintenance.py +63 -0
- sql_benchmarks/assets/reporting.py +385 -0
- sql_benchmarks/assets/semantic_gate.py +76 -0
- sql_benchmarks/cli.py +69 -0
- sql_benchmarks/config_loader.py +104 -0
- sql_benchmarks/constants.py +60 -0
- sql_benchmarks/coordinator.py +282 -0
- sql_benchmarks/definitions.py +115 -0
- sql_benchmarks/experiments/templates/experiment_template.yaml +99 -0
- sql_benchmarks/harness.py +84 -0
- sql_benchmarks/jobs.py +8 -0
- sql_benchmarks/partitions.py +17 -0
- sql_benchmarks/plugins/data_sources/declarative_gen.py +211 -0
- sql_benchmarks/plugins/data_sources/local_file_loader.py +62 -0
- sql_benchmarks/plugins/data_sources/tpc_h.py +61 -0
- sql_benchmarks/resources/__init__.py +0 -0
- sql_benchmarks/resources/actian.py +234 -0
- sql_benchmarks/resources/actian_client.py +231 -0
- sql_benchmarks/resources/base_engine.py +58 -0
- sql_benchmarks/resources/base_schema_client.py +104 -0
- sql_benchmarks/resources/duckdb.py +58 -0
- sql_benchmarks/resources/duckdb_client.py +157 -0
- sql_benchmarks/resources/postgres.py +188 -0
- sql_benchmarks/resources/postgres_client.py +184 -0
- sql_benchmarks/resources/quack.py +56 -0
- sql_benchmarks/resources/quack_client.py +169 -0
- sql_benchmarks/resources/typedb_client.py +370 -0
- sql_benchmarks/resources/typedb_engine.py +303 -0
- sql_benchmarks/scripts/__init__.py +0 -0
- sql_benchmarks/scripts/sql/acid_test/duckdb/test.sql +1 -0
- sql_benchmarks/scripts/sql/acid_test/postgres/test.sql +1 -0
- sql_benchmarks/scripts/sql/analytical_wall/actian/analytical_wall.sql +15 -0
- sql_benchmarks/scripts/sql/analytical_wall/analytical_wall.sql.template +19 -0
- sql_benchmarks/scripts/sql/analytical_wall/duckdb/analytical_wall.sql +15 -0
- sql_benchmarks/scripts/sql/analytical_wall/postgres/analytical_wall.sql +15 -0
- sql_benchmarks/scripts/sql/group_by/duckdb/groupby_antipattern.sql +11 -0
- sql_benchmarks/scripts/sql/group_by/duckdb/groupby_recommended_cte.sql +19 -0
- sql_benchmarks/scripts/sql/group_by/postgres/groupby_antipattern.sql +13 -0
- sql_benchmarks/scripts/sql/group_by/postgres/groupby_recommended_pk.sql +11 -0
- sql_benchmarks/scripts/sql/hypergraph/duckdb/q_1hop.sql +5 -0
- sql_benchmarks/scripts/sql/hypergraph/duckdb/q_2hop.sql +6 -0
- sql_benchmarks/scripts/sql/hypergraph/duckdb/q_full.sql +9 -0
- sql_benchmarks/scripts/sql/hypergraph/typedb/q_1hop.sql +4 -0
- sql_benchmarks/scripts/sql/hypergraph/typedb/q_2hop.sql +5 -0
- sql_benchmarks/scripts/sql/hypergraph/typedb/q_full.sql +6 -0
- sql_benchmarks/scripts/sql/joins/duckdb/join_antipattern.sql +5 -0
- sql_benchmarks/scripts/sql/joins/duckdb/join_recommended.sql +4 -0
- sql_benchmarks/scripts/sql/joins/postgres/join_antipattern.sql +5 -0
- sql_benchmarks/scripts/sql/joins/postgres/join_recommended.sql +4 -0
- sql_benchmarks/scripts/sql/null_logic/duckdb/2VL_identity.sql +14 -0
- sql_benchmarks/scripts/sql/null_logic/duckdb/3VL_identity.sql +5 -0
- sql_benchmarks/scripts/sql/null_logic/postgres/2VL_identity.sql +14 -0
- sql_benchmarks/scripts/sql/null_logic/postgres/3VL_identity.sql +5 -0
- sql_benchmarks/scripts/sql/null_sentinel/duckdb/2VL_identity.sql +14 -0
- sql_benchmarks/scripts/sql/null_sentinel/duckdb/3VL_identity.sql +5 -0
- sql_benchmarks/scripts/sql/null_sentinel/duckdb/sentinel_optimization.sql +16 -0
- sql_benchmarks/scripts/sql/null_sentinel/postgres/2VL_identity.sql +14 -0
- sql_benchmarks/scripts/sql/null_sentinel/postgres/3VL_identity.sql +5 -0
- sql_benchmarks/scripts/sql/null_sentinel/postgres/sentinel_optimization.sql +21 -0
- sql_benchmarks/scripts/sql/recursion/duckdb/recursive_cte.sql +26 -0
- sql_benchmarks/scripts/sql/recursion/postgres/recursive_cte.sql +30 -0
- sql_benchmarks/scripts/sql/recursive_graph/duckdb/q_bounded_3hop.sql +17 -0
- sql_benchmarks/scripts/sql/recursive_graph/duckdb/q_fixed_2hop.sql +6 -0
- sql_benchmarks/scripts/sql/recursive_graph/duckdb/q_transitive_closure.sql +17 -0
- sql_benchmarks/scripts/sql/recursive_graph/typedb/q_bounded_3hop.sql +6 -0
- sql_benchmarks/scripts/sql/recursive_graph/typedb/q_fixed_2hop.sql +5 -0
- sql_benchmarks/scripts/sql/recursive_graph/typedb/q_transitive_closure.sql +7 -0
- sql_benchmarks/scripts/sql/security/acid_resilience/duckdb/test.sql +1 -0
- sql_benchmarks/scripts/sql/selectivity/duckdb/q_0_1_percent.sql +1 -0
- sql_benchmarks/scripts/sql/selectivity/duckdb/q_10_percent.sql +1 -0
- sql_benchmarks/scripts/sql/selectivity/duckdb/q_1_percent.sql +1 -0
- sql_benchmarks/scripts/sql/selectivity/duckdb/q_20_percent.sql +1 -0
- sql_benchmarks/scripts/sql/selectivity/duckdb/q_5_percent.sql +1 -0
- sql_benchmarks/scripts/sql/selectivity/duckdb/q_filler.sql +1 -0
- sql_benchmarks/scripts/sql/selectivity/postgres/q_0_1_percent.sql +1 -0
- sql_benchmarks/scripts/sql/selectivity/postgres/q_10_percent.sql +1 -0
- sql_benchmarks/scripts/sql/selectivity/postgres/q_1_percent.sql +1 -0
- sql_benchmarks/scripts/sql/selectivity/postgres/q_20_percent.sql +1 -0
- sql_benchmarks/scripts/sql/selectivity/postgres/q_5_percent.sql +1 -0
- sql_benchmarks/scripts/sql/selectivity/postgres/q_filler.sql +1 -0
- sql_benchmarks/scripts/sql/selectivity/typedb/q_0_1_percent.sql +4 -0
- sql_benchmarks/scripts/sql/selectivity/typedb/q_10_percent.sql +4 -0
- sql_benchmarks/scripts/sql/selectivity/typedb/q_1_percent.sql +4 -0
- sql_benchmarks/scripts/sql/selectivity/typedb/q_20_percent.sql +4 -0
- sql_benchmarks/scripts/sql/selectivity/typedb/q_5_percent.sql +4 -0
- sql_benchmarks/scripts/sql/selectivity/typedb/q_filler.sql +4 -0
- sql_benchmarks/scripts/sql/sort_spill/duckdb/sort_full.sql +25 -0
- sql_benchmarks/scripts/sql/sort_spill/duckdb/sort_wide_rows.sql +25 -0
- sql_benchmarks/scripts/sql/sort_spill/postgres/sort_full.sql +25 -0
- sql_benchmarks/scripts/sql/sort_spill/postgres/sort_wide_rows.sql +26 -0
- sql_benchmarks/scripts/sql/tpch/duckdb/q3_shipping_priority.sql +25 -0
- sql_benchmarks/scripts/sql/tpch/postgres/q3_shipping_priority.sql +25 -0
- sql_benchmarks/utils/__init__.py +0 -0
- sql_benchmarks/utils/common.py +260 -0
- sql_benchmarks/utils/ddl.py +47 -0
- sql_benchmarks/utils/hasher.py +124 -0
- sql_benchmarks/utils/integrity_monitor.py +55 -0
- sql_benchmarks/utils/providers.py +184 -0
- sql_benchmarks/utils/scaling.py +88 -0
- sql_benchmarks/utils/schema.py +118 -0
- sql_benchmarks/utils/semantic_auditor.py +81 -0
- sql_benchmarks/utils/system.py +100 -0
- sql_benchmarks/validator.py +60 -0
- sqlbenchdag-0.1.0.dist-info/METADATA +221 -0
- sqlbenchdag-0.1.0.dist-info/RECORD +153 -0
- sqlbenchdag-0.1.0.dist-info/WHEEL +5 -0
- sqlbenchdag-0.1.0.dist-info/entry_points.txt +2 -0
- sqlbenchdag-0.1.0.dist-info/licenses/LICENSE +202 -0
- sqlbenchdag-0.1.0.dist-info/top_level.txt +4 -0
- venv/bin/activate_this.py +59 -0
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
-- Sort Spill: Multi-column sort on wide rows.
|
|
2
|
+
-- Hypothesis: At some row count, the sort set exceeds work_mem and Postgres
|
|
3
|
+
-- spills to disk, producing a visible performance cliff.
|
|
4
|
+
-- The work_mem is set intentionally tight in pg_settings to expose this cliff.
|
|
5
|
+
-- DuckDB (sort_spill/duckdb/) runs the same query as a control without this constraint.
|
|
6
|
+
|
|
7
|
+
SELECT
|
|
8
|
+
id,
|
|
9
|
+
region,
|
|
10
|
+
category,
|
|
11
|
+
department,
|
|
12
|
+
sub_category,
|
|
13
|
+
ROUND(CAST(price AS NUMERIC), 4) AS price,
|
|
14
|
+
ROUND(CAST(cost AS NUMERIC), 4) AS cost,
|
|
15
|
+
ROUND(CAST(margin AS NUMERIC), 4) AS margin,
|
|
16
|
+
quantity,
|
|
17
|
+
discount,
|
|
18
|
+
ROUND(CAST(price * quantity * (1.0 - discount) AS NUMERIC), 4) AS net_revenue
|
|
19
|
+
FROM {{ sort_data_table }}
|
|
20
|
+
ORDER BY
|
|
21
|
+
region ASC,
|
|
22
|
+
category ASC,
|
|
23
|
+
department ASC,
|
|
24
|
+
net_revenue DESC,
|
|
25
|
+
quantity DESC
|
|
26
|
+
LIMIT 1000;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
-- TPC-H Query 3 (Shipping Priority)
|
|
2
|
+
-- Modified for Jinja Templating
|
|
3
|
+
SELECT
|
|
4
|
+
l.l_orderkey,
|
|
5
|
+
SUM(l.l_extendedprice * (1 - l.l_discount)) AS revenue,
|
|
6
|
+
o.o_orderdate,
|
|
7
|
+
o.o_shippriority
|
|
8
|
+
FROM
|
|
9
|
+
{{ customer_table }} c,
|
|
10
|
+
{{ orders_table }} o,
|
|
11
|
+
{{ lineitem_table }} l
|
|
12
|
+
WHERE
|
|
13
|
+
c.c_mktsegment = 'BUILDING'
|
|
14
|
+
AND c.c_custkey = o.o_custkey
|
|
15
|
+
AND l.l_orderkey = o.o_orderkey
|
|
16
|
+
AND o.o_orderdate < DATE '1995-03-15'
|
|
17
|
+
AND l.l_shipdate > DATE '1995-03-15'
|
|
18
|
+
GROUP BY
|
|
19
|
+
l.l_orderkey,
|
|
20
|
+
o.o_orderdate,
|
|
21
|
+
o.o_shippriority
|
|
22
|
+
ORDER BY
|
|
23
|
+
revenue DESC,
|
|
24
|
+
o.o_orderdate
|
|
25
|
+
LIMIT 10;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
-- TPC-H Query 3 (Shipping Priority)
|
|
2
|
+
-- Modified for Jinja Templating
|
|
3
|
+
SELECT
|
|
4
|
+
l.l_orderkey,
|
|
5
|
+
SUM(l.l_extendedprice * (1 - l.l_discount)) AS revenue,
|
|
6
|
+
o.o_orderdate,
|
|
7
|
+
o.o_shippriority
|
|
8
|
+
FROM
|
|
9
|
+
{{ customer_table }} c,
|
|
10
|
+
{{ orders_table }} o,
|
|
11
|
+
{{ lineitem_table }} l
|
|
12
|
+
WHERE
|
|
13
|
+
c.c_mktsegment = 'BUILDING'
|
|
14
|
+
AND c.c_custkey = o.o_custkey
|
|
15
|
+
AND l.l_orderkey = o.o_orderkey
|
|
16
|
+
AND o.o_orderdate < DATE '1995-03-15'
|
|
17
|
+
AND l.l_shipdate > DATE '1995-03-15'
|
|
18
|
+
GROUP BY
|
|
19
|
+
l.l_orderkey,
|
|
20
|
+
o.o_orderdate,
|
|
21
|
+
o.o_shippriority
|
|
22
|
+
ORDER BY
|
|
23
|
+
revenue DESC,
|
|
24
|
+
o.o_orderdate
|
|
25
|
+
LIMIT 10;
|
|
File without changes
|
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import glob
|
|
3
|
+
import shutil
|
|
4
|
+
import yaml
|
|
5
|
+
import jinja2
|
|
6
|
+
import jinja2.meta
|
|
7
|
+
import numpy as np
|
|
8
|
+
from ..constants import ACTIVE_CONFIG_PATH, ENGINE_SQL_DIALECTS, SQL_DIR
|
|
9
|
+
from ..config_loader import ConfigLoader
|
|
10
|
+
from typing import Dict, Any
|
|
11
|
+
|
|
12
|
+
# Initialize the compiler once globally
|
|
13
|
+
# NOTE: If this fails to initialize due to a strict violation, the error will propagate up
|
|
14
|
+
# when Dagster tries to load definitions, which is the correct fail-hard behavior.
|
|
15
|
+
# ==========================================
|
|
16
|
+
# 1. CONTEXT & CONFIG LOADING
|
|
17
|
+
# ==========================================
|
|
18
|
+
try:
|
|
19
|
+
_GLOBAL_COMPILER = ConfigLoader()
|
|
20
|
+
except ValueError as e:
|
|
21
|
+
# Propagate structural errors during load time
|
|
22
|
+
raise e
|
|
23
|
+
|
|
24
|
+
def load_context() -> Dict[str, Any]:
|
|
25
|
+
"""
|
|
26
|
+
Returns a consolidated context dictionary containing all derived and raw configuration
|
|
27
|
+
needed by asset factories. This replaces the old context loading logic.
|
|
28
|
+
"""
|
|
29
|
+
raw_config = _GLOBAL_COMPILER.get_full_config()
|
|
30
|
+
|
|
31
|
+
# --- Tracing All Necessary Context for Asset Factories ---
|
|
32
|
+
|
|
33
|
+
# 1. Core Config Blocks
|
|
34
|
+
context = {
|
|
35
|
+
"full_config": raw_config,
|
|
36
|
+
"execution": _GLOBAL_COMPILER.execution,
|
|
37
|
+
"definitions": _GLOBAL_COMPILER.definitions,
|
|
38
|
+
"dataset_config": _GLOBAL_COMPILER.dataset, # Used by benchmark_factory for schema inference
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
# 2. Derived/Extracted Context (Crucial for Downstream Logic)
|
|
42
|
+
|
|
43
|
+
# A. Active Engines (Used by benchmark_factory to loop over engines)
|
|
44
|
+
context["engines"] = context["execution"].get("engines", [])
|
|
45
|
+
|
|
46
|
+
# B. Valid Tables (Set of table names for dependency checking)
|
|
47
|
+
table_defs = context["dataset_config"].get("tables", {})
|
|
48
|
+
context["tables"] = set(table_defs.keys())
|
|
49
|
+
|
|
50
|
+
# C. Legacy Contract Fulfillment
|
|
51
|
+
context["table_defs"] = table_defs
|
|
52
|
+
|
|
53
|
+
# D. Experiment Metadata
|
|
54
|
+
context["meta"] = raw_config.get("meta", {})
|
|
55
|
+
|
|
56
|
+
# E. Full Scenario Config
|
|
57
|
+
context["scenario_config"] = _GLOBAL_COMPILER.scenario_config
|
|
58
|
+
|
|
59
|
+
return context
|
|
60
|
+
|
|
61
|
+
def get_target_sql_dir(config):
|
|
62
|
+
suite = config.get("execution", {}).get("test_suite", "")
|
|
63
|
+
return os.path.join(SQL_DIR, suite) if suite else SQL_DIR
|
|
64
|
+
|
|
65
|
+
# ==========================================
|
|
66
|
+
# 2. SCHEMA PARSING
|
|
67
|
+
# ==========================================
|
|
68
|
+
def get_tables_used_in_sql(sql_path, valid_tables_set):
|
|
69
|
+
"""Parses SQL template to find dependencies ({{ table_name }})."""
|
|
70
|
+
with open(sql_path, "r") as f:
|
|
71
|
+
raw_template = f.read()
|
|
72
|
+
|
|
73
|
+
env = jinja2.Environment()
|
|
74
|
+
try:
|
|
75
|
+
ast = env.parse(raw_template)
|
|
76
|
+
required_vars = jinja2.meta.find_undeclared_variables(ast)
|
|
77
|
+
except Exception as e:
|
|
78
|
+
print(f"Jinja Parse Error {sql_path}: {e}")
|
|
79
|
+
return [], raw_template
|
|
80
|
+
|
|
81
|
+
used_tables = [
|
|
82
|
+
var.replace("_table", "")
|
|
83
|
+
for var in required_vars
|
|
84
|
+
if var.endswith("_table") and var.replace("_table", "") in valid_tables_set
|
|
85
|
+
]
|
|
86
|
+
return used_tables, raw_template
|
|
87
|
+
|
|
88
|
+
def extract_foreign_keys(table_def):
|
|
89
|
+
"""Returns list of dicts: [{'col': 'x', 'target': 'y', 'target_col': 'z'}]"""
|
|
90
|
+
fks = []
|
|
91
|
+
for col in table_def.get('columns', []):
|
|
92
|
+
if col.get('provider') == 'foreign_key':
|
|
93
|
+
fks.append({
|
|
94
|
+
'col': col['name'],
|
|
95
|
+
'target': col.get('target_table'),
|
|
96
|
+
'target_col': col.get('target_column')
|
|
97
|
+
})
|
|
98
|
+
return fks
|
|
99
|
+
|
|
100
|
+
def get_data_dependencies(table_name, table_configs):
|
|
101
|
+
"""Returns upstream dependencies for a specific table."""
|
|
102
|
+
deps = set()
|
|
103
|
+
t_conf = table_configs.get(table_name, {})
|
|
104
|
+
|
|
105
|
+
# Implicit FK deps
|
|
106
|
+
for fk in extract_foreign_keys(t_conf):
|
|
107
|
+
if fk['target']: deps.add(fk['target'])
|
|
108
|
+
|
|
109
|
+
# Explicit deps
|
|
110
|
+
for d in t_conf.get('deps', []): deps.add(d)
|
|
111
|
+
|
|
112
|
+
return list(deps)
|
|
113
|
+
|
|
114
|
+
# ==========================================
|
|
115
|
+
# 3. MATH & NORMALIZATION
|
|
116
|
+
# ==========================================
|
|
117
|
+
def normalize_distribution(options: list, weights: list):
|
|
118
|
+
"""
|
|
119
|
+
Robustly normalizes weights for numpy generation.
|
|
120
|
+
Used by BOTH Data Generator (Plugin) and Metadata (Dashboard).
|
|
121
|
+
"""
|
|
122
|
+
if len(options) != len(weights):
|
|
123
|
+
raise ValueError(f"Mismatch: {len(options)} options vs {len(weights)} weights")
|
|
124
|
+
|
|
125
|
+
w_arr = np.array(weights, dtype=float)
|
|
126
|
+
if w_arr.sum() <= 0: raise ValueError("Sum of weights must be positive")
|
|
127
|
+
|
|
128
|
+
# Normalize to sum to 1.0
|
|
129
|
+
norm_weights = w_arr / w_arr.sum()
|
|
130
|
+
|
|
131
|
+
# THE FIX: Handle floating point drift (numpy strict division)
|
|
132
|
+
# Guaranteed to sum to exactly 1.0
|
|
133
|
+
if len(norm_weights) > 0:
|
|
134
|
+
norm_weights[-1] = 1.0 - norm_weights[:-1].sum()
|
|
135
|
+
|
|
136
|
+
return options, norm_weights
|
|
137
|
+
|
|
138
|
+
# ==========================================
|
|
139
|
+
# 4. METADATA INFERENCE
|
|
140
|
+
# ==========================================
|
|
141
|
+
def infer_metadata_from_sql(sql_content, dataset_config):
|
|
142
|
+
"""Scans SQL for known data keys to derive metadata."""
|
|
143
|
+
mapping = {}
|
|
144
|
+
tables = dataset_config.get('tables', {})
|
|
145
|
+
|
|
146
|
+
# Build Value Map
|
|
147
|
+
if isinstance(tables, dict):
|
|
148
|
+
for table_def in tables.values():
|
|
149
|
+
for col in table_def.get('columns', []):
|
|
150
|
+
if 'options' in col and 'weights' in col:
|
|
151
|
+
try:
|
|
152
|
+
opts, probs = normalize_distribution(col['options'], col['weights'])
|
|
153
|
+
for opt, p in zip(opts, probs):
|
|
154
|
+
mapping[str(opt)] = float(p)
|
|
155
|
+
except ValueError: continue
|
|
156
|
+
|
|
157
|
+
# Scan SQL
|
|
158
|
+
meta = {}
|
|
159
|
+
for key, weight in mapping.items():
|
|
160
|
+
if f"'{key}'" in sql_content or f'"{key}"' in sql_content:
|
|
161
|
+
meta['selectivity_pct'] = weight * 100.0
|
|
162
|
+
meta['data_slice'] = key
|
|
163
|
+
break
|
|
164
|
+
return meta
|
|
165
|
+
|
|
166
|
+
# ==========================================
|
|
167
|
+
# 5. PARTITION KEYS
|
|
168
|
+
# ==========================================
|
|
169
|
+
|
|
170
|
+
def generate_partition_keys(matrix_config):
|
|
171
|
+
"""
|
|
172
|
+
Takes a matrix dict and returns sorted partition keys.
|
|
173
|
+
Used by: sensors.py, run_experiment.py (CLI), and potentially partitions.py
|
|
174
|
+
"""
|
|
175
|
+
import itertools
|
|
176
|
+
|
|
177
|
+
if not matrix_config:
|
|
178
|
+
return []
|
|
179
|
+
|
|
180
|
+
# 1. Sort keys to ensure consistent naming
|
|
181
|
+
keys = sorted(matrix_config.keys())
|
|
182
|
+
values = [matrix_config[k] for k in keys]
|
|
183
|
+
|
|
184
|
+
# 2. Cartesian Product
|
|
185
|
+
partition_keys = []
|
|
186
|
+
for combination in itertools.product(*values):
|
|
187
|
+
key_str = "_".join(str(v) for v in combination)
|
|
188
|
+
partition_keys.append(key_str)
|
|
189
|
+
|
|
190
|
+
return partition_keys
|
|
191
|
+
|
|
192
|
+
# ==========================================
|
|
193
|
+
# 6. ASSET NAMING UTILITY
|
|
194
|
+
# ==========================================
|
|
195
|
+
|
|
196
|
+
def get_engine_asset_prefix(engine_name: str) -> str:
|
|
197
|
+
"""
|
|
198
|
+
Resolves the engine resource key ('postgres', 'duckdb')
|
|
199
|
+
to the canonical asset prefix ('pg_', 'duckdb_').
|
|
200
|
+
This is the single source of truth for asset naming conventions across all factories.
|
|
201
|
+
"""
|
|
202
|
+
if engine_name == 'postgres':
|
|
203
|
+
return 'pg_'
|
|
204
|
+
# Default: Use the engine name itself followed by an underscore
|
|
205
|
+
return f'{engine_name}_'
|
|
206
|
+
|
|
207
|
+
def get_engine_sql_dialect(engine_name: str) -> str:
|
|
208
|
+
"""
|
|
209
|
+
Resolves an engine resource key to the SQL scenario directory it executes
|
|
210
|
+
(sql/<suite>/<dialect>/). The single source of truth for dialect reuse.
|
|
211
|
+
"""
|
|
212
|
+
return ENGINE_SQL_DIALECTS.get(engine_name, engine_name)
|
|
213
|
+
|
|
214
|
+
def copy_suite_queries(capsule_dir: str) -> int:
|
|
215
|
+
"""Embed into the capsule (as queries/) the exact SQL that ran: for each
|
|
216
|
+
engine in the capsule's config, the .sql files of its dialect directory.
|
|
217
|
+
This is the subset of the suite the experiment's engines actually used — a
|
|
218
|
+
reader's convenience copy, NOT a hash input. The Experiment ID hashes the
|
|
219
|
+
full suite from source, independently of the capsule. Returns file count."""
|
|
220
|
+
cfg_path = os.path.join(capsule_dir, "experiment_config.yaml")
|
|
221
|
+
if not os.path.exists(cfg_path):
|
|
222
|
+
return 0
|
|
223
|
+
with open(cfg_path) as f:
|
|
224
|
+
config = yaml.safe_load(f) or {}
|
|
225
|
+
execution = config.get("execution", {})
|
|
226
|
+
if not execution.get("test_suite"):
|
|
227
|
+
return 0
|
|
228
|
+
dialects = {get_engine_sql_dialect(e) for e in execution.get("engines", [])}
|
|
229
|
+
src = get_target_sql_dir(config)
|
|
230
|
+
dest = os.path.join(capsule_dir, "queries")
|
|
231
|
+
if os.path.isdir(dest):
|
|
232
|
+
shutil.rmtree(dest)
|
|
233
|
+
written = 0
|
|
234
|
+
for dialect in sorted(dialects):
|
|
235
|
+
dialect_dir = os.path.join(src, dialect)
|
|
236
|
+
if not os.path.isdir(dialect_dir):
|
|
237
|
+
continue
|
|
238
|
+
for sql_file in sorted(glob.glob(os.path.join(dialect_dir, "*.sql"))):
|
|
239
|
+
out = os.path.join(dest, dialect, os.path.basename(sql_file))
|
|
240
|
+
os.makedirs(os.path.dirname(out), exist_ok=True)
|
|
241
|
+
shutil.copy2(sql_file, out)
|
|
242
|
+
written += 1
|
|
243
|
+
return written
|
|
244
|
+
|
|
245
|
+
def get_engine_benchmark_group(engine_name: str) -> str:
|
|
246
|
+
"""
|
|
247
|
+
Dagster group name for an engine's benchmark assets. Shared by the
|
|
248
|
+
benchmark factory (asset creation) and execute_run (asset selection) so
|
|
249
|
+
the two can never drift apart.
|
|
250
|
+
"""
|
|
251
|
+
return f"dynamic_bench_{engine_name}"
|
|
252
|
+
|
|
253
|
+
def get_scoped_asset_name(base_name: str, exp_id: str) -> str:
|
|
254
|
+
"""
|
|
255
|
+
Generates a globally unique asset name prefixed by the experiment ID.
|
|
256
|
+
Format: e_<exp_id>__<base_name>
|
|
257
|
+
"""
|
|
258
|
+
if not exp_id or exp_id == "unknown":
|
|
259
|
+
return base_name
|
|
260
|
+
return f"e_{exp_id}__{base_name}"
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# We need to import the parser helper from common to avoid code duplication
|
|
2
|
+
# But to avoid circular imports, we just reimplement the tiny extractor or
|
|
3
|
+
# pass the FKs in. Let's keep it simple and self-contained.
|
|
4
|
+
|
|
5
|
+
class PostgresDDLGenerator:
|
|
6
|
+
def __init__(self, table_def, physical_name, partition_key):
|
|
7
|
+
self.def_ = table_def
|
|
8
|
+
self.name = physical_name
|
|
9
|
+
self.part = partition_key
|
|
10
|
+
|
|
11
|
+
def generate_pk_sql(self):
|
|
12
|
+
pk_cols = [c['name'] for c in self.def_.get('columns', []) if c.get('primary_key')]
|
|
13
|
+
if pk_cols:
|
|
14
|
+
return f"ALTER TABLE {self.name} ADD PRIMARY KEY ({', '.join(pk_cols)});"
|
|
15
|
+
return None
|
|
16
|
+
|
|
17
|
+
def generate_index_sqls(self):
|
|
18
|
+
sqls = []
|
|
19
|
+
for idx in self.def_.get('indexes', []):
|
|
20
|
+
cols = idx.get('columns', [])
|
|
21
|
+
if not cols: continue
|
|
22
|
+
# Index names are schema-global in Postgres, but the same table_def is
|
|
23
|
+
# applied to every partition's physical table. A fixed config name would
|
|
24
|
+
# collide across partitions — and CREATE INDEX IF NOT EXISTS would
|
|
25
|
+
# SILENTLY skip all but the first. Scope the name to the physical table
|
|
26
|
+
# so it is unique, and omit IF NOT EXISTS so a genuine collision is loud.
|
|
27
|
+
base = idx.get('name') or f"idx_{'_'.join(cols)}"
|
|
28
|
+
name = f"{base}_{self.name}"
|
|
29
|
+
sqls.append(f"CREATE INDEX {name} ON {self.name} ({', '.join(cols)});")
|
|
30
|
+
return sqls
|
|
31
|
+
|
|
32
|
+
def generate_fk_sqls(self):
|
|
33
|
+
sqls = []
|
|
34
|
+
# Local logic to find FKs (Clean and self-contained)
|
|
35
|
+
for col in self.def_.get('columns', []):
|
|
36
|
+
if col.get('provider') == 'foreign_key':
|
|
37
|
+
target = col.get('target_table')
|
|
38
|
+
target_col = col.get('target_column')
|
|
39
|
+
if not target or not target_col: continue
|
|
40
|
+
|
|
41
|
+
target_phys = f"{target}_{self.part}"
|
|
42
|
+
fk_name = f"fk_{self.name}_{col['name']}"
|
|
43
|
+
sqls.append(
|
|
44
|
+
f"ALTER TABLE {self.name} ADD CONSTRAINT {fk_name} "
|
|
45
|
+
f"FOREIGN KEY ({col['name']}) REFERENCES {target_phys} ({target_col});"
|
|
46
|
+
)
|
|
47
|
+
return sqls
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import hashlib
|
|
2
|
+
import json
|
|
3
|
+
import os
|
|
4
|
+
import re
|
|
5
|
+
import ast
|
|
6
|
+
from .common import get_target_sql_dir
|
|
7
|
+
|
|
8
|
+
def normalize_sql(content):
|
|
9
|
+
"""
|
|
10
|
+
Standardizes SQL for hashing using Regex.
|
|
11
|
+
Robust to Agent formatting, comments, and Jinja templates.
|
|
12
|
+
"""
|
|
13
|
+
# 1. Remove Block Comments /* ... */
|
|
14
|
+
content = re.sub(r'/\*[\s\S]*?\*/', ' ', content)
|
|
15
|
+
|
|
16
|
+
# 2. Remove Line Comments -- ...
|
|
17
|
+
content = re.sub(r'--.*', ' ', content)
|
|
18
|
+
|
|
19
|
+
# 3. Collapse Whitespace (Handling newlines/tabs)
|
|
20
|
+
content = " ".join(content.split())
|
|
21
|
+
|
|
22
|
+
# 4. Remove Trailing Semicolon (
|
|
23
|
+
content = content.strip().rstrip(';').strip()
|
|
24
|
+
|
|
25
|
+
# 5. Canonicalize Case (Select -> select)
|
|
26
|
+
return content.lower()
|
|
27
|
+
|
|
28
|
+
def normalize_python(content):
|
|
29
|
+
"""
|
|
30
|
+
Parses Python code to AST and regenerates it.
|
|
31
|
+
Ignores comments, docstrings, and formatting.
|
|
32
|
+
"""
|
|
33
|
+
try:
|
|
34
|
+
tree = ast.parse(content)
|
|
35
|
+
for node in ast.walk(tree):
|
|
36
|
+
if not isinstance(node, (ast.FunctionDef, ast.ClassDef, ast.AsyncFunctionDef, ast.Module)):
|
|
37
|
+
continue
|
|
38
|
+
if not (node.body and isinstance(node.body[0], ast.Expr)):
|
|
39
|
+
continue
|
|
40
|
+
val = node.body[0].value
|
|
41
|
+
if isinstance(val, ast.Constant) and isinstance(val.value, str):
|
|
42
|
+
node.body.pop(0) # Remove Docstring
|
|
43
|
+
return ast.unparse(tree)
|
|
44
|
+
except SyntaxError:
|
|
45
|
+
return content
|
|
46
|
+
|
|
47
|
+
def generate_experiment_hash(config_dict, root_dir):
|
|
48
|
+
hasher = hashlib.sha256()
|
|
49
|
+
|
|
50
|
+
# 1. Hash Config
|
|
51
|
+
clean_config = {k: v for k, v in config_dict.items() if k != 'meta'}
|
|
52
|
+
config_str = json.dumps(clean_config, sort_keys=True)
|
|
53
|
+
hasher.update(config_str.encode('utf-8'))
|
|
54
|
+
|
|
55
|
+
def hash_folder(folder_path, extension, normalizer=None, exclude_dirs=()):
|
|
56
|
+
if not os.path.exists(folder_path): return
|
|
57
|
+
|
|
58
|
+
for root, dirs, files in os.walk(folder_path):
|
|
59
|
+
dirs[:] = sorted(d for d in dirs if d not in exclude_dirs)
|
|
60
|
+
for file in sorted(files):
|
|
61
|
+
if file.endswith(extension):
|
|
62
|
+
rel_path = os.path.relpath(os.path.join(root, file), root_dir)
|
|
63
|
+
hasher.update(rel_path.encode('utf-8'))
|
|
64
|
+
|
|
65
|
+
with open(os.path.join(root, file), 'r', encoding='utf-8') as f:
|
|
66
|
+
content = f.read()
|
|
67
|
+
|
|
68
|
+
if normalizer:
|
|
69
|
+
content = normalizer(content)
|
|
70
|
+
|
|
71
|
+
hasher.update(content.encode('utf-8'))
|
|
72
|
+
|
|
73
|
+
# 2. Hash SQL Files
|
|
74
|
+
target_sql_dir = get_target_sql_dir(config_dict)
|
|
75
|
+
hash_folder(target_sql_dir, ".sql", normalizer=normalize_sql)
|
|
76
|
+
|
|
77
|
+
# 3. Hash ALL Python that can change what a measurement means: the whole
|
|
78
|
+
# package — orchestration (assets/), engines (resources/), data
|
|
79
|
+
# generators (plugins/), and the root/utils machinery (config_loader
|
|
80
|
+
# assembles engine_params; system.py owns the cold-cache primitive).
|
|
81
|
+
# Excluded: api/ only READS results — it cannot affect a measurement —
|
|
82
|
+
# and experiments/ holds configs/results, which are hashed separately.
|
|
83
|
+
# The ID fingerprints the QUESTION; runtime conditions (engine
|
|
84
|
+
# versions, hardware) are recorded in the capsule metadata instead.
|
|
85
|
+
hash_folder(
|
|
86
|
+
os.path.join(root_dir, "sql_benchmarks"),
|
|
87
|
+
".py",
|
|
88
|
+
normalizer=normalize_python,
|
|
89
|
+
exclude_dirs=("api", "experiments", "__pycache__"),
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
return hasher.hexdigest()[:8]
|
|
93
|
+
|
|
94
|
+
def generate_integrity_seal(results_dir):
|
|
95
|
+
"""
|
|
96
|
+
Generates a SHA-256 seal for the entire results capsule: every file in
|
|
97
|
+
the capsule folder (CSV, fragments, metadata, config, dashboard), the
|
|
98
|
+
seal file itself excluded. Detects silent modification of published
|
|
99
|
+
results; it is a checksum, not a signature — code provenance is the
|
|
100
|
+
Experiment ID's job, not this seal's.
|
|
101
|
+
"""
|
|
102
|
+
hasher = hashlib.sha256()
|
|
103
|
+
|
|
104
|
+
# Walk results_dir and hash all files deterministically
|
|
105
|
+
for root, dirs, files in os.walk(results_dir):
|
|
106
|
+
# Sort files to ensure deterministic hashing
|
|
107
|
+
for file in sorted(files):
|
|
108
|
+
# Skip the seal and its sidecars (timestamp proof, signature):
|
|
109
|
+
# they are computed FROM the seal, so they must not feed back into it.
|
|
110
|
+
if file == "integrity.seal" or file.startswith("integrity.seal."):
|
|
111
|
+
continue
|
|
112
|
+
|
|
113
|
+
file_path = os.path.join(root, file)
|
|
114
|
+
rel_path = os.path.relpath(file_path, results_dir)
|
|
115
|
+
|
|
116
|
+
# Hash metadata
|
|
117
|
+
hasher.update(rel_path.encode('utf-8'))
|
|
118
|
+
|
|
119
|
+
# Hash content
|
|
120
|
+
with open(file_path, 'rb') as f:
|
|
121
|
+
while chunk := f.read(8192):
|
|
122
|
+
hasher.update(chunk)
|
|
123
|
+
|
|
124
|
+
return hasher.hexdigest()
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import hashlib
|
|
3
|
+
|
|
4
|
+
class IntegrityMonitor:
|
|
5
|
+
"""
|
|
6
|
+
Monitors a directory for any changes after an initial snapshot.
|
|
7
|
+
Used to ensure the 'Clean Room' staging area remains isolated.
|
|
8
|
+
"""
|
|
9
|
+
def __init__(self, target_dir):
|
|
10
|
+
self.target_dir = target_dir
|
|
11
|
+
self.initial_state = self._compute_state()
|
|
12
|
+
|
|
13
|
+
def _compute_state(self):
|
|
14
|
+
state = {}
|
|
15
|
+
for root, _, files in os.walk(self.target_dir):
|
|
16
|
+
for file in files:
|
|
17
|
+
path = os.path.join(root, file)
|
|
18
|
+
rel_path = os.path.relpath(path, self.target_dir)
|
|
19
|
+
|
|
20
|
+
# Ignore runtime artifacts, transient directories, and experiment outputs
|
|
21
|
+
if any(x in rel_path for x in ["__pycache__", ".pyc", "experiments/", ".DS_Store"]):
|
|
22
|
+
continue
|
|
23
|
+
|
|
24
|
+
try:
|
|
25
|
+
with open(path, "rb") as f:
|
|
26
|
+
state[rel_path] = hashlib.sha256(f.read()).hexdigest()
|
|
27
|
+
except Exception:
|
|
28
|
+
continue
|
|
29
|
+
return state
|
|
30
|
+
|
|
31
|
+
def check_drift(self):
|
|
32
|
+
"""Returns a list of files that have been modified, added, or deleted."""
|
|
33
|
+
current_state = self._compute_state()
|
|
34
|
+
drift = []
|
|
35
|
+
|
|
36
|
+
# Check for modifications and deletions
|
|
37
|
+
for path, original_hash in self.initial_state.items():
|
|
38
|
+
if path not in current_state:
|
|
39
|
+
drift.append(f"DELETED: {path}")
|
|
40
|
+
elif current_state[path] != original_hash:
|
|
41
|
+
curr_hash = current_state[path]
|
|
42
|
+
drift.append(f"MODIFIED: {path} (Expected: {original_hash[:8]}... Got: {curr_hash[:8]}...)")
|
|
43
|
+
|
|
44
|
+
# Check for additions
|
|
45
|
+
for path in current_state:
|
|
46
|
+
if path not in self.initial_state:
|
|
47
|
+
drift.append(f"ADDED: {path}")
|
|
48
|
+
|
|
49
|
+
return drift
|
|
50
|
+
|
|
51
|
+
def get_current_hash(self) -> str:
|
|
52
|
+
"""Returns a single aggregate hash of the current directory state."""
|
|
53
|
+
state = self._compute_state()
|
|
54
|
+
combined = "".join(sorted(state.values()))
|
|
55
|
+
return hashlib.sha256(combined.encode()).hexdigest()
|