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