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) empty_body: bool,
21    pub(crate) vertex: VertexId,
22}
23
24impl TableEntry {
25    pub fn sheet_id(&self) -> SheetId {
26        self.range.start.sheet_id
27    }
28
29    pub fn col_index(&self, header: &str) -> Option<usize> {
30        let header_key = header.to_lowercase();
31        self.headers
32            .iter()
33            .position(|h| h.to_lowercase() == header_key)
34    }
35}
36
37impl DependencyGraph {
38    /// Empty table cells obstruct arrays just as populated table cells do.
39    pub(crate) fn table_intersects_spill(
40        &self,
41        anchor: crate::reference::CellRef,
42        end_row: u32,
43        end_col: u32,
44    ) -> bool {
45        self.tables.values().any(|t| {
46            t.sheet_id() == anchor.sheet_id
47                && t.range.start.coord.row() <= end_row
48                && anchor.coord.row() <= t.range.end.coord.row()
49                && t.range.start.coord.col() <= end_col
50                && anchor.coord.col() <= t.range.end.coord.col()
51        })
52    }
53
54    #[inline]
55    fn table_lookup_key(&self, name: &str) -> String {
56        if self.config.case_sensitive_tables {
57            name.to_string()
58        } else {
59            normalize_table_key(name)
60        }
61    }
62
63    fn canonical_table_name(&self, name: &str) -> Option<String> {
64        let key = self.table_lookup_key(name);
65        self.tables_lookup.get(&key).cloned()
66    }
67
68    pub(crate) fn resolve_table_entry(&self, name: &str) -> Option<&TableEntry> {
69        if self.config.case_sensitive_tables {
70            self.tables.get(name)
71        } else {
72            let key = self.table_lookup_key(name);
73            self.tables_lookup
74                .get(&key)
75                .and_then(|canon| self.tables.get(canon))
76        }
77    }
78
79    pub(crate) fn table_by_vertex(&self, vertex: VertexId) -> Option<&TableEntry> {
80        self.table_vertex_lookup
81            .get(&vertex)
82            .and_then(|name| self.tables.get(name))
83    }
84
85    /// Canonical names of every defined table, sorted for deterministic output.
86    pub fn table_names(&self) -> Vec<String> {
87        let mut names: Vec<String> = self.tables.values().map(|t| t.name.clone()).collect();
88        names.sort();
89        names
90    }
91
92    pub fn define_table(
93        &mut self,
94        name: &str,
95        range: RangeRef,
96        header_row: bool,
97        headers: Vec<String>,
98        totals_row: bool,
99    ) -> Result<(), ExcelError> {
100        if name.is_empty() {
101            return Err(ExcelError::new(ExcelErrorKind::Name)
102                .with_message("Table name cannot be empty".to_string()));
103        }
104
105        let key = self.table_lookup_key(name);
106        if let Some(existing) = self.tables_lookup.get(&key) {
107            return Err(ExcelError::new(ExcelErrorKind::Name).with_message(format!(
108                "Table collision under normalization: '{name}' conflicts with '{existing}'"
109            )));
110        }
111
112        // A table has a range, but the *vertex* that represents the table symbol has no
113        // position: it is identified by name. Parking it on the range's anchor cell put it
114        // in the sheet index, where grid queries and structural edits could reach it (#304).
115        // Dependencies on the table's cells are carried by the stripe registration below.
116        let sheet_id = range.start.sheet_id;
117        let vertex = self.allocate_symbol_vertex(VertexKind::Table, sheet_id);
118
119        // Register stripes for the full table region so cell edits inside the table
120        // propagate to formulas that depend on the table.
121        self.register_table_range_deps(vertex, &range);
122
123        let entry = TableEntry {
124            name: name.to_string(),
125            range,
126            header_row,
127            headers,
128            totals_row,
129            empty_body: range.start.coord.row() + 1 + u32::from(header_row)
130                > (range.end.coord.row() + 1).saturating_sub(u32::from(totals_row)),
131            vertex,
132        };
133
134        let original = name.to_string();
135        self.tables.insert(original.clone(), entry);
136        self.tables_lookup
137            .insert(self.table_lookup_key(&original), original.clone());
138        self.table_vertex_lookup.insert(vertex, original);
139        self.authority_note_symbol(Some(vertex));
140        self.bump_symbol_revision();
141        self.resolve_pending_symbol("table", name);
142        Ok(())
143    }
144
145    pub fn update_table(
146        &mut self,
147        name: &str,
148        new_range: RangeRef,
149        header_row: bool,
150        headers: Vec<String>,
151        totals_row: bool,
152    ) -> Result<(), ExcelError> {
153        let Some(canon) = self.canonical_table_name(name) else {
154            return Err(ExcelError::new(ExcelErrorKind::Name)
155                .with_message(format!("Unknown table: {name}")));
156        };
157
158        let vertex = self.tables.get(&canon).map(|t| t.vertex).ok_or_else(|| {
159            ExcelError::new(ExcelErrorKind::Name).with_message(format!("Unknown table: {name}"))
160        })?;
161
162        // Replace range deps (cleans old stripes).
163        self.remove_dependent_edges(vertex);
164        self.register_table_range_deps(vertex, &new_range);
165
166        if let Some(existing) = self.tables.get_mut(&canon) {
167            existing.range = new_range;
168            existing.header_row = header_row;
169            existing.headers = headers;
170            existing.totals_row = totals_row;
171            existing.empty_body = new_range.start.coord.row() + 1 + u32::from(header_row)
172                > (new_range.end.coord.row() + 1).saturating_sub(u32::from(totals_row));
173        }
174
175        // Propagate to dependents.
176        self.mark_dirty(vertex);
177        self.authority_note_symbol(Some(vertex));
178        self.bump_symbol_revision();
179        Ok(())
180    }
181
182    /// Resize only the row extent, preserving the table identity and columns.
183    pub fn resize_table(
184        &mut self,
185        name: &str,
186        new_range: RangeRef,
187    ) -> Result<RangeRef, ExcelError> {
188        self.resize_table_body(name, new_range, false)
189    }
190
191    pub fn resize_table_body(
192        &mut self,
193        name: &str,
194        new_range: RangeRef,
195        empty_body: bool,
196    ) -> Result<RangeRef, ExcelError> {
197        let entry = self.resolve_table_entry(name).cloned().ok_or_else(|| {
198            ExcelError::new(ExcelErrorKind::Name).with_message(format!("Unknown table: {name}"))
199        })?;
200        if new_range.start != entry.range.start
201            || new_range.end.sheet_id != entry.sheet_id()
202            || new_range.end.coord.col() != entry.range.end.coord.col()
203            || new_range.end.coord.row() < entry.range.start.coord.row()
204        {
205            return Err(ExcelError::new(ExcelErrorKind::Value)
206                .with_message("Table resize must preserve its sheet, start, and columns"));
207        }
208        let old_range = entry.range;
209        if old_range == new_range && entry.empty_body == empty_body {
210            return Ok(old_range);
211        }
212        let sheet_id = entry.sheet_id();
213        self.update_table(
214            name,
215            new_range,
216            entry.header_row,
217            entry.headers,
218            entry.totals_row,
219        )?;
220        let canon = self.canonical_table_name(name).expect("validated table");
221        self.tables
222            .get_mut(&canon)
223            .expect("validated table")
224            .empty_body = empty_body;
225        // Also invalidate ordinary cell/range readers in the old/new union.
226        self.mark_dirty_rects(&[(
227            sheet_id,
228            old_range.start.coord.row(),
229            old_range.end.coord.row().max(new_range.end.coord.row()),
230            old_range.start.coord.col(),
231            old_range.end.coord.col(),
232        )]);
233        Ok(old_range)
234    }
235
236    /// Logically empty tables whose reserved body contains this cell.
237    pub(crate) fn empty_tables_at(&self, cell: crate::reference::CellRef) -> Vec<TableEntry> {
238        self.tables
239            .values()
240            .filter(|t| {
241                t.empty_body
242                    && t.sheet_id() == cell.sheet_id
243                    && cell.coord.row() >= t.range.start.coord.row() + u32::from(t.header_row)
244                    && cell.coord.row()
245                        <= t.range
246                            .end
247                            .coord
248                            .row()
249                            .saturating_sub(u32::from(t.totals_row))
250                    && cell.coord.col() >= t.range.start.coord.col()
251                    && cell.coord.col() <= t.range.end.coord.col()
252            })
253            .cloned()
254            .collect()
255    }
256
257    pub fn delete_table(&mut self, name: &str) -> Result<(), ExcelError> {
258        let Some(canon) = self.canonical_table_name(name) else {
259            return Err(ExcelError::new(ExcelErrorKind::Name)
260                .with_message(format!("Unknown table: {name}")));
261        };
262
263        let Some(entry) = self.tables.remove(&canon) else {
264            return Err(ExcelError::new(ExcelErrorKind::Name)
265                .with_message(format!("Unknown table: {name}")));
266        };
267
268        self.tables_lookup.remove(&self.table_lookup_key(&canon));
269
270        let vertex = entry.vertex;
271        self.table_vertex_lookup.remove(&vertex);
272
273        // Clean range deps / stripes.
274        self.remove_dependent_edges(vertex);
275
276        // Mark deleted for debuggability; edges already removed.
277        self.store.mark_deleted(vertex, true);
278        self.vertex_values.remove(&vertex);
279        self.vertex_formulas.remove(&vertex);
280        self.clear_formula_vertex_dirty(vertex);
281        self.volatile_vertices.remove(&vertex);
282        self.authority_note_symbol(Some(vertex));
283        self.bump_symbol_revision();
284
285        Ok(())
286    }
287
288    fn register_table_range_deps(&mut self, table_vertex: VertexId, range: &RangeRef) {
289        use crate::reference::SharedRangeRef;
290        use crate::reference::SharedSheetLocator;
291        use formualizer_common::AxisBound;
292
293        // Reuse the same range-deps machinery as formulas/names.
294        let sheet_loc = SharedSheetLocator::Id(range.start.sheet_id);
295        let sr = AxisBound::new(range.start.coord.row(), range.start.coord.row_abs());
296        let sc = AxisBound::new(range.start.coord.col(), range.start.coord.col_abs());
297        let er = AxisBound::new(range.end.coord.row(), range.end.coord.row_abs());
298        let ec = AxisBound::new(range.end.coord.col(), range.end.coord.col_abs());
299
300        if let Ok(r) = SharedRangeRef::from_parts(sheet_loc, Some(sr), Some(sc), Some(er), Some(ec))
301        {
302            self.add_range_dependent_edges(table_vertex, &[r.into_owned()], range.start.sheet_id);
303        }
304    }
305}