Skip to main content

formualizer_workbook/backends/
json.rs

1use crate::IoError;
2use crate::traits::{
3    AccessGranularity, BackendCaps, CellData, MergedRange, NamedRange, SaveDestination, SheetData,
4    SpreadsheetReader, SpreadsheetWriter, TableDefinition,
5};
6use crate::traits::{DefinedName, DefinedNameDefinition, DefinedNameScope};
7use serde::{Deserialize, Serialize};
8use std::collections::BTreeMap;
9use std::fs::File;
10use std::io::{BufReader, Read, Write};
11use std::path::{Path, PathBuf};
12
13#[derive(Serialize, Deserialize, Debug, Default, Clone)]
14struct JsonWorkbook {
15    #[serde(default = "default_version")]
16    version: u32,
17    #[serde(default)]
18    compression: Option<CompressionType>,
19    #[serde(default)]
20    sources: Vec<JsonSource>,
21
22    #[serde(default)]
23    defined_names: Vec<JsonDefinedName>,
24
25    #[serde(default)]
26    sheets: BTreeMap<String, JsonSheet>,
27}
28
29type FormulaBatch = (String, Vec<(u32, u32, formualizer_parse::ASTNode)>);
30
31#[derive(Clone, Copy, Debug)]
32pub struct JsonReadOptions {
33    /// When true (default), invalid date/time strings are treated as an IO error.
34    ///
35    /// When false, invalid date/time values are preserved as text (never silently coerced).
36    pub strict_dates: bool,
37}
38
39impl Default for JsonReadOptions {
40    fn default() -> Self {
41        Self { strict_dates: true }
42    }
43}
44
45fn default_version() -> u32 {
46    1
47}
48
49#[derive(Serialize, Deserialize, Debug, Clone, Copy)]
50#[serde(rename_all = "lowercase")]
51#[derive(Default)]
52enum CompressionType {
53    #[default]
54    None,
55    Lz4,
56}
57
58#[derive(Serialize, Deserialize, Debug, Default, Clone)]
59struct JsonSheet {
60    #[serde(default)]
61    cells: Vec<JsonCell>,
62    #[serde(default)]
63    dimensions: Option<(u32, u32)>,
64    #[serde(default)]
65    hidden: bool,
66    #[serde(default)]
67    date_system_1904: bool,
68    #[serde(default)]
69    merged_cells: Vec<MergedRange>,
70    #[serde(default)]
71    tables: Vec<TableDefinition>,
72    #[serde(default)]
73    named_ranges: Vec<NamedRange>,
74}
75
76#[derive(Serialize, Deserialize, Debug, Clone)]
77struct JsonDefinedName {
78    name: String,
79
80    #[serde(default)]
81    scope: DefinedNameScope,
82
83    #[serde(default, skip_serializing_if = "Option::is_none")]
84    scope_sheet: Option<String>,
85
86    definition: JsonDefinedNameDefinition,
87}
88
89#[derive(Serialize, Deserialize, Debug, Clone)]
90#[serde(tag = "type", rename_all = "lowercase")]
91enum JsonDefinedNameDefinition {
92    Range {
93        address: formualizer_common::RangeAddress,
94    },
95    Literal {
96        value: JsonValue,
97    },
98}
99
100#[derive(Serialize, Deserialize, Debug, Clone)]
101struct JsonCell {
102    row: u32,
103    col: u32,
104    #[serde(default)]
105    value: Option<JsonValue>,
106    #[serde(default)]
107    formula: Option<String>,
108    #[serde(default)]
109    style: Option<u32>,
110}
111
112#[derive(Serialize, Deserialize, Debug, Clone)]
113#[serde(tag = "type", rename_all = "lowercase")]
114enum JsonSource {
115    Scalar { name: String, version: Option<u64> },
116    Table { name: String, version: Option<u64> },
117}
118
119#[derive(Serialize, Deserialize, Debug, Clone)]
120#[serde(tag = "type", content = "value")]
121enum JsonValue {
122    Int(i64),
123    Number(f64),
124    Text(String),
125    Boolean(bool),
126    Empty,
127    Date(String),
128    DateTime(String),
129    Time(String),
130    Duration(i64),
131    Array(Vec<Vec<JsonValue>>),
132    Error(String),
133    Pending,
134}
135
136pub struct JsonAdapter {
137    data: JsonWorkbook,
138    path: Option<PathBuf>,
139    read_options: JsonReadOptions,
140    caps: BackendCaps,
141}
142
143impl Default for JsonAdapter {
144    fn default() -> Self {
145        Self::new()
146    }
147}
148
149impl JsonAdapter {
150    pub fn new() -> Self {
151        Self::new_with_options(JsonReadOptions::default())
152    }
153
154    pub fn new_with_options(read_options: JsonReadOptions) -> Self {
155        Self {
156            data: JsonWorkbook::default(),
157            path: None,
158            read_options,
159            caps: BackendCaps {
160                read: true,
161                write: true,
162                streaming: false,
163                tables: true,
164                named_ranges: true,
165                formulas: true,
166                styles: true,
167                lazy_loading: false,
168                random_access: true,
169                bytes_input: true,
170                date_system_1904: false,
171                merged_cells: true,
172                rich_text: false,
173                hyperlinks: false,
174                data_validations: false,
175                shared_formulas: false,
176            },
177        }
178    }
179
180    pub fn read_options(&self) -> &JsonReadOptions {
181        &self.read_options
182    }
183
184    pub fn set_read_options(&mut self, opts: JsonReadOptions) {
185        self.read_options = opts;
186    }
187
188    fn migrate_legacy_named_ranges(&mut self) {
189        if !self.data.defined_names.is_empty() {
190            return;
191        }
192
193        use rustc_hash::FxHashSet;
194
195        let mut seen: FxHashSet<(DefinedNameScope, Option<String>, String)> = FxHashSet::default();
196        for (sheet_name, sheet) in &self.data.sheets {
197            for nr in &sheet.named_ranges {
198                let scope = match nr.scope {
199                    crate::traits::NamedRangeScope::Workbook => DefinedNameScope::Workbook,
200                    crate::traits::NamedRangeScope::Sheet => DefinedNameScope::Sheet,
201                };
202                let scope_sheet = match nr.scope {
203                    crate::traits::NamedRangeScope::Workbook => None,
204                    crate::traits::NamedRangeScope::Sheet => Some(sheet_name.clone()),
205                };
206                let key = (scope.clone(), scope_sheet.clone(), nr.name.clone());
207                if !seen.insert(key) {
208                    continue;
209                }
210
211                self.data.defined_names.push(JsonDefinedName {
212                    name: nr.name.clone(),
213                    scope,
214                    scope_sheet,
215                    definition: JsonDefinedNameDefinition::Range {
216                        address: nr.address.clone(),
217                    },
218                });
219            }
220        }
221    }
222
223    fn to_sheet_data(
224        js: &JsonSheet,
225        sheet_name: &str,
226        opts: &JsonReadOptions,
227    ) -> Result<SheetData, IoError> {
228        let mut cells: BTreeMap<(u32, u32), CellData> = BTreeMap::new();
229        for (idx, c) in js.cells.iter().enumerate() {
230            let lit = match c.value.as_ref() {
231                Some(v) => Some(json_to_literal(
232                    v,
233                    opts,
234                    &format!("sheets.{sheet_name}.cells[{idx}].value"),
235                )?),
236                None => None,
237            };
238            cells.insert(
239                (c.row, c.col),
240                CellData {
241                    value: lit,
242                    formula: c.formula.clone(),
243                    style: c.style,
244                },
245            );
246        }
247        Ok(SheetData {
248            cells,
249            dimensions: js.dimensions,
250            tables: js.tables.clone(),
251            named_ranges: js.named_ranges.clone(),
252            date_system_1904: js.date_system_1904,
253            merged_cells: js.merged_cells.clone(),
254            hidden: js.hidden,
255        })
256    }
257
258    pub fn to_json_string(&self) -> Result<String, IoError> {
259        Ok(serde_json::to_string_pretty(&self.data)?)
260    }
261
262    // Backend-specific helpers (not part of SpreadsheetWriter)
263    fn ensure_sheet_mut(&mut self, name: &str) -> &mut JsonSheet {
264        self.data.sheets.entry(name.to_string()).or_default()
265    }
266
267    pub fn set_dimensions(&mut self, sheet: &str, dims: Option<(u32, u32)>) {
268        self.ensure_sheet_mut(sheet).dimensions = dims;
269    }
270
271    pub fn set_date_system_1904(&mut self, sheet: &str, value: bool) {
272        self.ensure_sheet_mut(sheet).date_system_1904 = value;
273    }
274
275    pub fn set_merged_cells(&mut self, sheet: &str, merged: Vec<MergedRange>) {
276        self.ensure_sheet_mut(sheet).merged_cells = merged;
277    }
278
279    pub fn set_tables(&mut self, sheet: &str, tables: Vec<TableDefinition>) {
280        self.ensure_sheet_mut(sheet).tables = tables;
281    }
282
283    pub fn set_named_ranges(&mut self, sheet: &str, named: Vec<NamedRange>) {
284        self.ensure_sheet_mut(sheet).named_ranges = named;
285    }
286
287    pub fn set_defined_names(&mut self, names: Vec<DefinedName>) {
288        self.data.defined_names = names
289            .into_iter()
290            .map(|dn| match dn.definition {
291                DefinedNameDefinition::Range { address } => JsonDefinedName {
292                    name: dn.name,
293                    scope: dn.scope,
294                    scope_sheet: dn.scope_sheet,
295                    definition: JsonDefinedNameDefinition::Range { address },
296                },
297                DefinedNameDefinition::Literal { value } => JsonDefinedName {
298                    name: dn.name,
299                    scope: dn.scope,
300                    scope_sheet: dn.scope_sheet,
301                    definition: JsonDefinedNameDefinition::Literal {
302                        value: literal_to_json(&value),
303                    },
304                },
305            })
306            .collect();
307    }
308}
309
310impl SpreadsheetReader for JsonAdapter {
311    type Error = IoError;
312
313    fn access_granularity(&self) -> AccessGranularity {
314        AccessGranularity::Workbook
315    }
316
317    fn capabilities(&self) -> BackendCaps {
318        self.caps.clone()
319    }
320
321    fn sheet_names(&self) -> Result<Vec<String>, Self::Error> {
322        Ok(self.data.sheets.keys().cloned().collect())
323    }
324
325    fn open_path<P: AsRef<Path>>(path: P) -> Result<Self, Self::Error>
326    where
327        Self: Sized,
328    {
329        let file = File::open(path.as_ref())?;
330        let reader = BufReader::new(file);
331        let data: JsonWorkbook = serde_json::from_reader(reader)?;
332        let mut adapter = JsonAdapter {
333            data,
334            path: Some(path.as_ref().to_path_buf()),
335            ..JsonAdapter::new()
336        };
337        adapter.migrate_legacy_named_ranges();
338        Ok(adapter)
339    }
340
341    fn open_reader(reader: Box<dyn Read + Send + Sync>) -> Result<Self, Self::Error>
342    where
343        Self: Sized,
344    {
345        let data: JsonWorkbook = serde_json::from_reader(reader)?;
346        let mut adapter = JsonAdapter {
347            data,
348            ..JsonAdapter::new()
349        };
350        adapter.migrate_legacy_named_ranges();
351        Ok(adapter)
352    }
353
354    fn open_bytes(bytes: Vec<u8>) -> Result<Self, Self::Error>
355    where
356        Self: Sized,
357    {
358        let data: JsonWorkbook = serde_json::from_slice(&bytes)?;
359        let mut adapter = JsonAdapter {
360            data,
361            ..JsonAdapter::new()
362        };
363        adapter.migrate_legacy_named_ranges();
364        Ok(adapter)
365    }
366
367    fn defined_names(&mut self) -> Result<Vec<DefinedName>, Self::Error> {
368        self.migrate_legacy_named_ranges();
369        self.data
370            .defined_names
371            .iter()
372            .enumerate()
373            .map(|(idx, dn)| {
374                let def = match &dn.definition {
375                    JsonDefinedNameDefinition::Range { address } => DefinedNameDefinition::Range {
376                        address: address.clone(),
377                    },
378                    JsonDefinedNameDefinition::Literal { value } => {
379                        DefinedNameDefinition::Literal {
380                            value: json_to_literal(
381                                value,
382                                &self.read_options,
383                                &format!("defined_names[{idx}].definition.value"),
384                            )?,
385                        }
386                    }
387                };
388                Ok(DefinedName {
389                    name: dn.name.clone(),
390                    scope: dn.scope.clone(),
391                    scope_sheet: dn.scope_sheet.clone(),
392                    definition: def,
393                })
394            })
395            .collect::<Result<Vec<_>, IoError>>()
396    }
397
398    fn read_range(
399        &mut self,
400        sheet: &str,
401        start: (u32, u32),
402        end: (u32, u32),
403    ) -> Result<BTreeMap<(u32, u32), CellData>, Self::Error> {
404        if let Some(js) = self.data.sheets.get(sheet) {
405            let mut out = BTreeMap::new();
406            for c in &js.cells {
407                if c.row >= start.0 && c.row <= end.0 && c.col >= start.1 && c.col <= end.1 {
408                    let lit = match c.value.as_ref() {
409                        Some(v) => Some(json_to_literal(
410                            v,
411                            &self.read_options,
412                            &format!("sheets.{sheet}.cells[row={},col={}].value", c.row, c.col),
413                        )?),
414                        None => None,
415                    };
416                    out.insert(
417                        (c.row, c.col),
418                        CellData {
419                            value: lit,
420                            formula: c.formula.clone(),
421                            style: c.style,
422                        },
423                    );
424                }
425            }
426            Ok(out)
427        } else {
428            Ok(BTreeMap::new())
429        }
430    }
431
432    fn read_sheet(&mut self, sheet: &str) -> Result<SheetData, Self::Error> {
433        if let Some(js) = self.data.sheets.get(sheet) {
434            Self::to_sheet_data(js, sheet, &self.read_options)
435        } else {
436            Ok(SheetData {
437                cells: BTreeMap::new(),
438                dimensions: None,
439                tables: vec![],
440                named_ranges: vec![],
441                date_system_1904: false,
442                merged_cells: vec![],
443                hidden: false,
444            })
445        }
446    }
447
448    fn sheet_bounds(&self, sheet: &str) -> Option<(u32, u32)> {
449        self.data.sheets.get(sheet).and_then(|s| s.dimensions)
450    }
451
452    fn is_loaded(&self, _sheet: &str, _row: Option<u32>, _col: Option<u32>) -> bool {
453        true
454    }
455}
456
457impl SpreadsheetWriter for JsonAdapter {
458    type Error = IoError;
459
460    fn write_cell(
461        &mut self,
462        sheet: &str,
463        row: u32,
464        col: u32,
465        data: CellData,
466    ) -> Result<(), Self::Error> {
467        let sheet_entry = self.data.sheets.entry(sheet.to_string()).or_default();
468        if let Some(cell) = sheet_entry
469            .cells
470            .iter_mut()
471            .find(|c| c.row == row && c.col == col)
472        {
473            cell.value = data.value.as_ref().map(literal_to_json);
474            cell.formula = data.formula;
475            cell.style = data.style;
476        } else {
477            sheet_entry.cells.push(JsonCell {
478                row,
479                col,
480                value: data.value.as_ref().map(literal_to_json),
481                formula: data.formula,
482                style: data.style,
483            });
484        }
485        Ok(())
486    }
487
488    fn write_range(
489        &mut self,
490        sheet: &str,
491        cells: BTreeMap<(u32, u32), CellData>,
492    ) -> Result<(), Self::Error> {
493        for ((r, c), d) in cells {
494            self.write_cell(sheet, r, c, d)?;
495        }
496        Ok(())
497    }
498
499    fn clear_range(
500        &mut self,
501        sheet: &str,
502        start: (u32, u32),
503        end: (u32, u32),
504    ) -> Result<(), Self::Error> {
505        if let Some(js) = self.data.sheets.get_mut(sheet) {
506            js.cells.retain(|c| {
507                !(c.row >= start.0 && c.row <= end.0 && c.col >= start.1 && c.col <= end.1)
508            });
509        }
510        Ok(())
511    }
512
513    fn create_sheet(&mut self, name: &str) -> Result<(), Self::Error> {
514        self.data.sheets.entry(name.to_string()).or_default();
515        Ok(())
516    }
517
518    fn delete_sheet(&mut self, name: &str) -> Result<(), Self::Error> {
519        self.data.sheets.remove(name);
520        Ok(())
521    }
522
523    fn rename_sheet(&mut self, old: &str, new: &str) -> Result<(), Self::Error> {
524        if let Some(sheet) = self.data.sheets.remove(old) {
525            self.data.sheets.insert(new.to_string(), sheet);
526        }
527        Ok(())
528    }
529
530    fn flush(&mut self) -> Result<(), Self::Error> {
531        Ok(())
532    }
533
534    fn save(&mut self) -> Result<(), Self::Error> {
535        if let Some(path) = &self.path {
536            let mut file = File::create(path)?;
537            let s = serde_json::to_string_pretty(&self.data)?;
538            file.write_all(s.as_bytes())?;
539            Ok(())
540        } else {
541            Ok(())
542        }
543    }
544
545    fn save_to<'a>(&mut self, dest: SaveDestination<'a>) -> Result<Option<Vec<u8>>, Self::Error> {
546        match dest {
547            SaveDestination::InPlace => self.save().map(|_| None),
548            SaveDestination::Path(path) => {
549                let mut file = File::create(path)?;
550                let s = serde_json::to_string_pretty(&self.data)?;
551                file.write_all(s.as_bytes())?;
552                self.path = Some(path.to_path_buf());
553                Ok(None)
554            }
555            SaveDestination::Writer(writer) => {
556                let s = serde_json::to_string_pretty(&self.data)?;
557                writer.write_all(s.as_bytes())?;
558                Ok(None)
559            }
560            SaveDestination::Bytes => {
561                let s = serde_json::to_vec_pretty(&self.data)?;
562                Ok(Some(s))
563            }
564        }
565    }
566}
567
568fn literal_to_json(v: &formualizer_common::LiteralValue) -> JsonValue {
569    use formualizer_common::LiteralValue as L;
570    match v {
571        L::Int(i) => JsonValue::Int(*i),
572        L::Number(n) => JsonValue::Number(*n),
573        L::Text(s) => JsonValue::Text(s.clone()),
574        L::Boolean(b) => JsonValue::Boolean(*b),
575        L::Empty => JsonValue::Empty,
576        L::Array(arr) => JsonValue::Array(
577            arr.iter()
578                .map(|row| row.iter().map(literal_to_json).collect())
579                .collect(),
580        ),
581        L::Date(d) => JsonValue::Date(d.to_string()),
582        L::DateTime(dt) => JsonValue::DateTime(dt.to_string()),
583        L::Time(t) => JsonValue::Time(t.to_string()),
584        L::Duration(dur) => JsonValue::Duration(dur.num_seconds()),
585        L::Error(e) => JsonValue::Error(e.kind.to_string()),
586        L::Pending => JsonValue::Pending,
587    }
588}
589
590fn json_to_literal(
591    v: &JsonValue,
592    opts: &JsonReadOptions,
593    path: &str,
594) -> Result<formualizer_common::LiteralValue, IoError> {
595    use formualizer_common::LiteralValue as L;
596    Ok(match v {
597        JsonValue::Int(i) => L::Int(*i),
598        JsonValue::Number(n) => L::Number(*n),
599        JsonValue::Text(s) => L::Text(s.clone()),
600        JsonValue::Boolean(b) => L::Boolean(*b),
601        JsonValue::Empty => L::Empty,
602        JsonValue::Array(arr) => L::Array(
603            arr.iter()
604                .map(|row| {
605                    row.iter()
606                        .map(|v| json_to_literal(v, opts, path))
607                        .collect::<Result<Vec<_>, IoError>>()
608                })
609                .collect::<Result<Vec<_>, IoError>>()?,
610        ),
611        JsonValue::Date(s) => match chrono::NaiveDate::parse_from_str(s, "%Y-%m-%d") {
612            Ok(d) => L::Date(d),
613            Err(_) if opts.strict_dates => {
614                return Err(IoError::Backend {
615                    backend: "json".to_string(),
616                    message: format!("Invalid date at {path}: '{s}'"),
617                });
618            }
619            Err(_) => L::Text(s.clone()),
620        },
621        JsonValue::DateTime(s) => {
622            let parsed = chrono::NaiveDateTime::parse_from_str(s, "%Y-%m-%d %H:%M:%S")
623                .or_else(|_| chrono::NaiveDateTime::parse_from_str(s, "%Y-%m-%dT%H:%M:%S"));
624            match parsed {
625                Ok(dt) => L::DateTime(dt),
626                Err(_) if opts.strict_dates => {
627                    return Err(IoError::Backend {
628                        backend: "json".to_string(),
629                        message: format!("Invalid datetime at {path}: '{s}'"),
630                    });
631                }
632                Err(_) => L::Text(s.clone()),
633            }
634        }
635        JsonValue::Time(s) => match chrono::NaiveTime::parse_from_str(s, "%H:%M:%S") {
636            Ok(t) => L::Time(t),
637            Err(_) if opts.strict_dates => {
638                return Err(IoError::Backend {
639                    backend: "json".to_string(),
640                    message: format!("Invalid time at {path}: '{s}'"),
641                });
642            }
643            Err(_) => L::Text(s.clone()),
644        },
645        JsonValue::Duration(secs) => L::Duration(chrono::Duration::seconds(*secs)),
646        JsonValue::Error(code) => L::Error(
647            formualizer_common::error::ExcelError::from_error_string(code),
648        ),
649        JsonValue::Pending => L::Pending,
650    })
651}
652
653// Stream JSON workbook contents into the evaluation engine (values + formulas),
654// and propagate the workbook date system to the engine config.
655impl<R> formualizer_eval::engine::ingest::EngineLoadStream<R> for JsonAdapter
656where
657    R: formualizer_eval::traits::EvaluationContext,
658{
659    type Error = IoError;
660
661    fn stream_into_engine(
662        &mut self,
663        engine: &mut formualizer_eval::engine::Engine<R>,
664    ) -> Result<(), Self::Error> {
665        // Propagate date system: if any sheet declares 1904, treat workbook as 1904
666        let any_1904 = self.data.sheets.values().any(|s| s.date_system_1904);
667        engine.config.date_system = if any_1904 {
668            formualizer_eval::engine::DateSystem::Excel1904
669        } else {
670            formualizer_eval::engine::DateSystem::Excel1900
671        };
672
673        // Ensure all sheets exist in the graph first
674        for name in self.data.sheets.keys() {
675            engine
676                .add_sheet(name)
677                .map_err(|e| IoError::from_backend("json", e))?;
678        }
679
680        // Declare external sources (SourceVertex) before ingesting formulas.
681        for src in &self.data.sources {
682            match src {
683                JsonSource::Scalar { name, version } => engine
684                    .define_source_scalar(name, *version)
685                    .map_err(|e| IoError::from_backend("json", e))?,
686                JsonSource::Table { name, version } => {
687                    engine
688                        .define_source_table(name, *version)
689                        .map_err(|e| IoError::from_backend("json", e))?
690                }
691            }
692        }
693
694        // Ingest values via Arrow IngestBuilder per sheet
695        let chunk_rows: usize = 32 * 1024;
696        let mut eager_formula_batches: Vec<FormulaBatch> = Vec::new();
697        for (name, sheet) in self.data.sheets.iter() {
698            let dims = sheet.dimensions.unwrap_or_else(|| {
699                let mut max_r = 0u32;
700                let mut max_c = 0u32;
701                for c in &sheet.cells {
702                    if c.row > max_r {
703                        max_r = c.row;
704                    }
705                    if c.col > max_c {
706                        max_c = c.col;
707                    }
708                }
709                (max_r, max_c)
710            });
711            let rows = dims.0 as usize;
712            let cols = dims.1 as usize;
713
714            let mut aib = formualizer_eval::arrow_store::IngestBuilder::new(
715                name,
716                cols,
717                chunk_rows,
718                engine.config.date_system,
719            );
720            // Build a map for quick lookup
721            let mut cell_map: BTreeMap<(u32, u32), &JsonCell> = BTreeMap::new();
722            for c in &sheet.cells {
723                cell_map.insert((c.row, c.col), c);
724            }
725            for r in 1..=rows {
726                let mut row_vals: Vec<formualizer_common::LiteralValue> =
727                    vec![formualizer_common::LiteralValue::Empty; cols];
728                for c in 1..=cols {
729                    if let Some(cell) = cell_map.get(&(r as u32, c as u32))
730                        && let Some(v) = &cell.value
731                    {
732                        row_vals[c - 1] = json_to_literal(
733                            v,
734                            &self.read_options,
735                            &format!(
736                                "sheets.{name}.cells[row={},col={}].value",
737                                r as u32, c as u32
738                            ),
739                        )?;
740                    }
741                }
742                aib.append_row(&row_vals)
743                    .map_err(|e| IoError::from_backend("json", e))?;
744            }
745            let asheet = aib.finish();
746            let store = engine.sheet_store_mut();
747            if let Some(pos) = store.sheets.iter().position(|s| s.name.as_ref() == name) {
748                store.sheets[pos] = asheet;
749            } else {
750                store.sheets.push(asheet);
751            }
752
753            // Register native table metadata before formula ingest.
754            if let Some(sheet_id) = engine.sheet_id(name) {
755                for table in &sheet.tables {
756                    let (sr, sc, er, ec) = table.range;
757                    let sr0 = sr.saturating_sub(1);
758                    let sc0 = sc.saturating_sub(1);
759                    let er0 = er.saturating_sub(1);
760                    let ec0 = ec.saturating_sub(1);
761                    let start_ref = formualizer_eval::reference::CellRef::new(
762                        sheet_id,
763                        formualizer_eval::reference::Coord::new(sr0, sc0, true, true),
764                    );
765                    let end_ref = formualizer_eval::reference::CellRef::new(
766                        sheet_id,
767                        formualizer_eval::reference::Coord::new(er0, ec0, true, true),
768                    );
769                    let range_ref = formualizer_eval::reference::RangeRef::new(start_ref, end_ref);
770                    engine.define_table(
771                        &table.name,
772                        range_ref,
773                        table.header_row,
774                        table.headers.clone(),
775                        table.totals_row,
776                    )?;
777                }
778            }
779
780            // Formulas: either stage into graph now or defer
781            if engine.config.defer_graph_building {
782                for c in &sheet.cells {
783                    if let Some(f) = &c.formula {
784                        if f.is_empty() {
785                            continue;
786                        }
787                        engine.stage_formula_text(name, c.row, c.col, f.clone());
788                    }
789                }
790            } else {
791                let mut formulas: Vec<(u32, u32, formualizer_parse::ASTNode)> = Vec::new();
792                for c in &sheet.cells {
793                    if let Some(f) = &c.formula {
794                        if f.is_empty() {
795                            continue;
796                        }
797                        let with_eq = if f.starts_with('=') {
798                            f.clone()
799                        } else {
800                            format!("={f}")
801                        };
802                        match formualizer_parse::parser::parse(&with_eq) {
803                            Ok(parsed) => formulas.push((c.row, c.col, parsed)),
804                            Err(e) => {
805                                if let Some(recovered) = engine
806                                    .handle_formula_parse_error(
807                                        name,
808                                        c.row,
809                                        c.col,
810                                        &with_eq,
811                                        e.to_string(),
812                                    )
813                                    .map_err(IoError::Engine)?
814                                {
815                                    formulas.push((c.row, c.col, recovered));
816                                }
817                            }
818                        }
819                    }
820                }
821                if !formulas.is_empty() {
822                    eager_formula_batches.push((name.clone(), formulas));
823                }
824            }
825        }
826
827        if !engine.config.defer_graph_building && !eager_formula_batches.is_empty() {
828            let mut builder = engine.begin_bulk_ingest();
829            for (sheet_name, formulas) in eager_formula_batches {
830                let sid = builder.add_sheet(&sheet_name);
831                builder.add_formulas(sid, formulas.into_iter());
832            }
833            builder.finish().map_err(IoError::Engine)?;
834        }
835
836        // Register defined names into the dependency graph.
837        {
838            use formualizer_eval::engine::named_range::{NameScope, NamedDefinition};
839            use formualizer_eval::reference::{CellRef, Coord};
840            use rustc_hash::FxHashSet;
841
842            let defined = self
843                .defined_names()
844                .map_err(|e| IoError::from_backend("json", e))?;
845
846            let mut seen: FxHashSet<(DefinedNameScope, Option<String>, String)> =
847                FxHashSet::default();
848            for dn in defined {
849                let key = (dn.scope.clone(), dn.scope_sheet.clone(), dn.name.clone());
850                if !seen.insert(key) {
851                    continue;
852                }
853
854                let scope = match dn.scope {
855                    DefinedNameScope::Workbook => NameScope::Workbook,
856                    DefinedNameScope::Sheet => {
857                        let sheet_name =
858                            dn.scope_sheet.as_deref().ok_or_else(|| IoError::Backend {
859                                backend: "json".to_string(),
860                                message: format!(
861                                    "sheet-scoped defined name `{}` missing scope_sheet",
862                                    dn.name
863                                ),
864                            })?;
865                        let sid = engine
866                            .sheet_id(sheet_name)
867                            .ok_or_else(|| IoError::Backend {
868                                backend: "json".to_string(),
869                                message: format!("scope sheet not found: {sheet_name}"),
870                            })?;
871                        NameScope::Sheet(sid)
872                    }
873                };
874
875                let definition = match dn.definition {
876                    DefinedNameDefinition::Range { address } => {
877                        let sheet_id = engine
878                            .sheet_id(&address.sheet)
879                            .or_else(|| engine.add_sheet(&address.sheet).ok())
880                            .ok_or_else(|| IoError::Backend {
881                                backend: "json".to_string(),
882                                message: format!("sheet not found: {}", address.sheet),
883                            })?;
884
885                        let sr0 = address.start_row.saturating_sub(1);
886                        let sc0 = address.start_col.saturating_sub(1);
887                        let er0 = address.end_row.saturating_sub(1);
888                        let ec0 = address.end_col.saturating_sub(1);
889
890                        let start_ref = CellRef::new(sheet_id, Coord::new(sr0, sc0, true, true));
891                        if sr0 == er0 && sc0 == ec0 {
892                            NamedDefinition::Cell(start_ref)
893                        } else {
894                            let end_ref = CellRef::new(sheet_id, Coord::new(er0, ec0, true, true));
895                            let range_ref =
896                                formualizer_eval::reference::RangeRef::new(start_ref, end_ref);
897                            NamedDefinition::Range(range_ref)
898                        }
899                    }
900                    DefinedNameDefinition::Literal { value } => NamedDefinition::Literal(value),
901                };
902
903                engine.define_name(&dn.name, definition, scope)?;
904            }
905        }
906
907        // Finalize sheet indexes after load
908        for name in self.data.sheets.keys() {
909            engine.finalize_sheet_index(name);
910        }
911        engine.set_first_load_assume_new(false);
912        engine.reset_ensure_touched();
913        Ok(())
914    }
915}