kglite 0.16.0

Pure-Rust embedded Cypher knowledge graph engine with in-memory, mmap, and disk storage, and agent-facing schema introspection
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
//! Columnar SET cost: O(changes), not O(type)

use super::*;

/// A one-row columnar `SET` must journal a pre-image for the node it changed,
/// not for every node of the type.
///
/// This is the guard for the cost regression the post-merge benchmark caught:
/// `MATCH (i:Item {id: …}) SET i.priority = …` on a saved 100k-node graph ran
/// ~1.8× slower than the whole-graph clone it replaced. The mechanism is the
/// end-of-batch handle-refresh sweep in `execute_set`, which re-points every
/// node's `Arc<ColumnStore>` at the forked master. That sweep goes through
/// `node_weight_mut_silent` — silent towards the WAL recorder, but until this
/// guard existed it fell through to the *recorded* `node_weight_mut` on
/// `MemoryGraph`, so a single-property write cloned a `NodeData` per node of
/// the type into the journal.
///
/// Why the existing guards cannot see it: `journalled_statements_copy_zero_nodes`
/// reads `BACKEND_CLONE_NODES`, which counts backend clones only — the journal
/// path deliberately clones no backend, so the counter reads zero whether the
/// journal captured one pre-image or two hundred. The cost lives entirely
/// inside the journal, so the counter has to as well.
///
/// **Phase 2 re-point.** The `NodeData` bound above is kept (a columnar SET
/// must still not clone node weights), and the cost oracle it was written for
/// is now stated in the mechanism that carries the cost: one
/// `UndoEntry::ColumnarCell` per `(row, property)` the statement changed. The
/// `ColumnarHandles` entry the old bound coexisted with is gone, so without the
/// cell count this assertion would pass just as happily for a journal that
/// captured the whole store — which is exactly what it did before Phase 2.
#[test]
fn a_columnar_set_journals_one_pre_image_per_changed_node() {
    use crate::graph::storage::undo::{
        journal_columnar_cells, journal_node_pre_images, reset_journal_columnar_cells,
        reset_journal_node_pre_images,
    };

    let mut graph = wide_columnar();
    reset_journal_node_pre_images();
    reset_journal_columnar_cells();
    run(&mut graph, "MATCH (i:Item {id: 7}) SET i.priority = 3");
    let captured = journal_node_pre_images();
    let cells = journal_columnar_cells();

    assert!(
        captured <= 2,
        "a one-row columnar SET captured {captured} node pre-images across \
         {WIDE_ITEMS} nodes of the type; it must be O(nodes changed), not \
         O(nodes of the type) — the handle-refresh sweep is being journalled"
    );
    assert_eq!(
        cells, 1,
        "a one-row, one-property columnar SET must journal exactly one cell \
         pre-image across {WIDE_ITEMS} nodes of the type; {cells} means the \
         capture is sized by something other than the change"
    );
}

/// The cost oracle's second dimension: **properties**, not rows.
///
/// A three-property `SET` on one row journals three cells and nothing else. The
/// arm above cannot see a per-statement or per-type constant (it would read 1
/// either way); this one separates "one entry per changed cell" from "one entry
/// per statement", which is the shape the replaced mechanism had.
#[test]
fn a_columnar_set_journals_one_cell_per_changed_property() {
    use crate::graph::storage::undo::{journal_columnar_cells, reset_journal_columnar_cells};

    let mut graph = wide_columnar();
    reset_journal_columnar_cells();
    run(
        &mut graph,
        "MATCH (i:Item {id: 7}) SET i.qty = 1, i.priority = 3, i.rank = 5",
    );
    assert_eq!(
        journal_columnar_cells(),
        3,
        "three changed cells must journal three pre-images"
    );

    // Two rows x one property is the other axis.
    reset_journal_columnar_cells();
    run(&mut graph, "MATCH (i:Item) WHERE i.id < 2 SET i.qty = 9");
    assert_eq!(
        journal_columnar_cells(),
        2,
        "two changed rows must journal two pre-images, not {WIDE_ITEMS}"
    );
}

