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,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
|