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
//! The in-memory columnar master-store write path.
//!
//! Extracted from `write.rs` rather than left in place: `write.rs` reached its
//! 2500-line ceiling, and project doctrine is to split a file that outgrows it
//! rather than raise the allowlist. These three items are the natural seam --
//! they are the only ones that reach the per-type master `Arc<ColumnStore>`,
//! and `execute_set` / `execute_remove` must agree on all of them (a property
//! removed on a node's forked store while the master keeps the value is
//! resurrected by the next clause's handle sweep).
use crateValue;
use crate;
use crate;
use crate;
use NodeIndex;
use Arc;
/// One property write aimed at a node type's master column store.
///
/// `row_id` is `None` for a node whose properties are not `Columnar`, which is
/// one of the fallthrough conditions rather than a caller error — carrying the
/// `Option` here keeps the whole "can this go through the master?" decision in
/// one place.
pub
/// Whether a master-store write owes its caller the cell's prior value.
///
/// `Skip` is not an optimisation of the *journal* — the undo pre-image is taken
/// either way — it is the read the caller does not consume. `SET` discards it
/// (its index maintenance reads the old value through `node_view`, before the
/// write); `REMOVE` returns it. Reading it regardless cost a `Value` clone per
/// written row, which is an allocation per row for a string property.
pub
/// Write one property through the in-memory columnar master store, reporting
/// whether it landed there.
///
/// The fast path for `Columnar` storage: route the write through the per-type
/// master `Arc<ColumnStore>` once, instead of through each node's own handle.
/// Every node of the type points at the same allocation, so `Arc::make_mut` on
/// a *node's* handle would clone the whole store on every write — O(N²) for a
/// batch `SET`. Going through the master forks once; the per-node handles are
/// re-pointed in a single sweep at the end of the clause.
///
/// Returns `false` — leaving the caller to fall through to the per-node setter
/// — for disk-backed graphs (which have their own write path), for non-
/// `Columnar` nodes, for a `Columnar` node whose type is absent from
/// `column_stores`, and for `title`/`name`.
///
/// `title`/`name` are still excluded, but no longer because the title lives
/// somewhere else: the fallthrough now writes it through
/// [`GraphWrite::set_node_title`](crate::graph::storage::GraphWrite::set_node_title),
/// which lands in the store's reserved `__title__` column for every title-write
/// path (Cypher `SET`, `add_nodes` update/replace, connection titles). The
/// save-side consolidation chokepoint that used to reconcile an inline override
/// against a stale column — and rebuilt every store to do it (petekSuite bug 2)
/// — has nothing left to reconcile. The exclusion here is a routing detail: a
/// title is not a schema slot, so it cannot go through the cell writer below.
///
/// The property name is interned into the graph's `StringInterner` *before*
/// `column_stores` is borrowed. The per-node path gets this free via
/// `node.set_property(…, &mut graph.interner)`; the master path once used
/// `InternedKey::from_str()`, which only hashes, leaving `save()` unable to
/// resolve the key back to a string at serialize time. Symptom: every
/// Cypher-`SET` property on a 0.8.39 in-memory Sodir-scale graph survived
/// in-memory but vanished after save+load, with
/// `BUG: InternedKey N not found in StringInterner`.
///
/// # Journals, and why the capture must come first
///
/// - **WAL**: the write bypasses the recorded `GraphWrite` path, so the one
/// mutated node is captured explicitly. Exactly one node — there is no
/// end-of-clause sweep any more, because no node holds a store handle to
/// re-point (D1 Phase 3).
/// - **Undo**: the cell's prior value is captured, **before** the write, as
/// [`UndoEntry::ColumnarCell`]. That ordering is load-bearing — after the
/// write the prior value is gone — but it no longer forces a copy of
/// anything: the journal holds an `Option<Value>`, never a handle on the
/// store, so the master stays uniquely owned and `Arc::make_mut` below
/// mutates one cell in place whether or not a checkpoint is open.
///
/// The uniquely-owned half is asserted rather than argued — see the
/// `debug_assert!` at the write itself and
/// `dir_graph::rollback_tests::the_master_is_uniquely_owned_between_statements`.
pub
/// One resolved cell of a master column store — the addressing
/// [`write_column_master`] needs, with every name already interned.
pub
/// Write one cell of a type's master column store, with the journalling both
/// halves of the engine need.
///
/// The single place a columnar property value is written. Returns the prior
/// value (`None` when the type has no store, which is also the "nothing was
/// written" signal).
///
/// Ordering is the correctness argument, not a detail:
/// 1. read the cell's prior value (and, if the write introduces a new
/// property, the pre-growth schema) into the undo journal — after step 3
/// the prior value no longer exists anywhere;
/// 2. read the prior cell value again for the caller that asked for one — the
/// journal's copy is consumed by the journal;
/// 3. `Arc::make_mut` and write — **in place**, because nothing else holds the
/// master, which is what makes a one-cell write cost O(1);
/// 4. note the one mutated node for the WAL, since this bypasses the recorded
/// `GraphWrite` path.
///
/// # What is resolved once per statement, and what per row
///
/// The store handle, the type key, the property key and the key's column slot
/// are all facts about the `(type, property)` pair. The row loop used to
/// re-derive all four — a type-name hash, a `column_stores` probe for the
/// declared-type check, a second probe for the write, and two `TypeSchema`
/// lookups — so a 100k-row `SET` paid them 100k times to write one column.
/// What is left per row is the journal capture, one `column_stores` probe, one
/// slot lookup answered through a *shared* borrow, and the cell write. The two
/// `Arc::make_mut` uniqueness checks (store, then column) stay: they are the
/// price of the copy-on-write sharing a fork and a held view rely on, and
/// nothing safe removes them.
pub
/// Hand the node's pre-write state to the change-capture seam, before this
/// write destroys it.
///
/// **This is a choke point, not a hook, and the difference is the whole
/// point.** A columnar write goes straight into the master `ColumnStore`, so
/// no recorded `GraphWrite` call describes it and the seam is told after the
/// fact, by the `note_recorded_node_upsert` at the end of
/// [`write_column_master`]. A before-image read *there* is read after
/// `set_at_slot` has already replaced the value it claims to describe: the
/// event then reports the new value as `before`, while its `after` half and
/// its kind stay correct, so nothing else in the stream looks wrong.
/// `cdc::tests::a_columnar_set_captures_the_value_it_overwrote` fails with
/// `before == after` if this call is removed.
///
/// The seam *offers*-then-claims rather than recording immediately, so a
/// write that turns out not to happen leaves nothing behind — see
/// `RecordingGraph::note_node_before`.
///
/// Costs one bool read when enrichment is off (the default) and one
/// whole-entity read per changed entity per commit when it is on: repeat
/// writes to the same node find its first-touch image already taken.
/// The cold half of [`write_column_master`]: the type's store has no column for
/// `key` yet, so the write grows the schema.
///
/// Split out because the declared column type lives in `node_type_metadata` —
/// a field of `graph` the store borrow excludes — and because it runs once per
/// `(type, property)` in a statement's lifetime, never in the row loop. The
/// *outer* `None` keeps its one meaning, "the type has no master store".
///
/// A missing column does **not** mean a missing value, which is why this path
/// still honours a `PriorCell::Read`: on a mapped graph the value can live in
/// the store's mmap base or its overflow bag, both of which `get` resolves and
/// neither of which has a dense column until something writes one. Dropping
/// that read would make `REMOVE n.x` report nothing removed — and skip the
/// index eviction — for exactly the properties a `.kgl` load leaves there.
///
/// Declared metadata wins over the value in hand, because it knows `float64`
/// when the first value that happens to arrive is an integer — and a column
/// typed wrong is a column the next write demotes to `Mixed`, which cannot be
/// spilled.
// The argument list IS the write's context — every item is a cheap
// Copy/borrow the caller already holds, split out of write_column_master
// purely for the size ceiling; a params struct would be ceremony around
// one private call site on the measured-hot write path.