/// The same bound on the **mapped** backend, which is where it is easiest to
/// lose and hardest to notice.
///
/// `node_weight_mut_silent` has a trait *default* that forwards to the recorded
/// `node_weight_mut`. `MemoryGraph` overrides it, which is what the arm above
/// pins; `MappedGraph` had no reason to until it gained a journal, and adding
/// the journal without the override re-creates the O(type)-per-write cost
/// exactly. Measured, not assumed: with the override removed this captured
/// **200** pre-images for a one-row `SET`, against 0 with it.
///
/// This arm and `the_mapped_silent_write_path_records_nothing` guard the same
/// override from opposite ends — one at the seam, one through the statement
/// that actually reaches it — because the seam has a second caller
/// (`mutation::batch`'s columnar detach/reattach, gated on
/// `is_mapped() || is_disk()`) that no Cypher statement reaches today and so
/// no end-to-end test can cover.
#[test]
fn a_mapped_columnar_set_journals_one_pre_image_per_changed_node() {
    use crate::graph::storage::undo::{
        journal_columnar_cells, journal_node_pre_images, reset_journal_columnar_cells,
        reset_journal_node_pre_images,
    };

    let mut graph = wide_columnar_mapped();
    reset_journal_node_pre_images();
    reset_journal_columnar_cells();
    run(&mut graph, "MATCH (i:Item {id: 7}) SET i.priority = 3");
    let captured = journal_node_pre_images();
    let cells = journal_columnar_cells();

    assert_eq!(
        cells, 1,
        "a one-row columnar SET on a mapped graph must journal exactly one \
         cell pre-image, not {cells}"
    );
    assert!(
        captured <= 2,
        "a one-row columnar SET on a mapped graph captured {captured} node \
         pre-images across {WIDE_ITEMS} nodes of the type; the mapped \
         handle-refresh sweep is being journalled"
    );
}

/// The same statement on a heap-resident (unmapped) graph, as the control.
///
/// Pins that the bound above is a property of the write path rather than of
/// the mapping or of this fixture's size: if the unmapped path ever started
/// capturing per type, the mapped assertion alone would not say which layer
/// regressed. This used to run against a de-columnarized graph, a shape
/// construction no longer produces.
#[test]
fn an_unmapped_set_journals_one_pre_image_per_changed_node() {
    use crate::graph::storage::undo::{journal_node_pre_images, reset_journal_node_pre_images};

    let mut graph = wide_columnar();
    reset_journal_node_pre_images();
    run(&mut graph, "MATCH (i:Item {id: 7}) SET i.priority = 3");
    let captured = journal_node_pre_images();

    assert!(
        captured <= 2,
        "a one-row SET on an unmapped graph captured {captured} node \
         pre-images across {WIDE_ITEMS} nodes"
    );
}

/// Two columnar writes to the same cell in one statement must both be
/// visible, and the second must win.
///
/// The mechanism has been rewritten twice under this assertion, which is why
/// the assertion is what got kept. Pre-D1-Phase-3: the first write forked away
/// from `1 + N` node handles and registered an end-of-clause re-point sweep.
/// Post-Phase-3, pre-Phase-2: the first write forked away from the undo
/// journal's whole-store pre-image and the second mutated the fork in place.
/// Now: **neither write forks anything** — the journal holds one
/// `UndoEntry::ColumnarCell` per write, both mutate the backend's own store in
/// place, and reverse replay would restore the *first* capture if the statement
/// failed. The observable is unchanged across all three.
#[test]
fn two_columnar_writes_in_one_statement_both_land() {
    let mut graph = wide_columnar();

    // Locate the node up front: `id` is an inline canonical field, not a
    // column-store property, so it cannot be used to read back through the
    // per-node handle. `qty` is columnar and seeded to the node's index.
    let idx = graph
        .graph
        .node_indices()
        .find(|i| {
            graph
                .graph
                .node_view(*i)
                .and_then(|n| n.get_property_value("qty"))
                .map(|v| v == crate::datatypes::Value::Int64(1))
                .unwrap_or(false)
        })
        .expect("fixture seeds qty = node index");

    // Two SET clauses in one statement, same type and same property. Both
    // journal a cell pre-image and both mutate the master IN PLACE.
    let allocation_before = Arc::as_ptr(graph.column_store("Item").expect("master"));
    run(
        &mut graph,
        "MATCH (n:Item {id: 1}) SET n.qty = 111 SET n.qty = 222",
    );
    assert!(
        std::ptr::eq(
            allocation_before,
            Arc::as_ptr(graph.column_store("Item").expect("master"))
        ),
        "neither write may fork the master"
    );

    // Read back through the public route. Both writes must be visible.
    let node = graph.graph.node_view(idx).expect("node still present");
    assert_eq!(
        node.get_property_value("qty"),
        Some(crate::datatypes::Value::Int64(222)),
        "both writes must be visible; reading 1 means the second write landed \
         somewhere the read route does not resolve"
    );

    // And nothing but the backend holds the master.
    let master = graph.column_store("Item").expect("master");
    assert_eq!(
        Arc::strong_count(master),
        1,
        "nothing but the backend may hold the master, or every write pays a \
         whole-store copy"
    );
}

