selene_core/changeset.rs
1//! WAL change payloads per spec 02 section 9.
2//!
3//! The principal/audit actor lives in the WAL entry header per D12; these
4//! payloads carry only the graph mutation itself. Diff payloads keep key lists
5//! in canonical lexicographic order by [`DbString::as_str`] both in memory and on
6//! the wire (the derived [`DbString`] `Ord` is lexicographic through the inner
7//! string). Serialize canonicalizes (sorts) the lists before emitting — a no-op
8//! for diffs built via the constructors, but load-bearing because the diff
9//! fields are public and can be set non-canonically. Deserialize then validates
10//! the canonical invariant and rejects a non-canonical or out-of-order payload
11//! as malformed rather than re-sorting it.
12
13use serde::{Deserialize, Serialize};
14use smallvec::SmallVec;
15
16use crate::{
17 DbString, EdgeId, EdgeTypeDef, EdgeTypeDefV1, GraphId, GraphType, GraphTypeId, HnswIndexConfig,
18 IvfIndexConfig, LabelSet, NodeId, NodeTypeDef, NodeTypeDefV1, PropertyMap, RecordTypeDef,
19};
20
21mod diff;
22
23pub use diff::{LabelDiff, PropertyDiff};
24
25/// A graph or schema change carried by the WAL.
26#[allow(clippy::large_enum_variant)]
27#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
28// Invariant: serde+postcard tag stability - append new variants, never insert.
29// Reordering corrupts WAL files written under prior tag layouts.
30pub enum Change {
31 /// Node creation.
32 NodeCreated {
33 /// Created node ID.
34 id: NodeId,
35 /// Initial labels.
36 labels: LabelSet,
37 /// Initial properties.
38 properties: PropertyMap,
39 },
40 /// Node update.
41 NodeUpdated {
42 /// Updated node ID.
43 id: NodeId,
44 /// Label changes.
45 labels_diff: LabelDiff,
46 /// Property changes.
47 properties_diff: PropertyDiff,
48 },
49 /// Node deletion.
50 NodeDeleted {
51 /// Deleted node ID.
52 id: NodeId,
53 },
54 /// Edge creation.
55 EdgeCreated {
56 /// Created edge ID.
57 id: EdgeId,
58 /// Edge label.
59 label: DbString,
60 /// Source node ID.
61 source: NodeId,
62 /// Target node ID.
63 target: NodeId,
64 /// Initial properties.
65 properties: PropertyMap,
66 },
67 /// Edge update.
68 EdgeUpdated {
69 /// Updated edge ID.
70 id: EdgeId,
71 /// Property changes.
72 properties_diff: PropertyDiff,
73 },
74 /// Edge deletion.
75 EdgeDeleted {
76 /// Deleted edge ID.
77 id: EdgeId,
78 },
79 /// Schema mutation.
80 SchemaChanged {
81 /// Graph affected by the schema change.
82 graph: GraphId,
83 /// Schema change payload.
84 change: SchemaChange,
85 },
86 /// Node property removal.
87 NodePropertyRemoved {
88 /// Updated node ID.
89 id: NodeId,
90 /// Removed property key.
91 property: DbString,
92 },
93 /// Edge property removal.
94 EdgePropertyRemoved {
95 /// Updated edge ID.
96 id: EdgeId,
97 /// Removed property key.
98 property: DbString,
99 },
100 /// Node label removal.
101 NodeLabelRemoved {
102 /// Updated node ID.
103 id: NodeId,
104 /// Removed label.
105 label: DbString,
106 },
107 /// Bulk removal of every node carrying `label` plus all incident edges.
108 ///
109 /// This is the O(1)-WAL declarative truncate change (BRIEF-150, deletion-
110 /// reclamation audit Item 11). It carries **only** the label — never the
111 /// affected node/edge ids — so a `TRUNCATE NODE TYPE :L` of N nodes still
112 /// writes exactly one WAL change. Recovery re-derives the affected rows by
113 /// walking the recovered store ("replay walks store"), marking dead every
114 /// alive node with `label` and every alive edge incident to such a node, so
115 /// the recovered state is byte-identical to `MATCH (n:L) DETACH DELETE n`.
116 /// Live commit fan-out substitutes the change with staged per-row
117 /// `NodeDeleted`/`EdgeDeleted` tombstones when the mutator captured them
118 /// during execution. WAL/recovery replay carries this persisted declarative
119 /// variant, so provider-owned derived state must either handle it directly
120 /// or rebuild from the recovered graph snapshot before serving reads.
121 NodesOfTypeTruncated {
122 /// Node label whose instances (and incident edges) were removed.
123 label: DbString,
124 },
125 /// Bulk removal of every edge carrying `label`.
126 ///
127 /// The edge-type counterpart to [`Change::NodesOfTypeTruncated`]
128 /// (`TRUNCATE EDGE TYPE :L`). Carries only the label (O(1) WAL); recovery
129 /// re-derives the affected edges from the recovered store. Live commit
130 /// fan-out substitutes the change with staged per-row `EdgeDeleted`
131 /// tombstones when execution captured them; WAL/recovery replay carries
132 /// this persisted declarative variant, so providers must handle it directly
133 /// or rebuild before serving reads.
134 EdgesOfTypeTruncated {
135 /// Edge label whose instances were removed.
136 label: DbString,
137 },
138 /// Factory-reset of the entire graph: wipe **all** nodes and edges (every
139 /// label, including untyped/arbitrary-label rows) **and** reset the schema
140 /// to open (`bound_type` -> `None`), in one declarative O(1)-WAL change.
141 ///
142 /// This is the `DROP GRAPH` factory-reset change (BRIEF-152, deletion-
143 /// reclamation audit Item 10). Under D1 single-graph it targets the one
144 /// bound graph. It carries **nothing** — never the affected node/edge ids
145 /// nor any schema payload — so a reset of a graph with N rows still writes
146 /// exactly one WAL change. Recovery re-derives every affected row by walking
147 /// the recovered store ("replay walks store"), marking dead every alive node
148 /// and edge, and forces the recovered `bound_type` to `None`, so the
149 /// recovered state is byte-identical to `MATCH (n) DETACH DELETE n` followed
150 /// by a full schema drop. Live commit fan-out substitutes the change with
151 /// staged per-row `NodeDeleted`/`EdgeDeleted` tombstones when execution
152 /// captured them. WAL/recovery replay carries this persisted declarative
153 /// variant, so providers must handle it directly or rebuild before serving
154 /// reads. The MANIFEST epoch and WAL archive lineage are untouched: a
155 /// factory-reset is one committed WAL entry on top of the existing snapshot,
156 /// not a file-level wipe.
157 GraphReset {},
158}
159
160/// Schema change payload.
161#[allow(clippy::large_enum_variant)]
162#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
163pub enum SchemaChange {
164 /// Graph creation.
165 GraphCreated {
166 /// Created graph ID.
167 id: GraphId,
168 /// Graph name.
169 name: DbString,
170 /// Optional graph type assigned at creation.
171 graph_type: Option<GraphTypeId>,
172 },
173 /// Graph deletion.
174 GraphDropped {
175 /// Dropped graph ID.
176 id: GraphId,
177 },
178 /// Graph type creation.
179 GraphTypeCreated {
180 /// Created graph type definition.
181 graph_type: GraphType,
182 },
183 /// Graph type deletion.
184 GraphTypeDropped {
185 /// Dropped graph type ID.
186 id: GraphTypeId,
187 },
188 /// Node type addition.
189 NodeTypeAdded {
190 /// Owning graph type.
191 graph_type: GraphTypeId,
192 /// Node type label.
193 label: DbString,
194 /// Legacy node type definition.
195 def: NodeTypeDefV1,
196 },
197 /// Edge type addition.
198 EdgeTypeAdded {
199 /// Owning graph type.
200 graph_type: GraphTypeId,
201 /// Edge type label.
202 label: DbString,
203 /// Legacy edge type definition.
204 def: EdgeTypeDefV1,
205 },
206 /// Node type deletion.
207 NodeTypeDropped {
208 /// Owning graph type.
209 graph_type: GraphTypeId,
210 /// Dropped node type name.
211 name: DbString,
212 },
213 /// Edge type deletion.
214 EdgeTypeDropped {
215 /// Owning graph type.
216 graph_type: GraphTypeId,
217 /// Dropped edge type name.
218 name: DbString,
219 },
220 /// Record type addition.
221 RecordTypeAdded {
222 /// Owning graph type.
223 graph_type: GraphTypeId,
224 /// Record type definition.
225 def: RecordTypeDef,
226 },
227 /// Property index creation.
228 PropertyIndexCreated {
229 /// Indexed node label.
230 label: DbString,
231 /// Indexed property key.
232 property: DbString,
233 /// Declared index value kind.
234 kind: SchemaPropertyIndexKind,
235 },
236 /// Property index deletion.
237 PropertyIndexDropped {
238 /// Indexed node label.
239 label: DbString,
240 /// Indexed property key.
241 property: DbString,
242 },
243 /// Property index creation with optional explicit catalog name.
244 ///
245 /// Declared after every existing v1.1 variant so the `postcard`
246 /// discriminants of all earlier variants remain stable. Old WALs continue
247 /// to decode through [`SchemaChange::PropertyIndexCreated`].
248 PropertyIndexCreatedNamed {
249 /// Indexed node label.
250 label: DbString,
251 /// Indexed property key.
252 property: DbString,
253 /// Declared index value kind.
254 kind: SchemaPropertyIndexKind,
255 /// Optional explicit catalog name.
256 name: Option<DbString>,
257 },
258 /// Node type addition carrying v2 type-model fields.
259 ///
260 /// Declared after every existing v1.1 variant so the `postcard`
261 /// discriminants of all earlier variants remain stable. New code emits this
262 /// variant; old WALs continue to decode through [`SchemaChange::NodeTypeAdded`].
263 NodeTypeAddedV2 {
264 /// Owning graph type.
265 graph_type: GraphTypeId,
266 /// Node type label.
267 label: DbString,
268 /// Node type definition.
269 def: NodeTypeDef,
270 },
271 /// Edge type addition carrying v2 type-model fields.
272 ///
273 /// Declared after every existing v1.1 variant so the `postcard`
274 /// discriminants of all earlier variants remain stable. New code emits this
275 /// variant; old WALs continue to decode through [`SchemaChange::EdgeTypeAdded`].
276 EdgeTypeAddedV2 {
277 /// Owning graph type.
278 graph_type: GraphTypeId,
279 /// Edge type label.
280 label: DbString,
281 /// Edge type definition.
282 def: EdgeTypeDef,
283 },
284 /// Composite property index creation with optional explicit catalog name.
285 ///
286 /// Declared after every existing v1.1 variant so the `postcard`
287 /// discriminants of all earlier variants remain stable.
288 CompositePropertyIndexCreated {
289 /// Indexed node label.
290 label: DbString,
291 /// Indexed property keys in declaration order.
292 properties: SmallVec<[DbString; 4]>,
293 /// Declared index value kinds in declaration order.
294 kinds: SmallVec<[SchemaPropertyIndexKind; 4]>,
295 /// Optional explicit catalog name.
296 name: Option<DbString>,
297 },
298 /// Composite property index deletion.
299 ///
300 /// Declared after every existing v1.1 variant so the `postcard`
301 /// discriminants of all earlier variants remain stable.
302 CompositePropertyIndexDropped {
303 /// Indexed node label.
304 label: DbString,
305 /// Indexed property keys in declaration order.
306 properties: SmallVec<[DbString; 4]>,
307 },
308 /// Vector property index creation with optional explicit catalog name.
309 ///
310 /// Declared after every existing v1.1 variant so the `postcard`
311 /// discriminants of all earlier variants remain stable.
312 VectorIndexCreated {
313 /// Indexed node label.
314 label: DbString,
315 /// Indexed vector property key.
316 property: DbString,
317 /// Declared vector index algorithm.
318 kind: SchemaVectorIndexKind,
319 /// Required vector dimensionality for indexed rows.
320 dimension: u32,
321 /// Optional explicit catalog name.
322 name: Option<DbString>,
323 /// Optional HNSW construction parameters.
324 hnsw_config: Option<HnswIndexConfig>,
325 /// Optional IVF construction parameters.
326 ivf_config: Option<IvfIndexConfig>,
327 },
328 /// Vector property index deletion.
329 ///
330 /// Declared after every existing v1.1 variant so the `postcard`
331 /// discriminants of all earlier variants remain stable.
332 VectorIndexDropped {
333 /// Indexed node label.
334 label: DbString,
335 /// Indexed vector property key.
336 property: DbString,
337 },
338 /// Text property index creation with optional explicit catalog name.
339 ///
340 /// Declared after every existing v1.1 variant so the `postcard`
341 /// discriminants of all earlier variants remain stable.
342 TextIndexCreated {
343 /// Indexed node label.
344 label: DbString,
345 /// Indexed string property key.
346 property: DbString,
347 /// Optional explicit catalog name.
348 name: Option<DbString>,
349 },
350 /// Text property index deletion.
351 ///
352 /// Declared after every existing v1.1 variant so the `postcard`
353 /// discriminants of all earlier variants remain stable.
354 TextIndexDropped {
355 /// Indexed node label.
356 label: DbString,
357 /// Indexed string property key.
358 property: DbString,
359 },
360 /// Edge property index creation with optional explicit catalog name.
361 ///
362 /// Declared after every existing variant so the `postcard` discriminants of
363 /// all earlier variants remain stable.
364 EdgePropertyIndexCreated {
365 /// Indexed edge label.
366 label: DbString,
367 /// Indexed edge property key.
368 property: DbString,
369 /// Declared index value kind.
370 kind: SchemaPropertyIndexKind,
371 /// Optional explicit catalog name.
372 name: Option<DbString>,
373 },
374 /// Edge property index deletion.
375 ///
376 /// Declared after every existing variant so the `postcard` discriminants of
377 /// all earlier variants remain stable.
378 EdgePropertyIndexDropped {
379 /// Indexed edge label.
380 label: DbString,
381 /// Indexed edge property key.
382 property: DbString,
383 },
384}
385
386/// Schema-level vector index algorithm kind.
387///
388/// This mirrors storage-level vector index algorithm selection without making
389/// `selene-core` depend on graph storage internals.
390#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
391pub enum SchemaVectorIndexKind {
392 /// Exact in-memory row-set accelerator. ANN algorithms can be added as new
393 /// variants without changing the `(label, property)` catalog identity.
394 Flat,
395 /// Approximate HNSW index using squared Euclidean distance.
396 HnswSquaredEuclidean,
397 /// Approximate HNSW index using cosine distance.
398 HnswCosine,
399 /// Approximate HNSW index using negative inner product distance.
400 HnswNegativeInnerProduct,
401 /// Approximate IVF index using squared Euclidean distance.
402 IvfSquaredEuclidean,
403 /// Approximate IVF index using cosine distance.
404 IvfCosine,
405 /// Approximate IVF index using negative inner product distance.
406 IvfNegativeInnerProduct,
407 /// Compressed TurboQuant candidate index using cosine distance.
408 TurboQuantCosine,
409}
410
411/// Schema-level property index value kind.
412///
413/// This mirrors `selene_graph::TypedIndexKind` without making `selene-core`
414/// depend on graph storage internals.
415#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
416pub enum SchemaPropertyIndexKind {
417 /// Boolean value.
418 Bool,
419 /// Signed 64-bit integer.
420 I64,
421 /// Unsigned 64-bit integer.
422 U64,
423 /// Signed 128-bit integer.
424 I128,
425 /// Unsigned 128-bit integer.
426 U128,
427 /// Fixed-precision decimal value.
428 Decimal,
429 /// Finite 32-bit floating-point value.
430 F32,
431 /// Finite 64-bit floating-point value.
432 F64,
433 /// Database string.
434 String,
435 /// Civil date.
436 Date,
437 /// Civil local date-time.
438 LocalDateTime,
439 /// Zoned date-time.
440 ZonedDateTime,
441 /// Civil local time.
442 LocalTime,
443 /// Zoned time.
444 ZonedTime,
445 /// Duration.
446 Duration,
447 /// UUID.
448 Uuid,
449}
450
451#[cfg(test)]
452#[path = "changeset/tests.rs"]
453mod tests;
454
455#[cfg(test)]
456#[path = "changeset/proptests.rs"]
457mod proptests;