Skip to main content

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;