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    /// Workbook-level staged formula snapshot used to keep deferred edits undoable.
149    StagedFormulaStateChanged {
150        before: Vec<(String, u32, u32, String)>,
151        after: Vec<(String, u32, u32, String)>,
152    },
153}
154
155/// Audit trail for tracking all changes to the dependency graph
156#[derive(Debug, Default)]
157pub struct ChangeLog {
158    events: Vec<ChangeEvent>,
159    metas: Vec<ChangeEventMeta>,
160    enabled: bool,
161    /// Optional cap on retained events; when exceeded, oldest events are evicted (FIFO).
162    max_changelog_events: Option<usize>,
163    /// Track compound operations for atomic rollback
164    compound_depth: usize,
165    /// Monotonic sequence number per event
166    seqs: Vec<u64>,
167    /// Optional group id (compound) per event
168    groups: Vec<Option<u64>>,
169    next_seq: u64,
170    /// Stack of active group ids for nested compounds
171    group_stack: Vec<u64>,
172    next_group_id: u64,
173
174    current_meta: ChangeEventMeta,
175}
176
177impl ChangeLog {
178    pub fn new() -> Self {
179        Self {
180            events: Vec::new(),
181            metas: Vec::new(),
182            enabled: true,
183            max_changelog_events: None,
184            compound_depth: 0,
185            seqs: Vec::new(),
186            groups: Vec::new(),
187            next_seq: 0,
188            group_stack: Vec::new(),
189            next_group_id: 1,
190            current_meta: ChangeEventMeta::default(),
191        }
192    }
193
194    pub fn with_max_changelog_events(max: usize) -> Self {
195        let mut out = Self::new();
196        out.max_changelog_events = Some(max);
197        out
198    }
199
200    pub fn set_max_changelog_events(&mut self, max: Option<usize>) {
201        self.max_changelog_events = max;
202        self.enforce_cap();
203    }
204
205    fn enforce_cap(&mut self) {
206        let Some(max) = self.max_changelog_events else {
207            return;
208        };
209        if max == 0 {
210            self.clear();
211            return;
212        }
213        if self.events.len() <= max {
214            return;
215        }
216        let drop_n = self.events.len() - max;
217        self.events.drain(0..drop_n);
218        self.metas.drain(0..drop_n);
219        self.seqs.drain(0..drop_n);
220        self.groups.drain(0..drop_n);
221    }
222
223    pub fn record(&mut self, event: ChangeEvent) {
224        if self.enabled {
225            let seq = self.next_seq;
226            self.next_seq += 1;
227            let current_group = self.group_stack.last().copied();
228            self.events.push(event);
229            self.metas.push(self.current_meta.clone());
230            self.seqs.push(seq);
231            self.groups.push(current_group);
232            self.enforce_cap();
233        }
234    }
235
236    /// Record an event with explicit metadata (used for replay/redo).
237    pub fn record_with_meta(&mut self, event: ChangeEvent, meta: ChangeEventMeta) {
238        if self.enabled {
239            let seq = self.next_seq;
240            self.next_seq += 1;
241            let current_group = self.group_stack.last().copied();
242            self.events.push(event);
243            self.metas.push(meta);
244            self.seqs.push(seq);
245            self.groups.push(current_group);
246            self.enforce_cap();
247        }
248    }
249
250    /// Begin a compound operation (multiple changes from single action)
251    pub fn begin_compound(&mut self, description: String) {
252        self.compound_depth += 1;
253        if self.compound_depth == 1 {
254            // allocate new group id
255            let gid = self.next_group_id;
256            self.next_group_id += 1;
257            self.group_stack.push(gid);
258        } else {
259            // nested: reuse top id
260            if let Some(&gid) = self.group_stack.last() {
261                self.group_stack.push(gid);
262            }
263        }
264        if self.enabled {
265            self.record(ChangeEvent::CompoundStart {
266                description,
267                depth: self.compound_depth,
268            });
269        }
270    }
271
272    /// End a compound operation
273    pub fn end_compound(&mut self) {
274        if self.compound_depth > 0 {
275            if self.enabled {
276                self.record(ChangeEvent::CompoundEnd {
277                    depth: self.compound_depth,
278                });
279            }
280            self.compound_depth -= 1;
281            self.group_stack.pop();
282        }
283    }
284
285    pub fn events(&self) -> &[ChangeEvent] {
286        &self.events
287    }
288
289    pub fn patch_last_cell_event_old_state(
290        &mut self,
291        addr: CellRef,
292        old_value: Option<LiteralValue>,
293        old_formula: Option<ASTNode>,
294    ) {
295        // Walk backwards to find the most recent SetValue/SetFormula for this cell.
296        // This is used by Arrow-canonical callers that must capture old_value/old_formula
297        // from Arrow truth (graph value cache may be disabled).
298        for ev in self.events.iter_mut().rev() {
299            match ev {
300                ChangeEvent::SetValue {
301                    addr: a,
302                    old_value: ov,
303                    old_formula: of,
304                    ..
305                }
306                | ChangeEvent::SetFormula {
307                    addr: a,
308                    old_value: ov,
309                    old_formula: of,
310                    ..
311                } if *a == addr => {
312                    if ov.is_none() {
313                        *ov = old_value;
314                    }
315                    if of.is_none() {
316                        *of = old_formula;
317                    }
318                    break;
319                }
320                _ => {}
321            }
322        }
323    }
324
325    pub fn event_meta(&self, index: usize) -> Option<&ChangeEventMeta> {
326        self.metas.get(index)
327    }
328
329    pub fn set_actor_id(&mut self, actor_id: Option<String>) {
330        self.current_meta.actor_id = actor_id;
331    }
332
333    pub fn set_correlation_id(&mut self, correlation_id: Option<String>) {
334        self.current_meta.correlation_id = correlation_id;
335    }
336
337    pub fn set_reason(&mut self, reason: Option<String>) {
338        self.current_meta.reason = reason;
339    }
340
341    /// Truncate log (and metadata) to len
342    pub fn truncate(&mut self, len: usize) {
343        self.events.truncate(len);
344        self.metas.truncate(len);
345        self.seqs.truncate(len);
346        self.groups.truncate(len);
347    }
348
349    pub fn clear(&mut self) {
350        self.events.clear();
351        self.metas.clear();
352        self.seqs.clear();
353        self.groups.clear();
354        self.compound_depth = 0;
355        self.group_stack.clear();
356    }
357
358    pub fn len(&self) -> usize {
359        self.events.len()
360    }
361
362    pub fn is_empty(&self) -> bool {
363        self.events.is_empty()
364    }
365
366    /// Extract events from index to end
367    pub fn take_from(&mut self, index: usize) -> Vec<ChangeEvent> {
368        let events = self.events.split_off(index);
369        let _ = self.metas.split_off(index);
370        let _ = self.seqs.split_off(index);
371        let _ = self.groups.split_off(index);
372        events
373    }
374
375    /// Temporarily disable logging (for rollback operations)
376    pub fn set_enabled(&mut self, enabled: bool) {
377        self.enabled = enabled;
378    }
379
380    /// Get current compound depth (for testing)
381    pub fn compound_depth(&self) -> usize {
382        self.compound_depth
383    }
384
385    /// Return (sequence_number, group_id) metadata for event index
386    pub fn meta(&self, index: usize) -> Option<(u64, Option<u64>)> {
387        self.seqs
388            .get(index)
389            .copied()
390            .zip(self.groups.get(index).copied())
391    }
392
393    /// Collect indices belonging to the last (innermost) complete group. Fallback: last single event.
394    pub fn last_group_indices(&self) -> Vec<usize> {
395        if let Some(&last_gid) = self.groups.iter().rev().flatten().next() {
396            let idxs: Vec<usize> = self
397                .groups
398                .iter()
399                .enumerate()
400                .filter_map(|(i, g)| if *g == Some(last_gid) { Some(i) } else { None })
401                .collect();
402            if !idxs.is_empty() {
403                return idxs;
404            }
405        }
406        self.events.len().checked_sub(1).into_iter().collect()
407    }
408}
409
410/// Trait for pluggable logging strategies
411pub trait ChangeLogger {
412    fn record(&mut self, event: ChangeEvent);
413    fn set_enabled(&mut self, enabled: bool);
414    fn begin_compound(&mut self, description: String);
415    fn end_compound(&mut self);
416}
417
418impl ChangeLogger for ChangeLog {
419    fn record(&mut self, event: ChangeEvent) {
420        ChangeLog::record(self, event);
421    }
422
423    fn set_enabled(&mut self, enabled: bool) {
424        self.enabled = enabled;
425    }
426
427    fn begin_compound(&mut self, description: String) {
428        ChangeLog::begin_compound(self, description);
429    }
430
431    fn end_compound(&mut self) {
432        ChangeLog::end_compound(self);
433    }
434}
435
436/// Null logger for when change tracking not needed
437pub struct NullChangeLogger;
438
439impl ChangeLogger for NullChangeLogger {
440    fn record(&mut self, _: ChangeEvent) {}
441    fn set_enabled(&mut self, _: bool) {}
442    fn begin_compound(&mut self, _: String) {}
443    fn end_compound(&mut self) {}
444}