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,104 @@
1
+ import os
2
+ import yaml
3
+ import itertools
4
+ from typing import Dict, Any, Tuple, List
5
+ from .constants import ACTIVE_CONFIG_PATH
6
+
7
+ class ConfigLoader:
8
+ def __init__(self, config_path: str = ACTIVE_CONFIG_PATH):
9
+ self.config_path = config_path
10
+ self._raw_config: Dict[str, Any] = {}
11
+ self.execution: Dict[str, Any] = {}
12
+ self.definitions: Dict[str, Any] = {}
13
+ self.dataset: Dict[str, Any] = {}
14
+
15
+ # Public, consolidated artifacts
16
+ self.scenario_config: Dict[str, Dict[str, Any]] = {}
17
+ self.partition_keys: List[str] = []
18
+
19
+ self._load_and_validate()
20
+ self._compile_scenario_config()
21
+
22
+ def _load_and_validate(self) -> None:
23
+ """Loads YAML and performs initial structural checks."""
24
+ if not os.path.exists(self.config_path):
25
+ return
26
+
27
+ try:
28
+ with open(self.config_path, "r") as f:
29
+ self._raw_config = yaml.safe_load(f) or {}
30
+ except Exception as e:
31
+ raise ValueError(f"CRITICAL: Failed to parse {self.config_path}: {e}")
32
+
33
+ # --- STRICT SCHEMA & SEMANTIC VALIDATION ---
34
+ from .validator import ExperimentValidator
35
+ ExperimentValidator.validate(self._raw_config, source_label=self.config_path)
36
+
37
+ self.execution = self._raw_config.get("execution", {})
38
+ self.definitions = self._raw_config.get("definitions", {})
39
+ self.dataset = self._raw_config.get("dataset", {})
40
+
41
+ def _compile_scenario_config(self) -> None:
42
+ """
43
+ Translates the symbolic matrix into numeric parameters and generates partition keys.
44
+ This is the consolidated logic from partitions.py.
45
+ """
46
+ if "matrix" not in self.execution:
47
+ raise ValueError("CRITICAL: Experiment must define a 'matrix' strictly under 'execution.matrix'.")
48
+
49
+ matrix = self.execution["matrix"]
50
+ keys = sorted(list(matrix.keys()))
51
+ symbolic_values = [matrix[k] for k in keys]
52
+
53
+ for symbolic_combination in itertools.product(*symbolic_values):
54
+
55
+ # A. Create the Symbolic Partition Key (e.g., 'tiny_ssd')
56
+ key_str = "_".join(str(v) for v in symbolic_combination)
57
+ self.partition_keys.append(key_str)
58
+
59
+ # B. Translate to Numeric/Literal Values for Execution (The Payload)
60
+ numeric_params = {}
61
+ for dim_name, symbolic_value in zip(keys, symbolic_combination):
62
+ definition_map = self.definitions.get(dim_name, {})
63
+
64
+ # 1. Try to resolve the alias using the definition map
65
+ if symbolic_value in definition_map:
66
+ numeric_value = definition_map[symbolic_value]
67
+
68
+ # 2. STRICT VALIDATION (Non-hardcoded): Fail if expected alias is missing
69
+ elif isinstance(symbolic_value, str) and definition_map and symbolic_value not in definition_map:
70
+ raise ValueError(
71
+ f"STRICT VIOLATION: Alias '{symbolic_value}' in matrix dimension '{dim_name}' "
72
+ f"could not be resolved. Definition block 'definitions.{dim_name}' exists "
73
+ "but is missing this alias."
74
+ )
75
+
76
+ # 3. Otherwise, the value is a literal (e.g., 'ssd', 1000).
77
+ else:
78
+ numeric_value = symbolic_value
79
+
80
+ numeric_params[dim_name] = numeric_value
81
+
82
+ # C. Assemble namespaced engine params: the static execution.engine_params
83
+ # block merged with namespaced matrix dimensions ('postgres.work_mem'
84
+ # -> engine_params['postgres']['work_mem']). Varied dimensions override
85
+ # static values. Stored as a nested key so consumers never have to
86
+ # derive it themselves; the factory hands each engine ONLY its own
87
+ # namespace at run time.
88
+ engine_params = {
89
+ ns: dict(settings)
90
+ for ns, settings in (self.execution.get("engine_params") or {}).items()
91
+ }
92
+ for dim_name, value in numeric_params.items():
93
+ if "." in dim_name:
94
+ ns, param = dim_name.split(".", 1)
95
+ engine_params.setdefault(ns, {})[param] = value
96
+ if engine_params:
97
+ numeric_params["engine_params"] = engine_params
98
+
99
+ # D. Store the Numeric/Literal parameters under the Symbolic Key
100
+ self.scenario_config[key_str] = numeric_params
101
+
102
+ def get_full_config(self) -> Dict[str, Any]:
103
+ """Returns the full parsed configuration."""
104
+ return self._raw_config
@@ -0,0 +1,60 @@
1
+ import os
2
+
3
+ # 1. ANCHOR
4
+ CURRENT_FILE_PATH = os.path.abspath(__file__)
5
+
6
+ # 2. ZONES
7
+ PACKAGE_DIR = os.path.dirname(CURRENT_FILE_PATH) # sql_benchmarks/
8
+ ROOT_DIR = os.path.dirname(PACKAGE_DIR) # sql-benchmarks-dagster/
9
+
10
+ # 3. SUB-DIRECTORIES (Environment-Aware Redirection)
11
+ # Fallback to absolute paths relative to ROOT_DIR
12
+ EXPERIMENTS_DIR = os.path.join(PACKAGE_DIR, "experiments")
13
+ SCRIPTS_DIR = os.path.join(PACKAGE_DIR, "scripts")
14
+ SQL_DIR = os.path.join(SCRIPTS_DIR, "sql")
15
+
16
+ # OUTPUT REDIRECTION (Used for Zero-Copy Isolation)
17
+ DATA_DIR = os.getenv("SB_DATA_DIR", os.path.join(ROOT_DIR, "data"))
18
+ RESULTS_DIR = os.getenv("SB_RESULTS_DIR", os.path.join(EXPERIMENTS_DIR, "results"))
19
+ VIOLATIONS_DIR = os.getenv("SB_VIOLATIONS_DIR", os.path.join(EXPERIMENTS_DIR, "violations"))
20
+ REPORTS_DIR = os.getenv("SB_REPORTS_DIR", os.path.join(EXPERIMENTS_DIR, "reports"))
21
+
22
+ # 4. FILES
23
+ ACTIVE_CONFIG_PATH = os.getenv("ACTIVE_CONFIG_PATH", os.path.join(EXPERIMENTS_DIR, "active.yaml"))
24
+ CONFIG_ARCHIVE_DIR = os.path.join(EXPERIMENTS_DIR, "configs")
25
+ PROCESSED_SUFFIX = ".processed"
26
+ EXPERIMENT_EXTENSIONS = (".yaml", ".yml")
27
+
28
+ # 5. ENGINE REGISTRIES
29
+ # Convention: cross-engine registries live HERE; engine-specific vocabulary
30
+ # allowlists (PG_SETTING_KEYS, DUCKDB_SETTING_KEYS) stay colocated with their
31
+ # engine clients in resources/.
32
+ #
33
+ # Engines whose SQL dialect is another engine's: they reuse that engine's
34
+ # scenario directory (sql/<suite>/<dialect>/) instead of duplicating SQL.
35
+ # Quack is DuckDB served over a client-server protocol — identical dialect,
36
+ # different transport.
37
+ ENGINE_SQL_DIALECTS = {
38
+ "quack": "duckdb",
39
+ "quack_pushdown": "duckdb",
40
+ }
41
+
42
+ # Every engine the harness can run. A drift test asserts this matches the
43
+ # resources actually registered in definitions.py — add an engine there
44
+ # without updating this list and CI fails loudly.
45
+ KNOWN_ENGINES = ["actian", "duckdb", "postgres", "quack", "quack_pushdown", "typedb"]
46
+
47
+ # The lab's maker's mark — carried in every capsule's `generator` field and on
48
+ # every release tag (sqlbenchdag-<topic>-v<N>-<YYYYMMDD>). sql + bench + dag
49
+ # (Dagster). Verified unique on GitHub/PyPI; chosen once, do not change.
50
+ LAB_SLUG = "sqlbenchdag"
51
+
52
+ # 6. DAGSTER CONFIG
53
+ _package_name = os.path.basename(PACKAGE_DIR)
54
+ DAGSTER_MODULE_TARGET = f"{_package_name}.definitions"
55
+
56
+ # 6. EXECUTION TUNING
57
+ DEFAULT_CHUNK_SIZE = int(os.getenv("SB_CHUNK_SIZE", 500_000))
58
+
59
+ # 7. SAFETY
60
+ AUDIT_LOCK_PATH = os.path.join(ROOT_DIR, "audit.lock")
@@ -0,0 +1,282 @@
1
+ import os
2
+ import yaml
3
+ import shutil
4
+ import time
5
+ import subprocess
6
+ import sys
7
+ import json
8
+ from .validator import ExperimentValidator
9
+ from .constants import ROOT_DIR, CONFIG_ARCHIVE_DIR, EXPERIMENTS_DIR, PROCESSED_SUFFIX, RESULTS_DIR, VIOLATIONS_DIR, REPORTS_DIR, AUDIT_LOCK_PATH, ACTIVE_CONFIG_PATH
10
+ from .utils.hasher import generate_experiment_hash, generate_integrity_seal
11
+ from .utils.common import copy_suite_queries
12
+ from .utils.semantic_auditor import SemanticAuditor
13
+ from .harness import IsolationHarness
14
+
15
+ class ExperimentCoordinator:
16
+ """
17
+ Orchestrates the Zero-Copy experiment lifecycle:
18
+ Validation -> Redirection -> Execution -> Monitoring -> Commitment
19
+ """
20
+
21
+
22
+ def __init__(self, target_yaml: str, headless: bool = False):
23
+ self.target_yaml = target_yaml
24
+ self.headless = headless
25
+ self.config = None
26
+ self.exp_id = None
27
+ # Raw source bytes, captured at validation — the exact text that was
28
+ # parsed and hashed. Archived verbatim into the capsule so the stored
29
+ # config is the author's file, not a re-serialization of it.
30
+ self._source_yaml = None
31
+
32
+ def run(self) -> bool:
33
+ # 0. Safety Check
34
+ if os.path.exists(AUDIT_LOCK_PATH):
35
+ print("[CRITICAL] AUDIT LOCK ACTIVE. Experiment aborted for safety.")
36
+ return False
37
+
38
+ # Phase 1: STRICT VALIDATION
39
+ try:
40
+ with open(self.target_yaml, "r") as f:
41
+ self._source_yaml = f.read()
42
+ self.config = yaml.safe_load(self._source_yaml)
43
+
44
+ ExperimentValidator.validate(self.config, source_label=os.path.basename(self.target_yaml))
45
+
46
+ # Derive Identity (STRICT SHA-BASED)
47
+ self.exp_id = generate_experiment_hash(self.config, ROOT_DIR)
48
+
49
+ self.config["meta"] = self.config.get("meta", {})
50
+ self.config["meta"]["experiment_id"] = self.exp_id
51
+
52
+ # Check Registry
53
+ if os.path.exists(os.path.join(CONFIG_ARCHIVE_DIR, f"config_{self.exp_id}.yaml")):
54
+ print(f"[INFO] SKIPPING: Experiment {self.exp_id} already exists in registry.")
55
+ return True
56
+
57
+ except Exception as e:
58
+ print(f"[REJECTED] Experiment contract failed validation: {e}")
59
+ return False
60
+
61
+ # Phase 2: PREPARE EXECUTION (Isolated)
62
+ #
63
+ # Write the canonical active.yaml FIRST (with experiment_id injected).
64
+ # This is the single source of truth: every component that reads
65
+ # active.yaml will see the same config, and the file can be traced back
66
+ # to the exact experiment being run.
67
+ with open(ACTIVE_CONFIG_PATH, 'w') as f:
68
+ yaml.dump(self.config, f, sort_keys=False)
69
+ print(f"[INFO] active.yaml updated → experiment_id: {self.exp_id}")
70
+
71
+ harness = IsolationHarness(self.exp_id)
72
+ redirects = harness.provision()
73
+ os.environ.update(redirects)
74
+
75
+ # Point the scratchpad's active.yaml at the same config so that
76
+ # subprocesses running inside the scratchpad read the correct experiment.
77
+ active_path = os.path.join(redirects["SCRATCHPAD_ROOT"], "active.yaml")
78
+ os.environ["ACTIVE_CONFIG_PATH"] = active_path
79
+ os.makedirs(os.path.dirname(active_path), exist_ok=True)
80
+ with open(active_path, 'w') as f:
81
+ yaml.dump(self.config, f, sort_keys=False)
82
+
83
+ # Phase 3: EXECUTION
84
+ print(f"[INFO] Executing {self.exp_id} in isolated scratchpad...")
85
+
86
+ try:
87
+ # Prepare environment
88
+ local_env = os.environ.copy()
89
+ success = self._execute_direct(local_env)
90
+
91
+ if not success:
92
+ print(f"[FAILURE] Technical execution failed.")
93
+ return False
94
+
95
+ # Phase 4: CODE-DRIFT GATE
96
+ # The Experiment ID was computed from the code at submission; if
97
+ # the package changed during execution, the ID no longer names
98
+ # what actually ran. Refuse to finalize — loudly.
99
+ drift = harness.check_integrity()
100
+ if drift:
101
+ print(f"[CRITICAL] Code drift detected during execution — results NOT committed:")
102
+ for d in drift:
103
+ print(f" {d}")
104
+ return False
105
+
106
+ # Phase 5: FINAL VERIFICATION & REGISTRY
107
+ return self._finalize_results()
108
+
109
+ finally:
110
+ harness.cleanup()
111
+
112
+ def _execute_direct(self, local_env: dict) -> bool:
113
+ from .utils.common import generate_partition_keys
114
+
115
+ # Generate Partition Keys
116
+ matrix = self.config.get("execution", {}).get("matrix") or self.config.get("execution", {}).get("dimensions")
117
+ keys = generate_partition_keys(matrix)
118
+
119
+ overall_success = True
120
+ keys = keys if keys else [None]
121
+
122
+ for pk in keys:
123
+ cmd = [sys.executable, "execute_run.py"]
124
+ if pk:
125
+ print(f" -> Partition: {pk}")
126
+ cmd.extend(["--partition", pk])
127
+ else:
128
+ cmd.append("--all")
129
+
130
+ p = subprocess.run(cmd, cwd=ROOT_DIR, env=local_env)
131
+ if p.returncode != 0:
132
+ overall_success = False
133
+
134
+ # Final Reporting
135
+ cmd_report = [sys.executable, "execute_run.py", "--reporting"]
136
+ p_report = subprocess.run(cmd_report, cwd=ROOT_DIR, env=local_env)
137
+
138
+ return overall_success and p_report.returncode == 0
139
+
140
+ def _finalize_results(self) -> bool:
141
+ """
142
+ Verifies results in the scratchpad, then commits them to the canonical
143
+ results directory.
144
+
145
+ The scratchpad env var (SB_RESULTS_DIR) is set by the harness AFTER
146
+ coordinator constants are imported, so the module-level RESULTS_DIR
147
+ still points to the real experiments/results/ dir. We therefore look
148
+ for results in the scratchpad first, then copy them to the canonical dir.
149
+ """
150
+ # The subprocess wrote results here (env-redirected scratchpad)
151
+ scratchpad_results = os.environ.get("SB_RESULTS_DIR", RESULTS_DIR)
152
+ scratch_exp_folder = os.path.join(scratchpad_results, self.exp_id)
153
+
154
+ # Canonical destination (the real experiments/results/ dir)
155
+ canonical_exp_folder = os.path.join(RESULTS_DIR, self.exp_id)
156
+
157
+ csv_target = os.path.join(scratch_exp_folder, f"{self.exp_id}.csv")
158
+ dashboard_target = os.path.join(scratch_exp_folder, f"{self.exp_id}.html")
159
+
160
+ if not os.path.exists(csv_target) and not os.path.exists(dashboard_target):
161
+ print(f"[ERROR] Run finished but no results found (Checked {csv_target} and {dashboard_target})")
162
+ return False
163
+
164
+ # 1. Capture Metadata (in scratchpad first)
165
+ from .utils.system import capture_environment, generator_id
166
+ metadata = {
167
+ "experiment_id": self.exp_id,
168
+ "timestamp": time.time(),
169
+ "config_id": f"config_{self.exp_id}",
170
+ # Conditions, not identity: the bench this question was answered on
171
+ "environment": capture_environment(),
172
+ # Effective data seed (declarative_gen): explicit even when the
173
+ # YAML omits it — a capsule must state the seed its data came
174
+ # from, never leave it implied by a code default.
175
+ "dataset_seed": (self.config.get("dataset") or {}).get("seed", 42),
176
+ # Maker's mark: which tool + build produced this capsule.
177
+ "generator": generator_id(),
178
+ }
179
+ with open(os.path.join(scratch_exp_folder, f"metadata_{self.exp_id}.json"), "w") as f:
180
+ json.dump(metadata, f, indent=4)
181
+
182
+ # 1.5 Semantic Audit
183
+ auditor = SemanticAuditor()
184
+ violations = []
185
+ fragments_dir = os.path.join(scratch_exp_folder, "fragments")
186
+
187
+ if os.path.exists(fragments_dir):
188
+ for filename in os.listdir(fragments_dir):
189
+ file_path = os.path.join(fragments_dir, filename)
190
+ if filename.endswith(".json"):
191
+ with open(file_path, 'r') as f:
192
+ try:
193
+ data = json.load(f)
194
+ audit_res = auditor.audit_fragment(data)
195
+ if not audit_res["success"]:
196
+ violations.append(f"JSON {filename} failed audit: {audit_res['violations']}")
197
+ except json.JSONDecodeError as e:
198
+ violations.append(f"JSON {filename} is malformed: {e}")
199
+
200
+ is_semantically_valid = len(violations) == 0
201
+ if not is_semantically_valid:
202
+ print(f"[WARNING] Semantic Violation Detected in {self.exp_id}: {violations}")
203
+ violation_dest = os.path.join(VIOLATIONS_DIR, self.exp_id)
204
+ os.makedirs(violation_dest, exist_ok=True)
205
+ shutil.copy(csv_target, os.path.join(violation_dest, "results.csv"))
206
+ return False
207
+
208
+ # 2. Commit scratchpad → canonical results dir
209
+ # Guard: source and destination must be disjoint trees. A destination
210
+ # nested inside its source turns copytree into infinite recursive
211
+ # nesting (historical incident: 5K+ file explosion -> OOM).
212
+ src_real = os.path.realpath(scratch_exp_folder)
213
+ dst_real = os.path.realpath(canonical_exp_folder)
214
+ if src_real.startswith(dst_real + os.sep) or dst_real.startswith(src_real + os.sep):
215
+ raise RuntimeError(
216
+ f"REFUSED: nested copy {src_real} <-> {dst_real} would recurse infinitely."
217
+ )
218
+ if scratch_exp_folder != canonical_exp_folder:
219
+ if os.path.exists(canonical_exp_folder):
220
+ # Only ever delete a folder named exactly for this experiment
221
+ if os.path.basename(dst_real) != self.exp_id:
222
+ raise RuntimeError(
223
+ f"REFUSED: rmtree target '{dst_real}' is not this experiment's capsule."
224
+ )
225
+ shutil.rmtree(canonical_exp_folder)
226
+ shutil.copytree(scratch_exp_folder, canonical_exp_folder)
227
+ print(f"[INFO] Results committed: {scratch_exp_folder} → {canonical_exp_folder}")
228
+
229
+ # Update csv_target to canonical location for final log message
230
+ csv_target = os.path.join(canonical_exp_folder, f"{self.exp_id}.csv")
231
+
232
+ # 3. Archive the EXACT source config into the capsule.
233
+ # Byte-faithful: the author's original file, NOT a yaml.dump
234
+ # re-serialization. A round-trip launders formatting that carries intent
235
+ # — underscored ints (1_000_000 -> 1000000), folded prose blocks ->
236
+ # escaped one-liners, real unicode dashes -> \uXXXX — and so the stored
237
+ # file would misrepresent "the exact config that ran." The Experiment ID
238
+ # is hashed from the PARSED dict, so byte-faithful archival changes no
239
+ # ID; the ID itself is recorded in the folder name and metadata_<ID>.json.
240
+ experiment_config_dest = os.path.join(canonical_exp_folder, "experiment_config.yaml")
241
+ self._archive_source_config(experiment_config_dest)
242
+
243
+ # 3.4 Embed the queries that ran — the selected engines' dialect SQL —
244
+ # into queries/ so a reader sees them without tracing fragments to
245
+ # source. A convenience copy; the Experiment ID hashes the full suite
246
+ # from source independently (see utils.common.copy_suite_queries).
247
+ copy_suite_queries(canonical_exp_folder)
248
+
249
+ # 3.5 Seal the capsule: aggregate hash over every file in the final
250
+ # canonical folder (the seal itself excluded). verify_capsule.py
251
+ # recomputes and compares — tamper evidence for published results.
252
+ # (Designed in docs/integrity_sealing.md; wiring completed here.)
253
+ seal = generate_integrity_seal(canonical_exp_folder)
254
+ with open(os.path.join(canonical_exp_folder, "integrity.seal"), "w") as f:
255
+ f.write(seal)
256
+
257
+ # 4. Archive Config registry
258
+ registry_path = os.path.join(CONFIG_ARCHIVE_DIR, f"config_{self.exp_id}.yaml")
259
+ os.makedirs(os.path.dirname(registry_path), exist_ok=True)
260
+ shutil.copy(ACTIVE_CONFIG_PATH, registry_path)
261
+
262
+ # 5. Archive copy in experiments/archive
263
+ filename = os.path.basename(self.target_yaml)
264
+ clean_name = filename if not filename.endswith(PROCESSED_SUFFIX) else filename[:-len(PROCESSED_SUFFIX)]
265
+ archive_dest = os.path.join(EXPERIMENTS_DIR, "archive", clean_name)
266
+ os.makedirs(os.path.dirname(archive_dest), exist_ok=True)
267
+ shutil.copy(os.environ["ACTIVE_CONFIG_PATH"], archive_dest)
268
+
269
+ print(f"[SUCCESS] Experiment {self.exp_id} finalized. Results at {csv_target}")
270
+ return is_semantically_valid
271
+
272
+ def _archive_source_config(self, dest_path: str) -> None:
273
+ """Write the captured source YAML (the exact bytes that were parsed and
274
+ hashed) verbatim into the capsule. Fail loud if it was never captured —
275
+ an empty/missing archived config is a silent provenance hole."""
276
+ if not self._source_yaml:
277
+ raise RuntimeError(
278
+ "REFUSED: no source config captured; cannot archive the capsule's "
279
+ "experiment_config.yaml. (Was the coordinator run via run()?)"
280
+ )
281
+ with open(dest_path, "w", encoding="utf-8") as f:
282
+ f.write(self._source_yaml)
@@ -0,0 +1,115 @@
1
+ import os
2
+ from dagster import Definitions
3
+
4
+ # 1. FACTORIES (Dynamic Lists)
5
+ # We import the lists we explicitly built.
6
+ from .assets.data_factory import data_assets
7
+ from .assets.ingestion_factory import ingestion_assets
8
+ from .assets.benchmark_factory import benchmark_assets
9
+ from .assets.semantic_gate import get_semantic_gate_assets
10
+
11
+ # 2. STATIC ASSETS (Explicit Import)
12
+ # STOP using load_assets_from_modules here.
13
+ # It creates duplicate keys because it scans imported variables.
14
+ from .assets.reporting import performance_dashboard
15
+ from .assets.maintenance import cleanup_staging_data
16
+ from .assets.data_quality import quality_assets
17
+
18
+ semantic_gate_assets = get_semantic_gate_assets(benchmark_assets)
19
+
20
+ # 3. RESOURCES & INFRA
21
+ from .resources.postgres import PostgresEngine
22
+ from .resources.duckdb import DuckDBEngine
23
+ from .resources.actian import ActianEngine
24
+ from .resources.typedb_engine import TypeDBEngine
25
+ from .resources.quack import QuackEngine
26
+ from .constants import DATA_DIR
27
+ from .jobs import benchmark_job
28
+ # from .sensors import experiment_queue_sensor
29
+
30
+ # 4. CONFIG
31
+ pg_user = os.getenv("POSTGRES_USER", "postgres")
32
+ pg_password = os.getenv("POSTGRES_PASSWORD", "password")
33
+ pg_host = os.getenv("POSTGRES_HOST", "localhost")
34
+ pg_port = os.getenv("POSTGRES_PORT", "5432")
35
+ pg_db = os.getenv("POSTGRES_DB", "postgres")
36
+
37
+ postgres_url = f"postgresql://{pg_user}:{pg_password}@{pg_host}:{pg_port}/{pg_db}"
38
+
39
+ # 5. Definitions
40
+ all_assets = [
41
+ *data_assets,
42
+ *ingestion_assets,
43
+ *benchmark_assets,
44
+ performance_dashboard,
45
+ cleanup_staging_data,
46
+ *quality_assets,
47
+ *semantic_gate_assets
48
+ ]
49
+
50
+ typedb_address = os.getenv("TYPEDB_ADDRESS", "127.0.0.1:1729")
51
+
52
+ # ---------------------------------------------------------------------------
53
+ # Relation configs — keyed by base table name (no partition-key suffix).
54
+ # Used by TypeDBEngine.bulk_load to dispatch to bulk_load_relation.
55
+ # ---------------------------------------------------------------------------
56
+
57
+ # Supply-chain hypergraph (3-way: supplier × buyer × product)
58
+ SUPPLY_CHAIN_RELATION_CONFIGS = {
59
+ "supply_contract": {
60
+ "roles": {
61
+ "supplier_id": ["supplier", "supplier_role"],
62
+ "buyer_id": ["buyer", "buyer_role"],
63
+ "product_id": ["product", "product_role"],
64
+ },
65
+ "attributes": ["volume", "price_per_unit"],
66
+ }
67
+ }
68
+
69
+ # Recursive supply-graph (self-referential: company × company)
70
+ # Both roles are played by the same entity type — TypeDB handles this natively.
71
+ # "inference": "transitive" triggers _build_transitive_inference_schema after
72
+ # the relation is loaded, adding a 'reachable' relation + recursive rule so that
73
+ # q_transitive_closure.sql can query full reachability without explicit recursion.
74
+ RECURSIVE_GRAPH_RELATION_CONFIGS = {
75
+ "supplies": {
76
+ "roles": {
77
+ "from_id": ["company", "seller_role"],
78
+ "to_id": ["company", "buyer_role"],
79
+ },
80
+ "attributes": [],
81
+ "inference": "transitive",
82
+ }
83
+ }
84
+
85
+ # Active relation config — switch this to match the current experiment.
86
+ # The TypeDBEngine is instantiated once at Dagster load time, so only one
87
+ # experiment's relation config can be active per process.
88
+ ACTIVE_RELATION_CONFIGS = RECURSIVE_GRAPH_RELATION_CONFIGS
89
+
90
+ defs = Definitions(
91
+ assets=all_assets,
92
+ resources={
93
+ "postgres": PostgresEngine(connection_string=postgres_url),
94
+ "duckdb": DuckDBEngine(data_folder=os.path.join(DATA_DIR, "duckdb")),
95
+ "quack": QuackEngine(
96
+ data_folder=os.path.join(DATA_DIR, "quack"),
97
+ port=int(os.getenv("SB_QUACK_PORT", "9494")),
98
+ token=os.getenv("SB_QUACK_TOKEN", "sb-local-quack-token"),
99
+ ),
100
+ # Same protocol, server-side execution via remote.query() — own data
101
+ # folder and port so it never contends with the attach-mode engine.
102
+ "quack_pushdown": QuackEngine(
103
+ data_folder=os.path.join(DATA_DIR, "quack_pushdown"),
104
+ port=int(os.getenv("SB_QUACK_PUSHDOWN_PORT", "9495")),
105
+ token=os.getenv("SB_QUACK_TOKEN", "sb-local-quack-token"),
106
+ pushdown=True,
107
+ ),
108
+ "actian": ActianEngine(),
109
+ "typedb": TypeDBEngine(
110
+ address=typedb_address,
111
+ relation_configs=ACTIVE_RELATION_CONFIGS,
112
+ ),
113
+ },
114
+ jobs=[benchmark_job],
115
+ )
@@ -0,0 +1,99 @@
1
+ # =============================================================================
2
+ # EXPERIMENT TEMPLATE — copy this, fill it in, drop the copy in experiments/queue/
3
+ # =============================================================================
4
+ # This is a complete, VALID experiment. Copy it, rename it (e.g.
5
+ # my_experiment.yaml), edit the values, and place it in experiments/queue/ to run.
6
+ #
7
+ # The contract is enforced fail-fast at validation: sql_benchmarks/utils/schema.py
8
+ # (Pydantic). If a field is wrong or missing, the run is rejected before anything
9
+ # executes — there is no "partly valid" experiment.
10
+ #
11
+ # Two things worth knowing before you start:
12
+ # 1. The Experiment ID is a SHA-256 of the PARSED config + the SQL + the
13
+ # measurement code. Change the question (engines, data, seed, suite, matrix)
14
+ # and the ID changes. Reformat the file (comments, spacing, quote style) and
15
+ # it does NOT — formatting is normalized out, like a code comment.
16
+ # 2. Your file is archived into the capsule VERBATIM (byte-for-byte, comments
17
+ # and all). What you write here is what a reader sees in results/<ID>/.
18
+ # =============================================================================
19
+
20
+ meta:
21
+ # Human-readable identity. Optional, but always set them — they are how you and
22
+ # anyone else tell experiments apart. NOT hashed (the ID ignores meta), so you
23
+ # can fix a typo here without changing the ID.
24
+ name: "Short name of the experiment"
25
+ # The hypothesis, in prose. What question does this run answer?
26
+ description: >
27
+ One or more sentences stating the hypothesis and why this experiment tests
28
+ it. Folded blocks like this stay readable in the archived capsule.
29
+
30
+ dataset:
31
+ # The data generator plugin. declarative_gen builds synthetic data from the
32
+ # column specs below. (For file-based data, use `paths:` instead of `tables:`.)
33
+ source: sql_benchmarks.plugins.data_sources.declarative_gen
34
+ # Base seed for synthetic data. HASHED — change it and the ID changes, so two
35
+ # experiments on different data can never share an identity. Defaults to 42.
36
+ seed: 42
37
+ tables:
38
+ # Table name — referenced by the SQL suite as {{ <name>_table }}.
39
+ my_table:
40
+ # Row count: a literal int, or a name from `definitions.rows` below so the
41
+ # matrix can sweep across scales.
42
+ rows: rows
43
+ columns:
44
+ # sequence: monotonic 1..N. Mark the primary key here.
45
+ - name: id
46
+ provider: sequence
47
+ primary_key: true
48
+ # choice: categorical values with weights (weights should sum to ~1.0).
49
+ # Use this to control selectivity — the fraction of rows a filter matches.
50
+ - name: category
51
+ provider: choice
52
+ options: ["a", "b", "c"]
53
+ weights: [0.1, 0.3, 0.6]
54
+ # random_int / random_float: numeric ranges via min/max.
55
+ - name: amount
56
+ provider: random_float
57
+ min: 0.0
58
+ max: 100.0
59
+ # text_concat: builds a string column (e.g. padding to force real scan
60
+ # cost). `source` references another column; `prefix` is a literal.
61
+ - name: payload
62
+ provider: text_concat
63
+ prefix: "PADDING_"
64
+ source: id
65
+ # OPTIONAL. Indexes built at ingestion, OUTSIDE the timed query loop.
66
+ # Row stores (Postgres) use these; columnar engines ignore them and rely on
67
+ # their own min-max zonemaps. Drop this block if you don't want indexes.
68
+ indexes:
69
+ - name: idx_my_table_category
70
+ columns: [category]
71
+
72
+ # Named scales referenced by `rows:` above and swept by the matrix.
73
+ definitions:
74
+ rows:
75
+ medium: 1_000_000
76
+ large: 10_000_000
77
+
78
+ execution:
79
+ # The SQL suite to run: scripts/sql/<test_suite>/<dialect>/*.sql
80
+ test_suite: selectivity
81
+ # REQUIRED. Engines to run. Each maps to a SQL dialect directory.
82
+ # Available: duckdb, quack, quack_pushdown, postgres, actian, typedb.
83
+ engines:
84
+ - duckdb
85
+ - quack
86
+ - quack_pushdown
87
+ - postgres
88
+ # Replications per measured point. Raw per-rep timings are kept in the capsule.
89
+ replication: 5
90
+ # OPTIONAL per-engine tuning, namespaced by engine. Each engine sees only its
91
+ # own namespace. Delete if you don't need it.
92
+ # engine_params:
93
+ # postgres: {work_mem: "64MB", random_page_cost: 1.1}
94
+ # duckdb: {threads: 4}
95
+ # The matrix: every combination becomes one isolated, cold-cache measurement.
96
+ matrix:
97
+ rows:
98
+ - medium
99
+ - large