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
5impl DependencyGraph {
6    /// Add a new sheet to the workbook.
7    ///
8    /// Creates a new sheet with the given name. If a sheet with this name
9    /// already exists, returns its ID without error (idempotent operation).
10    pub fn add_sheet(&mut self, name: &str) -> Result<SheetId, ExcelError> {
11        if let Some(id) = self.sheet_reg.get_id(name) {
12            return Ok(id);
13        }
14
15        let sheet_id = self.sheet_reg.id_for(name);
16        self.sheet_indexes.entry(sheet_id).or_default();
17
18        // Heal formulas that were waiting on this sheet name.
19        self.heal_orphaned_formulas(name);
20        Ok(sheet_id)
21    }
22
23    /// Remove a sheet from the workbook.
24    pub fn remove_sheet(&mut self, sheet_id: SheetId) -> Result<(), ExcelError> {
25        let old_name = self.sheet_reg.name(sheet_id).to_string();
26        if old_name.is_empty() {
27            return Err(ExcelError::new(ExcelErrorKind::Value).with_message("Sheet does not exist"));
28        }
29
30        let sheet_count = self.sheet_reg.all_sheets().len();
31        if sheet_count <= 1 {
32            return Err(
33                ExcelError::new(ExcelErrorKind::Value).with_message("Cannot remove the last sheet")
34            );
35        }
36
37        self.begin_batch();
38
39        // Symbol vertices are not sheet residents: workbook names would otherwise be
40        // destroyed whenever the default sheet was removed. Sheet-scoped names on this
41        // sheet are retired below, through the name registry that owns them.
42        let vertices_to_delete: Vec<VertexId> = self
43            .grid_vertices_in_sheet(sheet_id)
44            .map(|(id, _)| id)
45            .collect();
46
47        // Formulas can reference this sheet either through explicit dependency edges
48        // (expanded refs) or compressed range deps. Track both.
49        let mut formulas_to_update: rustc_hash::FxHashSet<VertexId> =
50            rustc_hash::FxHashSet::default();
51
52        for &formula_id in self.vertex_formulas.keys() {
53            let deps = self.edges.out_edges(formula_id);
54            if deps
55                .iter()
56                .any(|&dep_id| self.store.sheet_id(dep_id) == sheet_id)
57            {
58                formulas_to_update.insert(formula_id);
59            }
60        }
61
62        for (&formula_id, ranges) in &self.formula_to_range_deps {
63            if ranges.iter().any(|r| match r.sheet {
64                SharedSheetLocator::Id(id) => id == sheet_id,
65                SharedSheetLocator::Name(ref n) => n.as_ref() == old_name,
66                SharedSheetLocator::Current => false,
67            }) {
68                formulas_to_update.insert(formula_id);
69            }
70        }
71
72        let formulas_to_update: Vec<VertexId> = formulas_to_update.into_iter().collect();
73
74        for &formula_id in &formulas_to_update {
75            self.tombstone_registry
76                .add_orphan(old_name.clone(), formula_id);
77            self.rewrite_formula_sheet_to_tombstone(formula_id, &old_name);
78        }
79
80        for formula_id in formulas_to_update {
81            self.mark_as_ref_error(formula_id);
82        }
83
84        // Invalidate defined names that reference the removed sheet.
85        //
86        // In canonical (Arrow-truth) mode, cell/formula vertices do not cache values in the graph,
87        // so we cannot rely on graph-stored ref errors. We must explicitly dirty name vertices and
88        // their dependents so that subsequent evaluation updates Arrow overlays.
89        let ref_err = LiteralValue::Error(ExcelError::new(ExcelErrorKind::Ref));
90        let mut name_vertices_to_update: Vec<VertexId> = Vec::new();
91        let mut dirty_vertices: Vec<VertexId> = Vec::new();
92
93        for nr in self.named_ranges.values_mut() {
94            match &nr.definition {
95                NamedDefinition::Cell(c) if c.sheet_id == sheet_id => {
96                    nr.definition = NamedDefinition::Literal(ref_err.clone());
97                    name_vertices_to_update.push(nr.vertex);
98                    dirty_vertices.push(nr.vertex);
99                    dirty_vertices.extend(nr.dependents.iter().copied());
100                }
101                NamedDefinition::Range(r)
102                    if r.start.sheet_id == sheet_id || r.end.sheet_id == sheet_id =>
103                {
104                    nr.definition = NamedDefinition::Literal(ref_err.clone());
105                    name_vertices_to_update.push(nr.vertex);
106                    dirty_vertices.push(nr.vertex);
107                    dirty_vertices.extend(nr.dependents.iter().copied());
108                }
109                _ => {}
110            }
111        }
112        for nr in self.sheet_named_ranges.values_mut() {
113            match &nr.definition {
114                NamedDefinition::Cell(c) if c.sheet_id == sheet_id => {
115                    nr.definition = NamedDefinition::Literal(ref_err.clone());
116                    name_vertices_to_update.push(nr.vertex);
117                    dirty_vertices.push(nr.vertex);
118                    dirty_vertices.extend(nr.dependents.iter().copied());
119                }
120                NamedDefinition::Range(r)
121                    if r.start.sheet_id == sheet_id || r.end.sheet_id == sheet_id =>
122                {
123                    nr.definition = NamedDefinition::Literal(ref_err.clone());
124                    name_vertices_to_update.push(nr.vertex);
125                    dirty_vertices.push(nr.vertex);
126                    dirty_vertices.extend(nr.dependents.iter().copied());
127                }
128                _ => {}
129            }
130        }
131
132        // Update cached values for name vertices after the map borrows end.
133        for vid in name_vertices_to_update {
134            self.update_vertex_value(vid, ref_err.clone());
135        }
136        for vid in dirty_vertices {
137            self.mark_vertex_dirty(vid);
138        }
139
140        for vertex_id in vertices_to_delete {
141            if let Some(cell_ref) = self.get_cell_ref_for_vertex(vertex_id) {
142                self.cell_to_vertex.remove(&cell_ref);
143            }
144
145            self.remove_all_edges(vertex_id);
146
147            if let Some(coord) = self.store.grid_addr(vertex_id)
148                && let Some(index) = self.sheet_indexes.get_mut(&sheet_id)
149            {
150                index.remove_vertex(coord, vertex_id);
151            }
152
153            self.clear_pending_name_references(vertex_id);
154            self.vertex_formulas.remove(&vertex_id);
155            self.vertex_values.remove(&vertex_id);
156
157            self.mark_deleted(vertex_id, true);
158        }
159
160        let sheet_names_to_remove: Vec<(SheetId, String)> = self
161            .sheet_named_ranges
162            .keys()
163            .filter(|(sid, _)| *sid == sheet_id)
164            .cloned()
165            .collect();
166
167        for key in sheet_names_to_remove {
168            if let Some(named_range) = self.sheet_named_ranges.remove(&key) {
169                if !self.config.case_sensitive_names {
170                    let normalized = key.1.to_lowercase();
171                    self.sheet_named_ranges_lookup
172                        .remove(&(sheet_id, normalized));
173                } else {
174                    self.sheet_named_ranges_lookup.remove(&key);
175                }
176                self.mark_named_vertex_deleted(&named_range);
177            }
178        }
179
180        self.sheet_indexes.remove(&sheet_id);
181
182        if self.default_sheet_id == sheet_id
183            && let Some(&new_default) = self.sheet_indexes.keys().next()
184        {
185            self.default_sheet_id = new_default;
186        }
187
188        self.sheet_reg.remove(sheet_id)?;
189        self.end_batch();
190
191        Ok(())
192    }
193
194    fn tombstone_marker(sheet_name: &str) -> String {
195        format!("__FZ_MISSING_SHEET__{sheet_name}")
196    }
197
198    fn rewrite_formula_sheet_to_tombstone(&mut self, vertex_id: VertexId, sheet_name: &str) {
199        let Some(ast_id) = self.vertex_formulas.get(&vertex_id).copied() else {
200            return;
201        };
202        let Some(ast) = self.data_store.retrieve_ast(ast_id, &self.sheet_reg) else {
203            return;
204        };
205
206        let marker = Self::tombstone_marker(sheet_name);
207        let mut updated_ast = ast.clone();
208        updated_ast.update_sheet_references(Some(sheet_name), &marker);
209
210        if updated_ast != ast {
211            let updated_ast_id = self.data_store.store_ast(&updated_ast, &self.sheet_reg);
212            self.vertex_formulas.insert(vertex_id, updated_ast_id);
213        }
214    }
215
216    fn heal_orphaned_formulas(&mut self, sheet_name: &str) {
217        let orphans = self.tombstone_registry.take_orphans(sheet_name);
218        let marker = Self::tombstone_marker(sheet_name);
219
220        for vertex_id in orphans {
221            let Some(ast_id) = self.vertex_formulas.get(&vertex_id).copied() else {
222                continue;
223            };
224            let Some(ast) = self.data_store.retrieve_ast(ast_id, &self.sheet_reg) else {
225                continue;
226            };
227
228            // If the formula was edited while the sheet was missing, it may no longer
229            // be in #REF! state; skip stale orphan entries in that case.
230            if !self.is_ref_error(vertex_id) {
231                continue;
232            }
233
234            // Heal only references that were explicitly tombstoned for this sheet.
235            let mut updated_ast = ast.clone();
236            updated_ast.update_sheet_references(Some(&marker), sheet_name);
237
238            if updated_ast == ast {
239                // Stale orphan entry (formula changed while sheet was missing).
240                continue;
241            }
242
243            let updated_ast_id = self.data_store.store_ast(&updated_ast, &self.sheet_reg);
244            self.vertex_formulas.insert(vertex_id, updated_ast_id);
245            self.rebuild_formula_dependencies(vertex_id, &updated_ast);
246        }
247    }
248    /// Rename an existing sheet.
249    pub fn rename_sheet(&mut self, sheet_id: SheetId, new_name: &str) -> Result<(), ExcelError> {
250        if new_name.is_empty() || new_name.len() > 255 {
251            return Err(ExcelError::new(ExcelErrorKind::Value).with_message("Invalid sheet name"));
252        }
253
254        let old_name = self.sheet_reg.name(sheet_id).to_string();
255
256        if old_name.is_empty() {
257            return Err(ExcelError::new(ExcelErrorKind::Value).with_message("Sheet does not exist"));
258        }
259
260        if let Some(existing_id) = self.sheet_reg.get_id(new_name) {
261            if existing_id != sheet_id {
262                return Err(ExcelError::new(ExcelErrorKind::Value)
263                    .with_message(format!("Sheet '{new_name}' already exists")));
264            }
265            return Ok(());
266        }
267
268        self.sheet_reg.rename(sheet_id, new_name)?;
269
270        self.begin_batch();
271
272        // Rescue formulas that were waiting for this exact sheet name to reappear.
273        self.heal_orphaned_formulas(new_name);
274
275        // Update still-valid references that explicitly mentioned the renamed sheet.
276        let formulas_to_update: Vec<VertexId> = self.vertex_formulas.keys().copied().collect();
277        for formula_id in formulas_to_update {
278            if let Some(ast_id) = self.vertex_formulas.get(&formula_id)
279                && let Some(ast) = self.data_store.retrieve_ast(*ast_id, &self.sheet_reg)
280            {
281                let mut updated_ast = ast.clone();
282                updated_ast.update_sheet_references(Some(&old_name), new_name);
283
284                if ast != updated_ast {
285                    self.rebuild_formula_dependencies(formula_id, &updated_ast);
286                    let updated_ast_id = self.data_store.store_ast(&updated_ast, &self.sheet_reg);
287                    self.vertex_formulas.insert(formula_id, updated_ast_id);
288                }
289            }
290        }
291
292        self.end_batch();
293        Ok(())
294    }
295
296    /// Duplicate an existing sheet.
297    pub fn duplicate_sheet(
298        &mut self,
299        source_sheet_id: SheetId,
300        new_name: &str,
301    ) -> Result<SheetId, ExcelError> {
302        if new_name.is_empty() || new_name.len() > 255 {
303            return Err(ExcelError::new(ExcelErrorKind::Value).with_message("Invalid sheet name"));
304        }
305
306        let source_name = self.sheet_reg.name(source_sheet_id).to_string();
307        if source_name.is_empty() {
308            return Err(
309                ExcelError::new(ExcelErrorKind::Value).with_message("Source sheet does not exist")
310            );
311        }
312
313        if self.sheet_reg.get_id(new_name).is_some() {
314            return Err(ExcelError::new(ExcelErrorKind::Value)
315                .with_message(format!("Sheet '{new_name}' already exists")));
316        }
317
318        let new_sheet_id = self.add_sheet(new_name)?;
319
320        self.begin_batch();
321
322        let source_vertices: Vec<(VertexId, GridAddr)> =
323            self.grid_vertices_in_sheet(source_sheet_id).collect();
324
325        let mut vertex_mapping = FxHashMap::default();
326
327        for (old_id, coord) in &source_vertices {
328            let row = coord.row();
329            let col = coord.col();
330            let kind = self.store.kind(*old_id);
331
332            let new_id = self
333                .store
334                .allocate(VertexAddr::grid(*coord), new_sheet_id, 0x01);
335            self.edges.add_vertex(VertexAddr::grid(*coord), new_id.0);
336            self.sheet_index_mut(new_sheet_id)
337                .add_vertex(*coord, new_id);
338
339            self.store.set_kind(new_id, kind);
340
341            if let Some(&value_ref) = self.vertex_values.get(old_id) {
342                self.vertex_values.insert(new_id, value_ref);
343            }
344
345            vertex_mapping.insert(*old_id, new_id);
346
347            let cell_ref = CellRef::new(new_sheet_id, Coord::new(row, col, true, true));
348            self.cell_to_vertex.insert(cell_ref, new_id);
349        }
350
351        let sheet_names: Vec<(String, NamedRange)> = self
352            .sheet_named_ranges
353            .iter()
354            .filter(|((sid, _), _)| *sid == source_sheet_id)
355            .map(|((_, name), range)| (name.clone(), range.clone()))
356            .collect();
357
358        for (name, mut named_range) in sheet_names {
359            named_range.scope = NameScope::Sheet(new_sheet_id);
360
361            match &mut named_range.definition {
362                NamedDefinition::Cell(cell_ref) if cell_ref.sheet_id == source_sheet_id => {
363                    cell_ref.sheet_id = new_sheet_id;
364                }
365                NamedDefinition::Range(range_ref) => {
366                    if range_ref.start.sheet_id == source_sheet_id {
367                        range_ref.start.sheet_id = new_sheet_id;
368                        range_ref.end.sheet_id = new_sheet_id;
369                    }
370                }
371                _ => {}
372            }
373
374            named_range.dependents.clear();
375            let name_vertex = self.allocate_name_vertex(named_range.scope);
376            if matches!(named_range.definition, NamedDefinition::Range(_)) {
377                self.store.set_kind(name_vertex, VertexKind::NamedArray);
378            } else {
379                self.store.set_kind(name_vertex, VertexKind::NamedScalar);
380            }
381            named_range.vertex = name_vertex;
382
383            let referenced_names = self.rebuild_name_dependencies(
384                name_vertex,
385                &named_range.definition,
386                named_range.scope,
387            )?;
388            if !referenced_names.is_empty() {
389                self.attach_vertex_to_names(name_vertex, &referenced_names);
390            }
391
392            self.sheet_named_ranges
393                .insert((new_sheet_id, name.clone()), named_range);
394            self.sheet_named_ranges_lookup
395                .insert((new_sheet_id, self.name_lookup_key(&name)), name.clone());
396            self.name_vertex_lookup
397                .insert(name_vertex, (NameScope::Sheet(new_sheet_id), name));
398        }
399
400        for (old_id, _) in &source_vertices {
401            if let Some(&new_id) = vertex_mapping.get(old_id)
402                && let Some(&ast_id) = self.vertex_formulas.get(old_id)
403                && let Some(ast) = self.data_store.retrieve_ast(ast_id, &self.sheet_reg)
404            {
405                let updated_ast = update_internal_sheet_references(
406                    &ast,
407                    &source_name,
408                    new_name,
409                    source_sheet_id,
410                    new_sheet_id,
411                );
412
413                let new_ast_id = self.data_store.store_ast(&updated_ast, &self.sheet_reg);
414                self.vertex_formulas.insert(new_id, new_ast_id);
415
416                if let Ok((deps, range_deps, _, name_vertices)) =
417                    self.extract_dependencies(&updated_ast, new_sheet_id)
418                {
419                    let mapped_deps: Vec<VertexId> = deps
420                        .iter()
421                        .map(|&dep_id| vertex_mapping.get(&dep_id).copied().unwrap_or(dep_id))
422                        .collect();
423
424                    self.add_dependent_edges(new_id, &mapped_deps);
425                    self.add_range_dependent_edges(new_id, &range_deps, new_sheet_id);
426
427                    if !name_vertices.is_empty() {
428                        self.attach_vertex_to_names(new_id, &name_vertices);
429                    }
430                }
431            }
432        }
433
434        self.end_batch();
435
436        Ok(new_sheet_id)
437    }
438}