Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 31 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,24 @@ reconstructed from Git history, release commits, and the shipped docs.
- Validate row-to-Arrow conversion with borrowed identity membership instead of
constructing discarded adjacency, and copy string properties directly into
Arrow buffers without temporary owned String clones.
- Add automatic Cypher routing through `grust_datafusion::cypher::RoutedGraph`:
one graph captured as a typed index and an Arrow snapshot, one bounded-read
admission per query, and DataFusion execution for single-node scan plans
whose every physical operator charges candidate work and intermediate bytes
against the caller's `ReadQueryPolicy`. Relationship joins, unsupported
shapes, small graphs and plans over a limit stay on the reference executor;
explain reports the chosen route and why DataFusion was declined.
- Add `run_prepared_read_query_indexed`, so a declined route keeps the original
admission and deadline instead of re-preparing the query.
- Split large single-batch Arrow tables into contiguous zero-copy partitions.
Upstream round-robin repartitioning charged every queued slice the full size
of its shared parent buffers, which intermittently exhausted a 256 MiB pool on
a one-million-node scan with nothing copied.
- Add `LadybugConfig::max_db_bytes`. Ladybug reserves its maximum database
size (8 TiB by default) as virtual address space per open database, so one
x86-64 Linux process can hold only about fifteen at once. `grust-ladybug`
unit tests now cap each database at 16 GiB, as Ladybug's own tests do;
parallel test threads previously failed intermittently with `Mmap ... failed`.

## 0.20.0 — Brine — 2026-09-14

Expand All @@ -21,6 +39,19 @@ reconstructed from Git history, release commits, and the shipped docs.
- Admit snapshot relationship-ordinal payload and construction work before
allocation, retaining reservations with emitted buffers. Combined input-policy
capture preserves the prepared request deadline and exact serialized limits.
- grust-turso no longer creates `<prefix>_edges_from_idx`, and bootstrap
drops it from existing stores. The edge primary key
`(from_id, label, to_id, identity_key)` leads with `from_id`, and Turso's
planner already answered every source lookup and traversal hop from that
key, so the index only cost a write per edge. Query plans and results are
unchanged. Other SQL dialects keep the index (`GraphSqlDialect::
edge_source_index`, default `true`). With the bulk_load example on a
4-core host, 1M edges: MVCC 12.7k -> 15.9k edges/s, WAL 43.6k -> 51.5k.
- MVCC loads end in a `TRUNCATE` checkpoint inside `put_graph`, as WAL loads
already did, so a load returns durable in the database file with the MVCC
in-memory store drained.
- Optional `mimalloc` feature on grust-turso: Turso's own default allocator,
installed as the binary's global allocator (MVCC loads about 9% faster).

## 0.19.0 — Mantis — 2026-09-14

Expand Down
19 changes: 19 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

13 changes: 13 additions & 0 deletions benchmarks/arrow-pipelines/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -103,3 +103,16 @@ diagnostic for physical plans and batch-versus-slice memory reporting. It
executes the captured physical plan, retains each error/mismatch and checks
the arithmetic count oracle. It does not report performance timings. See
[evidence](evidence/cypher-plan-memory-c658abc) for the captured partition plans.

### Warm routed-read profile

`cypher_route <repeats> <nodes>...` captures one `RoutedGraph` per size and then
times the complete bounded-read entrypoint on each forced route, alternating
route order: admission, route planning, execution and portable output. Capture
(index plus Arrow snapshot) is reported once per size and excluded from trials,
so this measures reuse of warm representations, unlike `cypher_end_to_end`.
Both routes run under the same `ReadQueryPolicy`; DataFusion additionally uses a
1 GiB tracked pool, no spill and four target partitions. Each query's first
result is the oracle for every later trial on either route; mismatches, errors
and all timings are retained. Its measurements set
`DEFAULT_MIN_DATAFUSION_NODES` for this scan workload only.
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
# Automatic routing: partition fix and warm route profile

Capitola (aarch64 macOS, 10 cores, 64 GiB), stable Rust 1.97.1, run
2026-09-14 23:19–23:22 UTC. The host was an interactive workstation during the
run (Chrome, WindowServer, Spotlight, Docker Desktop); load averages were about
27 at start and end, recorded in `host-start.txt` and `host-end.txt`. Exact
binary hashes are in `hashes.txt`:

