11"""IssunDB adapter.
22
3- IssunDB is an embedded engine with no bulk COPY path, so nodes and edges are inserted
4- one row at a time through `add_node`/`add_edge`. IssunDB allocates its own NodeId per
5- node, so the build keeps a per-label map from the dataset id to the allocated NodeId
6- and resolves edge endpoints through it. The dataset id is stored as a node property so
7- queries can return it. The binding returns query results as JSON of the shape
3+ IssunDB is an embedded engine with a bulk `IMPORT DATABASE` path, so ingestion is one
4+ import call over a generated Parquet/JSONL bundle rather than per-row inserts. IssunDB
5+ allocates its own NodeId per node, so the build offsets each label's dataset id into a
6+ globally unique `_id` (the imported NodeId) while keeping the original `id` as a node
7+ property, and resolves edge endpoints through the same offsets. The `id` property is
8+ what the catalog's Cypher filters on; IssunDB auto-indexes every scalar node property,
9+ so those filters become index/range scans with no explicit index DDL (an explicit
10+ node `CREATE INDEX` would provision a full-text index instead, which the workload does
11+ not use). The binding returns query results as JSON of the shape
812`{"columns": [...], "records": [{"values": [...]}]}`.
913"""
1014
@@ -63,10 +67,12 @@ def build(self, data_dir: Path) -> BuildResult:
6367 current_offset += (max_id if max_id is not None else 0 ) + 1
6468
6569 # 3. Process and write node Parquet files with _id column
70+ n_node_rows = 0
6671 for label in self .schema .nodes :
6772 parquet_path = data_dir / "nodes" / f"{ label .name } .parquet"
6873 dst_parquet = import_dir / f"{ label .name } .parquet"
6974 df = pl .read_parquet (parquet_path )
75+ n_node_rows += df .height
7076 df = df .with_columns (
7177 (
7278 pl .col (self .schema .id_column ).cast (pl .Int64 ) + offsets [label .name ]
@@ -77,10 +83,12 @@ def build(self, data_dir: Path) -> BuildResult:
7783
7884 # 4. Convert and write edge tables with offset endpoints
7985 edges_start = time .perf_counter ()
86+ n_edge_rows = 0
8087 for rel in self .schema .rels :
8188 parquet_path = data_dir / "edges" / f"{ rel .name } .parquet"
8289 jsonl_path = import_dir / f"{ rel .name } .jsonl"
8390 df = pl .read_parquet (parquet_path )
91+ n_edge_rows += df .height
8492 df = df .with_columns (
8593 [
8694 (
@@ -118,9 +126,15 @@ def build(self, data_dir: Path) -> BuildResult:
118126 # Clean up import temp files
119127 shutil .rmtree (import_dir )
120128
121- # Split query time and attribute preparation to edges_seconds and nodes_seconds
122- nodes_seconds = nodes_prep_time + query_time * 0.5
123- edges_seconds = edges_prep_time + query_time * 0.5
129+ # `IMPORT DATABASE` is a single bulk call whose internal node and edge phases
130+ # are not separately measurable, so its time is apportioned across the two
131+ # reported phases by row volume (not an arbitrary 50/50 split). Each phase
132+ # also carries its own deterministic preparation cost (the offset rewrite and
133+ # the JSONL conversion), which is genuinely per-phase and measured directly.
134+ total_rows = n_node_rows + n_edge_rows
135+ node_frac = n_node_rows / total_rows if total_rows else 0.5
136+ nodes_seconds = nodes_prep_time + query_time * node_frac
137+ edges_seconds = edges_prep_time + query_time * (1.0 - node_frac )
124138
125139 return BuildResult (nodes_seconds , edges_seconds )
126140
0 commit comments