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.
Files changed (153) hide show
  1. infrastructure/debug_aws.py +40 -0
  2. scripts/dev/backfill_queries.py +44 -0
  3. scripts/dev/timestamp_capsule.py +49 -0
  4. scripts/dev/upgrade_capsule.py +63 -0
  5. scripts/dev/verify_capsule.py +79 -0
  6. scripts/dev/verify_portability.py +103 -0
  7. scripts/tools/_plotlib.py +43 -0
  8. scripts/tools/analyze_scaling.py +39 -0
  9. scripts/tools/demo_breach_detection.py +61 -0
  10. scripts/tools/extract_results.py +57 -0
  11. scripts/tools/find_kink.py +104 -0
  12. scripts/tools/gen_experiment_catalog.py +103 -0
  13. scripts/tools/load_parquet_to_local.py +98 -0
  14. scripts/tools/plot_execution_modes.py +82 -0
  15. scripts/tools/plot_scaling.py +91 -0
  16. scripts/tools/plot_selectivity.py +139 -0
  17. scripts/tools/plot_threads.py +95 -0
  18. scripts/tools/regenerate_dashboard.py +58 -0
  19. scripts/tools/verify_doc_claims.py +88 -0
  20. sql_benchmarks/__init__.py +3 -0
  21. sql_benchmarks/api/__init__.py +0 -0
  22. sql_benchmarks/api/app.py +30 -0
  23. sql_benchmarks/api/data/__init__.py +0 -0
  24. sql_benchmarks/api/data/catalog_reader.py +73 -0
  25. sql_benchmarks/api/data/reader.py +125 -0
  26. sql_benchmarks/api/logic/__init__.py +0 -0
  27. sql_benchmarks/api/logic/comparator.py +44 -0
  28. sql_benchmarks/api/logic/ranker.py +127 -0
  29. sql_benchmarks/api/models/__init__.py +0 -0
  30. sql_benchmarks/api/models/catalog.py +22 -0
  31. sql_benchmarks/api/models/experiments.py +20 -0
  32. sql_benchmarks/api/models/recommend.py +11 -0
  33. sql_benchmarks/api/models/results.py +66 -0
  34. sql_benchmarks/api/routers/__init__.py +0 -0
  35. sql_benchmarks/api/routers/catalog.py +19 -0
  36. sql_benchmarks/api/routers/experiments.py +79 -0
  37. sql_benchmarks/api/routers/recommend.py +22 -0
  38. sql_benchmarks/api/routers/results.py +51 -0
  39. sql_benchmarks/assets/__init__.py +4 -0
  40. sql_benchmarks/assets/benchmark_factory.py +222 -0
  41. sql_benchmarks/assets/data_factory.py +54 -0
  42. sql_benchmarks/assets/data_quality.py +91 -0
  43. sql_benchmarks/assets/ingestion_factory.py +88 -0
  44. sql_benchmarks/assets/maintenance.py +63 -0
  45. sql_benchmarks/assets/reporting.py +385 -0
  46. sql_benchmarks/assets/semantic_gate.py +76 -0
  47. sql_benchmarks/cli.py +69 -0
  48. sql_benchmarks/config_loader.py +104 -0
  49. sql_benchmarks/constants.py +60 -0
  50. sql_benchmarks/coordinator.py +282 -0
  51. sql_benchmarks/definitions.py +115 -0
  52. sql_benchmarks/experiments/templates/experiment_template.yaml +99 -0
  53. sql_benchmarks/harness.py +84 -0
  54. sql_benchmarks/jobs.py +8 -0
  55. sql_benchmarks/partitions.py +17 -0
  56. sql_benchmarks/plugins/data_sources/declarative_gen.py +211 -0
  57. sql_benchmarks/plugins/data_sources/local_file_loader.py +62 -0
  58. sql_benchmarks/plugins/data_sources/tpc_h.py +61 -0
  59. sql_benchmarks/resources/__init__.py +0 -0
  60. sql_benchmarks/resources/actian.py +234 -0
  61. sql_benchmarks/resources/actian_client.py +231 -0
  62. sql_benchmarks/resources/base_engine.py +58 -0
  63. sql_benchmarks/resources/base_schema_client.py +104 -0
  64. sql_benchmarks/resources/duckdb.py +58 -0
  65. sql_benchmarks/resources/duckdb_client.py +157 -0
  66. sql_benchmarks/resources/postgres.py +188 -0
  67. sql_benchmarks/resources/postgres_client.py +184 -0
  68. sql_benchmarks/resources/quack.py +56 -0
  69. sql_benchmarks/resources/quack_client.py +169 -0
  70. sql_benchmarks/resources/typedb_client.py +370 -0
  71. sql_benchmarks/resources/typedb_engine.py +303 -0
  72. sql_benchmarks/scripts/__init__.py +0 -0
  73. sql_benchmarks/scripts/sql/acid_test/duckdb/test.sql +1 -0
  74. sql_benchmarks/scripts/sql/acid_test/postgres/test.sql +1 -0
  75. sql_benchmarks/scripts/sql/analytical_wall/actian/analytical_wall.sql +15 -0
  76. sql_benchmarks/scripts/sql/analytical_wall/analytical_wall.sql.template +19 -0
  77. sql_benchmarks/scripts/sql/analytical_wall/duckdb/analytical_wall.sql +15 -0
  78. sql_benchmarks/scripts/sql/analytical_wall/postgres/analytical_wall.sql +15 -0
  79. sql_benchmarks/scripts/sql/group_by/duckdb/groupby_antipattern.sql +11 -0
  80. sql_benchmarks/scripts/sql/group_by/duckdb/groupby_recommended_cte.sql +19 -0
  81. sql_benchmarks/scripts/sql/group_by/postgres/groupby_antipattern.sql +13 -0
  82. sql_benchmarks/scripts/sql/group_by/postgres/groupby_recommended_pk.sql +11 -0
  83. sql_benchmarks/scripts/sql/hypergraph/duckdb/q_1hop.sql +5 -0
  84. sql_benchmarks/scripts/sql/hypergraph/duckdb/q_2hop.sql +6 -0
  85. sql_benchmarks/scripts/sql/hypergraph/duckdb/q_full.sql +9 -0
  86. sql_benchmarks/scripts/sql/hypergraph/typedb/q_1hop.sql +4 -0
  87. sql_benchmarks/scripts/sql/hypergraph/typedb/q_2hop.sql +5 -0
  88. sql_benchmarks/scripts/sql/hypergraph/typedb/q_full.sql +6 -0
  89. sql_benchmarks/scripts/sql/joins/duckdb/join_antipattern.sql +5 -0
  90. sql_benchmarks/scripts/sql/joins/duckdb/join_recommended.sql +4 -0
  91. sql_benchmarks/scripts/sql/joins/postgres/join_antipattern.sql +5 -0
  92. sql_benchmarks/scripts/sql/joins/postgres/join_recommended.sql +4 -0
  93. sql_benchmarks/scripts/sql/null_logic/duckdb/2VL_identity.sql +14 -0
  94. sql_benchmarks/scripts/sql/null_logic/duckdb/3VL_identity.sql +5 -0
  95. sql_benchmarks/scripts/sql/null_logic/postgres/2VL_identity.sql +14 -0
  96. sql_benchmarks/scripts/sql/null_logic/postgres/3VL_identity.sql +5 -0
  97. sql_benchmarks/scripts/sql/null_sentinel/duckdb/2VL_identity.sql +14 -0
  98. sql_benchmarks/scripts/sql/null_sentinel/duckdb/3VL_identity.sql +5 -0
  99. sql_benchmarks/scripts/sql/null_sentinel/duckdb/sentinel_optimization.sql +16 -0
  100. sql_benchmarks/scripts/sql/null_sentinel/postgres/2VL_identity.sql +14 -0
  101. sql_benchmarks/scripts/sql/null_sentinel/postgres/3VL_identity.sql +5 -0
  102. sql_benchmarks/scripts/sql/null_sentinel/postgres/sentinel_optimization.sql +21 -0
  103. sql_benchmarks/scripts/sql/recursion/duckdb/recursive_cte.sql +26 -0
  104. sql_benchmarks/scripts/sql/recursion/postgres/recursive_cte.sql +30 -0
  105. sql_benchmarks/scripts/sql/recursive_graph/duckdb/q_bounded_3hop.sql +17 -0
  106. sql_benchmarks/scripts/sql/recursive_graph/duckdb/q_fixed_2hop.sql +6 -0
  107. sql_benchmarks/scripts/sql/recursive_graph/duckdb/q_transitive_closure.sql +17 -0
  108. sql_benchmarks/scripts/sql/recursive_graph/typedb/q_bounded_3hop.sql +6 -0
  109. sql_benchmarks/scripts/sql/recursive_graph/typedb/q_fixed_2hop.sql +5 -0
  110. sql_benchmarks/scripts/sql/recursive_graph/typedb/q_transitive_closure.sql +7 -0
  111. sql_benchmarks/scripts/sql/security/acid_resilience/duckdb/test.sql +1 -0
  112. sql_benchmarks/scripts/sql/selectivity/duckdb/q_0_1_percent.sql +1 -0
  113. sql_benchmarks/scripts/sql/selectivity/duckdb/q_10_percent.sql +1 -0
  114. sql_benchmarks/scripts/sql/selectivity/duckdb/q_1_percent.sql +1 -0
  115. sql_benchmarks/scripts/sql/selectivity/duckdb/q_20_percent.sql +1 -0
  116. sql_benchmarks/scripts/sql/selectivity/duckdb/q_5_percent.sql +1 -0
  117. sql_benchmarks/scripts/sql/selectivity/duckdb/q_filler.sql +1 -0
  118. sql_benchmarks/scripts/sql/selectivity/postgres/q_0_1_percent.sql +1 -0
  119. sql_benchmarks/scripts/sql/selectivity/postgres/q_10_percent.sql +1 -0
  120. sql_benchmarks/scripts/sql/selectivity/postgres/q_1_percent.sql +1 -0
  121. sql_benchmarks/scripts/sql/selectivity/postgres/q_20_percent.sql +1 -0
  122. sql_benchmarks/scripts/sql/selectivity/postgres/q_5_percent.sql +1 -0
  123. sql_benchmarks/scripts/sql/selectivity/postgres/q_filler.sql +1 -0
  124. sql_benchmarks/scripts/sql/selectivity/typedb/q_0_1_percent.sql +4 -0
  125. sql_benchmarks/scripts/sql/selectivity/typedb/q_10_percent.sql +4 -0
  126. sql_benchmarks/scripts/sql/selectivity/typedb/q_1_percent.sql +4 -0
  127. sql_benchmarks/scripts/sql/selectivity/typedb/q_20_percent.sql +4 -0
  128. sql_benchmarks/scripts/sql/selectivity/typedb/q_5_percent.sql +4 -0
  129. sql_benchmarks/scripts/sql/selectivity/typedb/q_filler.sql +4 -0
  130. sql_benchmarks/scripts/sql/sort_spill/duckdb/sort_full.sql +25 -0
  131. sql_benchmarks/scripts/sql/sort_spill/duckdb/sort_wide_rows.sql +25 -0
  132. sql_benchmarks/scripts/sql/sort_spill/postgres/sort_full.sql +25 -0
  133. sql_benchmarks/scripts/sql/sort_spill/postgres/sort_wide_rows.sql +26 -0
  134. sql_benchmarks/scripts/sql/tpch/duckdb/q3_shipping_priority.sql +25 -0
  135. sql_benchmarks/scripts/sql/tpch/postgres/q3_shipping_priority.sql +25 -0
  136. sql_benchmarks/utils/__init__.py +0 -0
  137. sql_benchmarks/utils/common.py +260 -0
  138. sql_benchmarks/utils/ddl.py +47 -0
  139. sql_benchmarks/utils/hasher.py +124 -0
  140. sql_benchmarks/utils/integrity_monitor.py +55 -0
  141. sql_benchmarks/utils/providers.py +184 -0
  142. sql_benchmarks/utils/scaling.py +88 -0
  143. sql_benchmarks/utils/schema.py +118 -0
  144. sql_benchmarks/utils/semantic_auditor.py +81 -0
  145. sql_benchmarks/utils/system.py +100 -0
  146. sql_benchmarks/validator.py +60 -0
  147. sqlbenchdag-0.1.0.dist-info/METADATA +221 -0
  148. sqlbenchdag-0.1.0.dist-info/RECORD +153 -0
  149. sqlbenchdag-0.1.0.dist-info/WHEEL +5 -0
  150. sqlbenchdag-0.1.0.dist-info/entry_points.txt +2 -0
  151. sqlbenchdag-0.1.0.dist-info/licenses/LICENSE +202 -0
  152. sqlbenchdag-0.1.0.dist-info/top_level.txt +4 -0
  153. 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()