sqlbenchdag 0.1.0__py3-none-any.whl

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