Skip to main content

formualizer_eval/engine/graph/editor/
change_log.rs

1//! Standalone change logging infrastructure for tracking graph mutations
2//!
3//! This module provides:
4//! - ChangeLog: Audit trail of all graph changes
5//! - ChangeEvent: Granular representation of individual changes
6//! - ChangeLogger: Trait for pluggable logging strategies
7
8use crate::SheetId;
9use crate::engine::named_range::{NameScope, NamedDefinition};
10use crate::engine::row_visibility::RowVisibilitySource;
11use crate::engine::vertex::VertexId;
12use crate::reference::CellRef;
13use formualizer_common::Coord as AbsCoord;
14use formualizer_common::LiteralValue;
15use formualizer_parse::parser::ASTNode;
16
17#[derive(Debug, Clone, PartialEq)]
18pub struct SpillSnapshot {
19    /// Declared target cells (row-major rectangle) owned by this spill anchor.
20    pub target_cells: Vec<CellRef>,
21    /// Row-major rectangular values corresponding to the target rectangle.
22    pub values: Vec<Vec<LiteralValue>>,
23}
24
25/// Per-event metadata attached by the caller.
26///
27/// This is intentionally lightweight (Strings) to avoid leaking application types
28/// into the engine layer.
29#[derive(Debug, Clone, PartialEq, Eq, Default)]
30pub struct ChangeEventMeta {
31    pub actor_id: Option<String>,
32    pub correlation_id: Option<String>,
33    pub reason: Option<String>,
34}
35
36/// Represents a single change to the dependency graph
37#[derive(Debug, Clone, PartialEq)]
38pub enum ChangeEvent {
39    // Simple events
40    SetValue {
41        addr: CellRef,
42        old_value: Option<LiteralValue>,
43        old_formula: Option<ASTNode>,
44        new: LiteralValue,
45    },
46    SetFormula {
47        addr: CellRef,
48        old_value: Option<LiteralValue>,
49        old_formula: Option<ASTNode>,
50        new: ASTNode,
51    },
52    SetRowVisibility {
53        sheet_id: SheetId,
54        row0: u32,
55        source: RowVisibilitySource,
56        old_hidden: bool,
57        new_hidden: bool,
58    },
59    /// Vertex creation snapshot (for undo). Minimal for now.
60    AddVertex {
61        id: VertexId,
62        coord: AbsCoord,
63        sheet_id: SheetId,
64        value: Option<LiteralValue>,
65        formula: Option<ASTNode>,
66        kind: Option<crate::engine::vertex::VertexKind>,
67        flags: Option<u8>,
68    },
69    RemoveVertex {
70        id: VertexId,
71        // Need to capture more for rollback!
72        old_value: Option<LiteralValue>,
73        old_formula: Option<ASTNode>,
74        old_dependencies: Vec<VertexId>, // outgoing
75        old_dependents: Vec<VertexId>,   // incoming
76        coord: Option<AbsCoord>,
77        sheet_id: Option<SheetId>,
78        kind: Option<crate::engine::vertex::VertexKind>,
79        flags: Option<u8>,
80    },
81
82    // Compound operation markers
83    CompoundStart {
84        description: String, // e.g., "InsertRows(sheet=0, before=5, count=2)"
85        depth: usize,
86    },
87    CompoundEnd {
88        depth: usize,
89    },
90
91    // Granular events for compound operations
92    VertexMoved {
93        id: VertexId,
94        sheet_id: SheetId,
95        old_coord: AbsCoord,
96        new_coord: AbsCoord,
97    },
98    FormulaAdjusted {
99        id: VertexId,
100        /// Cell address for replay. May be None for non-cell formula vertices.
101        addr: Option<CellRef>,
102        old_ast: ASTNode,
103        new_ast: ASTNode,
104    },
105    NamedRangeAdjusted {
106        name: String,
107        scope: NameScope,
108        old_definition: NamedDefinition,
109        new_definition: NamedDefinition,
110    },
111    EdgeAdded {
112        from: VertexId,
113        to: VertexId,
114    },
115    EdgeRemoved {
116        from: VertexId,
117        to: VertexId,
118    },
119
120    // Named range operations
121    DefineName {
122        name: String,
123        scope: NameScope,
124        definition: NamedDefinition,
125    },
126    UpdateName {
127        name: String,
128        scope: NameScope,
129        old_definition: NamedDefinition,
130        new_definition: NamedDefinition,
131    },
132    DeleteName {
133        name: String,
134        scope: NameScope,
135        old_definition: Option<NamedDefinition>,
136    },
137
138    // Spill region changes (dynamic arrays)
139    SpillCommitted {
140        anchor: VertexId,
141        old: Option<SpillSnapshot>,
142        new: SpillSnapshot,
143    },
144    SpillCleared {
145        anchor: VertexId,
146        old: SpillSnapshot,
147    },
148}
149
150/// Audit trail for tracking all changes to the dependency graph
151#[derive(Debug, Default)]
152pub struct ChangeLog {
153    events: Vec<ChangeEvent>,
154    metas: Vec<ChangeEventMeta>,
155    enabled: bool,
156    /// Optional cap on retained events; when exceeded, oldest events are evicted (FIFO).
157    max_changelog_events: Option<usize>,
158    /// Track compound operations for atomic rollback
159    compound_depth: usize,
160    /// Monotonic sequence number per event
161    seqs: Vec<u64>,
162    /// Optional group id (compound) per event
163    groups: Vec<Option<u64>>,
164    next_seq: u64,
165    /// Stack of active group ids for nested compounds
166    group_stack: Vec<u64>,
167    next_group_id: u64,
168
169    current_meta: ChangeEventMeta,
170}
171
172impl ChangeLog {
173    pub fn new() -> Self {
174        Self {
175            events: Vec::new(),
176            metas: Vec::new(),
177            enabled: true,
178            max_changelog_events: None,
179            compound_depth: 0,
180            seqs: Vec::new(),
181            groups: Vec::new(),
182            next_seq: 0,
183            group_stack: Vec::new(),
184            next_group_id: 1,
185            current_meta: ChangeEventMeta::default(),
186        }
187    }
188
189    pub fn with_max_changelog_events(max: usize) -> Self {
190        let mut out = Self::new();
191        out.max_changelog_events = Some(max);
192        out
193    }
194
195    pub fn set_max_changelog_events(&mut self, max: Option<usize>) {
196        self.max_changelog_events = max;
197        self.enforce_cap();
198    }
199
200    fn enforce_cap(&mut self) {
201        let Some(max) = self.max_changelog_events else {
202            return;
203        };
204        if max == 0 {
205            self.clear();
206            return;
207        }
208        if self.events.len() <= max {
209            return;
210        }
211        let drop_n = self.events.len() - max;
212        self.events.drain(0..drop_n);
213        self.metas.drain(0..drop_n);
214        self.seqs.drain(0..drop_n);
215        self.groups.drain(0..drop_n);
216    }
217
218    pub fn record(&mut self, event: ChangeEvent) {
219        if self.enabled {
220            let seq = self.next_seq;
221            self.next_seq += 1;
222            let current_group = self.group_stack.last().copied();
223            self.events.push(event);
224            self.metas.push(self.current_meta.clone());
225            self.seqs.push(seq);
226            self.groups.push(current_group);
227            self.enforce_cap();
228        }
229    }
230
231    /// Record an event with explicit metadata (used for replay/redo).
232    pub fn record_with_meta(&mut self, event: ChangeEvent, meta: ChangeEventMeta) {
233        if self.enabled {
234            let seq = self.next_seq;
235            self.next_seq += 1;
236            let current_group = self.group_stack.last().copied();
237            self.events.push(event);
238            self.metas.push(meta);
239            self.seqs.push(seq);
240            self.groups.push(current_group);
241            self.enforce_cap();
242        }
243    }
244
245    /// Begin a compound operation (multiple changes from single action)
246    pub fn begin_compound(&mut self, description: String) {
247        self.compound_depth += 1;
248        if self.compound_depth == 1 {
249            // allocate new group id
250            let gid = self.next_group_id;
251            self.next_group_id += 1;
252            self.group_stack.push(gid);
253        } else {
254            // nested: reuse top id
255            if let Some(&gid) = self.group_stack.last() {
256                self.group_stack.push(gid);
257            }
258        }
259        if self.enabled {
260            self.record(ChangeEvent::CompoundStart {
261                description,
262                depth: self.compound_depth,
263            });
264        }
265    }
266
267    /// End a compound operation
268    pub fn end_compound(&mut self) {
269        if self.compound_depth > 0 {
270            if self.enabled {
271                self.record(ChangeEvent::CompoundEnd {
272                    depth: self.compound_depth,
273                });
274            }
275            self.compound_depth -= 1;
276            self.group_stack.pop();
277        }
278    }
279
280    pub fn events(&self) -> &[ChangeEvent] {
281        &self.events
282    }
283
284    pub fn patch_last_cell_event_old_state(
285        &mut self,
286        addr: CellRef,
287        old_value: Option<LiteralValue>,
288        old_formula: Option<ASTNode>,
289    ) {
290        // Walk backwards to find the most recent SetValue/SetFormula for this cell.
291        // This is used by Arrow-canonical callers that must capture old_value/old_formula
292        // from Arrow truth (graph value cache may be disabled).
293        for ev in self.events.iter_mut().rev() {
294            match ev {
295                ChangeEvent::SetValue {
296                    addr: a,
297                    old_value: ov,
298                    old_formula: of,
299                    ..
300                }
301                | ChangeEvent::SetFormula {
302                    addr: a,
303                    old_value: ov,
304                    old_formula: of,
305                    ..
306                } if *a == addr => {
307                    if ov.is_none() {
308                        *ov = old_value;
309                    }
310                    if of.is_none() {
311                        *of = old_formula;
312                    }
313                    break;
314                }
315                _ => {}
316            }
317        }
318    }
319
320    pub fn event_meta(&self, index: usize) -> Option<&ChangeEventMeta> {
321        self.metas.get(index)
322    }
323
324    pub fn set_actor_id(&mut self, actor_id: Option<String>) {
325        self.current_meta.actor_id = actor_id;
326    }
327
328    pub fn set_correlation_id(&mut self, correlation_id: Option<String>) {
329        self.current_meta.correlation_id = correlation_id;
330    }
331
332    pub fn set_reason(&mut self, reason: Option<String>) {
333        self.current_meta.reason = reason;
334    }
335
336    /// Truncate log (and metadata) to len
337    pub fn truncate(&mut self, len: usize) {
338        self.events.truncate(len);
339        self.metas.truncate(len);
340        self.seqs.truncate(len);
341        self.groups.truncate(len);
342    }
343
344    pub fn clear(&mut self) {
345        self.events.clear();
346        self.metas.clear();
347        self.seqs.clear();
348        self.groups.clear();
349        self.compound_depth = 0;
350        self.group_stack.clear();
351    }
352
353    pub fn len(&self) -> usize {
354        self.events.len()
355    }
356
357    pub fn is_empty(&self) -> bool {
358        self.events.is_empty()
359    }
360
361    /// Extract events from index to end
362    pub fn take_from(&mut self, index: usize) -> Vec<ChangeEvent> {
363        let events = self.events.split_off(index);
364        let _ = self.metas.split_off(index);
365        let _ = self.seqs.split_off(index);
366        let _ = self.groups.split_off(index);
367        events
368    }
369
370    /// Temporarily disable logging (for rollback operations)
371    pub fn set_enabled(&mut self, enabled: bool) {
372        self.enabled = enabled;
373    }
374
375    /// Get current compound depth (for testing)
376    pub fn compound_depth(&self) -> usize {
377        self.compound_depth
378    }
379
380    /// Return (sequence_number, group_id) metadata for event index
381    pub fn meta(&self, index: usize) -> Option<(u64, Option<u64>)> {
382        self.seqs
383            .get(index)
384            .copied()
385            .zip(self.groups.get(index).copied())
386    }
387
388    /// Collect indices belonging to the last (innermost) complete group. Fallback: last single event.
389    pub fn last_group_indices(&self) -> Vec<usize> {
390        if let Some(&last_gid) = self.groups.iter().rev().flatten().next() {
391            let idxs: Vec<usize> = self
392                .groups
393                .iter()
394                .enumerate()
395                .filter_map(|(i, g)| if *g == Some(last_gid) { Some(i) } else { None })
396                .collect();
397            if !idxs.is_empty() {
398                return idxs;
399            }
400        }
401        self.events.len().checked_sub(1).into_iter().collect()
402    }
403}
404
405/// Trait for pluggable logging strategies
406pub trait ChangeLogger {
407    fn record(&mut self, event: ChangeEvent);
408    fn set_enabled(&mut self, enabled: bool);
409    fn begin_compound(&mut self, description: String);
410    fn end_compound(&mut self);
411}
412
413impl ChangeLogger for ChangeLog {
414    fn record(&mut self, event: ChangeEvent) {
415        ChangeLog::record(self, event);
416    }
417
418    fn set_enabled(&mut self, enabled: bool) {
419        self.enabled = enabled;
420    }
421
422    fn begin_compound(&mut self, description: String) {
423        ChangeLog::begin_compound(self, description);
424    }
425
426    fn end_compound(&mut self) {
427        ChangeLog::end_compound(self);
428    }
429}
430
431/// Null logger for when change tracking not needed
432pub struct NullChangeLogger;
433
434impl ChangeLogger for NullChangeLogger {
435    fn record(&mut self, _: ChangeEvent) {}
436    fn set_enabled(&mut self, _: bool) {}
437    fn begin_compound(&mut self, _: String) {}
438    fn end_compound(&mut self) {}
439}