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,184 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
|
|
3
|
+
def generate_sequence(rows: int, **kwargs):
|
|
4
|
+
start = kwargs.get("start", 1)
|
|
5
|
+
step = kwargs.get("step", 1)
|
|
6
|
+
return np.arange(start, start + (rows * step), step)
|
|
7
|
+
|
|
8
|
+
def generate_random_int(rows: int, **kwargs):
|
|
9
|
+
# Support both canonical (min_value) and YAML-style (min)
|
|
10
|
+
mn = kwargs.get("min_value") or kwargs.get("min")
|
|
11
|
+
mx = kwargs.get("max_value") or kwargs.get("max")
|
|
12
|
+
|
|
13
|
+
# Fallback to defaults if still None
|
|
14
|
+
if mn is None: mn = 0
|
|
15
|
+
if mx is None: mx = 100
|
|
16
|
+
return np.random.randint(mn, mx, size=rows)
|
|
17
|
+
|
|
18
|
+
def generate_random_float(rows: int, **kwargs):
|
|
19
|
+
# Support both canonical (min_value) and YAML-style (min)
|
|
20
|
+
mn = kwargs.get("min_value") or kwargs.get("min")
|
|
21
|
+
mx = kwargs.get("max_value") or kwargs.get("max")
|
|
22
|
+
|
|
23
|
+
# Fallback to defaults if still None
|
|
24
|
+
if mn is None: mn = 0.0
|
|
25
|
+
if mx is None: mx = 1.0
|
|
26
|
+
return np.random.uniform(mn, mx, size=rows)
|
|
27
|
+
|
|
28
|
+
def generate_choice(rows: int, **kwargs):
|
|
29
|
+
options = kwargs.get("options", [])
|
|
30
|
+
weights = kwargs.get("weights", None)
|
|
31
|
+
if not options:
|
|
32
|
+
raise ValueError("Provider 'choice' requires 'options' list.")
|
|
33
|
+
|
|
34
|
+
if weights is not None:
|
|
35
|
+
# Robust Normalization (Restore your fixed arithmetic)
|
|
36
|
+
w = np.array(weights, dtype=float)
|
|
37
|
+
if w.sum() <= 0:
|
|
38
|
+
raise ValueError("Weights must sum to a positive value.")
|
|
39
|
+
p = w / w.sum()
|
|
40
|
+
# Force strict 1.0 sum to satisfy numpy
|
|
41
|
+
if len(p) > 0:
|
|
42
|
+
p[-1] = 1.0 - p[:-1].sum()
|
|
43
|
+
else:
|
|
44
|
+
p = None
|
|
45
|
+
|
|
46
|
+
return np.random.choice(options, size=rows, p=p)
|
|
47
|
+
|
|
48
|
+
def generate_text_concat(rows: int, existing_data: dict, **kwargs):
|
|
49
|
+
source_col = kwargs.get("source")
|
|
50
|
+
prefix = kwargs.get("prefix", "")
|
|
51
|
+
|
|
52
|
+
if not source_col:
|
|
53
|
+
raise ValueError("Provider 'text_concat' requires 'source' column name.")
|
|
54
|
+
|
|
55
|
+
if source_col not in existing_data:
|
|
56
|
+
# Fallback if source hasn't been generated yet (should rely on column ordering)
|
|
57
|
+
# For now, just return prefixes to avoid crash, but warn?
|
|
58
|
+
# Ideally, we sort generation/topological sort columns.
|
|
59
|
+
# Assuming table definitions are ordered correctly by user.
|
|
60
|
+
return [prefix] * rows
|
|
61
|
+
|
|
62
|
+
return [f"{prefix}{x}" for x in existing_data[source_col]]
|
|
63
|
+
|
|
64
|
+
def generate_foreign_key(rows: int, table_name: str, **kwargs):
|
|
65
|
+
"""
|
|
66
|
+
Generates foreign keys.
|
|
67
|
+
Supports distributions:
|
|
68
|
+
- 'uniform' (Default): Random parent. Avg Depth ~ O(log N).
|
|
69
|
+
- 'chain': Linked List structure (parent = id - 1). Depth = N.
|
|
70
|
+
- 'zipf': Power law. Wide tree. Most nodes point to top few IDs.
|
|
71
|
+
"""
|
|
72
|
+
target_rows = kwargs.get("target_rows")
|
|
73
|
+
distribution = kwargs.get("distribution", "uniform")
|
|
74
|
+
|
|
75
|
+
# Range of valid IDs
|
|
76
|
+
mn = 1
|
|
77
|
+
# If target_rows is set, use it. Else assume self-reference up to current rows.
|
|
78
|
+
mx = (target_rows if target_rows else rows) + 1
|
|
79
|
+
|
|
80
|
+
if distribution == "chain":
|
|
81
|
+
# Point to previous ID. ID 1 points to NULL (represnted as 0 or 1? Let's say 1 to be valid FK)
|
|
82
|
+
# Actually standard FK usually allows NULL. But our schema might be strict int64.
|
|
83
|
+
# Let's point root to itself (id=1 -> parent=1) or make it a forest.
|
|
84
|
+
# For simplicity: parent_id = max(1, id - 1)
|
|
85
|
+
# But we generate an array of *values*. We don't know the current ID *per row* easily unless we assume row_idx+1 = ID.
|
|
86
|
+
# Yes, declarative_gen assumes sequence ID 1..N.
|
|
87
|
+
ids = np.arange(1, rows + 1)
|
|
88
|
+
parents = ids - 1
|
|
89
|
+
parents[0] = 1 # Root points to self? Or we need NULL support.
|
|
90
|
+
# If we return ints, we can't have None.
|
|
91
|
+
# Sticking to valid ID range [1, mx).
|
|
92
|
+
parents = np.maximum(1, parents)
|
|
93
|
+
return parents
|
|
94
|
+
|
|
95
|
+
elif distribution == "zipf":
|
|
96
|
+
# numpy zipf is z ~ 1/k^a. Returns ints >= 1.
|
|
97
|
+
# We need to map this to valid ID range.
|
|
98
|
+
# A common trick: parent_id = 1 + (zipf sample % mx)
|
|
99
|
+
# But we want frequent *low* IDs (1, 2, 3 as roots).
|
|
100
|
+
# Zipf(a=2) generates many 1s.
|
|
101
|
+
a = kwargs.get("zipf_a", 2.0)
|
|
102
|
+
samples = np.random.zipf(a, size=rows)
|
|
103
|
+
# Map samples to range [1, mx-1] using modulo?
|
|
104
|
+
# Better: min(samples, mx-1) to preserve frequency of 1.
|
|
105
|
+
parents = np.minimum(samples, mx - 1)
|
|
106
|
+
return parents
|
|
107
|
+
|
|
108
|
+
else: # uniform
|
|
109
|
+
return np.random.randint(mn, mx, size=rows)
|
|
110
|
+
|
|
111
|
+
def generate_zipf_edges(rows: int, **kwargs):
|
|
112
|
+
"""
|
|
113
|
+
Generate a directed edge list for a Zipf-distributed supply graph.
|
|
114
|
+
|
|
115
|
+
Each row represents one directed supply edge: (from_id, to_id).
|
|
116
|
+
The number of *outgoing* edges per node follows a power-law / Zipf
|
|
117
|
+
distribution controlled by the ``zipf_a`` parameter:
|
|
118
|
+
|
|
119
|
+
- zipf_a=2.0 → moderate skew (a few large hubs, long tail of leaves)
|
|
120
|
+
- zipf_a=1.5 → heavy skew (very dominant hubs)
|
|
121
|
+
- zipf_a=3.0 → mild skew (more uniform, closer to random graph)
|
|
122
|
+
|
|
123
|
+
Self-loops are excluded. The resulting edge list is what gets loaded as
|
|
124
|
+
the ``supplies`` relation in TypeDB and as the ``supplies`` table in DuckDB.
|
|
125
|
+
|
|
126
|
+
Columns produced: ``from_id`` (integer), ``to_id`` (integer).
|
|
127
|
+
|
|
128
|
+
Note: ``rows`` here is the *total number of edges*, not nodes. The number
|
|
129
|
+
of nodes is controlled by the ``n_nodes`` kwarg (default 500).
|
|
130
|
+
"""
|
|
131
|
+
n_nodes = int(kwargs.get("n_nodes", 500))
|
|
132
|
+
zipf_a = float(kwargs.get("zipf_a", 2.0))
|
|
133
|
+
seed = kwargs.get("seed", 42)
|
|
134
|
+
rng = np.random.default_rng(seed)
|
|
135
|
+
|
|
136
|
+
# --- 1. Assign each node an out-degree drawn from Zipf ---
|
|
137
|
+
# numpy's Zipf returns values >= 1. Cap at n_nodes-1 so a single node
|
|
138
|
+
# can't supply more companies than exist.
|
|
139
|
+
raw_degrees = rng.zipf(zipf_a, size=n_nodes)
|
|
140
|
+
out_degrees = np.minimum(raw_degrees, n_nodes - 1).astype(int)
|
|
141
|
+
|
|
142
|
+
# --- 2. Build edge list ---
|
|
143
|
+
# For each source node, sample ``out_degree`` distinct target nodes
|
|
144
|
+
# (excluding itself). Collect until we have exactly ``rows`` edges,
|
|
145
|
+
# cycling through nodes if needed.
|
|
146
|
+
from_ids = []
|
|
147
|
+
to_ids = []
|
|
148
|
+
node_ids = np.arange(1, n_nodes + 1)
|
|
149
|
+
|
|
150
|
+
for src_idx, deg in enumerate(out_degrees):
|
|
151
|
+
if len(from_ids) >= rows:
|
|
152
|
+
break
|
|
153
|
+
src_id = src_idx + 1 # 1-indexed
|
|
154
|
+
targets = node_ids[node_ids != src_id]
|
|
155
|
+
chosen = rng.choice(targets, size=min(deg, len(targets)), replace=False)
|
|
156
|
+
remaining = rows - len(from_ids)
|
|
157
|
+
chosen = chosen[:remaining]
|
|
158
|
+
from_ids.extend([src_id] * len(chosen))
|
|
159
|
+
to_ids.extend(chosen.tolist())
|
|
160
|
+
|
|
161
|
+
# Pad to exact row count if the degree distribution ran out early
|
|
162
|
+
while len(from_ids) < rows:
|
|
163
|
+
src = rng.integers(1, n_nodes + 1)
|
|
164
|
+
tgt = rng.integers(1, n_nodes + 1)
|
|
165
|
+
if src != tgt:
|
|
166
|
+
from_ids.append(int(src))
|
|
167
|
+
to_ids.append(int(tgt))
|
|
168
|
+
|
|
169
|
+
return {
|
|
170
|
+
"from_id": np.array(from_ids[:rows], dtype=np.int64),
|
|
171
|
+
"to_id": np.array(to_ids[:rows], dtype=np.int64),
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
PROVIDER_REGISTRY = {
|
|
176
|
+
"sequence": generate_sequence,
|
|
177
|
+
"random_int": generate_random_int,
|
|
178
|
+
"random_float": generate_random_float,
|
|
179
|
+
"choice": generate_choice,
|
|
180
|
+
"text_concat": generate_text_concat,
|
|
181
|
+
"foreign_key": generate_foreign_key,
|
|
182
|
+
"foreign key": generate_foreign_key, # Alias
|
|
183
|
+
"zipf_edges": generate_zipf_edges,
|
|
184
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Scaling-law analysis: fit each engine's duration to a power law t = a · N^alpha.
|
|
3
|
+
|
|
4
|
+
The exponent alpha is an engine's complexity class for the workload: ~0 is
|
|
5
|
+
flat, 0.5 is O(sqrt N), 1 is linear. Two engines sharing an alpha but separated
|
|
6
|
+
by a constant factor are the same algorithm at different fixed cost; a larger
|
|
7
|
+
alpha is a fundamentally worse scaling regime.
|
|
8
|
+
|
|
9
|
+
Single source of truth for both the CLI (scripts/tools/analyze_scaling.py) and
|
|
10
|
+
the reporting asset, which writes scaling.json into every capsule that has a
|
|
11
|
+
row-scaled matrix.
|
|
12
|
+
"""
|
|
13
|
+
import glob
|
|
14
|
+
import json
|
|
15
|
+
import math
|
|
16
|
+
import os
|
|
17
|
+
import statistics
|
|
18
|
+
from typing import Dict, Optional
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def power_law(points: Dict[int, float]):
|
|
22
|
+
"""Least-squares fit log10(t) ~ alpha*log10(N) + log10(a). Returns (alpha, r2)."""
|
|
23
|
+
xs = [math.log10(n) for n in points]
|
|
24
|
+
ys = [math.log10(points[n]) for n in points]
|
|
25
|
+
k = len(xs)
|
|
26
|
+
mx, my = sum(xs) / k, sum(ys) / k
|
|
27
|
+
sxx = sum((x - mx) ** 2 for x in xs)
|
|
28
|
+
if sxx == 0:
|
|
29
|
+
return 0.0, 1.0
|
|
30
|
+
alpha = sum((xs[i] - mx) * (ys[i] - my) for i in range(k)) / sxx
|
|
31
|
+
b = my - alpha * mx
|
|
32
|
+
ss_res = sum((ys[i] - (alpha * xs[i] + b)) ** 2 for i in range(k))
|
|
33
|
+
ss_tot = sum((y - my) ** 2 for y in ys)
|
|
34
|
+
r2 = 1 - ss_res / ss_tot if ss_tot else 1.0
|
|
35
|
+
return alpha, r2
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def regime(alpha: float) -> str:
|
|
39
|
+
if alpha < 0.15: return "near-constant"
|
|
40
|
+
if alpha < 0.45: return "sublinear"
|
|
41
|
+
if alpha < 0.65: return "~O(sqrt N)"
|
|
42
|
+
if alpha < 1.25: return "~linear"
|
|
43
|
+
return "superlinear"
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _points_from_fragments(results_dir: str, exp_id: str) -> Dict[str, Dict[int, float]]:
|
|
47
|
+
"""engine -> {rows: median_duration_seconds}, read from the capsule's raw fragments."""
|
|
48
|
+
series: Dict[str, Dict[int, list]] = {}
|
|
49
|
+
for path in glob.glob(os.path.join(results_dir, exp_id, "fragments", "*.json")):
|
|
50
|
+
with open(path) as f:
|
|
51
|
+
data = json.load(f)
|
|
52
|
+
params = data.get("parameters", {})
|
|
53
|
+
if "rows" not in params:
|
|
54
|
+
continue # needs a numeric row axis (TPC-H uses scale_factor, skip)
|
|
55
|
+
metrics = data.get("metrics", {})
|
|
56
|
+
raw = metrics.get("durations_raw") or (
|
|
57
|
+
[metrics["duration_seconds"]] if metrics.get("duration_seconds") is not None else []
|
|
58
|
+
)
|
|
59
|
+
if not raw:
|
|
60
|
+
continue # DNF
|
|
61
|
+
engine = data["meta"]["engine"]
|
|
62
|
+
series.setdefault(engine, {}).setdefault(int(params["rows"]), []).extend(raw)
|
|
63
|
+
return {e: {n: statistics.median(v) for n, v in by_rows.items()} for e, by_rows in series.items()}
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def analyze_capsule(results_dir: str, exp_id: str) -> Optional[dict]:
|
|
67
|
+
"""
|
|
68
|
+
Returns a scaling report for a capsule, or None if it has no row-scaled
|
|
69
|
+
timings (e.g. a single-scale or scale_factor-only experiment).
|
|
70
|
+
|
|
71
|
+
{engine: {alpha, r2, growth, n_points, regime, points: {rows: seconds}}}
|
|
72
|
+
"""
|
|
73
|
+
series = _points_from_fragments(results_dir, exp_id)
|
|
74
|
+
report = {}
|
|
75
|
+
for engine, pts in series.items():
|
|
76
|
+
if len(pts) < 2:
|
|
77
|
+
continue # a power law needs at least two scale points
|
|
78
|
+
alpha, r2 = power_law(pts)
|
|
79
|
+
lo, hi = min(pts), max(pts)
|
|
80
|
+
report[engine] = {
|
|
81
|
+
"alpha": round(alpha, 4),
|
|
82
|
+
"r2": round(r2, 4),
|
|
83
|
+
"growth": round(pts[hi] / pts[lo], 3),
|
|
84
|
+
"n_points": len(pts),
|
|
85
|
+
"regime": regime(alpha),
|
|
86
|
+
"points": pts,
|
|
87
|
+
}
|
|
88
|
+
return report or None
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
from pydantic import BaseModel, ConfigDict
|
|
2
|
+
from typing import List, Dict, Optional, Union, Any
|
|
3
|
+
import warnings
|
|
4
|
+
|
|
5
|
+
# Temporarily silence Dagster's Pydantic V2 Deprecation Warning
|
|
6
|
+
# Since this file is now fully V2, this filter might be unnecessary,
|
|
7
|
+
# but it's kept here in case the test runner needs it.
|
|
8
|
+
warnings.filterwarnings(
|
|
9
|
+
"ignore",
|
|
10
|
+
category=DeprecationWarning,
|
|
11
|
+
message="Support for class-based `config` is deprecated"
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
# ==========================================
|
|
15
|
+
# 1. TABLE & COLUMN DEFINITIONS (Synthetic)
|
|
16
|
+
# ==========================================
|
|
17
|
+
class ColumnDef(BaseModel):
|
|
18
|
+
name: str
|
|
19
|
+
provider: str # e.g., "sequence", "choice", "foreign_key"
|
|
20
|
+
|
|
21
|
+
# Optional parameters specific to providers
|
|
22
|
+
options: Optional[List[Any]] = None
|
|
23
|
+
weights: Optional[List[float]] = None
|
|
24
|
+
target_table: Optional[str] = None
|
|
25
|
+
target_column: Optional[str] = None
|
|
26
|
+
min_value: Optional[Union[int, float]] = None
|
|
27
|
+
max_value: Optional[Union[int, float]] = None
|
|
28
|
+
|
|
29
|
+
# Nullability Control
|
|
30
|
+
null_probability: Union[float, str] = 0.0 # Default 0.0 (No NULLs)
|
|
31
|
+
|
|
32
|
+
# Type override for DB DDL (e.g., "double precision")
|
|
33
|
+
type: Optional[str] = None
|
|
34
|
+
|
|
35
|
+
model_config = ConfigDict(extra='allow')
|
|
36
|
+
|
|
37
|
+
class IndexDef(BaseModel):
|
|
38
|
+
columns: List[str]
|
|
39
|
+
name: Optional[str] = None
|
|
40
|
+
unique: bool = False
|
|
41
|
+
method: str = "btree"
|
|
42
|
+
|
|
43
|
+
class TableDef(BaseModel):
|
|
44
|
+
# 'rows' can be an integer literal OR a variable name reference (string)
|
|
45
|
+
rows: Optional[Union[int, str]] = None
|
|
46
|
+
columns: Optional[List[ColumnDef]] = None
|
|
47
|
+
indexes: Optional[List[IndexDef]] = []
|
|
48
|
+
|
|
49
|
+
# --- V2 MIGRATION: Replaces class Config: extra = 'allow' ---
|
|
50
|
+
model_config = ConfigDict(extra='allow')
|
|
51
|
+
|
|
52
|
+
# ==========================================
|
|
53
|
+
# 2. DATASET CONFIGURATION (Polymorphic)
|
|
54
|
+
# ==========================================
|
|
55
|
+
class DatasetConfig(BaseModel):
|
|
56
|
+
model_config = ConfigDict(extra='allow')
|
|
57
|
+
source: Optional[str] = None
|
|
58
|
+
tables: Optional[Dict[str, Any]] = None
|
|
59
|
+
paths: Optional[Dict[str, str]] = None
|
|
60
|
+
# Base seed for synthetic data generation (declarative_gen). Default 42.
|
|
61
|
+
# The config is hashed, so changing the seed changes the Experiment ID —
|
|
62
|
+
# two experiments with different data can never share an identity.
|
|
63
|
+
seed: Optional[int] = None
|
|
64
|
+
|
|
65
|
+
# ==========================================
|
|
66
|
+
# 3. EXECUTION CONFIGURATION (The Matrix)
|
|
67
|
+
# ==========================================
|
|
68
|
+
class ExecutionConfig(BaseModel):
|
|
69
|
+
model_config = ConfigDict(extra='allow')
|
|
70
|
+
|
|
71
|
+
engines: List[str]
|
|
72
|
+
|
|
73
|
+
# Optional
|
|
74
|
+
test_suite: Optional[str] = None
|
|
75
|
+
replication: int = 1
|
|
76
|
+
|
|
77
|
+
# Per-engine tuning, namespaced by engine name. Each engine receives ONLY
|
|
78
|
+
# its own namespace at run time and owns the vocabulary inside it.
|
|
79
|
+
# Reserved namespaces: postgres, duckdb, actian, typedb, quack.
|
|
80
|
+
# engine_params:
|
|
81
|
+
# postgres: {work_mem: "64MB", random_page_cost: 1.1}
|
|
82
|
+
# duckdb: {memory_limit: "1GB", threads: 4}
|
|
83
|
+
# To VARY an engine param across partitions, declare it as a namespaced
|
|
84
|
+
# matrix dimension instead: matrix: {postgres.work_mem: [4MB, 1GB]}
|
|
85
|
+
engine_params: Optional[Dict[str, Dict[str, Any]]] = None
|
|
86
|
+
|
|
87
|
+
# The Matrix (can be named matrix or dimensions)
|
|
88
|
+
matrix: Optional[Dict[str, List[Any]]] = None
|
|
89
|
+
dimensions: Optional[Dict[str, List[Any]]] = None
|
|
90
|
+
|
|
91
|
+
# ==========================================
|
|
92
|
+
# 4. ROOT CONFIGURATION
|
|
93
|
+
# ==========================================
|
|
94
|
+
class MetaInfo(BaseModel):
|
|
95
|
+
# --- V2 MIGRATION: Replaces class Config: extra = 'allow' ---
|
|
96
|
+
model_config = ConfigDict(extra='allow')
|
|
97
|
+
# experiment_id is computed by the system (SHA-256 hash). Never set manually.
|
|
98
|
+
name: Optional[str] = None # Short human-readable identifier, e.g. "Sort Spill Cliff"
|
|
99
|
+
description: Optional[str] = None # Prose description of the hypothesis being tested
|
|
100
|
+
|
|
101
|
+
class ExperimentSchema(BaseModel):
|
|
102
|
+
meta: Optional[Dict[str, Optional[str]]] = None
|
|
103
|
+
dataset: Optional[DatasetConfig] = None
|
|
104
|
+
execution: Optional[ExecutionConfig] = None
|
|
105
|
+
|
|
106
|
+
# ==========================================
|
|
107
|
+
# 5. PUBLIC INTERFACE
|
|
108
|
+
# ==========================================
|
|
109
|
+
def validate_yaml_content(config_dict: dict) -> ExperimentSchema:
|
|
110
|
+
"""
|
|
111
|
+
Validates a raw dictionary against the V7 Schema.
|
|
112
|
+
Raises pydantic.ValidationError if the contract is violated.
|
|
113
|
+
"""
|
|
114
|
+
return ExperimentSchema(**config_dict)
|
|
115
|
+
|
|
116
|
+
def get_json_schema():
|
|
117
|
+
"""Returns the JSON Schema for Agents/LLMs."""
|
|
118
|
+
return ExperimentSchema.model_json_schema()
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import json
|
|
3
|
+
import pandas as pd
|
|
4
|
+
from typing import List, Dict, Any
|
|
5
|
+
|
|
6
|
+
class OntologyRegistry:
|
|
7
|
+
"""
|
|
8
|
+
Placeholder for Proof-of-Concept.
|
|
9
|
+
In the future, this would be a full business logic engine.
|
|
10
|
+
"""
|
|
11
|
+
def __init__(self):
|
|
12
|
+
# Example constraints
|
|
13
|
+
self.constraints = {
|
|
14
|
+
"total_count": {"min": 0},
|
|
15
|
+
"avg_value": {"min": 0}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
def validate_row(self, row: Dict[str, Any]) -> List[str]:
|
|
19
|
+
violations = []
|
|
20
|
+
for key, val in row.items():
|
|
21
|
+
if key in self.constraints:
|
|
22
|
+
limit = self.constraints[key].get("min")
|
|
23
|
+
if limit is not None and val < limit:
|
|
24
|
+
violations.append(f"Value {val} for {key} is below minimum {limit}")
|
|
25
|
+
return violations
|
|
26
|
+
|
|
27
|
+
class SemanticAuditor:
|
|
28
|
+
"""
|
|
29
|
+
Audits result sets for semantic hallucinations or business logic violations.
|
|
30
|
+
"""
|
|
31
|
+
def __init__(self):
|
|
32
|
+
self.registry = OntologyRegistry()
|
|
33
|
+
|
|
34
|
+
def audit_csv(self, csv_path: str) -> Dict[str, Any]:
|
|
35
|
+
"""
|
|
36
|
+
Reads a CSV and checks for violations.
|
|
37
|
+
"""
|
|
38
|
+
if not os.path.exists(csv_path):
|
|
39
|
+
return {"success": False, "error": f"File not found: {csv_path}"}
|
|
40
|
+
|
|
41
|
+
try:
|
|
42
|
+
df = pd.read_csv(csv_path)
|
|
43
|
+
except Exception as e:
|
|
44
|
+
return {"success": False, "error": f"Failed to read CSV: {e}"}
|
|
45
|
+
|
|
46
|
+
all_violations = []
|
|
47
|
+
for idx, row in df.iterrows():
|
|
48
|
+
row_violations = self.registry.validate_row(row.to_dict())
|
|
49
|
+
if row_violations:
|
|
50
|
+
all_violations.append({
|
|
51
|
+
"row_index": idx,
|
|
52
|
+
"violations": row_violations
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
return {
|
|
56
|
+
"success": len(all_violations) == 0,
|
|
57
|
+
"violations": all_violations,
|
|
58
|
+
"row_count": len(df)
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
def audit_fragment(self, fragment_data: Dict[str, Any]) -> Dict[str, Any]:
|
|
62
|
+
"""
|
|
63
|
+
Audits a single benchmark result fragment (JSON).
|
|
64
|
+
"""
|
|
65
|
+
metrics = fragment_data.get("metrics", {})
|
|
66
|
+
parameters = fragment_data.get("parameters", {})
|
|
67
|
+
|
|
68
|
+
# Flatten metrics and parameters for validation
|
|
69
|
+
flat_data = {**metrics, **parameters}
|
|
70
|
+
|
|
71
|
+
violations = self.registry.validate_row(flat_data)
|
|
72
|
+
|
|
73
|
+
# Additional logic: Timing hallucinations
|
|
74
|
+
duration = metrics.get("duration_seconds")
|
|
75
|
+
if duration is not None and duration < 0:
|
|
76
|
+
violations.append(f"Negative duration detected: {duration}")
|
|
77
|
+
|
|
78
|
+
return {
|
|
79
|
+
"success": len(violations) == 0,
|
|
80
|
+
"violations": violations
|
|
81
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import mmap
|
|
3
|
+
import platform
|
|
4
|
+
import sys
|
|
5
|
+
import psutil
|
|
6
|
+
import logging
|
|
7
|
+
|
|
8
|
+
# Use the dagster logger so these events show up in the UI
|
|
9
|
+
logger = logging.getLogger("dagster")
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def capture_environment() -> dict:
|
|
13
|
+
"""
|
|
14
|
+
Records the runtime CONDITIONS of an experiment for the result capsule.
|
|
15
|
+
|
|
16
|
+
The Experiment ID fingerprints the question (config + SQL + code); this
|
|
17
|
+
block records the lab bench it was answered on. Same ID on different
|
|
18
|
+
hardware or engine versions = same question, distinct observation.
|
|
19
|
+
"""
|
|
20
|
+
import duckdb
|
|
21
|
+
import dagster
|
|
22
|
+
|
|
23
|
+
env = {
|
|
24
|
+
"python": platform.python_version(),
|
|
25
|
+
"duckdb": duckdb.__version__,
|
|
26
|
+
"dagster": dagster.__version__,
|
|
27
|
+
"os": f"{platform.system()} {platform.release()}",
|
|
28
|
+
"machine": platform.machine(),
|
|
29
|
+
"cpu_count_logical": psutil.cpu_count(logical=True),
|
|
30
|
+
"cpu_count_physical": psutil.cpu_count(logical=False),
|
|
31
|
+
"ram_total_gb": round(psutil.virtual_memory().total / (1024 ** 3), 1),
|
|
32
|
+
}
|
|
33
|
+
try:
|
|
34
|
+
import polars
|
|
35
|
+
env["polars"] = polars.__version__
|
|
36
|
+
except ImportError:
|
|
37
|
+
pass
|
|
38
|
+
return env
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def generator_id() -> str:
|
|
42
|
+
"""
|
|
43
|
+
The capsule's maker's mark + the exact code revision that produced it —
|
|
44
|
+
a SLSA-style 'builder' identity. Stamped into every capsule's metadata so
|
|
45
|
+
each artifact says which tool, at which build, generated it.
|
|
46
|
+
Falls back to the bare slug outside a git checkout.
|
|
47
|
+
"""
|
|
48
|
+
import subprocess
|
|
49
|
+
from ..constants import LAB_SLUG, ROOT_DIR
|
|
50
|
+
try:
|
|
51
|
+
sha = subprocess.run(
|
|
52
|
+
["git", "rev-parse", "--short", "HEAD"],
|
|
53
|
+
cwd=ROOT_DIR, capture_output=True, text=True, timeout=5,
|
|
54
|
+
).stdout.strip()
|
|
55
|
+
return f"{LAB_SLUG}@{sha}" if sha else LAB_SLUG
|
|
56
|
+
except Exception:
|
|
57
|
+
return LAB_SLUG
|
|
58
|
+
|
|
59
|
+
def thrash_os_cache(override_gb=None):
|
|
60
|
+
"""
|
|
61
|
+
Forces OS Page Cache eviction by writing to a memory-mapped file
|
|
62
|
+
larger than available RAM.
|
|
63
|
+
|
|
64
|
+
Args:
|
|
65
|
+
override_gb (float): Optional. Force a specific flood size.
|
|
66
|
+
If None, auto-detects Total RAM + 20%.
|
|
67
|
+
"""
|
|
68
|
+
try:
|
|
69
|
+
# 1. AUTO-DETECT RAM (Safety First)
|
|
70
|
+
if os.getenv("SB_SILICON_SAFE") == "1":
|
|
71
|
+
target_gb = 0.1 # Minimal thrash for flow verification
|
|
72
|
+
log_msg = "SILICON SAFE MODE: Minimal Cache Thrash (100MB)"
|
|
73
|
+
elif override_gb:
|
|
74
|
+
target_gb = float(override_gb)
|
|
75
|
+
log_msg = f"Manual Flood: {target_gb} GB"
|
|
76
|
+
else:
|
|
77
|
+
# We use 'available' memory to stay within system limits
|
|
78
|
+
available_gb = psutil.virtual_memory().available / (1024 ** 3)
|
|
79
|
+
# Cap at 50% of available OR 4GB, whichever is smaller
|
|
80
|
+
target_gb = min(available_gb * 0.5, 4.0)
|
|
81
|
+
log_msg = f"Auto-Flood: {target_gb:.2f} GB (Available: {available_gb:.2f} GB)"
|
|
82
|
+
|
|
83
|
+
logger.info(log_msg)
|
|
84
|
+
|
|
85
|
+
# 2. FAST FLOOD (MMAP)
|
|
86
|
+
if target_gb <= 0: return # Skip if negative or zero
|
|
87
|
+
|
|
88
|
+
size_bytes = int(target_gb * 1024 * 1024 * 1024)
|
|
89
|
+
|
|
90
|
+
# Anonymous map (-1) and context manager to ensure it closes
|
|
91
|
+
with mmap.mmap(-1, size_bytes) as mm:
|
|
92
|
+
page_size = 4096
|
|
93
|
+
# Dirty the pages (Stride to be faster and less intensive)
|
|
94
|
+
for i in range(0, size_bytes, page_size * 4):
|
|
95
|
+
mm[i] = 1
|
|
96
|
+
|
|
97
|
+
logger.info("OS Cache Thrashed.")
|
|
98
|
+
|
|
99
|
+
except Exception as e:
|
|
100
|
+
logger.warning(f"Cache thrash failed: {e}")
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import os
|
|
2
|
+
from .utils.schema import validate_yaml_content
|
|
3
|
+
from .utils.semantic_auditor import OntologyRegistry
|
|
4
|
+
|
|
5
|
+
class ExperimentValidator:
|
|
6
|
+
"""
|
|
7
|
+
Ensures the experiment "Contract" is strictly valid before any execution.
|
|
8
|
+
Combines Pydantic schema validation with business logic rules.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
@staticmethod
|
|
12
|
+
def validate(config_dict: dict, source_label: str = "config"):
|
|
13
|
+
# 1. Structural & Static Schema Validation
|
|
14
|
+
try:
|
|
15
|
+
validate_yaml_content(config_dict)
|
|
16
|
+
except Exception as e:
|
|
17
|
+
raise ValueError(f"SCHEMA ERROR in {source_label}:\n{e}")
|
|
18
|
+
|
|
19
|
+
# 2. Logic & Ontology Validation
|
|
20
|
+
execution = config_dict.get("execution", {})
|
|
21
|
+
matrix = execution.get("matrix") or execution.get("dimensions") or {}
|
|
22
|
+
|
|
23
|
+
# A. Matrix Validation (Numeric constraints)
|
|
24
|
+
if "rows" in matrix:
|
|
25
|
+
for r in matrix["rows"]:
|
|
26
|
+
if isinstance(r, (int, float)) and r < 0:
|
|
27
|
+
raise ValueError(f"Negative value {r} not allowed in 'rows'")
|
|
28
|
+
|
|
29
|
+
if "selectivity" in matrix:
|
|
30
|
+
for s in matrix["selectivity"]:
|
|
31
|
+
if isinstance(s, (int, float)) and (s < 0 or s > 1):
|
|
32
|
+
raise ValueError(f"Selectivity {s} for 'selectivity' must be between 0 and 1")
|
|
33
|
+
|
|
34
|
+
dataset = config_dict.get("dataset", {})
|
|
35
|
+
tables = dataset.get("tables", {})
|
|
36
|
+
|
|
37
|
+
if isinstance(tables, dict):
|
|
38
|
+
# B. Stats Validation (Weights)
|
|
39
|
+
for table_name, table_def in tables.items():
|
|
40
|
+
if not isinstance(table_def, dict): continue
|
|
41
|
+
for col in table_def.get("columns", []):
|
|
42
|
+
if "weights" in col:
|
|
43
|
+
weights = col["weights"]
|
|
44
|
+
if any(w < 0 for w in weights):
|
|
45
|
+
raise ValueError(f"LOGIC ERROR: Negative weight in {table_name}.{col['name']}")
|
|
46
|
+
if sum(weights) <= 0:
|
|
47
|
+
raise ValueError(f"LOGIC ERROR: Weights sum to zero or less in {table_name}.{col['name']}")
|
|
48
|
+
|
|
49
|
+
# C. Integrity Validation (Foreign Keys)
|
|
50
|
+
defined_tables = set(tables.keys())
|
|
51
|
+
for table_name, table_def in tables.items():
|
|
52
|
+
if not isinstance(table_def, dict): continue
|
|
53
|
+
for col in table_def.get("columns", []):
|
|
54
|
+
if col.get("provider") == "foreign_key":
|
|
55
|
+
target = col.get("target_table")
|
|
56
|
+
if target not in defined_tables:
|
|
57
|
+
raise ValueError(f"Broken FK in '{table_name}.{col['name']}'. Target table '{target}' not defined")
|
|
58
|
+
|
|
59
|
+
print(f"[SUCCESS] Contract '{source_label}' validated.")
|
|
60
|
+
return True
|