- `before_cypher_end_to_end` is the retained `6544dc4` binary that produced the
[original failure](../cypher-conversion-6544dc4) (same SHA-256).
- `after_cypher_end_to_end` and `cypher_route` are built from branch
`claude/auto-datafusion-route` (base `a134e05`) with the zero-copy partition
split and the router. `cypher_route` forces each route, so the threshold
constant later set from these results does not affect them.

## Partition fix, one million nodes

Ten process pairs, alternating order, each running three trials per route with
the unchanged 256 MiB pool, spill disabled and four target partitions:

| Binary | DataFusion trials | Memory exhaustion | Median DataFusion total (s) |
| --- | ---: | ---: | ---: |
| before | 30 | 2 | 0.798 |
| after | 30 | 0 | 0.797 |

Both before-binary failures are the retained `Memory Exhausted while SpillPool
(DiskManager is disabled)` error; all 60 indexed trials passed. Two of thirty
against none of thirty is not statistically decisive by itself (two-sided
Fisher p ≈ 0.49). The structural evidence is stronger: the fixed physical plan
contains no `RoundRobinBatch` repartition (regression
`single_large_batches_are_split_without_round_robin`), and DataFusion 55.1
`repartition/mod.rs` is where each queued slice was charged its shared parent
buffers. The fix did not change measured DataFusion time.

## Warm route profile

`cypher_route 5 1000 3000 10000 30000 100000 300000 1000000`: one `RoutedGraph`
per size, then the complete bounded-read entrypoint on each forced route, five
alternating trials. All trials passed with identical results on both routes.
Median seconds per query (q0 filtered count, q1 top-7 ids by ORDER BY, q2
group-by with ORDER BY):

| Nodes | q0 reference | q0 DataFusion | q1 reference | q1 DataFusion | q2 reference | q2 DataFusion |
| ---: | ---: | ---: | ---: | ---: | ---: | ---: |
| 1,000 | 0.00087 | 0.00098 | 0.00088 | 0.00055 | 0.00172 | 0.00090 |
| 3,000 | 0.00223 | 0.00058 | 0.00227 | 0.00051 | 0.00449 | 0.00090 |
| 10,000 | 0.00809 | 0.00072 | 0.00828 | 0.00096 | 0.01746 | 0.00106 |
| 30,000 | 0.02698 | 0.00086 | 0.02802 | 0.00099 | 0.05764 | 0.00109 |
| 100,000 | 0.09058 | 0.00096 | 0.09583 | 0.00109 | 0.19741 | 0.00119 |
| 300,000 | 0.28274 | 0.00141 | 0.30139 | 0.00143 | 0.60173 | 0.00144 |
| 1,000,000 | 0.97369 | 0.00224 | 1.07067 | 0.00304 | 1.99825 | 0.00228 |

