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