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    #[inline]
38    fn table_lookup_key(&self, name: &str) -> String {
39        if self.config.case_sensitive_tables {
40            name.to_string()
41        } else {
42            normalize_table_key(name)
43        }
44    }
45
46    fn canonical_table_name(&self, name: &str) -> Option<String> {
47        let key = self.table_lookup_key(name);
48        self.tables_lookup.get(&key).cloned()
49    }
50
51    pub(crate) fn resolve_table_entry(&self, name: &str) -> Option<&TableEntry> {
52        if self.config.case_sensitive_tables {
53            self.tables.get(name)
54        } else {
55            let key = self.table_lookup_key(name);
56            self.tables_lookup
57                .get(&key)
58                .and_then(|canon| self.tables.get(canon))
59        }
60    }
61
62    pub(crate) fn table_by_vertex(&self, vertex: VertexId) -> Option<&TableEntry> {
63        self.table_vertex_lookup
64            .get(&vertex)
65            .and_then(|name| self.tables.get(name))
66    }
67
68    /// Canonical names of every defined table, sorted for deterministic output.
69    pub fn table_names(&self) -> Vec<String> {
70        let mut names: Vec<String> = self.tables.values().map(|t| t.name.clone()).collect();
71        names.sort();
72        names
73    }
74
75    pub fn define_table(
76        &mut self,
77        name: &str,
78        range: RangeRef,
79        header_row: bool,
80        headers: Vec<String>,
81        totals_row: bool,
82    ) -> Result<(), ExcelError> {
83        if name.is_empty() {
84            return Err(ExcelError::new(ExcelErrorKind::Name)
85                .with_message("Table name cannot be empty".to_string()));
86        }
87
88        let key = self.table_lookup_key(name);
89        if let Some(existing) = self.tables_lookup.get(&key) {
90            return Err(ExcelError::new(ExcelErrorKind::Name).with_message(format!(
91                "Table collision under normalization: '{name}' conflicts with '{existing}'"
92            )));
93        }
94
95        let anchor = range.start;
96        let sheet_id = anchor.sheet_id;
97        let packed_coord = formualizer_common::Coord::new(anchor.coord.row(), anchor.coord.col());
98        let vertex = self.store.allocate(packed_coord, sheet_id, 0x01);
99        self.edges.add_vertex(packed_coord, vertex.0);
100        self.sheet_index_mut(sheet_id)
101            .add_vertex(packed_coord, vertex);
102        self.store.set_kind(vertex, VertexKind::Table);
103
104        // Register stripes for the full table region so cell edits inside the table
105        // propagate to formulas that depend on the table.
106        self.register_table_range_deps(vertex, &range);
107
108        let entry = TableEntry {
109            name: name.to_string(),
110            range,
111            header_row,
112            headers,
113            totals_row,
114            vertex,
115        };
116
117        let original = name.to_string();
118        self.tables.insert(original.clone(), entry);
119        self.tables_lookup
120            .insert(self.table_lookup_key(&original), original.clone());
121        self.table_vertex_lookup.insert(vertex, original);
122        self.bump_symbol_revision();
123        Ok(())
124    }
125
126    pub fn update_table(
127        &mut self,
128        name: &str,
129        new_range: RangeRef,
130        header_row: bool,
131        headers: Vec<String>,
132        totals_row: bool,
133    ) -> Result<(), ExcelError> {
134        let Some(canon) = self.canonical_table_name(name) else {
135            return Err(ExcelError::new(ExcelErrorKind::Name)
136                .with_message(format!("Unknown table: {name}")));
137        };
138
139        let vertex = self.tables.get(&canon).map(|t| t.vertex).ok_or_else(|| {
140            ExcelError::new(ExcelErrorKind::Name).with_message(format!("Unknown table: {name}"))
141        })?;
142
143        // Replace range deps (cleans old stripes).
144        self.remove_dependent_edges(vertex);
145        self.register_table_range_deps(vertex, &new_range);
146
147        if let Some(existing) = self.tables.get_mut(&canon) {
148            existing.range = new_range;
149            existing.header_row = header_row;
150            existing.headers = headers;
151            existing.totals_row = totals_row;
152        }
153
154        // Propagate to dependents.
155        self.mark_dirty(vertex);
156        self.bump_symbol_revision();
157        Ok(())
158    }
159
160    pub fn delete_table(&mut self, name: &str) -> Result<(), ExcelError> {
161        let Some(canon) = self.canonical_table_name(name) else {
162            return Err(ExcelError::new(ExcelErrorKind::Name)
163                .with_message(format!("Unknown table: {name}")));
164        };
165
166        let Some(entry) = self.tables.remove(&canon) else {
167            return Err(ExcelError::new(ExcelErrorKind::Name)
168                .with_message(format!("Unknown table: {name}")));
169        };
170
171        self.tables_lookup.remove(&self.table_lookup_key(&canon));
172
173        let vertex = entry.vertex;
174        self.table_vertex_lookup.remove(&vertex);
175
176        // Clean range deps / stripes.
177        self.remove_dependent_edges(vertex);
178
179        // Mark deleted for debuggability; edges already removed.
180        self.store.mark_deleted(vertex, true);
181        self.vertex_values.remove(&vertex);
182        self.vertex_formulas.remove(&vertex);
183        self.clear_formula_vertex_dirty(vertex);
184        self.volatile_vertices.remove(&vertex);
185        self.bump_symbol_revision();
186
187        Ok(())
188    }
189
190    fn register_table_range_deps(&mut self, table_vertex: VertexId, range: &RangeRef) {
191        use crate::reference::SharedRangeRef;
192        use crate::reference::SharedSheetLocator;
193        use formualizer_common::AxisBound;
194
195        // Reuse the same range-deps machinery as formulas/names.
196        let sheet_loc = SharedSheetLocator::Id(range.start.sheet_id);
197        let sr = AxisBound::new(range.start.coord.row(), range.start.coord.row_abs());
198        let sc = AxisBound::new(range.start.coord.col(), range.start.coord.col_abs());
199        let er = AxisBound::new(range.end.coord.row(), range.end.coord.row_abs());
200        let ec = AxisBound::new(range.end.coord.col(), range.end.coord.col_abs());
201
202        if let Ok(r) = SharedRangeRef::from_parts(sheet_loc, Some(sr), Some(sc), Some(er), Some(ec))
203        {
204            self.add_range_dependent_edges(table_vertex, &[r.into_owned()], range.start.sheet_id);
205        }
206    }
207}