Skip to main content

formualizer_eval/engine/graph/
tables.rs

1use crate::SheetId;
2use crate::engine::graph::DependencyGraph;
3use crate::engine::vertex::{VertexId, VertexKind};
4use crate::reference::RangeRef;
5use formualizer_common::{ExcelError, ExcelErrorKind};
6
7#[inline]
8fn normalize_table_key(name: &str) -> String {
9    name.to_lowercase()
10}
11
12/// Native workbook table (Excel ListObject) metadata.
13#[derive(Debug, Clone)]
14pub(crate) struct TableEntry {
15    pub(crate) name: String,
16    pub(crate) range: RangeRef,
17    pub(crate) header_row: bool,
18    pub(crate) headers: Vec<String>,
19    pub(crate) totals_row: bool,
20    pub(crate) vertex: VertexId,
21}
22
23impl TableEntry {
24    pub fn sheet_id(&self) -> SheetId {
25        self.range.start.sheet_id
26    }
27
28    pub fn col_index(&self, header: &str) -> Option<usize> {
29        let header_key = header.to_lowercase();
30        self.headers
31            .iter()
32            .position(|h| h.to_lowercase() == header_key)
33    }
34}
35
36impl DependencyGraph {
37    /// Empty table cells obstruct arrays just as populated table cells do.
38    pub(crate) fn table_intersects_spill(
39        &self,
40        anchor: crate::reference::CellRef,
41        end_row: u32,
42        end_col: u32,
43    ) -> bool {
44        self.tables.values().any(|t| {
45            t.sheet_id() == anchor.sheet_id
46                && t.range.start.coord.row() <= end_row
47                && anchor.coord.row() <= t.range.end.coord.row()
48                && t.range.start.coord.col() <= end_col
49                && anchor.coord.col() <= t.range.end.coord.col()
50        })
51    }
52
53    #[inline]
54    fn table_lookup_key(&self, name: &str) -> String {
55        if self.config.case_sensitive_tables {
56            name.to_string()
57        } else {
58            normalize_table_key(name)
59        }
60    }
61
62    fn canonical_table_name(&self, name: &str) -> Option<String> {
63        let key = self.table_lookup_key(name);
64        self.tables_lookup.get(&key).cloned()
65    }
66
67    pub(crate) fn resolve_table_entry(&self, name: &str) -> Option<&TableEntry> {
68        if self.config.case_sensitive_tables {
69            self.tables.get(name)
70        } else {
71            let key = self.table_lookup_key(name);
72            self.tables_lookup
73                .get(&key)
74                .and_then(|canon| self.tables.get(canon))
75        }
76    }
77
78    pub(crate) fn table_by_vertex(&self, vertex: VertexId) -> Option<&TableEntry> {
79        self.table_vertex_lookup
80            .get(&vertex)
81            .and_then(|name| self.tables.get(name))
82    }
83
84    /// Canonical names of every defined table, sorted for deterministic output.
85    pub fn table_names(&self) -> Vec<String> {
86        let mut names: Vec<String> = self.tables.values().map(|t| t.name.clone()).collect();
87        names.sort();
88        names
89    }
90
91    pub fn define_table(
92        &mut self,
93        name: &str,
94        range: RangeRef,
95        header_row: bool,
96        headers: Vec<String>,
97        totals_row: bool,
98    ) -> Result<(), ExcelError> {
99        if name.is_empty() {
100            return Err(ExcelError::new(ExcelErrorKind::Name)
101                .with_message("Table name cannot be empty".to_string()));
102        }
103
104        let key = self.table_lookup_key(name);
105        if let Some(existing) = self.tables_lookup.get(&key) {
106            return Err(ExcelError::new(ExcelErrorKind::Name).with_message(format!(
107                "Table collision under normalization: '{name}' conflicts with '{existing}'"
108            )));
109        }
110
111        // A table has a range, but the *vertex* that represents the table symbol has no
112        // position: it is identified by name. Parking it on the range's anchor cell put it
113        // in the sheet index, where grid queries and structural edits could reach it (#304).
114        // Dependencies on the table's cells are carried by the stripe registration below.
115        let sheet_id = range.start.sheet_id;
116        let vertex = self.allocate_symbol_vertex(VertexKind::Table, sheet_id);
117
118        // Register stripes for the full table region so cell edits inside the table
119        // propagate to formulas that depend on the table.
120        self.register_table_range_deps(vertex, &range);
121
122        let entry = TableEntry {
123            name: name.to_string(),
124            range,
125            header_row,
126            headers,
127            totals_row,
128            vertex,
129        };
130
131        let original = name.to_string();
132        self.tables.insert(original.clone(), entry);
133        self.tables_lookup
134            .insert(self.table_lookup_key(&original), original.clone());
135        self.table_vertex_lookup.insert(vertex, original);
136        self.authority_note_symbol(Some(vertex));
137        self.bump_symbol_revision();
138        self.resolve_pending_symbol("table", name);
139        Ok(())
140    }
141
142    pub fn update_table(
143        &mut self,
144        name: &str,
145        new_range: RangeRef,
146        header_row: bool,
147        headers: Vec<String>,
148        totals_row: bool,
149    ) -> Result<(), ExcelError> {
150        let Some(canon) = self.canonical_table_name(name) else {
151            return Err(ExcelError::new(ExcelErrorKind::Name)
152                .with_message(format!("Unknown table: {name}")));
153        };
154
155        let vertex = self.tables.get(&canon).map(|t| t.vertex).ok_or_else(|| {
156            ExcelError::new(ExcelErrorKind::Name).with_message(format!("Unknown table: {name}"))
157        })?;
158
159        // Replace range deps (cleans old stripes).
160        self.remove_dependent_edges(vertex);
161        self.register_table_range_deps(vertex, &new_range);
162
163        if let Some(existing) = self.tables.get_mut(&canon) {
164            existing.range = new_range;
165            existing.header_row = header_row;
166            existing.headers = headers;
167            existing.totals_row = totals_row;
168        }
169
170        // Propagate to dependents.
171        self.mark_dirty(vertex);
172        self.authority_note_symbol(Some(vertex));
173        self.bump_symbol_revision();
174        Ok(())
175    }
176
177    pub fn delete_table(&mut self, name: &str) -> Result<(), ExcelError> {
178        let Some(canon) = self.canonical_table_name(name) else {
179            return Err(ExcelError::new(ExcelErrorKind::Name)
180                .with_message(format!("Unknown table: {name}")));
181        };
182
183        let Some(entry) = self.tables.remove(&canon) else {
184            return Err(ExcelError::new(ExcelErrorKind::Name)
185                .with_message(format!("Unknown table: {name}")));
186        };
187
188        self.tables_lookup.remove(&self.table_lookup_key(&canon));
189
190        let vertex = entry.vertex;
191        self.table_vertex_lookup.remove(&vertex);
192
193        // Clean range deps / stripes.
194        self.remove_dependent_edges(vertex);
195
196        // Mark deleted for debuggability; edges already removed.
197        self.store.mark_deleted(vertex, true);
198        self.vertex_values.remove(&vertex);
199        self.vertex_formulas.remove(&vertex);
200        self.clear_formula_vertex_dirty(vertex);
201        self.volatile_vertices.remove(&vertex);
202        self.authority_note_symbol(Some(vertex));
203        self.bump_symbol_revision();
204
205        Ok(())
206    }
207
208    fn register_table_range_deps(&mut self, table_vertex: VertexId, range: &RangeRef) {
209        use crate::reference::SharedRangeRef;
210        use crate::reference::SharedSheetLocator;
211        use formualizer_common::AxisBound;
212
213        // Reuse the same range-deps machinery as formulas/names.
214        let sheet_loc = SharedSheetLocator::Id(range.start.sheet_id);
215        let sr = AxisBound::new(range.start.coord.row(), range.start.coord.row_abs());
216        let sc = AxisBound::new(range.start.coord.col(), range.start.coord.col_abs());
217        let er = AxisBound::new(range.end.coord.row(), range.end.coord.row_abs());
218        let ec = AxisBound::new(range.end.coord.col(), range.end.coord.col_abs());
219
220        if let Ok(r) = SharedRangeRef::from_parts(sheet_loc, Some(sr), Some(sc), Some(er), Some(ec))
221        {
222            self.add_range_dependent_edges(table_vertex, &[r.into_owned()], range.start.sheet_id);
223        }
224    }
225}