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,84 @@
1
+ import os
2
+ import shutil
3
+ import tempfile
4
+ import time
5
+ from .constants import ROOT_DIR, DATA_DIR, RESULTS_DIR, VIOLATIONS_DIR, REPORTS_DIR
6
+ from .utils.integrity_monitor import IntegrityMonitor
7
+
8
+ class IsolationHarness:
9
+ """
10
+ Provides a 'Clean Room' for experiment execution.
11
+ Redirects all file outputs to a scratchpad to prevent state contamination.
12
+ """
13
+
14
+ def __init__(self, experiment_id: str):
15
+ self.experiment_id = experiment_id
16
+ self.scratchpad_root = None
17
+ self._monitor = None
18
+
19
+ def provision(self) -> dict:
20
+ """Creates the scratchpad and returns the redirection environment."""
21
+ self.scratchpad_root = tempfile.mkdtemp(prefix=f"sb_{self.experiment_id}_")
22
+
23
+ # Create standard layout inside scratchpad
24
+ paths = {
25
+ "SB_DATA_DIR": os.path.join(self.scratchpad_root, "data"),
26
+ "SB_RESULTS_DIR": os.path.join(self.scratchpad_root, "results"),
27
+ "SB_VIOLATIONS_DIR": os.path.join(self.scratchpad_root, "violations"),
28
+ "SB_REPORTS_DIR": os.path.join(self.scratchpad_root, "reports"),
29
+ "SCRATCHPAD_ROOT": self.scratchpad_root
30
+ }
31
+
32
+ for p in paths.values():
33
+ os.makedirs(p, exist_ok=True)
34
+
35
+ # Ensure critical subdirectories exist
36
+ os.makedirs(os.path.join(paths["SB_DATA_DIR"], "staging"), exist_ok=True)
37
+ os.makedirs(os.path.join(paths["SB_DATA_DIR"], "duckdb"), exist_ok=True)
38
+ os.makedirs(os.path.join(paths["SB_RESULTS_DIR"], "fragments"), exist_ok=True)
39
+
40
+ # Snapshot the package source NOW: if any code changes while the
41
+ # experiment runs, the Experiment ID no longer describes what ran.
42
+ from .constants import PACKAGE_DIR
43
+ self._monitor = IntegrityMonitor(PACKAGE_DIR)
44
+
45
+ return paths
46
+
47
+ def check_integrity(self) -> list:
48
+ """
49
+ Compares the package source against the snapshot taken at provision().
50
+ Returns drift entries (MODIFIED/ADDED/DELETED) — any code change during
51
+ an experiment invalidates the run, because the Experiment ID was
52
+ computed from the code as it stood at submission.
53
+
54
+ History note: a previous implementation here only inspected a
55
+ sentinel file named secure_pill.txt (planted by its own test) and
56
+ could never detect real tampering. Replaced with the IntegrityMonitor
57
+ snapshot mechanism this docstring's promises always implied.
58
+ """
59
+ if self._monitor is None:
60
+ return []
61
+ return self._monitor.check_drift()
62
+
63
+ def cleanup(self):
64
+ """Removes the scratchpad — and ONLY a scratchpad.
65
+
66
+ Guarded after a historical incident: a miscomputed path turned a
67
+ cleanup into a recursive deletion of real work ("brilliant
68
+ amnesia"). rmtree here refuses any path that is not a genuine
69
+ sb_-prefixed directory under the system temp dir. Refusal is loud;
70
+ a leaked scratchpad is recoverable, a deleted repo is not.
71
+ """
72
+ if not self.scratchpad_root:
73
+ return
74
+ real = os.path.realpath(self.scratchpad_root)
75
+ tmp_root = os.path.realpath(tempfile.gettempdir())
76
+ if not (real.startswith(tmp_root + os.sep)
77
+ and os.path.basename(real).startswith("sb_")):
78
+ print(f"[CRITICAL] cleanup() refused: '{real}' is not a temp scratchpad. "
79
+ f"Leaving it untouched.")
80
+ return
81
+ if os.path.exists(real):
82
+ # Wait a beat for any lagging file handles
83
+ time.sleep(0.1)
84
+ shutil.rmtree(real)
sql_benchmarks/jobs.py ADDED
@@ -0,0 +1,8 @@
1
+ from dagster import define_asset_job, AssetSelection
2
+
3
+ # Defines a job that selects ALL assets (Ingestion -> Benchmarks)
4
+ # This is what the Sensor triggers when it sees a new YAML file.
5
+ benchmark_job = define_asset_job(
6
+ name="benchmark_job",
7
+ selection=AssetSelection.all()
8
+ )
@@ -0,0 +1,17 @@
1
+ from dagster import StaticPartitionsDefinition
2
+ from .config_loader import ConfigLoader
3
+
4
+ # Initialize the Configuration Compiler
5
+ try:
6
+ COMPILER = ConfigLoader()
7
+ except ValueError as e:
8
+ # If the compiler raises a structural error, we raise it here to fail hard at load time.
9
+ raise e
10
+
11
+ # Extract the compiled artifacts for Dagster
12
+ partitions_def = StaticPartitionsDefinition(COMPILER.partition_keys)
13
+ SCENARIO_CONFIG = COMPILER.scenario_config
14
+
15
+ # If the config file was missing, we must ensure Dagster boots cleanly
16
+ if not COMPILER.partition_keys:
17
+ partitions_def = StaticPartitionsDefinition(["init"])
@@ -0,0 +1,211 @@
1
+ import os
2
+ import polars as pl
3
+ import numpy as np
4
+ from dagster import MaterializeResult, MetadataValue
5
+ from ...utils.schema import TableDef # Strict Pydantic Model
6
+ from ...utils.providers import PROVIDER_REGISTRY
7
+ from ...constants import DEFAULT_CHUNK_SIZE
8
+
9
+
10
+ def resolve_params_recursive(obj, params):
11
+ """Recursively replace string values in obj that match keys in params."""
12
+ if isinstance(obj, dict):
13
+ return {k: resolve_params_recursive(v, params) for k, v in obj.items()}
14
+ elif isinstance(obj, list):
15
+ return [resolve_params_recursive(i, params) for i in obj]
16
+ elif isinstance(obj, str) and obj in params:
17
+ return params[obj]
18
+ # Handle Pydantic Models by dumping them first (if passed directly)
19
+ elif hasattr(obj, "model_dump"):
20
+ return resolve_params_recursive(obj.model_dump(), params)
21
+ return obj
22
+
23
+ def generate(context, params, table_name, target_path, dataset_config):
24
+
25
+ # 1. SCHEMA VALIDATION
26
+ if 'tables' not in dataset_config:
27
+ raise ValueError("Missing 'tables' section in dataset config.")
28
+
29
+ dataset_config = resolve_params_recursive(dataset_config, params)
30
+
31
+ tables = dataset_config.get("tables", {})
32
+ raw_table_def = tables.get(table_name)
33
+ if not raw_table_def:
34
+ raise ValueError(f"Table '{table_name}' not defined in dataset config.")
35
+
36
+ # Validate using Pydantic (Throws ValidationError if invalid)
37
+ # We strip unknown fields if strict mode is issue, but Schema is set to 'allow' extra.
38
+ table_model = TableDef(**tables[table_name])
39
+
40
+ # 2. RESOLVE ROW COUNT
41
+ # Priority: Matrix Params > Table Config
42
+ base_rows = params.get('rows')
43
+ if base_rows is None:
44
+ base_rows = table_model.rows
45
+
46
+ # Handle variable reference (e.g. rows: "small")
47
+ if isinstance(base_rows, str) and not base_rows.isdigit():
48
+ var_name = base_rows
49
+ if var_name in params:
50
+ base_rows = params[var_name]
51
+ else:
52
+ raise ValueError(f"Table '{table_name}' expects param '{var_name}' (from matrix) but it was missing.")
53
+
54
+ if base_rows is None:
55
+ raise ValueError(f"Could not resolve row count for '{table_name}'.")
56
+
57
+ row_count = int(base_rows)
58
+
59
+ # Helper to resolve rows for ANY table (current or target)
60
+ def _resolve_rows(t_name, t_def):
61
+ r = params.get('rows')
62
+ if r is None:
63
+ r = t_def.get('rows')
64
+
65
+ # Handle variable references
66
+ if isinstance(r, str) and not r.isdigit():
67
+ if r in params:
68
+ r = params[r]
69
+ else:
70
+ # Fallback: maybe the target table uses a fixed number?
71
+ # If we can't resolve it from params, we can't guess.
72
+ pass
73
+
74
+ return int(r) if r else None
75
+
76
+ # Batch Generation Strategy
77
+
78
+ # Prepare parent directory
79
+ parent_dir = os.path.dirname(target_path)
80
+ if parent_dir:
81
+ os.makedirs(parent_dir, exist_ok=True)
82
+
83
+ # ... (rest of logic) ...
84
+
85
+ chunk_count = (row_count // DEFAULT_CHUNK_SIZE) + (1 if row_count % DEFAULT_CHUNK_SIZE > 0 else 0)
86
+
87
+ print(f"[Gen] Generating {row_count} rows in batches of {DEFAULT_CHUNK_SIZE}...")
88
+
89
+ # Initialize empty list for temp chunks
90
+ temp_files = []
91
+
92
+ try:
93
+ for i in range(chunk_count):
94
+ offset = i * DEFAULT_CHUNK_SIZE
95
+ current_size = min(DEFAULT_CHUNK_SIZE, row_count - offset)
96
+
97
+ chunk_path = f"{target_path}.part_{i}"
98
+ _generate_chunk(offset, current_size, table_model, dataset_config, params, row_count, chunk_path,
99
+ base_seed=dataset_config.get("seed", 42))
100
+ temp_files.append(chunk_path)
101
+ print(f"[Gen] Batch {i+1} done.")
102
+
103
+ # Merge efficiently using Polars scan
104
+ # If single chunk, just rename
105
+ if len(temp_files) == 1:
106
+ import shutil
107
+ shutil.move(temp_files[0], target_path)
108
+ else:
109
+ pl.scan_parquet(f"{target_path}.part_*").collect().write_parquet(target_path)
110
+
111
+ finally:
112
+ # Cleanup
113
+ for f in temp_files:
114
+ if os.path.exists(f):
115
+ os.remove(f)
116
+
117
+ return MaterializeResult(
118
+ metadata={
119
+ "path": MetadataValue.path(target_path),
120
+ "row_count": MetadataValue.int(row_count),
121
+ "table": table_name
122
+ }
123
+ )
124
+
125
+ def _generate_chunk(offset, size, table_model, dataset_config, params, total_rows, output_path,
126
+ base_seed=42):
127
+ # Enable reproducible generation. The base seed comes from dataset.seed
128
+ # in the YAML (default 42, preserving all pre-seed-param experiment IDs'
129
+ # data). Because the config is hashed, a different seed automatically
130
+ # yields a different Experiment ID — seed changes are never silent.
131
+ # Offset ensures different chunks get different sequences, but the same
132
+ # chunk always gets the same sequence.
133
+ np.random.seed(base_seed + offset)
134
+
135
+ data = {}
136
+ if not table_model.columns:
137
+ # Empty table with rows?
138
+ data["_row_id"] = np.arange(offset, offset + size) # Dummy
139
+ else:
140
+ for col_def in table_model.columns:
141
+ p_name = col_def.provider
142
+ generator_func = PROVIDER_REGISTRY.get(p_name)
143
+
144
+ if not generator_func:
145
+ raise ValueError(f"Unknown provider '{p_name}'")
146
+
147
+ kwargs = col_def.model_dump()
148
+ # Note: kwargs are already resolved at the top level
149
+
150
+ # Dynamic Params substitution - Removed as per instruction
151
+ # for k, v in kwargs.items():
152
+ # if isinstance(v, str) and v in params:
153
+ # kwargs[k] = params[v]
154
+
155
+ kwargs['table_name'] = params.get('table_name', 'unknown')
156
+
157
+ # Contextual Handling
158
+ # Foreign Key needs target_rows to generate valid IDs
159
+ if p_name in ["foreign_key", "foreign key"]:
160
+ target_table = col_def.target_table
161
+ if target_table:
162
+ tables = dataset_config.get("tables", {})
163
+ target_def = tables.get(target_table, {})
164
+ target_rows = None
165
+
166
+ # Resolve target table's row count
167
+ target_row_val = target_def.get('rows') or params.get('rows')
168
+ if isinstance(target_row_val, str) and target_row_val in params:
169
+ target_rows = int(params[target_row_val])
170
+ elif target_row_val is not None:
171
+ target_rows = int(target_row_val)
172
+ else:
173
+ target_rows = total_rows # Fallback to current table's row count
174
+
175
+ kwargs['target_rows'] = target_rows
176
+
177
+ # EXECUTE PROVIDER
178
+ # Note: Most providers don't care about offset (random).
179
+ # Sequence might? 'sequence' provider usually starts at 1.
180
+ # We need to handle 'sequence' specially or pass offset via kwargs if provider supports it.
181
+ if p_name == "sequence":
182
+ kwargs['start'] = offset + 1
183
+
184
+ # Text Concat needs existing data (in this chunk)
185
+ if p_name == "text_concat":
186
+ results = generator_func(size, existing_data=data, **kwargs)
187
+ else:
188
+ results = generator_func(size, **kwargs)
189
+
190
+ # Multi-column providers return a dict of {col_name: array}.
191
+ # Expand them directly into data instead of nesting under one key.
192
+ if isinstance(results, dict):
193
+ data.update(results)
194
+ else:
195
+ data[col_def.name] = results
196
+
197
+ df = pl.DataFrame(data)
198
+
199
+ # Apply Null Masks natively
200
+ for col_def in table_model.columns:
201
+ # null_probability will be a float here because it was resolved globally
202
+ if col_def.null_probability > 0:
203
+ mask = np.random.rand(size) < col_def.null_probability
204
+
205
+ # Use Polars expression to nullify values
206
+ # This preserves the underlying Arrow type (Int64, Utf8) instead of casting to Object
207
+ df = df.with_columns(
208
+ pl.when(pl.lit(mask)).then(None).otherwise(pl.col(col_def.name)).alias(col_def.name)
209
+ )
210
+
211
+ df.write_parquet(output_path)
@@ -0,0 +1,62 @@
1
+ import polars as pl
2
+ import os
3
+ from dagster import MaterializeResult, MetadataValue
4
+
5
+ # CHANGED: 'output_dir' -> 'target_path'
6
+ def generate(context, params, table_name, target_path, dataset_config):
7
+ """
8
+ Normalizes REAL data (CSV/Parquet/JSON) into standard Parquet for the platform.
9
+ """
10
+ partition_key = context.partition_key
11
+
12
+ # 1. GET PATH
13
+ paths = dataset_config.get("paths", {})
14
+ source_template = paths.get(table_name)
15
+
16
+ if not source_template:
17
+ raise ValueError(f"CRITICAL: No path defined for table '{table_name}' in dataset.paths")
18
+
19
+ # INTERPOLATE
20
+ # Support {topology} or other vars from the partition params
21
+ source_file = source_template.format(**params)
22
+
23
+ if not os.path.exists(source_file):
24
+ raise FileNotFoundError(f"CRITICAL: Real data file not found: {source_file}")
25
+
26
+ context.log.info(f"Normalizing real data from {source_file}...")
27
+
28
+ # 2. STREAMING CONVERT (Lazy Evaluation)
29
+ # Ensure directory exists
30
+ os.makedirs(os.path.dirname(target_path), exist_ok=True)
31
+
32
+ file_ext = os.path.splitext(source_file)[1].lower()
33
+
34
+ try:
35
+ # Create a LazyFrame (Scan) based on extension
36
+ if file_ext == ".csv":
37
+ q = pl.scan_csv(source_file, infer_schema_length=10000)
38
+ elif file_ext == ".parquet":
39
+ q = pl.scan_parquet(source_file)
40
+ elif file_ext in [".json", ".ndjson"]:
41
+ q = pl.scan_ndjson(source_file)
42
+ else:
43
+ raise ValueError(f"Unsupported file type: {file_ext}")
44
+
45
+ # 3. SINK TO PARQUET
46
+ # Use the explicit target_path provided by the factory
47
+ q.sink_parquet(target_path)
48
+
49
+ # 4. METADATA
50
+ row_count = pl.scan_parquet(target_path).select(pl.len()).collect().item()
51
+
52
+ except Exception as e:
53
+ raise RuntimeError(f"Failed to process {source_file}: {e}")
54
+
55
+ return MaterializeResult(
56
+ metadata={
57
+ "path": MetadataValue.path(target_path),
58
+ "row_count": MetadataValue.int(row_count),
59
+ "source_file": MetadataValue.path(source_file),
60
+ "loader_backend": "polars_streaming"
61
+ }
62
+ )
@@ -0,0 +1,61 @@
1
+ import duckdb
2
+ import os
3
+ from dagster import MaterializeResult, MetadataValue
4
+
5
+ # CHANGED: 'output_dir' -> 'target_path'
6
+ def generate(context, params, table_name, target_path, dataset_config):
7
+ partition_key = context.partition_key
8
+
9
+ if 'tables' not in dataset_config:
10
+ raise ValueError("Critical: 'dataset.tables' missing in active.yaml")
11
+
12
+ if not dataset_config['tables'].get(table_name):
13
+ raise ValueError(f"Table '{table_name}' requested but not defined in YAML.")
14
+
15
+ scale_factor = params.get('scale_factor', 0.01)
16
+
17
+ context.log.info(f"Generating TPC-H table '{table_name}' with SF={scale_factor}...")
18
+
19
+ # 1. Setup DuckDB
20
+ con = duckdb.connect(database=':memory:')
21
+ con.install_extension("tpch")
22
+ con.load_extension("tpch")
23
+
24
+ # 2. Generate Data
25
+ con.execute(f"CALL dbgen(sf={scale_factor})")
26
+
27
+ # 3. Export with Type Casting
28
+ # Ensure directory exists (Defensive)
29
+ os.makedirs(os.path.dirname(target_path), exist_ok=True)
30
+
31
+ # Get columns for the table
32
+ cols_info = con.execute(f"DESCRIBE {table_name}").fetchall()
33
+
34
+ # Build a SELECT statement that casts Decimals to Doubles
35
+ select_parts = []
36
+ for col_name, col_type, _, _, _, _ in cols_info:
37
+ if "DECIMAL" in col_type:
38
+ select_parts.append(f"CAST({col_name} AS DOUBLE) AS {col_name}")
39
+ else:
40
+ select_parts.append(col_name)
41
+
42
+ select_query = ", ".join(select_parts)
43
+
44
+ # USE THE PASSED TARGET_PATH
45
+ con.execute(f"""
46
+ COPY (SELECT {select_query} FROM {table_name})
47
+ TO '{target_path}'
48
+ (FORMAT PARQUET, COMPRESSION 'SNAPPY')
49
+ """)
50
+
51
+ # 4. Metadata
52
+ row_count = con.execute(f"SELECT count(*) FROM {table_name}").fetchone()[0]
53
+
54
+ return MaterializeResult(
55
+ metadata={
56
+ "path": MetadataValue.path(target_path),
57
+ "row_count": MetadataValue.int(row_count),
58
+ "scale_factor": MetadataValue.float(scale_factor),
59
+ "source": "duckdb_tpch_extension"
60
+ }
61
+ )
File without changes
@@ -0,0 +1,234 @@
1
+ import os
2
+ import time
3
+ import socket
4
+ from dagster import ConfigurableResource
5
+ from typing import Dict, Any, Optional
6
+ from pydantic import ConfigDict, PrivateAttr
7
+ from .base_engine import IBenchmarkEngine
8
+ from .actian_client import ActianClient
9
+
10
+
11
+ class ActianEngine(ConfigurableResource):
12
+ """
13
+ Dagster Resource for Actian Vector on EC2.
14
+
15
+ Manages SSH tunnel lifecycle and provides cold-cache enforcement
16
+ via remote service restart.
17
+ """
18
+
19
+ # --- CONFIGURATION ---
20
+ # Local connection (through tunnel)
21
+ local_port: int = 27832
22
+
23
+ # Remote EC2 configuration
24
+ ec2_host: str = os.getenv("ACTIAN_EC2_HOST", "")
25
+ ec2_user: str = os.getenv("ACTIAN_EC2_USER", "ingres")
26
+ ssh_key_path: str = os.getenv("ACTIAN_SSH_KEY", os.path.expanduser("~/.ssh/benchmark-key-pair.pem"))
27
+
28
+ # Actian database configuration
29
+ database: str = os.getenv("ACTIAN_DATABASE", "benchmark_db")
30
+ db_user: str = os.getenv("ACTIAN_USER", "ingres")
31
+ db_password: str = os.getenv("ACTIAN_PASSWORD", "AntiGravity")
32
+
33
+ # Remote paths
34
+ remote_data_dir: str = "/tmp/benchmark_data"
35
+ vwload_path: str = "/opt/Actian/VectorVW/ingres/bin/vwload"
36
+ actian_sql_path: str = "/opt/Actian/VectorVW/ingres/bin/sql"
37
+
38
+ model_config = ConfigDict(extra='forbid')
39
+
40
+ # Private attributes for runtime state
41
+ _tunnel: Any = PrivateAttr(default=None)
42
+ _ssh_client: Any = PrivateAttr(default=None)
43
+
44
+ def _ensure_tunnel(self):
45
+ """Establishes SSH tunnel if not already active."""
46
+ if self._tunnel is not None and self._tunnel.is_active:
47
+ return
48
+
49
+ if not self.ec2_host:
50
+ raise ValueError("ACTIAN_EC2_HOST environment variable must be set")
51
+
52
+ from sshtunnel import SSHTunnelForwarder
53
+
54
+ self._tunnel = SSHTunnelForwarder(
55
+ (self.ec2_host, 22),
56
+ ssh_username=self.ec2_user,
57
+ ssh_pkey=self.ssh_key_path,
58
+ remote_bind_address=('localhost', 27832),
59
+ local_bind_address=('localhost', self.local_port),
60
+ )
61
+ self._tunnel.start()
62
+ print(f"[Actian] SSH tunnel established: localhost:{self._tunnel.local_bind_port} -> {self.ec2_host}:27832")
63
+
64
+ # Give the tunnel a moment to stabilize
65
+ time.sleep(1)
66
+
67
+ def _ensure_ssh(self):
68
+ """Establishes SSH client for remote commands."""
69
+ if self._ssh_client is not None:
70
+ # Check if connection is still alive
71
+ try:
72
+ self._ssh_client.exec_command("echo ok", timeout=5)
73
+ return
74
+ except Exception:
75
+ self._ssh_client = None
76
+
77
+ if not self.ec2_host:
78
+ raise ValueError("ACTIAN_EC2_HOST environment variable must be set")
79
+
80
+ import paramiko
81
+ # 1. Expand the path (handling both the env var and the default)
82
+ full_key_path = os.path.expanduser(self.ssh_key_path)
83
+
84
+ # 2. Architect Fix: Explicitly load as RSAKey.
85
+ # This bypasses the logic where Paramiko scans for DSSKey and crashes.
86
+ try:
87
+ pkey = paramiko.RSAKey.from_private_key_file(full_key_path)
88
+ except Exception as e:
89
+ raise RuntimeError(f"Could not load RSA key from {full_key_path}: {e}")
90
+
91
+ # 3. Connect using 'pkey' instead of 'key_filename'
92
+ self._ssh_client = paramiko.SSHClient()
93
+ self._ssh_client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
94
+
95
+ self._ssh_client.connect(
96
+ hostname=self.ec2_host,
97
+ username=self.ec2_user,
98
+ pkey=pkey, # Use the loaded object here
99
+ timeout=30
100
+ )
101
+ # self._ssh_client = paramiko.SSHClient()
102
+ # self._ssh_client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
103
+ # self._ssh_client.connect(
104
+ # hostname=self.ec2_host,
105
+ # username=self.ec2_user,
106
+ # key_filename=self.ssh_key_path,
107
+ # timeout=30
108
+ # )
109
+ print(f"[Actian] SSH connection established to {self.ec2_host}")
110
+
111
+ def _ssh_exec(self, command: str, timeout: int = 300) -> tuple[str, str, int]:
112
+ """Execute a command on the remote EC2 instance."""
113
+ self._ensure_ssh()
114
+
115
+ stdin, stdout, stderr = self._ssh_client.exec_command(command, timeout=timeout)
116
+ exit_code = stdout.channel.recv_exit_status()
117
+
118
+ return stdout.read().decode(), stderr.read().decode(), exit_code
119
+
120
+ def _get_client(self) -> ActianClient:
121
+ """Factory method for creating the Actian client."""
122
+ self._ensure_tunnel()
123
+
124
+ connection_params = {
125
+ "host": "localhost",
126
+ "port": self._tunnel.local_bind_port,
127
+ "user": self.db_user,
128
+ "password": self.db_password,
129
+ "database": self.database
130
+ }
131
+
132
+ # Pass SSH details for vwload operations
133
+ ssh_params = {
134
+ "ec2_host": self.ec2_host,
135
+ "ec2_user": self.ec2_user,
136
+ "ssh_key_path": self.ssh_key_path,
137
+ "remote_data_dir": self.remote_data_dir,
138
+ "vwload_path": self.vwload_path,
139
+ "actian_sql_path": self.actian_sql_path,
140
+ }
141
+
142
+ return ActianClient(connection_params, ssh_params)
143
+
144
+ # --- IBenchmarkEngine Implementation ---
145
+
146
+ def get_engine_name(self) -> str:
147
+ return "actian"
148
+
149
+ def clear_cache(self, settings: dict = None):
150
+ """
151
+ Enforces cold cache by restarting Actian Vector service on EC2.
152
+
153
+ This clears both:
154
+ 1. X100 engine memory segments
155
+ 2. Linux page cache (via service restart)
156
+ """
157
+ print("[Actian] Clearing cache via service restart...")
158
+
159
+ # Restart Actian Vector service
160
+ stdout, stderr, exit_code = self._ssh_exec(
161
+ "sudo systemctl restart actian-vectorVW",
162
+ timeout=120
163
+ )
164
+
165
+ if exit_code != 0:
166
+ raise RuntimeError(f"Failed to restart Actian service: {stderr}")
167
+
168
+ # Wait for service to be ready
169
+ self._wait_for_ready()
170
+ print("[Actian] Service restarted, cache cleared")
171
+
172
+ def run_query(self, sql: str, partition_key: str, engine_params: Dict[str, Any] = None) -> Optional[float]:
173
+ """Execute a benchmark query with cold cache.
174
+
175
+ engine_params is the 'actian' namespace — accepted but not yet applied
176
+ (Vector session tuning is a future capability).
177
+ """
178
+ self.clear_cache()
179
+ client = self._get_client()
180
+ return client.run_query(sql, {})
181
+
182
+ def bulk_load(self, filepath: str, table_name: str, partition_key: str) -> None:
183
+ """
184
+ Bulk load data into Actian Vector.
185
+
186
+ 1. SCP the file to EC2
187
+ 2. Convert to CSV if needed (vwload prefers CSV)
188
+ 3. Run vwload on EC2
189
+ """
190
+ self._ensure_tunnel()
191
+ self._wait_for_ready()
192
+
193
+ client = self._get_client()
194
+ client.bulk_load(filepath, table_name, partition_key)
195
+
196
+ def _wait_for_ready(self, timeout: int = 120):
197
+ """Wait for Actian to be ready to accept connections."""
198
+ self._ensure_tunnel()
199
+
200
+ start = time.time()
201
+ while time.time() - start < timeout:
202
+ try:
203
+ with socket.create_connection(
204
+ ('localhost', self._tunnel.local_bind_port),
205
+ timeout=2
206
+ ):
207
+ # Port is open, but let's verify the service is actually responding
208
+ time.sleep(2)
209
+ return
210
+ except (socket.timeout, ConnectionRefusedError, OSError):
211
+ time.sleep(2)
212
+
213
+ raise TimeoutError(
214
+ f"Actian Vector on {self.ec2_host} failed to respond within {timeout}s"
215
+ )
216
+
217
+ def cleanup(self):
218
+ """Clean up SSH tunnel and connections."""
219
+ if self._ssh_client:
220
+ self._ssh_client.close()
221
+ self._ssh_client = None
222
+
223
+ if self._tunnel:
224
+ self._tunnel.stop()
225
+ self._tunnel = None
226
+
227
+ print("[Actian] Connections closed")
228
+
229
+ def __del__(self):
230
+ """Ensure cleanup on garbage collection."""
231
+ try:
232
+ self.cleanup()
233
+ except Exception:
234
+ pass