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,370 @@
1
+ import time
2
+ import polars as pl
3
+ from typing import Dict, Any, Optional
4
+ from typedb.driver import TypeDB, Credentials, DriverOptions, TransactionType
5
+
6
+ from .base_schema_client import SchemaFirstClient
7
+
8
+
9
+ class TypeDBClient(SchemaFirstClient):
10
+ """
11
+ Concrete SchemaFirstClient for TypeDB.
12
+
13
+ Implements the three loading steps using the TypeDB Python driver:
14
+ _prepare_store → drop / create TypeDB database
15
+ _define_schema → SCHEMA transaction with a TypeQL ``define`` block
16
+ _insert_batch → WRITE transaction with a TypeQL ``insert`` block
17
+
18
+ Query execution uses a READ transaction and forces full result
19
+ materialisation via ``list(answer.as_concept_rows())``.
20
+ """
21
+
22
+ def __init__(self, address: str, db_name: str):
23
+ self.address = address
24
+ self.db_name = db_name
25
+
26
+ # ------------------------------------------------------------------
27
+ # SchemaFirstClient — abstract step implementations
28
+ # ------------------------------------------------------------------
29
+
30
+ def _prepare_store(self, entity_type: str) -> None:
31
+ """Drop the TypeDB database if it exists, then recreate it."""
32
+ with self._driver() as driver:
33
+ if driver.databases.contains(self.db_name):
34
+ driver.databases.get(self.db_name).delete()
35
+ driver.databases.create(self.db_name)
36
+
37
+ def _define_schema(self, entity_type: str, df: pl.DataFrame) -> None:
38
+ """Run a SCHEMA transaction containing a TypeQL ``define`` block."""
39
+ schema_tql = self._build_schema(entity_type, df)
40
+ with self._driver() as driver:
41
+ with driver.transaction(self.db_name, TransactionType.SCHEMA) as tx:
42
+ tx.query(schema_tql).resolve()
43
+ tx.commit()
44
+
45
+ def _insert_batch(self, entity_type: str, rows: list) -> None:
46
+ """Run a WRITE transaction containing a TypeQL ``insert`` block."""
47
+ insert_tql = self._build_insert(entity_type, rows)
48
+ with self._driver() as driver:
49
+ with driver.transaction(self.db_name, TransactionType.WRITE) as tx:
50
+ tx.query(insert_tql).resolve()
51
+ tx.commit()
52
+
53
+ # ------------------------------------------------------------------
54
+ # SchemaFirstClient — run_query
55
+ # ------------------------------------------------------------------
56
+
57
+ def run_query(self, typeql: str, scenario_params: Dict[str, Any]) -> Optional[float]:
58
+ """
59
+ Execute a TypeQL query and return wall-clock duration including
60
+ full result consumption (consistent with DuckDB / Actian timing).
61
+
62
+ Returns ``None`` if the TypeDB server crashes during evaluation (e.g.
63
+ stack overflow from deep recursive function calls on Zipf hub graphs).
64
+ Crashes are logged but do not propagate so that the benchmark
65
+ infrastructure can record a null result and continue.
66
+ """
67
+ from typedb.common.exception import TypeDBDriverException # local import — optional dep
68
+ try:
69
+ with self._driver() as driver:
70
+ with driver.transaction(self.db_name, TransactionType.READ) as tx:
71
+ start = time.time()
72
+ answer = tx.query(typeql).resolve()
73
+ list(answer.as_concept_rows()) # force full materialisation
74
+ end = time.time()
75
+ return end - start
76
+ except TypeDBDriverException as exc:
77
+ print(
78
+ f"[TypeDBClient] Query failed on '{self.db_name}' "
79
+ f"(TypeDB server error — likely stack overflow or OOM "
80
+ f"during recursive evaluation): {exc}"
81
+ )
82
+ return None
83
+ except Exception as exc:
84
+ print(f"[TypeDBClient] Query failed on '{self.db_name}' (unexpected): {exc}")
85
+ return None
86
+
87
+ # ------------------------------------------------------------------
88
+ # TypeQL builders (TypeDB-specific, not part of base contract)
89
+ # ------------------------------------------------------------------
90
+
91
+ def _build_schema(self, entity_type: str, df: pl.DataFrame) -> str:
92
+ """
93
+ Produce a TypeQL ``define`` block from a Polars schema.
94
+
95
+ Example output:
96
+ define
97
+ entity skewed_data_ssd_small,
98
+ owns id @key,
99
+ owns selectivity_code,
100
+ owns payload;
101
+ attribute id, value integer;
102
+ attribute selectivity_code, value string;
103
+ attribute payload, value string;
104
+ """
105
+ attr_defs = []
106
+ owns_clauses = []
107
+
108
+ for col_name, dtype in df.schema.items():
109
+ typeql_value = self._polars_to_typeql_value(dtype)
110
+ attr_defs.append(f" attribute {col_name}, value {typeql_value};")
111
+ if col_name == "id":
112
+ owns_clauses.append(f" owns {col_name} @key")
113
+ else:
114
+ owns_clauses.append(f" owns {col_name}")
115
+
116
+ owns_str = ",\n".join(owns_clauses) + ";"
117
+ attr_str = "\n".join(attr_defs)
118
+
119
+ return f"define\n entity {entity_type},\n{owns_str}\n{attr_str}"
120
+
121
+ def _build_insert(self, entity_type: str, rows: list) -> str:
122
+ """
123
+ Produce a single TypeQL ``insert`` block for a batch of rows.
124
+
125
+ Each variable is uniquely named ($x0, $x1, …) to avoid clashes
126
+ within the same transaction.
127
+ """
128
+ stmts = []
129
+ for i, row in enumerate(rows):
130
+ parts = [f"$x{i} isa {entity_type}"]
131
+ for col, val in row.items():
132
+ if val is None:
133
+ continue
134
+ if isinstance(val, bool):
135
+ parts.append(f"has {col} {str(val).lower()}")
136
+ elif isinstance(val, str):
137
+ escaped = val.replace("\\", "\\\\").replace('"', '\\"')
138
+ parts.append(f'has {col} "{escaped}"')
139
+ else:
140
+ parts.append(f"has {col} {val}")
141
+ stmts.append(", ".join(parts) + ";")
142
+
143
+ return "insert\n " + "\n ".join(stmts)
144
+
145
+ @staticmethod
146
+ def _polars_to_typeql_value(dtype) -> str:
147
+ """Map a Polars dtype to the appropriate TypeQL value type."""
148
+ if dtype in (
149
+ pl.Int8, pl.Int16, pl.Int32, pl.Int64,
150
+ pl.UInt8, pl.UInt16, pl.UInt32, pl.UInt64,
151
+ ):
152
+ return "integer"
153
+ if dtype in (pl.Float32, pl.Float64):
154
+ return "double"
155
+ if dtype == pl.Boolean:
156
+ return "boolean"
157
+ if dtype == pl.Date or isinstance(dtype, pl.Datetime):
158
+ return "datetime"
159
+ return "string"
160
+
161
+ # ------------------------------------------------------------------
162
+ # Multi-table loading (hypergraph / relation support)
163
+ # ------------------------------------------------------------------
164
+
165
+ def initialize_db(self) -> None:
166
+ """
167
+ Drop and recreate the TypeDB database.
168
+
169
+ Called once per partition at the start of a bulk-load phase so that
170
+ multiple entity types can be loaded into the same database without
171
+ each one wiping the previous data (the original ``_prepare_store``
172
+ behaviour).
173
+ """
174
+ with self._driver() as driver:
175
+ if driver.databases.contains(self.db_name):
176
+ driver.databases.get(self.db_name).delete()
177
+ driver.databases.create(self.db_name)
178
+
179
+ def load_entity(self, filepath: str, entity_type: str) -> None:
180
+ """
181
+ Define schema for an entity type and load its data without dropping
182
+ the database first.
183
+
184
+ Use this instead of ``bulk_load()`` when multiple entity types must
185
+ coexist in the same database (e.g. a supply-chain hypergraph where
186
+ supplier, buyer, product and supply_contract all live in one DB).
187
+ """
188
+ df = pl.read_parquet(filepath)
189
+ self._define_schema(entity_type, df)
190
+ rows = df.to_dicts()
191
+ for i in range(0, len(rows), self.BATCH_SIZE):
192
+ self._insert_batch(entity_type, rows[i : i + self.BATCH_SIZE])
193
+ print(f"[TypeDBClient] Loaded {len(rows)} rows into '{entity_type}'")
194
+
195
+ def bulk_load_relation(
196
+ self,
197
+ filepath: str,
198
+ relation_type: str,
199
+ role_map: Dict[str, list],
200
+ attributes: list,
201
+ ) -> None:
202
+ """
203
+ Define a TypeDB n-ary relation schema and load data into it.
204
+
205
+ All referenced entity types must already exist in the database
206
+ (i.e. ``load_entity()`` must have been called for each role player).
207
+
208
+ Args:
209
+ filepath: Path to the Parquet file whose rows become relations.
210
+ relation_type: TypeDB relation type name (e.g. ``supply_contract_small``).
211
+ role_map: Mapping of DataFrame column name → [entity_type, role_name].
212
+ Example: ``{"supplier_id": ["supplier_small", "supplier_role"]}``.
213
+ attributes: Column names that become relation-owned attributes
214
+ (everything not in ``role_map`` and not ``id``).
215
+ """
216
+ df = pl.read_parquet(filepath)
217
+ schema_tql = self._build_relation_schema(relation_type, role_map, attributes, df)
218
+ with self._driver() as driver:
219
+ with driver.transaction(self.db_name, TransactionType.SCHEMA) as tx:
220
+ tx.query(schema_tql).resolve()
221
+ tx.commit()
222
+
223
+ rows = df.to_dicts()
224
+ for i in range(0, len(rows), self.BATCH_SIZE):
225
+ insert_tql = self._build_relation_insert(
226
+ relation_type, role_map, attributes, rows[i : i + self.BATCH_SIZE]
227
+ )
228
+ with self._driver() as driver:
229
+ with driver.transaction(self.db_name, TransactionType.WRITE) as tx:
230
+ tx.query(insert_tql).resolve()
231
+ tx.commit()
232
+
233
+ print(f"[TypeDBClient] Loaded {len(rows)} relations into '{relation_type}'")
234
+
235
+ def _build_relation_schema(
236
+ self,
237
+ relation_type: str,
238
+ role_map: Dict[str, list],
239
+ attributes: list,
240
+ df: pl.DataFrame,
241
+ ) -> str:
242
+ """
243
+ Produce a TypeQL ``define`` block that augments an existing schema with:
244
+ - attribute types for the relation's own attributes
245
+ - the relation type declaration (roles + owned attributes)
246
+ - ``plays`` declarations for each role-player entity type
247
+
248
+ Example output (supply_contract_small with 3 roles)::
249
+
250
+ define
251
+ attribute volume, value integer;
252
+ attribute price_per_unit, value double;
253
+ relation supply_contract_small,
254
+ relates supplier_role,
255
+ relates buyer_role,
256
+ relates product_role,
257
+ owns volume,
258
+ owns price_per_unit;
259
+ supplier_small plays supply_contract_small:supplier_role;
260
+ buyer_small plays supply_contract_small:buyer_role;
261
+ product_small plays supply_contract_small:product_role;
262
+ """
263
+ # Attribute type definitions (relation-owned columns only)
264
+ attr_defs = []
265
+ for col in attributes:
266
+ typeql_val = self._polars_to_typeql_value(df.schema[col])
267
+ attr_defs.append(f" attribute {col}, value {typeql_val};")
268
+
269
+ # Unique role names (preserve declaration order)
270
+ role_names = list(dict.fromkeys(entry[1] for entry in role_map.values()))
271
+
272
+ # Relation type body
273
+ relates_clauses = [f" relates {r}" for r in role_names]
274
+ owns_clauses = [f" owns {col}" for col in attributes]
275
+ relation_body = ",\n".join(relates_clauses + owns_clauses) + ";"
276
+ relation_def = f" relation {relation_type},\n{relation_body}"
277
+
278
+ # Plays declarations (one per role player)
279
+ plays_lines = [
280
+ f" {entry[0]} plays {relation_type}:{entry[1]};"
281
+ for entry in role_map.values()
282
+ ]
283
+
284
+ return "define\n" + "\n".join(attr_defs + [relation_def] + plays_lines)
285
+
286
+ def _build_relation_insert(
287
+ self,
288
+ relation_type: str,
289
+ role_map: Dict[str, list],
290
+ attributes: list,
291
+ rows: list,
292
+ ) -> str:
293
+ """
294
+ Produce a single TypeQL ``match … insert`` block for a batch of
295
+ relation rows.
296
+
297
+ Each row uses unique variable names (``$e{i}_{col}``) so that all
298
+ match constraints are satisfied in a single solution, and the insert
299
+ phase creates one relation per row in one round trip.
300
+
301
+ Example (one row, supply_contract)::
302
+
303
+ match
304
+ $e0_supplier_id isa supplier_small, has id 3;
305
+ $e0_buyer_id isa buyer_small, has id 7;
306
+ $e0_product_id isa product_small, has id 2;
307
+ insert
308
+ (supplier_role: $e0_supplier_id, buyer_role: $e0_buyer_id,
309
+ product_role: $e0_product_id) isa supply_contract_small,
310
+ has volume 450, has price_per_unit 12.5;
311
+ """
312
+ match_parts: list[str] = []
313
+ insert_parts: list[str] = []
314
+
315
+ for i, row in enumerate(rows):
316
+ # Match: bind each role-player entity by its integer key
317
+ for col, (entity_type, _) in role_map.items():
318
+ var = f"$e{i}_{col}"
319
+ match_parts.append(f" {var} isa {entity_type}, has id {row[col]};")
320
+
321
+ # Insert: build the relation with roles and attribute values
322
+ roles_str = ", ".join(
323
+ f"{entry[1]}: $e{i}_{col}" for col, entry in role_map.items()
324
+ )
325
+ attr_parts: list[str] = []
326
+ for col in attributes:
327
+ val = row.get(col)
328
+ if val is None:
329
+ continue
330
+ if isinstance(val, bool):
331
+ attr_parts.append(f"has {col} {str(val).lower()}")
332
+ elif isinstance(val, str):
333
+ escaped = val.replace("\\", "\\\\").replace('"', '\\"')
334
+ attr_parts.append(f'has {col} "{escaped}"')
335
+ else:
336
+ attr_parts.append(f"has {col} {val}")
337
+
338
+ attr_str = (", " + ", ".join(attr_parts)) if attr_parts else ""
339
+ insert_parts.append(f" ({roles_str}) isa {relation_type}{attr_str};")
340
+
341
+ return "match\n" + "\n".join(match_parts) + "\ninsert\n" + "\n".join(insert_parts)
342
+
343
+ def apply_inference_schema(self, tql: str) -> None:
344
+ """
345
+ Apply a TypeQL ``define`` block as a SCHEMA transaction.
346
+
347
+ Used to augment an existing database with inferred relation types and
348
+ inference rules *after* the base data has been loaded. Calling this
349
+ multiple times with non-conflicting definitions is safe (TypeDB is
350
+ idempotent for ``define`` of already-existing types/rules).
351
+
352
+ Args:
353
+ tql: A complete TypeQL ``define`` block, e.g. the transitive
354
+ reachability rule generated by
355
+ ``TypeDBEngine._build_transitive_inference_schema()``.
356
+ """
357
+ with self._driver() as driver:
358
+ with driver.transaction(self.db_name, TransactionType.SCHEMA) as tx:
359
+ tx.query(tql).resolve()
360
+ tx.commit()
361
+ print(f"[TypeDBClient] Applied inference schema to '{self.db_name}'")
362
+
363
+ # ------------------------------------------------------------------
364
+ # Driver factory
365
+ # ------------------------------------------------------------------
366
+
367
+ def _driver(self):
368
+ creds = Credentials("admin", "password")
369
+ opts = DriverOptions(is_tls_enabled=False)
370
+ return TypeDB.driver(self.address, creds, opts)
@@ -0,0 +1,303 @@
1
+ import time
2
+ import socket
3
+ import docker
4
+ from docker.errors import NotFound, APIError
5
+ from dagster import ConfigurableResource
6
+ from typing import Dict, Any, Optional
7
+ from pydantic import ConfigDict
8
+ from .typedb_client import TypeDBClient
9
+ from ..utils.system import thrash_os_cache
10
+
11
+ # ---------------------------------------------------------------------------
12
+ # Module-level partition tracker
13
+ #
14
+ # Records which TypeDB database names have already been initialised (drop +
15
+ # create) in the current process. This lets bulk_load be called once per
16
+ # table while only wiping the database on the very first call for a given
17
+ # partition, so multiple entity types can coexist in the same database.
18
+ #
19
+ # The set is naturally cleared between benchmark runs because execute_run.py
20
+ # starts a fresh Python process each time.
21
+ # ---------------------------------------------------------------------------
22
+ _INITIALIZED_PARTITIONS: set = set()
23
+
24
+
25
+ class TypeDBEngine(ConfigurableResource):
26
+ """
27
+ Dagster Resource for TypeDB (local Docker).
28
+
29
+ Manages the Docker container lifecycle and provides cold-cache enforcement
30
+ by killing and recreating the container (data is preserved in a named
31
+ Docker volume, identical to the PostgresEngine approach).
32
+
33
+ Multi-table support
34
+ -------------------
35
+ When an experiment loads several tables into the same partition (e.g. a
36
+ supply-chain hypergraph with supplier, buyer, product, supply_contract),
37
+ this engine initialises the TypeDB database *once* on the first
38
+ ``bulk_load`` call and then adds each subsequent table additively.
39
+
40
+ Relation tables are declared via ``relation_configs``::
41
+
42
+ relation_configs = {
43
+ "supply_contract": { # base table name (without partition key)
44
+ "roles": {
45
+ "supplier_id": ["supplier", "supplier_role"],
46
+ "buyer_id": ["buyer", "buyer_role"],
47
+ "product_id": ["product", "product_role"],
48
+ },
49
+ "attributes": ["volume", "price_per_unit"],
50
+ }
51
+ }
52
+
53
+ Entity tables (not in ``relation_configs``) are loaded via
54
+ ``TypeDBClient.load_entity()``. Relation tables are loaded via
55
+ ``TypeDBClient.bulk_load_relation()`` using a ``match … insert`` pattern.
56
+ """
57
+
58
+ # --- CONFIGURATION ---
59
+ address: str = "127.0.0.1:1729"
60
+ container_name: str = "bench_typedb"
61
+ docker_image: str = "typedb/typedb:latest"
62
+ relation_configs: Dict[str, Any] = {}
63
+
64
+ model_config = ConfigDict(extra="forbid")
65
+
66
+ # ------------------------------------------------------------------
67
+ # IBenchmarkEngine interface
68
+ # ------------------------------------------------------------------
69
+
70
+ def get_engine_name(self) -> str:
71
+ return "typedb"
72
+
73
+ def clear_cache(self, settings: dict = None):
74
+ """
75
+ Enforces a cold-cache run:
76
+ 1. Flood host RAM to evict the OS page cache.
77
+ 2. Kill and recreate the TypeDB container (clears DBMS buffer pool).
78
+ Data survives because it lives in a named Docker volume.
79
+ """
80
+ thrash_os_cache()
81
+ self._restart_container()
82
+ self._wait_for_ready()
83
+
84
+ def run_query(self, sql: str, partition_key: str, engine_params: Dict[str, Any] = None) -> Optional[float]:
85
+ """engine_params is the 'typedb' namespace — accepted but not yet applied."""
86
+ self.clear_cache()
87
+ client = self._get_client(partition_key)
88
+ return client.run_query(sql, {})
89
+
90
+ def bulk_load(self, filepath: str, table_name: str, partition_key: str) -> None:
91
+ """
92
+ Load one table into TypeDB for the given partition.
93
+
94
+ On the *first* call for a partition the database is dropped and
95
+ recreated (clean slate). Subsequent calls for the same partition add
96
+ to the existing database so that multiple entity types and relations
97
+ can coexist.
98
+
99
+ Entity tables use ``TypeDBClient.load_entity()``.
100
+ Relation tables (declared in ``relation_configs``) use
101
+ ``TypeDBClient.bulk_load_relation()`` with a match-insert pattern.
102
+ """
103
+ self._ensure_container()
104
+ self._wait_for_ready()
105
+ client = self._get_client(partition_key)
106
+ db_key = self._db_name(partition_key)
107
+
108
+ # Initialise DB once per partition per process
109
+ if db_key not in _INITIALIZED_PARTITIONS:
110
+ client.initialize_db()
111
+ _INITIALIZED_PARTITIONS.add(db_key)
112
+
113
+ # Dispatch: relation vs entity
114
+ base_name = self._base_table_name(table_name, partition_key)
115
+ if base_name in self.relation_configs:
116
+ config = self.relation_configs[base_name]
117
+ role_map = self._resolve_role_map(config["roles"], partition_key)
118
+ client.bulk_load_relation(
119
+ filepath,
120
+ table_name,
121
+ role_map,
122
+ config.get("attributes", []),
123
+ )
124
+ # After loading the relation, optionally define an inference rule
125
+ # so that subsequent read queries can traverse the graph transitively.
126
+ if config.get("inference") == "transitive":
127
+ role_values = list(role_map.values())
128
+ entity_type = role_values[0][0] # same for both roles (self-referential)
129
+ from_role = role_values[0][1]
130
+ to_role = role_values[1][1]
131
+ tql = self._build_transitive_inference_schema(
132
+ table_name, entity_type, from_role, to_role
133
+ )
134
+ client.apply_inference_schema(tql)
135
+ else:
136
+ client.load_entity(filepath, table_name)
137
+
138
+ # ------------------------------------------------------------------
139
+ # Internal helpers
140
+ # ------------------------------------------------------------------
141
+
142
+ def _get_client(self, partition_key: str) -> TypeDBClient:
143
+ db_name = self._db_name(partition_key)
144
+ return TypeDBClient(address=self.address, db_name=db_name)
145
+
146
+ @staticmethod
147
+ def _db_name(partition_key: str) -> str:
148
+ """Convert a partition key into a valid TypeDB database name."""
149
+ return f"bench_{partition_key}".replace("-", "_").replace("~", "_")
150
+
151
+ @staticmethod
152
+ def _base_table_name(table_name: str, partition_key: str) -> str:
153
+ """
154
+ Strip the partition-key suffix to obtain the base table name.
155
+
156
+ Example: ``supply_contract_small`` with ``small`` → ``supply_contract``.
157
+ """
158
+ suffix = f"_{partition_key}"
159
+ return table_name[: -len(suffix)] if table_name.endswith(suffix) else table_name
160
+
161
+ @staticmethod
162
+ def _build_transitive_inference_schema(
163
+ relation_type: str,
164
+ entity_type: str,
165
+ from_role: str,
166
+ to_role: str,
167
+ ) -> str:
168
+ """
169
+ Generate a TypeQL ``define`` block containing a recursive stream function
170
+ that computes the transitive closure over ``relation_type``.
171
+
172
+ TypeDB 3.x does not support inference rules (``rule`` keyword) — functions
173
+ are the supported mechanism. The generated ``fun reachable`` uses the
174
+ ``{ base } or { recursive }`` pattern inside its ``match`` body; TypeDB
175
+ evaluates it lazily with tabling so cycles terminate automatically.
176
+
177
+ Args:
178
+ relation_type: Fully qualified relation name, e.g.
179
+ ``supplies_small_small``.
180
+ entity_type: Entity type that plays both roles (self-referential),
181
+ e.g. ``company_small_small``.
182
+ from_role: Name of the "source" role, e.g. ``seller_role``.
183
+ to_role: Name of the "destination" role, e.g. ``buyer_role``.
184
+
185
+ Returns:
186
+ TypeQL define block as a string ready to pass to
187
+ ``TypeDBClient.apply_inference_schema()``.
188
+
189
+ Example output::
190
+
191
+ define
192
+ fun reachable($from: company_small_small) -> { company_small_small }:
193
+ match
194
+ { (seller_role: $from, buyer_role: $to) isa supplies_small_small; } or
195
+ { (seller_role: $from, buyer_role: $via) isa supplies_small_small;
196
+ let $to in reachable($via); };
197
+ return { $to };
198
+ """
199
+ return (
200
+ "define\n"
201
+ f" fun reachable($from: {entity_type}) -> {{ {entity_type} }}:\n"
202
+ " match\n"
203
+ f" {{ ({from_role}: $from, {to_role}: $to) isa {relation_type}; }} or\n"
204
+ f" {{ ({from_role}: $from, {to_role}: $via) isa {relation_type};\n"
205
+ " let $to in reachable($via); };\n"
206
+ " return { $to };"
207
+ )
208
+
209
+ @staticmethod
210
+ def _resolve_role_map(role_configs: dict, partition_key: str) -> dict:
211
+ """
212
+ Expand base entity type names in role configs to full runtime names
213
+ by appending the partition key.
214
+
215
+ Input: ``{"supplier_id": ["supplier", "supplier_role"]}``
216
+ Output: ``{"supplier_id": ["supplier_small", "supplier_role"]}``
217
+ """
218
+ return {
219
+ col: [f"{entry[0]}_{partition_key}", entry[1]]
220
+ for col, entry in role_configs.items()
221
+ }
222
+
223
+ # ------------------------------------------------------------------
224
+ # Docker lifecycle
225
+ # ------------------------------------------------------------------
226
+
227
+ def _ensure_container(self):
228
+ """Start the container if it is not already running."""
229
+ client = docker.from_env()
230
+ try:
231
+ container = client.containers.get(self.container_name)
232
+ if container.status != "running":
233
+ container.start()
234
+ except NotFound:
235
+ self._start_container(client)
236
+
237
+ def _restart_container(self):
238
+ """Kill any existing container and start a fresh one."""
239
+ client = docker.from_env()
240
+ self._kill_container(client)
241
+ self._start_container(client)
242
+
243
+ def _kill_container(self, client=None):
244
+ client = client or docker.from_env()
245
+ try:
246
+ container = client.containers.get(self.container_name)
247
+ container.remove(force=True)
248
+ except NotFound:
249
+ pass
250
+ except APIError as e:
251
+ raise RuntimeError(f"Failed to remove TypeDB container '{self.container_name}': {e}")
252
+
253
+ def _start_container(self, client=None):
254
+ client = client or docker.from_env()
255
+
256
+ max_retries = 3
257
+ for attempt in range(max_retries):
258
+ try:
259
+ client.containers.run(
260
+ image=self.docker_image,
261
+ name=self.container_name,
262
+ detach=True,
263
+ ports={"1729/tcp": 1729},
264
+ volumes={
265
+ "typedb_bench_data": {
266
+ "bind": "/var/lib/typedb/data",
267
+ "mode": "rw",
268
+ }
269
+ },
270
+ )
271
+ return
272
+ except APIError as e:
273
+ if ("port is already allocated" in str(e) or "Conflict" in str(e)) and attempt < max_retries - 1:
274
+ time.sleep(2)
275
+ self._kill_container(client)
276
+ continue
277
+ raise RuntimeError(f"TypeDB container failed to start: {e}")
278
+
279
+ def _wait_for_ready(self, timeout: int = 60):
280
+ """
281
+ Poll port 1729 until TypeDB accepts TCP connections, then attempt
282
+ a lightweight driver ping to confirm the server is fully up.
283
+ """
284
+ start = time.time()
285
+ while time.time() - start < timeout:
286
+ if self._port_open():
287
+ try:
288
+ from typedb.driver import TypeDB, Credentials, DriverOptions # noqa: PLC0415
289
+ creds = Credentials("admin", "password")
290
+ opts = DriverOptions(is_tls_enabled=False)
291
+ with TypeDB.driver(self.address, creds, opts) as driver:
292
+ driver.databases.all()
293
+ return
294
+ except Exception:
295
+ pass
296
+ time.sleep(1)
297
+
298
+ raise TimeoutError(f"TypeDB at {self.address} did not become ready within {timeout}s")
299
+
300
+ def _port_open(self) -> bool:
301
+ host, port = self.address.split(":")
302
+ with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
303
+ return s.connect_ex((host, int(port))) == 0
File without changes
@@ -0,0 +1 @@
1
+ SELECT 1;
@@ -0,0 +1 @@
1
+ SELECT 1;