sqlbenchdag 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- infrastructure/debug_aws.py +40 -0
- scripts/dev/backfill_queries.py +44 -0
- scripts/dev/timestamp_capsule.py +49 -0
- scripts/dev/upgrade_capsule.py +63 -0
- scripts/dev/verify_capsule.py +79 -0
- scripts/dev/verify_portability.py +103 -0
- scripts/tools/_plotlib.py +43 -0
- scripts/tools/analyze_scaling.py +39 -0
- scripts/tools/demo_breach_detection.py +61 -0
- scripts/tools/extract_results.py +57 -0
- scripts/tools/find_kink.py +104 -0
- scripts/tools/gen_experiment_catalog.py +103 -0
- scripts/tools/load_parquet_to_local.py +98 -0
- scripts/tools/plot_execution_modes.py +82 -0
- scripts/tools/plot_scaling.py +91 -0
- scripts/tools/plot_selectivity.py +139 -0
- scripts/tools/plot_threads.py +95 -0
- scripts/tools/regenerate_dashboard.py +58 -0
- scripts/tools/verify_doc_claims.py +88 -0
- sql_benchmarks/__init__.py +3 -0
- sql_benchmarks/api/__init__.py +0 -0
- sql_benchmarks/api/app.py +30 -0
- sql_benchmarks/api/data/__init__.py +0 -0
- sql_benchmarks/api/data/catalog_reader.py +73 -0
- sql_benchmarks/api/data/reader.py +125 -0
- sql_benchmarks/api/logic/__init__.py +0 -0
- sql_benchmarks/api/logic/comparator.py +44 -0
- sql_benchmarks/api/logic/ranker.py +127 -0
- sql_benchmarks/api/models/__init__.py +0 -0
- sql_benchmarks/api/models/catalog.py +22 -0
- sql_benchmarks/api/models/experiments.py +20 -0
- sql_benchmarks/api/models/recommend.py +11 -0
- sql_benchmarks/api/models/results.py +66 -0
- sql_benchmarks/api/routers/__init__.py +0 -0
- sql_benchmarks/api/routers/catalog.py +19 -0
- sql_benchmarks/api/routers/experiments.py +79 -0
- sql_benchmarks/api/routers/recommend.py +22 -0
- sql_benchmarks/api/routers/results.py +51 -0
- sql_benchmarks/assets/__init__.py +4 -0
- sql_benchmarks/assets/benchmark_factory.py +222 -0
- sql_benchmarks/assets/data_factory.py +54 -0
- sql_benchmarks/assets/data_quality.py +91 -0
- sql_benchmarks/assets/ingestion_factory.py +88 -0
- sql_benchmarks/assets/maintenance.py +63 -0
- sql_benchmarks/assets/reporting.py +385 -0
- sql_benchmarks/assets/semantic_gate.py +76 -0
- sql_benchmarks/cli.py +69 -0
- sql_benchmarks/config_loader.py +104 -0
- sql_benchmarks/constants.py +60 -0
- sql_benchmarks/coordinator.py +282 -0
- sql_benchmarks/definitions.py +115 -0
- sql_benchmarks/experiments/templates/experiment_template.yaml +99 -0
- sql_benchmarks/harness.py +84 -0
- sql_benchmarks/jobs.py +8 -0
- sql_benchmarks/partitions.py +17 -0
- sql_benchmarks/plugins/data_sources/declarative_gen.py +211 -0
- sql_benchmarks/plugins/data_sources/local_file_loader.py +62 -0
- sql_benchmarks/plugins/data_sources/tpc_h.py +61 -0
- sql_benchmarks/resources/__init__.py +0 -0
- sql_benchmarks/resources/actian.py +234 -0
- sql_benchmarks/resources/actian_client.py +231 -0
- sql_benchmarks/resources/base_engine.py +58 -0
- sql_benchmarks/resources/base_schema_client.py +104 -0
- sql_benchmarks/resources/duckdb.py +58 -0
- sql_benchmarks/resources/duckdb_client.py +157 -0
- sql_benchmarks/resources/postgres.py +188 -0
- sql_benchmarks/resources/postgres_client.py +184 -0
- sql_benchmarks/resources/quack.py +56 -0
- sql_benchmarks/resources/quack_client.py +169 -0
- sql_benchmarks/resources/typedb_client.py +370 -0
- sql_benchmarks/resources/typedb_engine.py +303 -0
- sql_benchmarks/scripts/__init__.py +0 -0
- sql_benchmarks/scripts/sql/acid_test/duckdb/test.sql +1 -0
- sql_benchmarks/scripts/sql/acid_test/postgres/test.sql +1 -0
- sql_benchmarks/scripts/sql/analytical_wall/actian/analytical_wall.sql +15 -0
- sql_benchmarks/scripts/sql/analytical_wall/analytical_wall.sql.template +19 -0
- sql_benchmarks/scripts/sql/analytical_wall/duckdb/analytical_wall.sql +15 -0
- sql_benchmarks/scripts/sql/analytical_wall/postgres/analytical_wall.sql +15 -0
- sql_benchmarks/scripts/sql/group_by/duckdb/groupby_antipattern.sql +11 -0
- sql_benchmarks/scripts/sql/group_by/duckdb/groupby_recommended_cte.sql +19 -0
- sql_benchmarks/scripts/sql/group_by/postgres/groupby_antipattern.sql +13 -0
- sql_benchmarks/scripts/sql/group_by/postgres/groupby_recommended_pk.sql +11 -0
- sql_benchmarks/scripts/sql/hypergraph/duckdb/q_1hop.sql +5 -0
- sql_benchmarks/scripts/sql/hypergraph/duckdb/q_2hop.sql +6 -0
- sql_benchmarks/scripts/sql/hypergraph/duckdb/q_full.sql +9 -0
- sql_benchmarks/scripts/sql/hypergraph/typedb/q_1hop.sql +4 -0
- sql_benchmarks/scripts/sql/hypergraph/typedb/q_2hop.sql +5 -0
- sql_benchmarks/scripts/sql/hypergraph/typedb/q_full.sql +6 -0
- sql_benchmarks/scripts/sql/joins/duckdb/join_antipattern.sql +5 -0
- sql_benchmarks/scripts/sql/joins/duckdb/join_recommended.sql +4 -0
- sql_benchmarks/scripts/sql/joins/postgres/join_antipattern.sql +5 -0
- sql_benchmarks/scripts/sql/joins/postgres/join_recommended.sql +4 -0
- sql_benchmarks/scripts/sql/null_logic/duckdb/2VL_identity.sql +14 -0
- sql_benchmarks/scripts/sql/null_logic/duckdb/3VL_identity.sql +5 -0
- sql_benchmarks/scripts/sql/null_logic/postgres/2VL_identity.sql +14 -0
- sql_benchmarks/scripts/sql/null_logic/postgres/3VL_identity.sql +5 -0
- sql_benchmarks/scripts/sql/null_sentinel/duckdb/2VL_identity.sql +14 -0
- sql_benchmarks/scripts/sql/null_sentinel/duckdb/3VL_identity.sql +5 -0
- sql_benchmarks/scripts/sql/null_sentinel/duckdb/sentinel_optimization.sql +16 -0
- sql_benchmarks/scripts/sql/null_sentinel/postgres/2VL_identity.sql +14 -0
- sql_benchmarks/scripts/sql/null_sentinel/postgres/3VL_identity.sql +5 -0
- sql_benchmarks/scripts/sql/null_sentinel/postgres/sentinel_optimization.sql +21 -0
- sql_benchmarks/scripts/sql/recursion/duckdb/recursive_cte.sql +26 -0
- sql_benchmarks/scripts/sql/recursion/postgres/recursive_cte.sql +30 -0
- sql_benchmarks/scripts/sql/recursive_graph/duckdb/q_bounded_3hop.sql +17 -0
- sql_benchmarks/scripts/sql/recursive_graph/duckdb/q_fixed_2hop.sql +6 -0
- sql_benchmarks/scripts/sql/recursive_graph/duckdb/q_transitive_closure.sql +17 -0
- sql_benchmarks/scripts/sql/recursive_graph/typedb/q_bounded_3hop.sql +6 -0
- sql_benchmarks/scripts/sql/recursive_graph/typedb/q_fixed_2hop.sql +5 -0
- sql_benchmarks/scripts/sql/recursive_graph/typedb/q_transitive_closure.sql +7 -0
- sql_benchmarks/scripts/sql/security/acid_resilience/duckdb/test.sql +1 -0
- sql_benchmarks/scripts/sql/selectivity/duckdb/q_0_1_percent.sql +1 -0
- sql_benchmarks/scripts/sql/selectivity/duckdb/q_10_percent.sql +1 -0
- sql_benchmarks/scripts/sql/selectivity/duckdb/q_1_percent.sql +1 -0
- sql_benchmarks/scripts/sql/selectivity/duckdb/q_20_percent.sql +1 -0
- sql_benchmarks/scripts/sql/selectivity/duckdb/q_5_percent.sql +1 -0
- sql_benchmarks/scripts/sql/selectivity/duckdb/q_filler.sql +1 -0
- sql_benchmarks/scripts/sql/selectivity/postgres/q_0_1_percent.sql +1 -0
- sql_benchmarks/scripts/sql/selectivity/postgres/q_10_percent.sql +1 -0
- sql_benchmarks/scripts/sql/selectivity/postgres/q_1_percent.sql +1 -0
- sql_benchmarks/scripts/sql/selectivity/postgres/q_20_percent.sql +1 -0
- sql_benchmarks/scripts/sql/selectivity/postgres/q_5_percent.sql +1 -0
- sql_benchmarks/scripts/sql/selectivity/postgres/q_filler.sql +1 -0
- sql_benchmarks/scripts/sql/selectivity/typedb/q_0_1_percent.sql +4 -0
- sql_benchmarks/scripts/sql/selectivity/typedb/q_10_percent.sql +4 -0
- sql_benchmarks/scripts/sql/selectivity/typedb/q_1_percent.sql +4 -0
- sql_benchmarks/scripts/sql/selectivity/typedb/q_20_percent.sql +4 -0
- sql_benchmarks/scripts/sql/selectivity/typedb/q_5_percent.sql +4 -0
- sql_benchmarks/scripts/sql/selectivity/typedb/q_filler.sql +4 -0
- sql_benchmarks/scripts/sql/sort_spill/duckdb/sort_full.sql +25 -0
- sql_benchmarks/scripts/sql/sort_spill/duckdb/sort_wide_rows.sql +25 -0
- sql_benchmarks/scripts/sql/sort_spill/postgres/sort_full.sql +25 -0
- sql_benchmarks/scripts/sql/sort_spill/postgres/sort_wide_rows.sql +26 -0
- sql_benchmarks/scripts/sql/tpch/duckdb/q3_shipping_priority.sql +25 -0
- sql_benchmarks/scripts/sql/tpch/postgres/q3_shipping_priority.sql +25 -0
- sql_benchmarks/utils/__init__.py +0 -0
- sql_benchmarks/utils/common.py +260 -0
- sql_benchmarks/utils/ddl.py +47 -0
- sql_benchmarks/utils/hasher.py +124 -0
- sql_benchmarks/utils/integrity_monitor.py +55 -0
- sql_benchmarks/utils/providers.py +184 -0
- sql_benchmarks/utils/scaling.py +88 -0
- sql_benchmarks/utils/schema.py +118 -0
- sql_benchmarks/utils/semantic_auditor.py +81 -0
- sql_benchmarks/utils/system.py +100 -0
- sql_benchmarks/validator.py +60 -0
- sqlbenchdag-0.1.0.dist-info/METADATA +221 -0
- sqlbenchdag-0.1.0.dist-info/RECORD +153 -0
- sqlbenchdag-0.1.0.dist-info/WHEEL +5 -0
- sqlbenchdag-0.1.0.dist-info/entry_points.txt +2 -0
- sqlbenchdag-0.1.0.dist-info/licenses/LICENSE +202 -0
- sqlbenchdag-0.1.0.dist-info/top_level.txt +4 -0
- venv/bin/activate_this.py +59 -0
|
@@ -0,0 +1,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;
|