Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
5b5a708
Route bounded Cypher scans to DataFusion automatically
alexy Sep 14, 2026
1d1e41f
grust-turso: retire the unused edge-source index; MVCC loads end in T…
alexy Sep 14, 2026
3e8b13a
grust-turso: MVCC stores run their automatic checkpoints PASSIVE
alexy Sep 14, 2026
a26fdff
grust-turso: MVCC loads run without automatic checkpoints
alexy Sep 15, 2026
0f08407
Coordinate grust-best reruns across grust, eigen, quegee and lakecat
alexy Sep 15, 2026
0291906
grust-turso: MVCC loads checkpoint every 64 MiB instead of never
alexy Sep 15, 2026
41f352d
Revert "grust-turso: MVCC stores run their automatic checkpoints PASS…
alexy Sep 15, 2026
3d3ba0b
Revert "grust-turso: MVCC loads checkpoint every 64 MiB instead of ne…
alexy Sep 15, 2026
5dff9ac
Revert "grust-turso: MVCC loads run without automatic checkpoints"
alexy Sep 15, 2026
56ceff0
grust-turso: connect_shared handles and a hot-node concurrent-writes …
alexy Sep 15, 2026
8b1b140
grust-turso: MVCC loads skip per-commit fsync; synchronous is settable
alexy Sep 15, 2026
2b6e536
grust-turso: MVCC group commit for concurrent single-statement writes
alexy Sep 15, 2026
383ca18
grust-turso: MVCC loads into an empty table build its secondary index…
alexy Sep 15, 2026
9fdbdb9
Revert "grust-turso: MVCC loads into an empty table build its seconda…
alexy Sep 15, 2026
6741e85
grust-turso: probe reopening a WAL-loaded database as MVCC
alexy Sep 15, 2026
438e697
grust-turso: probe switching a live MVCC store to WAL and back
alexy Sep 15, 2026
71bf278
grust-turso: opt-in MVCC bulk loads through WAL
alexy Sep 15, 2026
de0d44f
grust-turso: parallel MVCC bulk loads
alexy Sep 15, 2026
f6d3391
grust-turso: parallel MVCC loads checkpoint between rounds
alexy Sep 15, 2026
91f6e3f
grust-turso: parallel load writers get a small page cache and no GC pass
alexy Sep 16, 2026
48b4252
grust-turso: a load into empty tables inserts without the conflict cl…
alexy Sep 16, 2026
618caa0
grust-turso: round size and batch size are tunable for load sweeps
alexy Sep 16, 2026
e5d7b4e
grust-turso: drop the fresh-load INSERT path, which measured slower
alexy Sep 16, 2026
3f1ed19
grust-turso: put_graph loads with foreign keys off on every path
alexy Sep 16, 2026
acac24e
grust-turso: back off before retrying a conflicted concurrent transac…
alexy Sep 16, 2026
c8ec310
grust-turso: set_mvcc_group_commit toggles Turso's engine group commit
alexy Sep 16, 2026
abea518
grust: turso-mimalloc feature forwards Turso's allocator through the …
alexy Sep 17, 2026
1369f37
docs: "Turso under strain" book chapter and Brine post section
alexy Sep 17, 2026
3a73954
Stamp Brine TextPack 0.20.0-1369f3 with the Turso section
alexy Sep 17, 2026
6b8a3ba
book: Turso chapter does not compare NORMAL-sync A4 against Neo4j
alexy Sep 17, 2026
a04ebd7
book: A7 is not compared against Neo4j; the gap is in the harness path
alexy Sep 17, 2026
3674fc5
book: Turso's default build uses mimalloc; Grust's facade is what tur…
alexy Sep 17, 2026
6af6dc8
Rebuild book with the Turso under strain chapter
alexy Sep 17, 2026
02d1e80
grust-ladybug: copy_bench example times every bulk-load path; lbug 0.…
alexy Sep 17, 2026
836d482
copy_bench: free functions instead of a borrowing struct
alexy Sep 17, 2026
d0e2409
copy_bench: VARIANTS selects which paths run
alexy Sep 18, 2026
6a4d7a5
copy_bench: rel tables reference their own node tables; CSR offsets a…
alexy Sep 18, 2026
3148a86
grust-ladybug: bulk loads COPY from a temporary CSV; empty tables ski…
alexy Sep 18, 2026
93c7180
grust-ladybug: fix bulk-copy compile errors (borrow, Value conversion)
alexy Sep 18, 2026
f84f04e
grust-ladybug: round-trip test ignores the id reads add to props
alexy Sep 18, 2026
6af2006
grust-ladybug: the round-trip test's second put carries its endpoints
alexy Sep 18, 2026
da0ddea
Merge pull request #7 from querygraph/ladybug-bulk-copy
alexy Sep 18, 2026
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.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ categories = ["database", "data-structures"]
[workspace.dependencies]
async-trait = "0.1.89"
helix-db = "=3.0.0"
lbug = "0.20.2"
lbug = "0.20.4"
lancedb = "0.30.0"
redis = "1.6.0"
r2d2 = "0.8.10"
Expand Down
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}
Loading