/// A multi-node `CREATE` journals **one** append pre-image per type, not one
/// per created node.
///
/// The append undo is absolute — truncate the type's store back to the row
/// count the statement started at — so the first capture is the whole story and
/// every later one describes an intermediate state the first then overrides.
/// One per node meant one journal entry *and one `Arc<TypeSchema>` clone* per
/// created node (~4% of `CREATE`), all of it redundant.
///
/// Two types in one statement is the second axis: the dedup is per `(statement,
/// type)`, so a statement touching two stores must capture two pre-images — a
/// per-*statement* dedup would read 1 here and lose the second type's undo
/// entirely.
#[test]
fn a_multi_node_create_journals_one_append_pre_image_per_type() {
    use crate::graph::storage::undo::{journal_columnar_appends, reset_journal_columnar_appends};

    let mut graph = wide_columnar();

    reset_journal_columnar_appends();
    run(
        &mut graph,
        "CREATE (:Item {id: 900, name: 'x', qty: 1}), \
                (:Item {id: 901, name: 'y', qty: 2}), \
                (:Item {id: 902, name: 'z', qty: 3})",
    );
    assert_eq!(
        journal_columnar_appends(),
        1,
        "three rows appended to one type must journal one append pre-image"
    );

    reset_journal_columnar_appends();
    run(
        &mut graph,
        "CREATE (:Item {id: 910, name: 'p', qty: 1}), \
                (:Item {id: 911, name: 'q', qty: 2}), \
                (:Widget {id: 1, name: 'w'}), (:Widget {id: 2, name: 'v'})",
    );
    assert_eq!(
        journal_columnar_appends(),
        2,
        "two types must journal one append pre-image each — a per-statement \
         dedup would drop the second type's undo"
    );
}

/// The dedup's correctness half: a failed statement that created several nodes
/// of a **brand-new type** must leave no trace of the type at all.
///
/// This is the arm that fails if the dedup keeps the *last* pre-image instead
/// of the first: the last one names `prior_row_count = N-1` and
/// `store_was_new = false`, so the replay would truncate to N-1 rows and leave
/// an all-but-one-row store — and the type — behind.
#[test]
fn a_failed_multi_node_create_of_a_new_type_rolls_back_to_nothing() {
    let mut graph = wide_columnar();
    assert!(
        graph.column_store("Batch").is_none(),
        "precondition: the type must not exist yet, or this is vacuous"
    );

    assert_rolls_back(
        &mut graph,
        "CREATE (:Batch {id: 1, name: 'a', qty: 1}), \
                (:Batch {id: 2, name: 'b', qty: 2}), \
                (:Batch {id: 3, name: 'c', qty: 3}) \
         WITH 1 AS ignored MATCH (m:Item {id: 2}) \
         SET m.qty = duration({months: 2147483648})",
        None,
    );

    assert!(
        graph.column_store("Batch").is_none(),
        "a rolled-back CREATE of a new type left its master store behind"
    );
    assert_eq!(
        graph.type_indices.get("Batch").map(|b| b.len()),
        None,
        "and must leave no type bucket"
    );
}

/// The same, on a type whose store **already had rows**: the truncation has to
/// land on the pre-statement count exactly, not on an intermediate one.
///
/// `assert_rolls_back` fingerprints the master's rows, so a store truncated to
/// N-1 instead of N-3 shows up as a row-count mismatch.
#[test]
fn a_failed_multi_node_create_truncates_to_the_pre_statement_row_count() {
    let mut graph = wide_columnar();
    let before = graph
        .column_store("Item")
        .expect("fixture installs the master")
        .row_count();

    assert_rolls_back(
        &mut graph,
        "CREATE (:Item {id: 800, name: 'a', qty: 1}), \
                (:Item {id: 801, name: 'b', qty: 2}), \
                (:Item {id: 802, name: 'c', qty: 3}) \
         WITH 1 AS ignored MATCH (m:Item {id: 2}) \
         SET m.qty = duration({months: 2147483648})",
        None,
    );

    assert_eq!(
        graph
            .column_store("Item")
            .expect("master survives")
            .row_count(),
        before,
        "the rolled-back appends must truncate to the pre-statement row count"
    );
}

