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,88 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import yaml
|
|
3
|
+
from dagster import asset, AssetExecutionContext
|
|
4
|
+
from typing import List
|
|
5
|
+
from ..constants import DATA_DIR, EXPERIMENTS_DIR, ACTIVE_CONFIG_PATH
|
|
6
|
+
from ..partitions import partitions_def
|
|
7
|
+
from ..utils.common import load_context, get_engine_asset_prefix, get_scoped_asset_name
|
|
8
|
+
|
|
9
|
+
CTX = load_context()
|
|
10
|
+
ACTIVE_ENGINES = CTX.get('engines', [])
|
|
11
|
+
EXP_ID = CTX['meta'].get("experiment_id", "unknown")
|
|
12
|
+
|
|
13
|
+
def load_dataset_config():
|
|
14
|
+
"""Reads the active experiment YAML to find the tables configured for the dataset."""
|
|
15
|
+
config_path = ACTIVE_CONFIG_PATH
|
|
16
|
+
if not os.path.exists(config_path):
|
|
17
|
+
return {}
|
|
18
|
+
with open(config_path, "r") as f:
|
|
19
|
+
conf = yaml.safe_load(f) or {}
|
|
20
|
+
return conf.get("dataset", {}).get("tables", {})
|
|
21
|
+
|
|
22
|
+
TABLES_CONFIG = load_dataset_config()
|
|
23
|
+
|
|
24
|
+
ingestion_assets: List[object] = []
|
|
25
|
+
|
|
26
|
+
def make_ingestion_asset(engine: str, table_name: str):
|
|
27
|
+
"""
|
|
28
|
+
Creates a single ingestion asset for a given engine and table,
|
|
29
|
+
delegating execution to the engine's bulk_load method.
|
|
30
|
+
"""
|
|
31
|
+
prefix = get_engine_asset_prefix(engine)
|
|
32
|
+
base_asset_name = f"{prefix}{table_name}_table"
|
|
33
|
+
scoped_name = get_scoped_asset_name(base_asset_name, EXP_ID)
|
|
34
|
+
|
|
35
|
+
tags = {}
|
|
36
|
+
tags["experiment_scope"] = "partitioned"
|
|
37
|
+
|
|
38
|
+
deps = [
|
|
39
|
+
get_scoped_asset_name(f"{table_name}_parquet", EXP_ID),
|
|
40
|
+
get_scoped_asset_name(f"{table_name}_quality", EXP_ID)
|
|
41
|
+
]
|
|
42
|
+
|
|
43
|
+
@asset(
|
|
44
|
+
name=scoped_name,
|
|
45
|
+
group_name="ingestion",
|
|
46
|
+
partitions_def=partitions_def,
|
|
47
|
+
deps=deps,
|
|
48
|
+
required_resource_keys={engine},
|
|
49
|
+
op_tags=tags
|
|
50
|
+
)
|
|
51
|
+
def _ingest(context: AssetExecutionContext):
|
|
52
|
+
partition_key = context.partition_key
|
|
53
|
+
|
|
54
|
+
# 1. Dynamic Resource Retrieval (Polymorphic)
|
|
55
|
+
db = getattr(context.resources, engine)
|
|
56
|
+
|
|
57
|
+
# 2. FIND THE EXACT FILE
|
|
58
|
+
filename = f"{table_name}_{partition_key}.parquet"
|
|
59
|
+
parquet_path = os.path.join(DATA_DIR, "staging", filename)
|
|
60
|
+
|
|
61
|
+
if not os.path.exists(parquet_path):
|
|
62
|
+
raise FileNotFoundError(f"Parquet file not found: {parquet_path}")
|
|
63
|
+
|
|
64
|
+
# 3. DEFINE TARGET TABLE NAME
|
|
65
|
+
target_table_name = f"{table_name}_{partition_key}"
|
|
66
|
+
|
|
67
|
+
context.log.info(f"Ingesting {parquet_path} into {engine} table '{target_table_name}'...")
|
|
68
|
+
|
|
69
|
+
# 4. Use the polymorphic bulk_load method
|
|
70
|
+
db.bulk_load(
|
|
71
|
+
filepath=parquet_path,
|
|
72
|
+
table_name=target_table_name,
|
|
73
|
+
partition_key=partition_key,
|
|
74
|
+
table_def=TABLES_CONFIG.get(table_name),
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
return None
|
|
78
|
+
|
|
79
|
+
return _ingest
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
# --- Dynamic Asset Creation ---
|
|
83
|
+
# Loop 1: Iterate over the configured engines (e.g., duckdb, postgres)
|
|
84
|
+
for engine in ACTIVE_ENGINES:
|
|
85
|
+
# Loop 2: Iterate over the correct list of table names from your verified config logic
|
|
86
|
+
for table_name in TABLES_CONFIG.keys():
|
|
87
|
+
asset_obj = make_ingestion_asset(engine, table_name)
|
|
88
|
+
ingestion_assets.append(asset_obj)
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import shutil
|
|
2
|
+
import os
|
|
3
|
+
from dagster import asset
|
|
4
|
+
from ..constants import DATA_DIR
|
|
5
|
+
|
|
6
|
+
# We define this asset to depend on the LAST step of the pipeline.
|
|
7
|
+
# Usually, this is the Reporting/Dashboard asset, or the Benchmarks themselves.
|
|
8
|
+
# Since reporting aggregates everything, depending on 'performance_dashboard'
|
|
9
|
+
# ensures the run is fully complete before we wipe the source data.
|
|
10
|
+
# Note: You need to import the upstream asset key or use string reference.
|
|
11
|
+
|
|
12
|
+
@asset(
|
|
13
|
+
group_name="maintenance",
|
|
14
|
+
deps=["performance_dashboard"], # Runs ONLY after the dashboard is updated
|
|
15
|
+
description="Deletes the heavy staging Parquet files after a successful run."
|
|
16
|
+
)
|
|
17
|
+
def cleanup_staging_data(context):
|
|
18
|
+
"""
|
|
19
|
+
Removes the generated parquet files to prevent disk overflow.
|
|
20
|
+
This runs per-partition if your job is partitioned, or once per run.
|
|
21
|
+
"""
|
|
22
|
+
# 1. Identify the Target Directory
|
|
23
|
+
# We need to be careful not to delete the 'results' folder, only 'staging'.
|
|
24
|
+
# Assuming DATA_DIR points to 'experiments/data' or similar.
|
|
25
|
+
|
|
26
|
+
# If we are in a partitioned run, we might want to delete only that partition's data.
|
|
27
|
+
if context.has_partition_key:
|
|
28
|
+
partition = context.partition_key
|
|
29
|
+
# Assuming data_factory creates a folder named after the partition or table
|
|
30
|
+
# This requires knowing exactly how data_factory names its output.
|
|
31
|
+
# Based on previous code: output_dir = os.path.join(DATA_DIR, partition_key)
|
|
32
|
+
|
|
33
|
+
target_dir = os.path.join(DATA_DIR, partition)
|
|
34
|
+
|
|
35
|
+
if os.path.exists(target_dir):
|
|
36
|
+
context.log.info(f"Partitioned Run: Deleting staging data at {target_dir}")
|
|
37
|
+
shutil.rmtree(target_dir)
|
|
38
|
+
else:
|
|
39
|
+
context.log.warning(f"Target dir {target_dir} not found. Already cleaned?")
|
|
40
|
+
|
|
41
|
+
else:
|
|
42
|
+
# Non-partitioned run (Standard cleanup)
|
|
43
|
+
# We assume standard structure creates subfolders in DATA_DIR
|
|
44
|
+
# DANGER: Be specific. Don't just wipe DATA_DIR if it contains results.
|
|
45
|
+
|
|
46
|
+
# Let's list the subdirectories in DATA_DIR and assume they are data folders
|
|
47
|
+
# generated by the factory.
|
|
48
|
+
if os.path.exists(DATA_DIR):
|
|
49
|
+
context.log.info(f"Full Run: Cleaning up staging directory {DATA_DIR}")
|
|
50
|
+
|
|
51
|
+
# Option A: Delete everything inside DATA_DIR
|
|
52
|
+
# shutil.rmtree(DATA_DIR)
|
|
53
|
+
# os.makedirs(DATA_DIR) # Recreate empty folder
|
|
54
|
+
|
|
55
|
+
# Option B (Safer): Delete specific known subfolders if possible.
|
|
56
|
+
# For now, since this is staging, Option A is acceptable IF results are stored elsewhere.
|
|
57
|
+
# (Your results are likely in 'experiments/results', right?)
|
|
58
|
+
|
|
59
|
+
for item in os.listdir(DATA_DIR):
|
|
60
|
+
item_path = os.path.join(DATA_DIR, item)
|
|
61
|
+
if os.path.isdir(item_path):
|
|
62
|
+
shutil.rmtree(item_path)
|
|
63
|
+
context.log.info(f"Deleted {item}")
|
|
@@ -0,0 +1,385 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import polars as pl
|
|
3
|
+
import pandas as pd
|
|
4
|
+
import plotly.express as px
|
|
5
|
+
from dagster import asset, AssetExecutionContext, MetadataValue, MaterializeResult, DagsterEventType, EventRecordsFilter
|
|
6
|
+
|
|
7
|
+
from ..constants import RESULTS_DIR, REPORTS_DIR
|
|
8
|
+
from ..utils.common import load_context
|
|
9
|
+
from .benchmark_factory import benchmark_assets
|
|
10
|
+
import glob
|
|
11
|
+
import json
|
|
12
|
+
|
|
13
|
+
all_benchmark_keys = [k.key for k in benchmark_assets]
|
|
14
|
+
|
|
15
|
+
# ==========================================
|
|
16
|
+
# 1. PURE LOGIC (Testable)
|
|
17
|
+
# ==========================================
|
|
18
|
+
# Use environment-aware path resolution
|
|
19
|
+
def parse_fragments_to_records(experiment_id):
|
|
20
|
+
"""
|
|
21
|
+
Scans the results directory for the given experiment_id
|
|
22
|
+
and parses all fragment JSONs into a flat list of records.
|
|
23
|
+
"""
|
|
24
|
+
# Look into the isolated experiment folder as per specification
|
|
25
|
+
fragments_pattern = os.path.join(RESULTS_DIR, experiment_id, "fragments", "*.json")
|
|
26
|
+
fragment_files = glob.glob(fragments_pattern)
|
|
27
|
+
|
|
28
|
+
records = []
|
|
29
|
+
|
|
30
|
+
for fpath in fragment_files:
|
|
31
|
+
try:
|
|
32
|
+
with open(fpath, "r") as f:
|
|
33
|
+
data = json.load(f)
|
|
34
|
+
|
|
35
|
+
meta = data.get("meta", {})
|
|
36
|
+
metrics = data.get("metrics", {})
|
|
37
|
+
params = data.get("parameters", {})
|
|
38
|
+
|
|
39
|
+
if meta.get("experiment_id") != experiment_id:
|
|
40
|
+
continue
|
|
41
|
+
|
|
42
|
+
asset_name = meta.get("asset", "unknown_asset")
|
|
43
|
+
|
|
44
|
+
# Extract Partition from Filename: asset_name__PARTITION.json
|
|
45
|
+
filename = os.path.basename(fpath)
|
|
46
|
+
partition_name = "default"
|
|
47
|
+
|
|
48
|
+
# Logic: Split by asset_name + "__"
|
|
49
|
+
# It's safer to use the known separator "__"
|
|
50
|
+
if "__" in filename:
|
|
51
|
+
# Remove extension
|
|
52
|
+
name_no_ext = os.path.splitext(filename)[0]
|
|
53
|
+
parts = name_no_ext.split("__")
|
|
54
|
+
if len(parts) >= 2:
|
|
55
|
+
partition_name = parts[-1]
|
|
56
|
+
|
|
57
|
+
# DNF fragments have duration_seconds=None; preserve as None in the CSV
|
|
58
|
+
# so downstream analysis can distinguish "timed out" from "0ms".
|
|
59
|
+
raw_duration = metrics.get("duration_seconds")
|
|
60
|
+
duration_val = float(raw_duration) if raw_duration is not None else None
|
|
61
|
+
|
|
62
|
+
# Per-replication spread (older fragments lack durations_raw)
|
|
63
|
+
raw_list = metrics.get("durations_raw") or []
|
|
64
|
+
duration_min = min(raw_list) if raw_list else duration_val
|
|
65
|
+
duration_max = max(raw_list) if raw_list else duration_val
|
|
66
|
+
|
|
67
|
+
row = {
|
|
68
|
+
"Asset": asset_name,
|
|
69
|
+
"Partition": partition_name,
|
|
70
|
+
"Duration": duration_val,
|
|
71
|
+
"Duration_Min": duration_min,
|
|
72
|
+
"Duration_Max": duration_max,
|
|
73
|
+
"DNF": bool(metrics.get("dnf", False)),
|
|
74
|
+
"Engine": str(meta.get("engine", "Unknown")),
|
|
75
|
+
# None (blank in CSV) when the experiment has no 'rows'
|
|
76
|
+
# dimension (e.g. TPC-H uses scale_factor) — 0 must never
|
|
77
|
+
# be the disguise for "absent".
|
|
78
|
+
"Rows": int(params["rows"]) if "rows" in params else None,
|
|
79
|
+
"Selectivity": float(params.get("derived_selectivity", 0.0) or 0.0)
|
|
80
|
+
}
|
|
81
|
+
# Note: the old 'System' column (engine + disk_type label) was
|
|
82
|
+
# removed as a duplicate of 'Engine'; disk_type survives as its
|
|
83
|
+
# own column via the parameter merge below.
|
|
84
|
+
|
|
85
|
+
# Merge ALL parameters into the row (Dynamic Columns)
|
|
86
|
+
# This ensures 'null_probability' etc appear in CSV.
|
|
87
|
+
# Case-insensitive guard: 'rows' must not duplicate 'Rows'.
|
|
88
|
+
existing = {c.lower() for c in row}
|
|
89
|
+
for k, v in params.items():
|
|
90
|
+
if k.lower() not in existing:
|
|
91
|
+
row[k] = v
|
|
92
|
+
|
|
93
|
+
records.append(row)
|
|
94
|
+
|
|
95
|
+
except Exception as e:
|
|
96
|
+
print(f"Skipping malformed fragment {fpath}: {e}")
|
|
97
|
+
continue
|
|
98
|
+
|
|
99
|
+
return records
|
|
100
|
+
|
|
101
|
+
# ==========================================
|
|
102
|
+
# 2. THE ASSET (Orchestration)
|
|
103
|
+
# ==========================================
|
|
104
|
+
@asset(
|
|
105
|
+
deps=all_benchmark_keys,
|
|
106
|
+
group_name="reporting",
|
|
107
|
+
description="Generates a Multi-View Dashboard."
|
|
108
|
+
)
|
|
109
|
+
def performance_dashboard(context: AssetExecutionContext):
|
|
110
|
+
instance = context.instance
|
|
111
|
+
try:
|
|
112
|
+
CTX = load_context()
|
|
113
|
+
EXP_ID = CTX['meta'].get("experiment_id", "unknown")
|
|
114
|
+
except Exception:
|
|
115
|
+
context.log.warning("Could not read active context.")
|
|
116
|
+
return
|
|
117
|
+
|
|
118
|
+
# 1. FETCH & PARSE (New Logic)
|
|
119
|
+
records = parse_fragments_to_records(EXP_ID)
|
|
120
|
+
|
|
121
|
+
if not records:
|
|
122
|
+
context.log.info(f"No records found for experiment: {EXP_ID}")
|
|
123
|
+
return
|
|
124
|
+
|
|
125
|
+
# 3. PREPARE DATA
|
|
126
|
+
df = pl.DataFrame(records)
|
|
127
|
+
|
|
128
|
+
# Deduplicate: Keep last run for same (Asset, Partition, Engine)
|
|
129
|
+
# Rows might technically differ but Partition should cover it.
|
|
130
|
+
unique_keys = ["Asset", "Partition", "Engine", "Rows"]
|
|
131
|
+
# Filter keys that actually exist (to be safe if Partition is missing in legacy)
|
|
132
|
+
unique_keys = [k for k in unique_keys if k in df.columns]
|
|
133
|
+
|
|
134
|
+
df = df.unique(subset=unique_keys, keep="last").sort("Rows")
|
|
135
|
+
pldf = df.to_pandas()
|
|
136
|
+
|
|
137
|
+
# 3.5 SANITIZE DATA (The "Gas in the Trunk")
|
|
138
|
+
# ------------------------------------------
|
|
139
|
+
# Drop columns that are effectively "Null" or "Default" across the entire dataset
|
|
140
|
+
# This prevents "Selectivity=0.0" from appearing in titles when it wasn't used.
|
|
141
|
+
|
|
142
|
+
cols_to_drop = []
|
|
143
|
+
for col in pldf.columns:
|
|
144
|
+
if col in ["Asset", "Engine", "Partition", "Duration"]:
|
|
145
|
+
continue
|
|
146
|
+
|
|
147
|
+
# Check if column is all null or all default (0.0 for float, 0 for int)
|
|
148
|
+
is_all_null = pldf[col].isnull().all()
|
|
149
|
+
is_all_zero = (pldf[col] == 0).all() and pd.api.types.is_numeric_dtype(pldf[col])
|
|
150
|
+
is_single_value = pldf[col].nunique() <= 1
|
|
151
|
+
|
|
152
|
+
# Heuristic: If it's 0 everywhere, it's likely a parser default, UNLESS it's "Rows" (which shouldn't be 0)
|
|
153
|
+
# or if the user explicitly set 0. But for Selectivity/Skew, 0 is often "Not Used".
|
|
154
|
+
if col != "Rows" and (is_all_null or is_all_zero):
|
|
155
|
+
cols_to_drop.append(col)
|
|
156
|
+
# Also drop constant columns from the "Matrix Params" consideration (but keep in DF for reference if needed?)
|
|
157
|
+
# Actually, simpler to just drop them from the DF used for plotting considerations.
|
|
158
|
+
|
|
159
|
+
if cols_to_drop:
|
|
160
|
+
context.log.info(f"Dropping irrelevant/default columns: {cols_to_drop}")
|
|
161
|
+
pldf = pldf.drop(columns=cols_to_drop)
|
|
162
|
+
|
|
163
|
+
# Fill NaNs in remaining parameters to prevent fragmentation
|
|
164
|
+
# e.g. if 'null_probability' is present in some rows but NaN in others, fill with 0 via Polars upstream or Pandas here.
|
|
165
|
+
# For numeric params, 0 is usually safe default for "parameter not present".
|
|
166
|
+
numeric_cols = pldf.select_dtypes(include=['number']).columns
|
|
167
|
+
pldf[numeric_cols] = pldf[numeric_cols].fillna(0)
|
|
168
|
+
|
|
169
|
+
# 4. RENDER (Visualization - Matrix Explorer)
|
|
170
|
+
figures_html = []
|
|
171
|
+
|
|
172
|
+
# Identify Matrix Parameters (numeric columns that aren't fixed labels)
|
|
173
|
+
excluded_cols = {"Asset", "Partition", "Duration", "Duration_Min", "Duration_Max", "Engine"}
|
|
174
|
+
matrix_params = [c for c in pldf.columns if c not in excluded_cols and pd.api.types.is_numeric_dtype(pldf[c])]
|
|
175
|
+
|
|
176
|
+
context.log.info(f"Matrix Params Detected: {matrix_params}")
|
|
177
|
+
if "null_probability" in pldf.columns:
|
|
178
|
+
context.log.info(f"Unique Null Probs: {pldf['null_probability'].unique().tolist()}")
|
|
179
|
+
|
|
180
|
+
# -------------------------------------------------------------------------
|
|
181
|
+
# 1. Comparison by Engine (The Basics)
|
|
182
|
+
# -------------------------------------------------------------------------
|
|
183
|
+
try:
|
|
184
|
+
# Side-by-Side Engine Comparison (Fixed Rows Scaling)
|
|
185
|
+
# Check if "Rows" is a parameter, as it's the standard X-axis
|
|
186
|
+
if "Rows" in matrix_params:
|
|
187
|
+
fig_compare = px.line(
|
|
188
|
+
pldf,
|
|
189
|
+
x="Rows",
|
|
190
|
+
y="Duration",
|
|
191
|
+
color="Engine",
|
|
192
|
+
line_dash="Asset",
|
|
193
|
+
symbol="Engine",
|
|
194
|
+
log_x=True,
|
|
195
|
+
log_y=True,
|
|
196
|
+
markers=True,
|
|
197
|
+
title="<b>Global Comparison</b>: Engine Scaling (Rows vs Duration)"
|
|
198
|
+
)
|
|
199
|
+
figures_html.append(fig_compare.to_html(full_html=False, include_plotlyjs='cdn'))
|
|
200
|
+
except Exception as e:
|
|
201
|
+
context.log.warning(f"Global Plot Error: {e}")
|
|
202
|
+
|
|
203
|
+
# -------------------------------------------------------------------------
|
|
204
|
+
# 2. DYNAMIC DISCOVERY ENGINE (The "Smart Logic")
|
|
205
|
+
# -------------------------------------------------------------------------
|
|
206
|
+
unique_systems = sorted(pldf["Engine"].unique())
|
|
207
|
+
|
|
208
|
+
for system in unique_systems:
|
|
209
|
+
system_df = pldf[pldf["Engine"] == system].copy()
|
|
210
|
+
|
|
211
|
+
# A. Discover Roles
|
|
212
|
+
# -----------------
|
|
213
|
+
# Candidates for X-Axis: Numeric params with > 1 unique value
|
|
214
|
+
x_candidates = []
|
|
215
|
+
for p in matrix_params:
|
|
216
|
+
if system_df[p].nunique() > 1:
|
|
217
|
+
x_candidates.append(p)
|
|
218
|
+
|
|
219
|
+
# Heuristic: Prefer "Rows" if available, else param with max cardinality
|
|
220
|
+
if "Rows" in x_candidates:
|
|
221
|
+
x_axis = "Rows"
|
|
222
|
+
elif x_candidates:
|
|
223
|
+
# Pick max cardinality
|
|
224
|
+
x_axis = max(x_candidates, key=lambda c: system_df[c].nunique())
|
|
225
|
+
else:
|
|
226
|
+
# Fallback if nothing varies (single point)
|
|
227
|
+
x_axis = matrix_params[0] if matrix_params else "Asset"
|
|
228
|
+
|
|
229
|
+
# B. Classify Remaining Parameters (Series vs Slices)
|
|
230
|
+
# ---------------------------------------------------
|
|
231
|
+
other_params = [p for p in matrix_params if p != x_axis]
|
|
232
|
+
|
|
233
|
+
# Slices: High Cardinality OR Orthogonal Dimensions we want to isolate
|
|
234
|
+
# Series: Low Cardinality dimensions we want to compare on one chart
|
|
235
|
+
|
|
236
|
+
slice_params = []
|
|
237
|
+
series_params = ["Asset"] # Always compare Assets (Logic) on same chart
|
|
238
|
+
|
|
239
|
+
for p in other_params:
|
|
240
|
+
# Exclude parameters that are just case variants of X-Axis (Rows vs rows)
|
|
241
|
+
if p.lower() == x_axis.lower():
|
|
242
|
+
continue
|
|
243
|
+
|
|
244
|
+
unique_count = system_df[p].nunique()
|
|
245
|
+
# If it has only 1 value, it doesn't matter (it's fixed context),
|
|
246
|
+
# but we can treat it as a Slice to be safe/explicit in title.
|
|
247
|
+
# If it has many values (>5), slice it to avoid clutter.
|
|
248
|
+
# If it has few values (2-5), add to SERIES (lines).
|
|
249
|
+
if unique_count > 5:
|
|
250
|
+
slice_params.append(p)
|
|
251
|
+
elif unique_count > 1:
|
|
252
|
+
series_params.append(p)
|
|
253
|
+
else:
|
|
254
|
+
# It's a fixed value, add to slice context implicitly
|
|
255
|
+
slice_params.append(p)
|
|
256
|
+
|
|
257
|
+
# C. Generate Plots via Slicing
|
|
258
|
+
# -----------------------------
|
|
259
|
+
# Group by all slice parameters to create distinct scenarios
|
|
260
|
+
if slice_params:
|
|
261
|
+
# Sort to ensure consistent grouping order
|
|
262
|
+
slice_params = sorted(slice_params)
|
|
263
|
+
grouped = system_df.groupby(slice_params)
|
|
264
|
+
else:
|
|
265
|
+
grouped = [((), system_df)]
|
|
266
|
+
|
|
267
|
+
for group_keys, slice_df in grouped:
|
|
268
|
+
if not isinstance(group_keys, tuple):
|
|
269
|
+
group_keys = (group_keys,)
|
|
270
|
+
|
|
271
|
+
# 1. Build Title
|
|
272
|
+
title_parts = [f"<b>{system}</b>: Varying <b>{x_axis}</b>"]
|
|
273
|
+
if slice_params:
|
|
274
|
+
ctx_str = ", ".join([f"{k}={v}" for k, v in zip(slice_params, group_keys)])
|
|
275
|
+
title_parts.append(f"<span style='font-size:12px'>({ctx_str})</span>")
|
|
276
|
+
title = "<br>".join(title_parts)
|
|
277
|
+
|
|
278
|
+
# 2. Construct Series Column (Legend)
|
|
279
|
+
# Combine all series params into one string column "Series"
|
|
280
|
+
# e.g. "2VL - 10% Nulls"
|
|
281
|
+
slice_df = slice_df.copy()
|
|
282
|
+
|
|
283
|
+
if len(series_params) > 1:
|
|
284
|
+
# We have multiple dimensions (Asset + NullProb + ...)
|
|
285
|
+
# Create a composite key
|
|
286
|
+
def make_series_label(row):
|
|
287
|
+
parts = []
|
|
288
|
+
for sp in series_params:
|
|
289
|
+
val = row[sp]
|
|
290
|
+
# Beautify: If val is float, format it?
|
|
291
|
+
# For now, simplistic str()
|
|
292
|
+
parts.append(str(val))
|
|
293
|
+
return " / ".join(parts)
|
|
294
|
+
|
|
295
|
+
slice_df["_Series_"] = slice_df.apply(make_series_label, axis=1)
|
|
296
|
+
color_col = "_Series_"
|
|
297
|
+
symbol_col = "Asset" # Keep using Asset for symbol if present
|
|
298
|
+
else:
|
|
299
|
+
# Simple case: Only Asset varies
|
|
300
|
+
slice_df["_Series_"] = slice_df[series_params[0]].astype(str)
|
|
301
|
+
color_col = "_Series_"
|
|
302
|
+
symbol_col = "_Series_"
|
|
303
|
+
|
|
304
|
+
try:
|
|
305
|
+
# 3. Plot
|
|
306
|
+
scenario_df = slice_df
|
|
307
|
+
scenario_df = scenario_df.sort_values(by=x_axis)
|
|
308
|
+
|
|
309
|
+
fig = px.line(
|
|
310
|
+
scenario_df,
|
|
311
|
+
x=x_axis,
|
|
312
|
+
y="Duration",
|
|
313
|
+
color=color_col,
|
|
314
|
+
symbol=symbol_col if symbol_col in slice_df.columns else None,
|
|
315
|
+
markers=True,
|
|
316
|
+
log_y=True,
|
|
317
|
+
log_x=True if "Rows" in x_axis or "null" in x_axis.lower() else False,
|
|
318
|
+
title=title,
|
|
319
|
+
labels={x_axis: x_axis, "Duration": "Duration (s)", "_Series_": " / ".join(series_params)}
|
|
320
|
+
)
|
|
321
|
+
figures_html.append(fig.to_html(full_html=False, include_plotlyjs=False))
|
|
322
|
+
except Exception as e:
|
|
323
|
+
context.log.warning(f"Dynamic Plot Error ({system}): {e}")
|
|
324
|
+
|
|
325
|
+
# -------------------------------------------------------------------------
|
|
326
|
+
# 3. 3D Landscape (The "Bonus")
|
|
327
|
+
# -------------------------------------------------------------------------
|
|
328
|
+
if "null_probability" in matrix_params and "Rows" in matrix_params:
|
|
329
|
+
try:
|
|
330
|
+
fig_3d = px.scatter_3d(
|
|
331
|
+
pldf,
|
|
332
|
+
x="Rows",
|
|
333
|
+
y="null_probability",
|
|
334
|
+
z="Duration",
|
|
335
|
+
color="Engine",
|
|
336
|
+
symbol="Asset",
|
|
337
|
+
log_x=True,
|
|
338
|
+
log_z=True,
|
|
339
|
+
title="<b>3D Landscape</b>: Rows x Nulls x Duration",
|
|
340
|
+
height=800
|
|
341
|
+
)
|
|
342
|
+
figures_html.append(fig_3d.to_html(full_html=False, include_plotlyjs=False))
|
|
343
|
+
except Exception as e:
|
|
344
|
+
context.log.warning(f"3D Plot Error: {e}")
|
|
345
|
+
|
|
346
|
+
# -------------------------------------------------------------------------
|
|
347
|
+
# 4. SAVE (Side Effect)
|
|
348
|
+
# -------------------------------------------------------------------------
|
|
349
|
+
exp_dir = os.path.join(RESULTS_DIR, EXP_ID)
|
|
350
|
+
os.makedirs(exp_dir, exist_ok=True)
|
|
351
|
+
|
|
352
|
+
html_path = os.path.join(exp_dir, f"{EXP_ID}.html")
|
|
353
|
+
csv_path = os.path.join(exp_dir, f"{EXP_ID}.csv")
|
|
354
|
+
|
|
355
|
+
with open(html_path, "w") as f:
|
|
356
|
+
f.write(f"<h1>Benchmark: {EXP_ID}</h1>")
|
|
357
|
+
f.write(f"<p>Generated at: {pd.Timestamp.now()}</p><hr>")
|
|
358
|
+
f.write("<br><hr><br>".join(figures_html))
|
|
359
|
+
|
|
360
|
+
# -------------------------------------------------------------------------
|
|
361
|
+
# 5. WRITE CSV
|
|
362
|
+
# -------------------------------------------------------------------------
|
|
363
|
+
df.write_csv(csv_path)
|
|
364
|
+
|
|
365
|
+
# -------------------------------------------------------------------------
|
|
366
|
+
# 6. SCALING LAW (auto-analysis, sealed with the capsule)
|
|
367
|
+
# Fit each engine's power-law exponent from the raw fragments. Written
|
|
368
|
+
# before the coordinator seals the capsule, so the exponents are
|
|
369
|
+
# tamper-protected alongside the timings. Absent for experiments without
|
|
370
|
+
# a row-scaled matrix (e.g. TPC-H scale_factor) — that's expected.
|
|
371
|
+
# -------------------------------------------------------------------------
|
|
372
|
+
from ..utils.scaling import analyze_capsule
|
|
373
|
+
scaling = analyze_capsule(RESULTS_DIR, EXP_ID)
|
|
374
|
+
if scaling:
|
|
375
|
+
with open(os.path.join(exp_dir, "scaling.json"), "w") as f:
|
|
376
|
+
json.dump(scaling, f, indent=2)
|
|
377
|
+
alphas = {e: r["alpha"] for e, r in scaling.items()}
|
|
378
|
+
context.log.info(f"Scaling law (alpha per engine): {alphas}")
|
|
379
|
+
|
|
380
|
+
return MaterializeResult(
|
|
381
|
+
metadata={
|
|
382
|
+
"dashboard_path": MetadataValue.path(html_path),
|
|
383
|
+
"results_csv_path": MetadataValue.path(csv_path),
|
|
384
|
+
"experiment_id": EXP_ID
|
|
385
|
+
})
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import json
|
|
3
|
+
from dagster import asset, AssetExecutionContext, MaterializeResult, MetadataValue
|
|
4
|
+
from ..constants import RESULTS_DIR, VIOLATIONS_DIR
|
|
5
|
+
from ..partitions import partitions_def
|
|
6
|
+
from ..utils.common import load_context, get_scoped_asset_name
|
|
7
|
+
from ..utils.semantic_auditor import SemanticAuditor
|
|
8
|
+
|
|
9
|
+
CTX = load_context()
|
|
10
|
+
EXP_ID = CTX['meta'].get("experiment_id", "unknown")
|
|
11
|
+
|
|
12
|
+
def make_semantic_gate_asset(benchmark_asset_key):
|
|
13
|
+
"""
|
|
14
|
+
Creates a SemanticGate asset that validates the results of a benchmark.
|
|
15
|
+
"""
|
|
16
|
+
# The gate name should be semantic_gate_<benchmark_name>
|
|
17
|
+
benchmark_name = benchmark_asset_key.path[-1]
|
|
18
|
+
base_gate_name = f"semantic_gate_{benchmark_name.replace(f'e_{EXP_ID}__', '')}"
|
|
19
|
+
asset_name = get_scoped_asset_name(base_gate_name, EXP_ID)
|
|
20
|
+
|
|
21
|
+
@asset(
|
|
22
|
+
name=asset_name,
|
|
23
|
+
group_name="semantic_firewall",
|
|
24
|
+
partitions_def=partitions_def,
|
|
25
|
+
deps=[benchmark_asset_key],
|
|
26
|
+
description=f"Audits benchmark results for {benchmark_name} for hallucinations."
|
|
27
|
+
)
|
|
28
|
+
def _gate(context: AssetExecutionContext):
|
|
29
|
+
partition_key = context.partition_key
|
|
30
|
+
|
|
31
|
+
# 1. Locate the fragment produced by the dependency
|
|
32
|
+
# matches: benchmark_factory.py:30
|
|
33
|
+
from ..utils.common import load_context
|
|
34
|
+
ctx = load_context()
|
|
35
|
+
fragment_path = os.path.join(
|
|
36
|
+
RESULTS_DIR,
|
|
37
|
+
"fragments",
|
|
38
|
+
f"{benchmark_name}__{partition_key}.json"
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
if not os.path.exists(fragment_path):
|
|
42
|
+
context.log.warning(f"Fragment not found for audit: {fragment_path}")
|
|
43
|
+
return MaterializeResult(metadata={"status": "missing_fragment"})
|
|
44
|
+
|
|
45
|
+
# 2. Audit
|
|
46
|
+
with open(fragment_path, "r") as f:
|
|
47
|
+
data = json.load(f)
|
|
48
|
+
|
|
49
|
+
auditor = SemanticAuditor()
|
|
50
|
+
report = auditor.audit_fragment(data)
|
|
51
|
+
|
|
52
|
+
# 3. Handle Violations
|
|
53
|
+
if not report["success"]:
|
|
54
|
+
context.log.error(f"SEMANTIC HALLUCINATION DETECTED: {report['violations']}")
|
|
55
|
+
# In a real system, we might raise an error or mark it with a high-severity tag.
|
|
56
|
+
# For this PoC, we add metadata.
|
|
57
|
+
metadata = {
|
|
58
|
+
"audit_status": "FAILED",
|
|
59
|
+
"violations": MetadataValue.json(report["violations"]),
|
|
60
|
+
"fragment_path": MetadataValue.path(fragment_path)
|
|
61
|
+
}
|
|
62
|
+
else:
|
|
63
|
+
metadata = {
|
|
64
|
+
"audit_status": "PASSED",
|
|
65
|
+
"fragment_path": MetadataValue.path(fragment_path)
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return MaterializeResult(metadata=metadata)
|
|
69
|
+
|
|
70
|
+
return _gate
|
|
71
|
+
|
|
72
|
+
def get_semantic_gate_assets(benchmark_assets):
|
|
73
|
+
"""
|
|
74
|
+
Generates gate assets based on a list of benchmark assets.
|
|
75
|
+
"""
|
|
76
|
+
return [make_semantic_gate_asset(b.key) for b in benchmark_assets]
|
sql_benchmarks/cli.py
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
"""Command-line entry point for running experiments.
|
|
2
|
+
|
|
3
|
+
Exposed as the `sqlbench` console script (see pyproject.toml). The top-level
|
|
4
|
+
run_experiment.py is a thin shim over this so `./run.sh` keeps working.
|
|
5
|
+
"""
|
|
6
|
+
import os
|
|
7
|
+
import argparse
|
|
8
|
+
import sys
|
|
9
|
+
|
|
10
|
+
from .coordinator import ExperimentCoordinator
|
|
11
|
+
from .constants import EXPERIMENTS_DIR, EXPERIMENT_EXTENSIONS, PROCESSED_SUFFIX
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def _is_safe_path(path: str, base: str) -> bool:
|
|
15
|
+
"""Returns True only if path is within base (prevents directory traversal)."""
|
|
16
|
+
return os.path.realpath(path).startswith(os.path.realpath(base) + os.sep)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def resolve_targets(target_input: str) -> list:
|
|
20
|
+
"""Resolves input string to a list of YAML files."""
|
|
21
|
+
# Absolute or relative path to a specific file
|
|
22
|
+
if os.path.isfile(target_input):
|
|
23
|
+
abs_path = os.path.abspath(target_input)
|
|
24
|
+
# Must live within the experiments directory
|
|
25
|
+
if not _is_safe_path(abs_path, EXPERIMENTS_DIR):
|
|
26
|
+
print(f"[ERROR] Path '{target_input}' is outside the experiments directory.")
|
|
27
|
+
return []
|
|
28
|
+
return [abs_path]
|
|
29
|
+
|
|
30
|
+
# Symbolic name (e.g. "queue", "archive") — resolved relative to EXPERIMENTS_DIR only
|
|
31
|
+
symbolic_path = os.path.join(EXPERIMENTS_DIR, target_input)
|
|
32
|
+
if os.path.isdir(symbolic_path) and _is_safe_path(symbolic_path, EXPERIMENTS_DIR):
|
|
33
|
+
return sorted([
|
|
34
|
+
os.path.join(symbolic_path, f)
|
|
35
|
+
for f in os.listdir(symbolic_path)
|
|
36
|
+
if f.endswith(EXPERIMENT_EXTENSIONS) and not f.endswith(PROCESSED_SUFFIX)
|
|
37
|
+
])
|
|
38
|
+
|
|
39
|
+
return []
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def main():
|
|
43
|
+
parser = argparse.ArgumentParser(description="SQL Benchmarks Experiment Runner (Thin Wrapper)")
|
|
44
|
+
parser.add_argument("target", help="YAML file or directory (e.g., 'queue')")
|
|
45
|
+
parser.add_argument("--auto", action="store_true", help="Automated mode")
|
|
46
|
+
args = parser.parse_args()
|
|
47
|
+
|
|
48
|
+
targets = resolve_targets(args.target)
|
|
49
|
+
if not targets:
|
|
50
|
+
print(f"[ERROR] No valid targets found for '{args.target}'")
|
|
51
|
+
sys.exit(1)
|
|
52
|
+
|
|
53
|
+
print(f"[RUNNER] Processing {len(targets)} targets...")
|
|
54
|
+
|
|
55
|
+
overall_success = True
|
|
56
|
+
for target in targets:
|
|
57
|
+
print(f"\n>>> COORDINATING: {os.path.basename(target)}")
|
|
58
|
+
coordinator = ExperimentCoordinator(target, headless=args.auto)
|
|
59
|
+
if not coordinator.run():
|
|
60
|
+
overall_success = False
|
|
61
|
+
if not args.auto:
|
|
62
|
+
break # Stop on first failure in interactive mode
|
|
63
|
+
|
|
64
|
+
if not overall_success:
|
|
65
|
+
sys.exit(1)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
if __name__ == "__main__":
|
|
69
|
+
main()
|