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
//! Change capture and commit hooks: what one committed unit of work changed.
//!
//! For hosts that observe or veto commits (an audit log, replication, a journal of their own):
//! register a [`CommitHook`] through
//! [`GraphStorage::register_commit_hook`](crate::storage::GraphStorage::register_commit_hook)
//! and receive a [`ChangeSet`] per committed unit. A storage implementer finds the same types in
//! [`crate::storage`]. Guide: the book page `users_guide/transactions.md`.
//!
//! # Change sets
//!
//! Change capture: what one committed unit changed, as an ordered list of [`Mutation`]s
//! (`roadmap/transactions.md`, section 2.3).
//!
//! A [`ChangeSet`] is what a commit hook (persistence, replication, CDC, audit) receives. It is
//! built from the undo log of [`TraversalGraph`](crate::graph::TraversalGraph) when the
//! outermost unit commits, and only while change capture is on (section 2.4.3: a graph without
//! hooks pays nothing for capture; the undo log keeps the before-values for rollback anyway).
//!
//! ## Content
//!
//! - The **position** of the commit: [`ChangeSet::commit_seq`], the monotonic commit time
//! [`ChangeSet::committed_at`] (microseconds since the Unix epoch, UTC) and the auto-id
//! sequences after the unit ([`ChangeSet::node_sequence`], [`ChangeSet::edge_sequence`]), so a
//! journal replayed into an empty graph never hands out an id of a dropped or rolled-back
//! element again.
//! - The mutations in **execution order**, each with its before AND after values: a consumer
//! can replay them deterministically (a drop and a re-add of the same id stay two operations)
//! and a CDC consumer gets the old and the new value.
//! - **Cascades are explicit.** Dropping a vertex removes its incident edges first: the
//! `ChangeSet` lists one [`Mutation::DropEdge`] per removed edge (in removal order, a
//! self-loop once) *before* the [`Mutation::DropVertex`]. Replaying the list in order never
//! drops an edge twice and needs no knowledge of the cascade rule.
//! - A **jpath write or removal** (`property(jpath("a.b[1]"), v)`) is a
//! [`Mutation::SetProperty`] of the top-level key with the whole new value, so the format does
//! not depend on the path syntax.
//! - **Schema changes** are in the same stream ([`Mutation::SetSchema`]), so a replay applies
//! schema and data in the order they happened; so are **catalog changes**
//! ([`Mutation::SetDefinition`], one mutation kind for every definition kind).
//! - A write of an equal value is still a [`Mutation::SetProperty`] (`before == after`): it was a
//! write. [`ChangeSet::compact`] drops it.
//!
//! ## Building change sets
//!
//! The graph builds the change sets of its own commits. Outside it (a storage that captures
//! changes itself, a replication feed, a test) a change set is built with
//! [`ChangeSet::builder`] (commit sequence number and time, then the auto-id sequences and the
//! metadata) and one constructor per mutation kind ([`Mutation::add_vertex`],
//! [`Mutation::drop_vertex`], [`Mutation::add_edge`], [`Mutation::drop_edge`],
//! [`Mutation::set_property`], [`Mutation::remove_property`], [`Mutation::add_label`],
//! [`Mutation::remove_label`], [`Mutation::set_edge_label`], [`Mutation::set_schema`],
//! [`Mutation::set_definition`]; the images [`VertexState::new`], [`EdgeState::new`]).
//! [`ChangeSetBuilder::build`] makes the cheap checks; a built change set applies
//! ([`GraphStorage::apply_change_set`](crate::graph::GraphStorage::apply_change_set)), replays
//! and serializes like a captured one.
//!
//! ## Serialization
//!
//! With the `serde` feature every type here serializes as JSON, and a serialized change set
//! replays into an IDENTICAL graph without any schema: property values (in element images,
//! property writes and commit attributes, at any nesting depth) carry an explicit per-value
//! type tag for every logical type plain JSON cannot hold (CLAUDE.md rule 7 allows explicit
//! tags; content is never sniffed). Today that is only `uuid`, written in TinkerPop GraphSON
//! style as `{"@type": "g:UUID", "@value": "<canonical lowercase uuid>"}`. JSON-native types
//! stay untagged.
//!
//! Escaping: a user object that has an `"@type"` key of its own is wrapped as
//! `{"@type": "graphersal:Object", "@value": {..the object..}}` (its keys are then taken
//! literally, its values are read as tagged values again), so no user map can be misread as a
//! tag. On reading, an object with an `"@type"` key must be exactly one of these two forms
//! (`@type` and `@value` only, a canonical uuid string or an object payload); anything else is
//! a deserialization error, never a guess. The tags are used for the serialized `ChangeSet`
//! only; GraphML and the rest of the physical codec are unchanged.
//!
//! # Commit hooks
//!
//! Commit hooks: how a host (a server, a persistence layer, a replica, an audit log) learns
//! what each unit of work changed, and how it can veto a commit (`roadmap/transactions.md`,
//! sections 2.1, 2.2, 2.5).
//!
//! A [`CommitHook`] is registered on a storage with
//! [`GraphStorage::register_commit_hook`](crate::graph::GraphStorage::register_commit_hook).
//! Only a storage that is atomic and captures changes supports hooks
//! ([`StorageCapabilities::change_capture`](crate::graph::StorageCapabilities::change_capture),
//! which requires `atomic`); every other storage rejects the registration with
//! [`GraphError::Unsupported`](crate::error::GraphError::Unsupported). [`TraversalGraph`](crate::TraversalGraph)
//! supports them.
//!
//! ## Events
//!
//! | Event | When | Can veto? |
//! |---|---|---|
//! | [`CommitHook::before_commit`] | the unit validated, before it is final | yes: `Err` rolls the unit back |
//! | [`CommitHook::after_commit`] | after the unit is final | no |
//! | [`CommitHook::after_rollback`] | after the unit was rolled back | no |
//!
//! - The events belong to the OUTERMOST unit (one traversal, a GraphML import into a live graph,
//! an [`apply_change_set`](crate::graph::GraphStorage::apply_change_set)); a nested unit
//! (a savepoint) emits nothing of its own, its surviving changes are part of the outer
//! unit's [`ChangeSet`].
//! - Several hooks run in registration order. The first `before_commit` that returns `Err`
//! stops the commit: the remaining `before_commit` hooks are not called, the unit is rolled
//! back, ALL hooks receive `after_rollback` with [`RollbackReason::Vetoed`], and the caller
//! gets [`GraphError::CommitRejected`](crate::error::GraphError::CommitRejected) carrying the
//! hook's error (inside a [`TraverserError`](crate::error::TraverserError) for a traversal).
//! - A unit that changed nothing (a read-only traversal, `drop()` over an empty set) emits no
//! event and does not advance the commit sequence number. A unit that changed nothing but
//! raised an auto-id sequence
//! ([`apply_change_set`](crate::graph::GraphStorage::apply_change_set) of a change set whose
//! mutations were all compacted away) is a *sequence-only commit*: it gets a commit sequence
//! number and the hooks see a [`ChangeSet`] without mutations whose
//! [`node_sequence`](ChangeSet::node_sequence)/[`edge_sequence`](ChangeSet::edge_sequence)
//! carry the raise (a veto rolls it back like any commit; the raise stays in memory).
//! - A hook gets the [`ChangeSet`] only, never the graph: it runs while the writer holds the
//! graph, so re-entering it is impossible by signature; the before and after values in the
//! change set are enough to persist, replicate or audit the change.
//! - Hooks run synchronously, strictly in commit sequence order.
//! - While at least one hook is registered, the storage builds a [`ChangeSet`] for every
//! committed unit (change capture). Without hooks nothing is captured and nothing is paid.
//! - Hooks cannot be removed in 0.1.0 (a server registers them once, before it publishes the
//! graph); removal may be added later.
//!
//! **Deviation from TinkerPop:** TinkerPop's `EventStrategy` calls a `MutationListener` for
//! each mutation after it happened. Graphersal's hooks are called once per committed unit with
//! the whole [`ChangeSet`], before the commit is final (with a veto) and after it; there are no
//! per-mutation events. See `book/src/for_developers/tinkerpop_deviations.md`.
//!
//! ## Write-ahead logging
//!
//! A server persists in `before_commit`: when the write fails, the hook returns `Err` and the
//! in-memory graph rolls back, so memory and journal never diverge and the client never gets
//! "ok" for data that is lost on restart. The library's own journal (`persist::Journal`,
//! feature `persist`) has a slot of its own: its `before_commit` runs after every hook
//! registered here, whatever the order of registration, so no later hook can veto a unit that
//! is already in the journal. A hand-written write-ahead hook registered here gets no such
//! guarantee: register it last. At recovery the server loads its last snapshot into a
//! graph that nobody else sees yet and replays its journal with
//! [`TraversalGraph::replay`](crate::graph::TraversalGraph::replay), which runs outside units
//! and fires no hooks (it must not re-write the journal it reads).
pub use crate;