Skip to main content

selene_graph/mutator/
factory_reset.rs

1//! `DROP GRAPH` factory-reset for the transaction mutator.
2//!
3//! Extracted from the mutator module to keep `mutator.rs` under the 700-LOC
4//! file cap. The reset reuses the same change-free row-removal cores the
5//! BRIEF-150 truncate path uses (`remove_node_row` / `remove_edge_row`), so the
6//! resulting in-memory state is byte-identical to `MATCH (n) DETACH DELETE n`
7//! plus a full schema drop.
8
9use selene_core::Change;
10
11use crate::Mutator;
12use crate::error::GraphResult;
13use crate::store::RowIndex;
14
15impl<'tx, 'g> Mutator<'tx, 'g> {
16    /// Factory-reset the entire graph: wipe every node and edge and reset the
17    /// schema to open, recording exactly **one** declarative
18    /// [`Change::GraphReset`] (deletion-reclamation audit Item 10, BRIEF-152).
19    ///
20    /// This is the `DROP GRAPH` primitive. Under D1 single-graph it targets the
21    /// one bound graph. Behaviour and invariants:
22    ///
23    /// * **Wipes ALL rows, including untyped ones.** Live rows are enumerated
24    ///   from the two alive [`roaring::RoaringBitmap`]s
25    ///   ([`crate::SeleneGraph::live_nodes`] / [`crate::SeleneGraph::live_edges`]),
26    ///   **never** per label — so nodes/edges whose labels are not declared
27    ///   types (legal in an open GG01 graph) are removed too. A per-type
28    ///   truncate would silently miss them.
29    /// * **Resets the schema to open.** `meta.bound_type` is set to `None`,
30    ///   making a previously closed (GG02) graph open (GG01) again. There are no
31    ///   standalone type defs outside `bound_type`, so clearing it is the
32    ///   complete schema reset. As a side effect, the commit-time closed-graph
33    ///   validation loop (`write_txn`) is skipped entirely once `bound_type` is
34    ///   `None`, which is correct — nothing is left to validate.
35    /// * **O(1) WAL.** Exactly one [`Change::GraphReset`] is pushed regardless of
36    ///   the number of rows removed. The per-row `NodeDeleted`/`EdgeDeleted`
37    ///   tombstones are staged into the fan-out buffer (`truncate_expansions`)
38    ///   only, never into the persisted changeset, so derived state (e.g. the
39    ///   label/property indexes) is reclaimed without leaks while the WAL stays
40    ///   constant-size.
41    /// * **Idempotent.** On an already-empty + open graph the row enumeration is
42    ///   empty and `bound_type` is already `None`; a `GraphReset` is still pushed
43    ///   with an empty staged expansion (which the fan-out expander drops), so a
44    ///   second `DROP GRAPH` is a clean observable no-op, never an error.
45    ///
46    /// The MANIFEST epoch and WAL archive lineage are untouched: this is one
47    /// committed WAL entry on top of the existing snapshot, not a file-level
48    /// wipe.
49    ///
50    /// # Errors
51    ///
52    /// Returns a [`crate::GraphError`] only if the change-free removal cores hit
53    /// a structural inconsistency (e.g. a missing edge row for a live index
54    /// entry) — the same error surface the truncate path exposes.
55    pub fn factory_reset(&mut self) -> GraphResult<()> {
56        // Snapshot every live row BEFORE any removal: removal mutates the alive
57        // bitmaps and adjacency/index structures we are iterating (the same
58        // clone-collect discipline truncate_node_type uses).
59        let node_rows: Vec<u32> = self.txn.read().node_store.alive.iter().collect();
60        let edge_rows: Vec<u32> = self.txn.read().edge_store.alive.iter().collect();
61
62        let mut expansion = Vec::with_capacity(node_rows.len() + edge_rows.len());
63        for row in node_rows {
64            // Every row came from the alive bitmap, so its external id is mapped
65            // (an unmapped row would be a never-committed hole, never alive).
66            let Some(id) = self.txn.read().node_id_for_row(RowIndex::new(row)) else {
67                continue;
68            };
69            // remove_node_row scrubs idx_label, property/composite indexes,
70            // adjacency, and node liveness. Its returned incident-edge set is
71            // discarded here because the alive-edge bitmap below is the
72            // authoritative superset (it also covers untyped edges between
73            // untyped nodes), so we clear every edge row directly.
74            let _ = self.remove_node_row(id, row as usize)?;
75            expansion.push(Change::NodeDeleted { id });
76        }
77
78        // Remove every still-alive edge row. remove_node_row detached incident
79        // edges from adjacency but did NOT clear edge liveness / edge-label
80        // index, so iterate the full alive-edge set captured before removal.
81        for row in edge_rows {
82            // Defensive: a row may already be dead if it shared two truncated
83            // endpoints — remove_edge_row is only called for still-alive rows.
84            if !self.txn.read().edge_store.is_alive(row) {
85                continue;
86            }
87            let Some(id) = self.txn.read().edge_id_for_row(RowIndex::new(row)) else {
88                continue;
89            };
90            debug_assert!(
91                self.txn.read().row_for_edge_id(id) == Some(RowIndex::new(row)),
92                "edge row/id round-trip must hold"
93            );
94            self.remove_edge_row(id, row as usize)?;
95            expansion.push(Change::EdgeDeleted { id });
96        }
97
98        // Reset the schema to open. Different from DROP TYPE (which keeps the
99        // graph closed by setting bound_type = Some(next)); factory-reset clears
100        // the WHOLE bound_type to None.
101        self.txn.guard_mut().meta.bound_type = None;
102
103        // Push EXACTLY ONE declarative change (O(1) WAL) and stage the per-row
104        // tombstones for subscriber fan-out, keyed to this change's index.
105        let index = self.txn.changes.len();
106        self.txn.changes.push(Change::GraphReset {});
107        self.txn.truncate_expansions.push((index, expansion));
108        Ok(())
109    }
110}