formualizer_eval/engine/graph/
tables.rs1use 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#[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 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 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 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 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 self.remove_dependent_edges(vertex);
178
179 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 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}