One-time capture (index plus Arrow snapshot) took 0.0012 s at 1,000 nodes,
0.057 s at 100,000 and 1.055 s at 1,000,000, and is excluded from trials. The
reference route was faster only for q0 at 1,000 nodes, so
`DEFAULT_MIN_DATAFUSION_NODES` is 3,000. These are single-node scans on one
loaded host; they do not qualify joins, other shapes, other hosts or backends.
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
3f9578586e43a07c8f7c2e970ba4489ab0c8f3ece5e7542a2384d89469f54443 before_cypher_end_to_end
d4f4f68ad822f96c8fa54792f13004d4b49d681775b59782ae4aa1aa3341fd6c after_cypher_end_to_end
5e262f2d5a0914099da9991efd82cd3698e5e738184ee6d5a89c5393675520aa cypher_route
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Mon Sep 14 23:22:45 UTC 2026
1:22 up 3 days, 7:24, 6 users, load averages: 26.96 25.72 24.88
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
Mon Sep 14 23:19:22 UTC 2026
1:19 up 3 days, 7:20, 6 users, load averages: 27.14 26.65 24.98
10
68719476736
%CPU COMMAND
43.8 /Applications/Google Chrome.app/Contents/Frameworks/Google Chrome Framework.framework/Versions/152.0.7977.84/Helpe
24.1 /System/Library/PrivateFrameworks/SkyLight.framework/Resources/WindowServer -daemon
24.1 /System/Library/Frameworks/CoreServices.framework/Versions/A/Frameworks/Metadata.framework/Versions/A/Support/mds
23.6 /Applications/Google Chrome.app/Contents/Frameworks/Google Chrome Framework.framework/Versions/152.0.7977.84/Helpe
15.3 /System/Library/Frameworks/CoreServices.framework/Versions/A/Frameworks/Metadata.framework/Versions/A/Support/mds_
12.0 /System/Library/Frameworks/WebKit.framework/Versions/A/XPCServices/com.apple.WebKit.WebContent.xpc/Contents/MacOS/
10.2 /Applications/Docker.app/Contents/MacOS/Docker Desktop.app/Contents/Frameworks/Docker Desktop Helper (Renderer).ap
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{"event":"configuration","source":"c658abce7ff26101f1ea75474098fac002e21de9-dirty","nodes":1000000,"repeats":3,"query":"MATCH (n:N) WHERE n.bucket = 3 RETURN count(*) AS count","deadline":null,"datafusion_working_memory_bytes":268435456,"datafusion_target_partitions":4,"datafusion_input_partitions":1,"datafusion_output_rows":1,"datafusion_output_bytes":1024,"indexed_memory_limit":null,"spill":false,"boundary":"row graph fixture excluded; cold representation per trial; preparation includes index or Arrow conversion and engine registration; query includes parsing, planning, execution and portable output; teardown excluded; no equivalent full resource envelope or automatic routing threshold claimed"}
{"event":"trial","route":"indexed","trial":0,"status":"pass","expected":62500,"columns":["count"],"rows":[[{"type":"int","value":62500}]],"preparation_seconds":0.1175565,"query_seconds":1.641187583,"total_seconds":1.758744083}
{"event":"trial","route":"datafusion","trial":0,"status":"pass","expected":62500,"columns":["count"],"rows":[[{"type":"int","value":62500}]],"preparation_seconds":0.787802083,"query_seconds":0.011109917,"total_seconds":0.798912}
{"event":"trial","route":"datafusion","trial":1,"status":"pass","expected":62500,"columns":["count"],"rows":[[{"type":"int","value":62500}]],"preparation_seconds":0.7819865,"query_seconds":0.002232125,"total_seconds":0.7842186250000001}
{"event":"trial","route":"indexed","trial":1,"status":"pass","expected":62500,"columns":["count"],"rows":[[{"type":"int","value":62500}]],"preparation_seconds":0.106516167,"query_seconds":1.526532,"total_seconds":1.633048167}
{"event":"trial","route":"indexed","trial":2,"status":"pass","expected":62500,"columns":["count"],"rows":[[{"type":"int","value":62500}]],"preparation_seconds":0.117787666,"query_seconds":1.321058083,"total_seconds":1.438845749}
{"event":"trial","route":"datafusion","trial":2,"status":"pass","expected":62500,"columns":["count"],"rows":[[{"type":"int","value":62500}]],"preparation_seconds":0.791805042,"query_seconds":0.00229525,"total_seconds":0.794100292}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{"event":"configuration","source":"6544dc410cf57addd06e2ba2057762ba713e953c","nodes":1000000,"repeats":3,"query":"MATCH (n:N) WHERE n.bucket = 3 RETURN count(*) AS count","deadline":null,"datafusion_working_memory_bytes":268435456,"datafusion_target_partitions":4,"datafusion_input_partitions":1,"datafusion_output_rows":1,"datafusion_output_bytes":1024,"indexed_memory_limit":null,"spill":false,"boundary":"row graph fixture excluded; cold representation per trial; preparation includes index or Arrow conversion and engine registration; query includes parsing, planning, execution and portable output; teardown excluded; no equivalent full resource envelope or automatic routing threshold claimed"}
{"event":"trial","route":"indexed","trial":0,"status":"pass","expected":62500,"columns":["count"],"rows":[[{"type":"int","value":62500}]],"preparation_seconds":0.113686375,"query_seconds":1.9037511249999999,"total_seconds":2.0174374999999998}
{"event":"trial","route":"datafusion","trial":0,"status":"pass","expected":62500,"columns":["count"],"rows":[[{"type":"int","value":62500}]],"preparation_seconds":0.8163245,"query_seconds":0.012801583,"total_seconds":0.829126083}
{"event":"trial","route":"datafusion","trial":1,"status":"pass","expected":62500,"columns":["count"],"rows":[[{"type":"int","value":62500}]],"preparation_seconds":0.809607208,"query_seconds":0.002892708,"total_seconds":0.8124999159999999}
{"event":"trial","route":"indexed","trial":1,"status":"pass","expected":62500,"columns":["count"],"rows":[[{"type":"int","value":62500}]],"preparation_seconds":0.116661792,"query_seconds":1.557994375,"total_seconds":1.674656167}
{"event":"trial","route":"indexed","trial":2,"status":"pass","expected":62500,"columns":["count"],"rows":[[{"type":"int","value":62500}]],"preparation_seconds":0.106825584,"query_seconds":1.303186875,"total_seconds":1.410012459}
{"event":"trial","route":"datafusion","trial":2,"status":"pass","expected":62500,"columns":["count"],"rows":[[{"type":"int","value":62500}]],"preparation_seconds":0.79118775,"query_seconds":0.002813458,"total_seconds":0.794001208}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{"event":"configuration","source":"c658abce7ff26101f1ea75474098fac002e21de9-dirty","nodes":1000000,"repeats":3,"query":"MATCH (n:N) WHERE n.bucket = 3 RETURN count(*) AS count","deadline":null,"datafusion_working_memory_bytes":268435456,"datafusion_target_partitions":4,"datafusion_input_partitions":1,"datafusion_output_rows":1,"datafusion_output_bytes":1024,"indexed_memory_limit":null,"spill":false,"boundary":"row graph fixture excluded; cold representation per trial; preparation includes index or Arrow conversion and engine registration; query includes parsing, planning, execution and portable output; teardown excluded; no equivalent full resource envelope or automatic routing threshold claimed"}
{"event":"trial","route":"indexed","trial":0,"status":"pass","expected":62500,"columns":["count"],"rows":[[{"type":"int","value":62500}]],"preparation_seconds":0.119819209,"query_seconds":1.682823875,"total_seconds":1.802643084}
{"event":"trial","route":"datafusion","trial":0,"status":"pass","expected":62500,"columns":["count"],"rows":[[{"type":"int","value":62500}]],"preparation_seconds":0.8161745,"query_seconds":0.003446916,"total_seconds":0.819621416}
{"event":"trial","route":"datafusion","trial":1,"status":"pass","expected":62500,"columns":["count"],"rows":[[{"type":"int","value":62500}]],"preparation_seconds":0.795003042,"query_seconds":0.0020855,"total_seconds":0.797088542}
{"event":"trial","route":"indexed","trial":1,"status":"pass","expected":62500,"columns":["count"],"rows":[[{"type":"int","value":62500}]],"preparation_seconds":0.108852917,"query_seconds":1.5090406669999998,"total_seconds":1.617893584}
{"event":"trial","route":"indexed","trial":2,"status":"pass","expected":62500,"columns":["count"],"rows":[[{"type":"int","value":62500}]],"preparation_seconds":0.119839833,"query_seconds":1.2847146249999999,"total_seconds":1.4045544579999998}
{"event":"trial","route":"datafusion","trial":2,"status":"pass","expected":62500,"columns":["count"],"rows":[[{"type":"int","value":62500}]],"preparation_seconds":0.786687583,"query_seconds":0.002204083,"total_seconds":0.7888916659999999}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{"event":"configuration","source":"6544dc410cf57addd06e2ba2057762ba713e953c","nodes":1000000,"repeats":3,"query":"MATCH (n:N) WHERE n.bucket = 3 RETURN count(*) AS count","deadline":null,"datafusion_working_memory_bytes":268435456,"datafusion_target_partitions":4,"datafusion_input_partitions":1,"datafusion_output_rows":1,"datafusion_output_bytes":1024,"indexed_memory_limit":null,"spill":false,"boundary":"row graph fixture excluded; cold representation per trial; preparation includes index or Arrow conversion and engine registration; query includes parsing, planning, execution and portable output; teardown excluded; no equivalent full resource envelope or automatic routing threshold claimed"}
{"event":"trial","route":"indexed","trial":0,"status":"pass","expected":62500,"columns":["count"],"rows":[[{"type":"int","value":62500}]],"preparation_seconds":0.108929542,"query_seconds":1.602852959,"total_seconds":1.711782501}
{"event":"trial","route":"datafusion","trial":0,"status":"pass","expected":62500,"columns":["count"],"rows":[[{"type":"int","value":62500}]],"preparation_seconds":0.790513042,"query_seconds":0.004116166,"total_seconds":0.794629208}
{"event":"trial","route":"datafusion","trial":1,"status":"pass","expected":62500,"columns":["count"],"rows":[[{"type":"int","value":62500}]],"preparation_seconds":0.792127334,"query_seconds":0.0027765,"total_seconds":0.794903834}
{"event":"trial","route":"indexed","trial":1,"status":"pass","expected":62500,"columns":["count"],"rows":[[{"type":"int","value":62500}]],"preparation_seconds":0.106862708,"query_seconds":1.534455042,"total_seconds":1.64131775}
{"event":"trial","route":"indexed","trial":2,"status":"pass","expected":62500,"columns":["count"],"rows":[[{"type":"int","value":62500}]],"preparation_seconds":0.111730125,"query_seconds":1.302880542,"total_seconds":1.414610667}
{"event":"trial","route":"datafusion","trial":2,"status":"pass","expected":62500,"columns":["count"],"rows":[[{"type":"int","value":62500}]],"preparation_seconds":0.791784167,"query_seconds":0.002815917,"total_seconds":0.7946000839999999}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{"event":"configuration","source":"c658abce7ff26101f1ea75474098fac002e21de9-dirty","nodes":1000000,"repeats":3,"query":"MATCH (n:N) WHERE n.bucket = 3 RETURN count(*) AS count","deadline":null,"datafusion_working_memory_bytes":268435456,"datafusion_target_partitions":4,"datafusion_input_partitions":1,"datafusion_output_rows":1,"datafusion_output_bytes":1024,"indexed_memory_limit":null,"spill":false,"boundary":"row graph fixture excluded; cold representation per trial; preparation includes index or Arrow conversion and engine registration; query includes parsing, planning, execution and portable output; teardown excluded; no equivalent full resource envelope or automatic routing threshold claimed"}
{"event":"trial","route":"indexed","trial":0,"status":"pass","expected":62500,"columns":["count"],"rows":[[{"type":"int","value":62500}]],"preparation_seconds":0.124976875,"query_seconds":1.613661167,"total_seconds":1.738638042}
{"event":"trial","route":"datafusion","trial":0,"status":"pass","expected":62500,"columns":["count"],"rows":[[{"type":"int","value":62500}]],"preparation_seconds":0.799491125,"query_seconds":0.003307584,"total_seconds":0.8027987089999999}
{"event":"trial","route":"datafusion","trial":1,"status":"pass","expected":62500,"columns":["count"],"rows":[[{"type":"int","value":62500}]],"preparation_seconds":0.805642917,"query_seconds":0.002171541,"total_seconds":0.8078144580000001}
{"event":"trial","route":"indexed","trial":1,"status":"pass","expected":62500,"columns":["count"],"rows":[[{"type":"int","value":62500}]],"preparation_seconds":0.112691958,"query_seconds":1.543033416,"total_seconds":1.6557253740000002}
{"event":"trial","route":"indexed","trial":2,"status":"pass","expected":62500,"columns":["count"],"rows":[[{"type":"int","value":62500}]],"preparation_seconds":0.108725583,"query_seconds":1.302997875,"total_seconds":1.411723458}
{"event":"trial","route":"datafusion","trial":2,"status":"pass","expected":62500,"columns":["count"],"rows":[[{"type":"int","value":62500}]],"preparation_seconds":0.795059167,"query_seconds":0.002525375,"total_seconds":0.7975845420000001}
Loading