/// **Replaces `every_node_shares_the_master_column_store_handle`.**
///
/// That test pinned the pre-D1 design: `enable_columnar` pointed every node of
/// a type at the master, so its strong count was `1 + nodes-of-type` and every
/// first-write-of-a-statement forked the whole store. D1 Phase 3 deleted the
/// node-held handle, and this is the inverted assertion: *no* node holds one,
/// and the master is uniquely owned.
///
/// Keeping the coverage rather than the assertion is deliberate — the property
/// this file cares about is what the refcount implies for `Arc::make_mut`, and
/// that has flipped from "always copies" to "copies only under a checkpoint".
#[test]
fn no_node_holds_a_column_store_handle() {
    let graph = wide_columnar();
    let master = graph
        .column_store("Item")
        .expect("the fixture installs a master store for Item");

    assert_eq!(
        Arc::strong_count(master),
        1,
        "the backend must be the only owner of the master; a second handle \
         means something re-introduced a replica, and every columnar write \
         would silently go back to copying the whole store"
    );

    // Non-vacuity: the nodes really are columnar, they just carry row ids.
    let columnar = graph
        .graph
        .node_indices()
        .filter(|idx| {
            matches!(
                graph.graph.node_weight(*idx).map(|n| &n.properties),
                Some(PropertyStorage::Columnar(_))
            )
        })
        .count();
    assert_eq!(
        columnar, WIDE_ITEMS,
        "every node of the type must still be columnar, or the refcount above \
         is 1 because the fixture stopped being saved"
    );
}

/// **Replaces `fork_detection_is_a_no_op_while_nodes_hold_strong_handles`.**
///
/// The reference-count invariant the whole programme turns on. Phase 2
/// strengthened it from "uniquely owned *between* statements" to "uniquely
/// owned *always*, on a non-forked backend": the undo journal used to hold the
/// pre-statement store, so mid-statement the count was ≥ 2 and `Arc::make_mut`
/// forked; it now holds cell values only, so nothing but the backend ever owns
/// the master and the write mutates it in place.
///
/// The in-statement half is asserted through **allocation identity**, which is
/// the only way to see mid-statement ownership from outside the statement: an
/// `Arc::make_mut` that forked would leave the backend pointing at a different
/// allocation once the statement returned. The refcount alone cannot say that
/// — it reads 1 either way, because the fork is what dropped the second
/// handle. The `debug_assert!` inside `write_column_master` asserts the same
/// property at the instant it holds.
#[test]
fn the_master_is_uniquely_owned_between_statements() {
    let mut graph = wide_columnar();

    assert_eq!(
        Arc::strong_count(graph.column_store("Item").expect("master")),
        1,
        "precondition: uniquely owned before any statement"
    );
    let allocation_before = Arc::as_ptr(graph.column_store("Item").expect("master"));

    run(&mut graph, "MATCH (n:Item {id: 1}) SET n.qty = 111");

    assert_eq!(
        Arc::strong_count(graph.column_store("Item").expect("master")),
        1,
        "a committed statement must leave nothing else holding the master"
    );
    assert!(
        std::ptr::eq(
            allocation_before,
            Arc::as_ptr(graph.column_store("Item").expect("master"))
        ),
        "the statement replaced the master's allocation, so `Arc::make_mut` \
         forked mid-statement: something held a second handle while the write \
         ran and the write copied {WIDE_ITEMS} rows to change one cell"
    );

    // And the same across a statement that *fails*, where the journal is read
    // rather than dropped. A replay that reinstalled a store would show here.
    let allocation_before = Arc::as_ptr(graph.column_store("Item").expect("master"));
    expect_failure(
        &mut graph,
        "MATCH (n:Item {id: 2}) SET n.qty = 222 \
         WITH n MATCH (m:Item {id: 3}) SET m.qty = duration({months: 2147483648})",
        None,
    );
    assert!(
        std::ptr::eq(
            allocation_before,
            Arc::as_ptr(graph.column_store("Item").expect("master"))
        ),
        "a rolled-back statement must restore cells into the live store, not \
         swap a pre-statement copy back in"
    );
    assert_eq!(
        Arc::strong_count(graph.column_store("Item").expect("master")),
        1,
        "and must leave the master uniquely owned"
    );
    assert_eq!(
        graph
            .graph
            .node_view(
                graph
                    .graph
                    .node_indices()
                    .find(|i| graph.graph.get_node_id(*i)
                        == Some(crate::datatypes::Value::Int64(1)))
                    .expect("node 1")
            )
            .and_then(|n| n.get_property_value("qty")),
        Some(crate::datatypes::Value::Int64(111)),
        "and the write must actually be visible"
    );
}