Skip to main content

formualizer_eval/engine/graph/
sheets.rs

1use super::ast_utils::update_internal_sheet_references;
2use super::*;
3use formualizer_common::{ExcelError, ExcelErrorKind, LiteralValue};
4
5const TOMBSTONE_SHEET_PREFIX: &str = "__FZ_MISSING_SHEET__";
6
7impl DependencyGraph {
8    /// Add a new sheet to the workbook.
9    ///
10    /// Creates a new sheet with the given name. If a sheet with this name
11    /// already exists, returns its ID without error (idempotent operation).
12    pub fn add_sheet(&mut self, name: &str) -> Result<SheetId, ExcelError> {
13        if let Some(id) = self.sheet_reg.get_id(name) {
14            return Ok(id);
15        }
16
17        let sheet_id = self.sheet_reg.id_for(name);
18        self.sheet_indexes.entry(sheet_id).or_default();
19
20        // Heal formulas that were waiting on this sheet name.
21        self.heal_orphaned_formulas(name);
22        self.resolve_pending_symbol("sheet", name);
23        Ok(sheet_id)
24    }
25
26    /// Remove a sheet from the workbook.
27    pub fn remove_sheet(&mut self, sheet_id: SheetId) -> Result<(), ExcelError> {
28        let result = self.remove_sheet_impl(sheet_id);
29        if result.is_ok() {
30            self.drop_retired_ids_of_sheet(sheet_id);
31        }
32        self.authority_end_structural();
33        result
34    }
35
36    /// Formula vertices with a cell or range reference to `sheet_id` in
37    /// their text (names are handled through their definitions).
38    fn formulas_referencing_sheet(&self, sheet_id: SheetId) -> Vec<VertexId> {
39        use crate::engine::refs::{self, SemanticReference};
40        struct Probe<'a> {
41            graph: &'a DependencyGraph,
42            sheet_id: SheetId,
43            hit: bool,
44        }
45        fn visit(
46            p: &mut Probe<'_>,
47            r: SemanticReference<'_>,
48            key: Option<SheetId>,
49        ) -> Result<(), ExcelError> {
50            let name = match &r {
51                SemanticReference::Cell(c) => c.sheet.name(),
52                SemanticReference::FiniteRange(rg) | SemanticReference::OpenRange(rg) => {
53                    rg.sheet.name()
54                }
55                _ => return Ok(()),
56            };
57            let id = match (key, name) {
58                (Some(id), _) => Some(id),
59                (None, Some(n)) => p.graph.sheet_id(n),
60                (None, None) => None,
61            };
62            if id == Some(p.sheet_id) {
63                p.hit = true;
64            }
65            Ok(())
66        }
67        let mut out = Vec::new();
68        for (v, f) in self.vertex_formulas.iter() {
69            // Sheet references do not change under relocation: a member's
70            // template names the member's sheets.
71            let ast = f.root();
72            let mut probe = Probe {
73                graph: self,
74                sheet_id,
75                hit: false,
76            };
77            let _ = refs::visit_arena_references_keyed(
78                ast,
79                &mut probe,
80                |p| p.graph.data_store(),
81                |p| p.graph.sheet_reg(),
82                visit,
83            );
84            if probe.hit {
85                out.push(v);
86            }
87        }
88        out.sort_unstable();
89        out
90    }
91
92    fn remove_sheet_impl(&mut self, sheet_id: SheetId) -> Result<(), ExcelError> {
93        self.authority_note_structural(true);
94        let old_name = self.sheet_reg.name(sheet_id).to_string();
95        if old_name.is_empty() {
96            return Err(ExcelError::new(ExcelErrorKind::Value).with_message("Sheet does not exist"));
97        }
98
99        let sheet_count = self.sheet_reg.all_sheets().len();
100        if sheet_count <= 1 {
101            return Err(
102                ExcelError::new(ExcelErrorKind::Value).with_message("Cannot remove the last sheet")
103            );
104        }
105
106        self.begin_batch();
107
108        // Symbol vertices are not sheet residents: workbook names would otherwise be
109        // destroyed whenever the default sheet was removed. Sheet-scoped names on this
110        // sheet are retired below, through the name registry that owns them.
111        let vertices_to_delete: Vec<VertexId> = self
112            .grid_vertices_in_sheet(sheet_id)
113            .map(|(id, _)| id)
114            .collect();
115
116        // Formulas that reference this sheet: every cell or range reference
117        // whose sheet is this one, from the formula text (legacy read its
118        // edges and compressed range deps; same set).
119        let formulas_to_update = self.formulas_referencing_sheet(sheet_id);
120
121        for &formula_id in &formulas_to_update {
122            self.tombstone_registry
123                .add_orphan(old_name.clone(), formula_id);
124            self.rewrite_formula_sheet_to_tombstone(formula_id, &old_name);
125        }
126
127        for formula_id in formulas_to_update {
128            self.mark_as_ref_error(formula_id);
129        }
130
131        // Invalidate defined names that reference the removed sheet.
132        //
133        // In canonical (Arrow-truth) mode, cell/formula vertices do not cache values in the graph,
134        // so we cannot rely on graph-stored ref errors. We must explicitly dirty name vertices and
135        // their dependents so that subsequent evaluation updates Arrow overlays.
136        let ref_err = LiteralValue::Error(ExcelError::new(ExcelErrorKind::Ref));
137        let mut name_vertices_to_update: Vec<VertexId> = Vec::new();
138        let mut dirty_vertices: Vec<VertexId> = Vec::new();
139
140        for nr in self.named_ranges.values_mut() {
141            match &nr.definition {
142                NamedDefinition::Cell(c) if c.sheet_id == sheet_id => {
143                    nr.definition = NamedDefinition::Literal(ref_err.clone());
144                    name_vertices_to_update.push(nr.vertex);
145                    dirty_vertices.push(nr.vertex);
146                }
147                NamedDefinition::Range(r)
148                    if r.start.sheet_id == sheet_id || r.end.sheet_id == sheet_id =>
149                {
150                    nr.definition = NamedDefinition::Literal(ref_err.clone());
151                    name_vertices_to_update.push(nr.vertex);
152                    dirty_vertices.push(nr.vertex);
153                }
154                _ => {}
155            }
156        }
157        for nr in self.sheet_named_ranges.values_mut() {
158            match &nr.definition {
159                NamedDefinition::Cell(c) if c.sheet_id == sheet_id => {
160                    nr.definition = NamedDefinition::Literal(ref_err.clone());
161                    name_vertices_to_update.push(nr.vertex);
162                    dirty_vertices.push(nr.vertex);
163                }
164                NamedDefinition::Range(r)
165                    if r.start.sheet_id == sheet_id || r.end.sheet_id == sheet_id =>
166                {
167                    nr.definition = NamedDefinition::Literal(ref_err.clone());
168                    name_vertices_to_update.push(nr.vertex);
169                    dirty_vertices.push(nr.vertex);
170                }
171                _ => {}
172            }
173        }
174
175        // Update cached values for name vertices after the map borrows end.
176        for vid in name_vertices_to_update {
177            self.update_vertex_value_ref(vid, &ref_err);
178        }
179        for &vid in &dirty_vertices {
180            self.mark_vertex_dirty(vid);
181        }
182        // Their readers, through the names' closure (after the resync).
183        self.mark_dirty_many(&dirty_vertices);
184
185        for vertex_id in vertices_to_delete {
186            if let Some(cell_ref) = self.get_cell_ref_for_vertex(vertex_id) {
187                self.cell_to_vertex.remove(&cell_ref);
188            }
189
190            self.remove_all_edges(vertex_id);
191
192            if let Some(coord) = self.store.grid_addr(vertex_id)
193                && let Some(index) = self.sheet_indexes.get_mut(&sheet_id)
194            {
195                index.remove_vertex(coord, vertex_id);
196            }
197
198            self.clear_pending_name_references(vertex_id);
199            self.forget_declared_dynamic_anchor(vertex_id);
200            self.vertex_formulas.remove(&vertex_id);
201            self.vertex_values.remove(&vertex_id);
202
203            self.mark_deleted(vertex_id, true);
204        }
205
206        let sheet_names_to_remove: Vec<(SheetId, String)> = self
207            .sheet_named_ranges
208            .keys()
209            .filter(|(sid, _)| *sid == sheet_id)
210            .cloned()
211            .collect();
212
213        for key in sheet_names_to_remove {
214            if let Some(named_range) = self.sheet_named_ranges.remove(&key) {
215                if !self.config.case_sensitive_names {
216                    let normalized = key.1.to_lowercase();
217                    self.sheet_named_ranges_lookup
218                        .remove(&(sheet_id, normalized));
219                } else {
220                    self.sheet_named_ranges_lookup.remove(&key);
221                }
222                self.mark_named_vertex_deleted(&named_range);
223            }
224        }
225
226        self.sheet_indexes.remove(&sheet_id);
227
228        if self.default_sheet_id == sheet_id
229            && let Some(&new_default) = self.sheet_indexes.keys().next()
230        {
231            self.default_sheet_id = new_default;
232        }
233
234        self.sheet_reg.remove(sheet_id)?;
235        self.end_batch();
236
237        Ok(())
238    }
239
240    fn tombstone_marker(sheet_name: &str) -> String {
241        format!("{TOMBSTONE_SHEET_PREFIX}{sheet_name}")
242    }
243
244    /// Whether `sheet_name` is a removed sheet's tombstone marker. Such a
245    /// reference stays a preparation failure (`#REF!`) under either
246    /// preparation policy: the tombstone registry heals it when the sheet
247    /// returns, and `heal_orphaned_formulas` relies on the formula staying
248    /// in the ref-error set until every removed sheet is back.
249    pub(crate) fn is_tombstone_sheet(sheet_name: &str) -> bool {
250        sheet_name.starts_with(TOMBSTONE_SHEET_PREFIX)
251    }
252
253    fn rewrite_formula_sheet_to_tombstone(&mut self, vertex_id: VertexId, sheet_name: &str) {
254        let Some(ast) = self.get_formula(vertex_id) else {
255            return;
256        };
257
258        let marker = Self::tombstone_marker(sheet_name);
259        let mut updated_ast = ast.clone();
260        updated_ast.update_sheet_references(Some(sheet_name), &marker);
261
262        if updated_ast != ast {
263            let updated_ast_id = self.data_store.store_ast(&updated_ast, &self.sheet_reg);
264            self.materialize_vertex(vertex_id);
265            self.vertex_formulas.insert(vertex_id, updated_ast_id);
266        }
267    }
268
269    fn heal_orphaned_formulas(&mut self, sheet_name: &str) {
270        let orphans = self.tombstone_registry.take_orphans(sheet_name);
271        let marker = Self::tombstone_marker(sheet_name);
272
273        for vertex_id in orphans {
274            let Some(ast) = self.get_formula(vertex_id) else {
275                continue;
276            };
277
278            // If the formula was edited while the sheet was missing, it may no longer
279            // be in #REF! state; skip stale orphan entries in that case.
280            if !self.is_ref_error(vertex_id) {
281                continue;
282            }
283
284            // Heal only references that were explicitly tombstoned for this sheet.
285            let mut updated_ast = ast.clone();
286            updated_ast.update_sheet_references(Some(&marker), sheet_name);
287
288            if updated_ast == ast {
289                // Stale orphan entry (formula changed while sheet was missing).
290                continue;
291            }
292
293            let updated_ast_id = self.data_store.store_ast(&updated_ast, &self.sheet_reg);
294            self.materialize_vertex(vertex_id);
295            self.vertex_formulas.insert(vertex_id, updated_ast_id);
296            self.rebuild_formula_dependencies(vertex_id, &updated_ast);
297        }
298    }
299    /// Rename an existing sheet.
300    pub fn rename_sheet(&mut self, sheet_id: SheetId, new_name: &str) -> Result<(), ExcelError> {
301        let result = self.rename_sheet_impl(sheet_id, new_name);
302        self.authority_end_structural();
303        result
304    }
305
306    fn rename_sheet_impl(&mut self, sheet_id: SheetId, new_name: &str) -> Result<(), ExcelError> {
307        self.authority_note_structural(true);
308        if new_name.is_empty() || new_name.len() > 255 {
309            return Err(ExcelError::new(ExcelErrorKind::Value).with_message("Invalid sheet name"));
310        }
311
312        let old_name = self.sheet_reg.name(sheet_id).to_string();
313
314        if old_name.is_empty() {
315            return Err(ExcelError::new(ExcelErrorKind::Value).with_message("Sheet does not exist"));
316        }
317
318        if let Some(existing_id) = self.sheet_reg.get_id(new_name) {
319            if existing_id != sheet_id {
320                return Err(ExcelError::new(ExcelErrorKind::Value)
321                    .with_message(format!("Sheet '{new_name}' already exists")));
322            }
323            return Ok(());
324        }
325
326        self.sheet_reg.rename(sheet_id, new_name)?;
327        // Name formulas are not rewritten by a rename (legacy): one that
328        // spells the old name kept its edges to this sheet's cells, so an
329        // edit there re-evaluates it (to #REF!). The authority keeps that
330        // edge through this alias; a sheet that takes the name ends it.
331        let old_key = old_name.to_ascii_lowercase();
332        self.renamed_sheet_aliases
333            .retain(|k, _| *k != new_name.to_ascii_lowercase());
334        self.renamed_sheet_aliases.insert(old_key, sheet_id);
335
336        self.begin_batch();
337
338        // Rescue formulas that were waiting for this exact sheet name to reappear.
339        self.heal_orphaned_formulas(new_name);
340
341        // Update still-valid references that explicitly mentioned the renamed sheet.
342        let formulas_to_update: Vec<VertexId> = self.vertex_formulas.keys().collect();
343        for formula_id in formulas_to_update {
344            if let Some(ast) = self.get_formula(formula_id) {
345                let mut updated_ast = ast.clone();
346                updated_ast.update_sheet_references(Some(&old_name), new_name);
347
348                if ast != updated_ast {
349                    self.rebuild_formula_dependencies(formula_id, &updated_ast);
350                    let updated_ast_id = self.data_store.store_ast(&updated_ast, &self.sheet_reg);
351                    self.vertex_formulas.insert(formula_id, updated_ast_id);
352                }
353            }
354        }
355
356        self.end_batch();
357        Ok(())
358    }
359
360    /// Duplicate an existing sheet.
361    pub fn duplicate_sheet(
362        &mut self,
363        source_sheet_id: SheetId,
364        new_name: &str,
365    ) -> Result<SheetId, ExcelError> {
366        let result = self.duplicate_sheet_impl(source_sheet_id, new_name);
367        self.authority_end_structural();
368        result
369    }
370
371    fn duplicate_sheet_impl(
372        &mut self,
373        source_sheet_id: SheetId,
374        new_name: &str,
375    ) -> Result<SheetId, ExcelError> {
376        self.authority_note_structural(true);
377        if new_name.is_empty() || new_name.len() > 255 {
378            return Err(ExcelError::new(ExcelErrorKind::Value).with_message("Invalid sheet name"));
379        }
380
381        let source_name = self.sheet_reg.name(source_sheet_id).to_string();
382        if source_name.is_empty() {
383            return Err(
384                ExcelError::new(ExcelErrorKind::Value).with_message("Source sheet does not exist")
385            );
386        }
387
388        if self.sheet_reg.get_id(new_name).is_some() {
389            return Err(ExcelError::new(ExcelErrorKind::Value)
390                .with_message(format!("Sheet '{new_name}' already exists")));
391        }
392
393        let new_sheet_id = self.add_sheet(new_name)?;
394
395        self.begin_batch();
396
397        let source_vertices: Vec<(VertexId, GridAddr)> =
398            self.grid_vertices_in_sheet(source_sheet_id).collect();
399
400        let mut vertex_mapping = FxHashMap::default();
401
402        for (old_id, coord) in &source_vertices {
403            let row = coord.row();
404            let col = coord.col();
405            let kind = self.store.kind(*old_id);
406
407            let new_id = self
408                .store
409                .allocate(VertexAddr::grid(*coord), new_sheet_id, 0x01);
410            #[cfg(any(test, feature = "legacy_oracle"))]
411            {
412                self.edges.add_vertex(VertexAddr::grid(*coord), new_id.0);
413                self.oracle_cell_vertex_created((new_sheet_id, coord.row(), coord.col()), new_id);
414            }
415            self.sheet_index_mut(new_sheet_id)
416                .add_vertex(*coord, new_id);
417
418            self.store.set_kind(new_id, kind);
419
420            if let Some(&value_ref) = self.vertex_values.get(old_id) {
421                self.vertex_values.insert(new_id, value_ref);
422            }
423
424            vertex_mapping.insert(*old_id, new_id);
425
426            let cell_ref = CellRef::new(new_sheet_id, Coord::new(row, col, true, true));
427            self.cell_to_vertex.insert(cell_ref, new_id);
428        }
429
430        let sheet_names: Vec<(String, NamedRange)> = self
431            .sheet_named_ranges
432            .iter()
433            .filter(|((sid, _), _)| *sid == source_sheet_id)
434            .map(|((_, name), range)| (name.clone(), range.clone()))
435            .collect();
436
437        for (name, mut named_range) in sheet_names {
438            named_range.scope = NameScope::Sheet(new_sheet_id);
439
440            match &mut named_range.definition {
441                NamedDefinition::Cell(cell_ref) if cell_ref.sheet_id == source_sheet_id => {
442                    cell_ref.sheet_id = new_sheet_id;
443                }
444                NamedDefinition::Range(range_ref)
445                    if range_ref.start.sheet_id == source_sheet_id =>
446                {
447                    range_ref.start.sheet_id = new_sheet_id;
448                    range_ref.end.sheet_id = new_sheet_id;
449                }
450                _ => {}
451            }
452
453            #[cfg(any(test, feature = "legacy_oracle"))]
454            named_range.dependents.clear();
455            let name_vertex = self.allocate_name_vertex(named_range.scope);
456            if matches!(named_range.definition, NamedDefinition::Range(_)) {
457                self.store.set_kind(name_vertex, VertexKind::NamedArray);
458            } else {
459                self.store.set_kind(name_vertex, VertexKind::NamedScalar);
460            }
461            named_range.vertex = name_vertex;
462
463            let referenced_names = self.rebuild_name_dependencies(
464                name_vertex,
465                &named_range.definition,
466                named_range.scope,
467            )?;
468            if !referenced_names.is_empty() {
469                self.attach_vertex_to_names(name_vertex, &referenced_names);
470            }
471
472            self.sheet_named_ranges
473                .insert((new_sheet_id, name.clone()), named_range);
474            self.sheet_named_ranges_lookup
475                .insert((new_sheet_id, self.name_lookup_key(&name)), name.clone());
476            self.name_vertex_lookup
477                .insert(name_vertex, (NameScope::Sheet(new_sheet_id), name));
478        }
479
480        for (old_id, _) in &source_vertices {
481            if let Some(&new_id) = vertex_mapping.get(old_id)
482                && let Some(ast) = self.get_formula(*old_id)
483            {
484                let updated_ast = update_internal_sheet_references(
485                    &ast,
486                    &source_name,
487                    new_name,
488                    source_sheet_id,
489                    new_sheet_id,
490                );
491
492                let new_ast_id = self.data_store.store_ast(&updated_ast, &self.sheet_reg);
493                self.vertex_formulas.insert(new_id, new_ast_id);
494
495                if let Ok((deps, range_deps, vertexless, name_vertices)) =
496                    self.extract_dependencies(&updated_ast, new_sheet_id)
497                {
498                    let mapped_deps: Vec<VertexId> = deps
499                        .iter()
500                        .map(|&dep_id| vertex_mapping.get(&dep_id).copied().unwrap_or(dep_id))
501                        .collect();
502
503                    self.add_dependent_edges(new_id, &mapped_deps);
504                    self.note_vertexless_deps(
505                        new_id,
506                        vertexless
507                            .iter()
508                            .map(|c| (c.sheet_id, c.coord.row(), c.coord.col())),
509                    );
510                    self.add_range_dependent_edges(new_id, &range_deps, new_sheet_id);
511
512                    if !name_vertices.is_empty() {
513                        self.attach_vertex_to_names(new_id, &name_vertices);
514                    }
515                }
516            }
517        }
518
519        self.end_batch();
520
521        Ok(new_sheet_id)
522    }
523}