Skip to main content

formualizer_workbook/backends/
calamine.rs

1use crate::load_limits::enforce_sheet_dimension_limits;
2use crate::traits::{
3    AccessGranularity, AdapterLoadStats, BackendCaps, CalcSettings, CellData, DefinedName,
4    DefinedNameDefinition, DefinedNameScope, MergedRange, SheetData, SpreadsheetReader,
5};
6use formualizer_common::{DateSystem, ExcelError, ExcelErrorKind, LiteralValue};
7use parking_lot::RwLock;
8use std::collections::{BTreeMap, BTreeSet, HashSet};
9use std::fs::File;
10use std::io::{BufRead, BufReader, Read, Seek, SeekFrom};
11use std::path::Path;
12use std::sync::{Arc, Mutex, OnceLock};
13
14use calamine::{Data, DataRef, Range, Reader, Xlsx, XlsxFormulaMetadata, open_workbook_from_rs};
15use formualizer_common::RangeAddress;
16use formualizer_eval::arrow_store::{IngestBuilder, OverlayValue, map_error_code};
17use formualizer_eval::engine::ingest::EngineLoadStream;
18use formualizer_eval::engine::{
19    CancelToken, DeferredFormulaPackage, Engine as EvalEngine, ExplicitPartitionLegacyMembers,
20    FormulaCompressedPreparation, FormulaCompressedSourceBatch, FormulaCompressedSourceReport,
21    FormulaIngestBatch, FormulaIngestRecord, FormulaSpoolDiskPolicy, PartitionLegacyMember,
22    PartitionLegacyMemberKind, PartitionReconciliation, PartitionedSourceFormulaFamily,
23    SourceCoord, SourceFamilyId, SourceFormulaFamily, SourceFormulaOrder, SourceRect,
24};
25use formualizer_eval::traits::EvaluationContext;
26use formualizer_parse::parser::{ASTNode, ReferenceType};
27use quick_xml::Reader as XmlReader;
28use quick_xml::events::{BytesRef, BytesStart, Event};
29use quick_xml::name::QName;
30use zip::ZipArchive;
31
32mod compressed_evidence;
33mod formula_replay;
34
35use compressed_evidence::{EvidenceRecord, MonotonicFormulaEvidence};
36use formula_replay::{
37    CalamineDeferredFormulaReplay, FormulaReplaySpool, FormulaSpoolLimits,
38    HybridFormulaReplaySpool, SpoolFormulaRecord, replay_spool_per_cell_filtered_with_family,
39};
40
41struct SharedFile {
42    file: Mutex<File>,
43    len: u64,
44}
45
46/// Per-cursor readahead window for the shared-file policy.
47///
48/// ZIP central-directory and part reads are small and mostly forward, so an
49/// unbuffered cursor turns each into its own lock/seek/read syscall. The window
50/// is keyed by absolute offset, so a seek that lands inside it is still served
51/// from memory.
52///
53/// This widens the window in which a cursor can serve bytes a concurrent writer
54/// has since changed. `SharedFile` was never a snapshot, so no guarantee moves:
55/// a concurrently mutated file still yields ordinary EOF/I/O/ZIP errors or
56/// ordinary stale bytes, never a mapped-memory fault.
57const SHARED_FILE_READAHEAD: usize = 64 * 1024;
58
59#[derive(Clone)]
60struct SharedFileCursor {
61    source: Arc<SharedFile>,
62    position: u64,
63    /// Bytes covering `[buffer_start, buffer_start + buffer.len())`.
64    buffer: Vec<u8>,
65    buffer_start: u64,
66}
67
68impl SharedFileCursor {
69    fn new(source: Arc<SharedFile>) -> Self {
70        Self {
71            source,
72            position: 0,
73            buffer: Vec::new(),
74            buffer_start: 0,
75        }
76    }
77
78    /// Serves `buf` from the readahead window when it covers `self.position`.
79    fn read_buffered(&mut self, buf: &mut [u8]) -> Option<usize> {
80        let offset = usize::try_from(self.position.checked_sub(self.buffer_start)?).ok()?;
81        let available = self.buffer.len().checked_sub(offset)?;
82        if available == 0 {
83            return None;
84        }
85        let read = buf.len().min(available);
86        buf[..read].copy_from_slice(&self.buffer[offset..offset + read]);
87        self.position += read as u64;
88        Some(read)
89    }
90
91    /// One locked seek+read. Fills the readahead window unless `direct` is set,
92    /// in which case the caller's larger buffer is filled straight through.
93    fn read_physical(&mut self, buf: &mut [u8], limit: usize) -> std::io::Result<usize> {
94        let mut file = self
95            .source
96            .file
97            .lock()
98            .map_err(|_| std::io::Error::other("shared XLSX file lock poisoned"))?;
99        file.seek(SeekFrom::Start(self.position))?;
100        if limit >= SHARED_FILE_READAHEAD {
101            let read = file.read(&mut buf[..limit])?;
102            drop(file);
103            self.position += read as u64;
104            self.buffer.clear();
105            return Ok(read);
106        }
107        let window = limit.max(
108            SHARED_FILE_READAHEAD
109                .min(usize::try_from(self.source.len - self.position).unwrap_or(usize::MAX)),
110        );
111        self.buffer.resize(window, 0);
112        let filled = file.read(&mut self.buffer[..window])?;
113        drop(file);
114        self.buffer.truncate(filled);
115        self.buffer_start = self.position;
116        Ok(self.read_buffered(buf).unwrap_or(0))
117    }
118}
119
120#[cfg(any(unix, windows))]
121struct MappedXlsxSource {
122    // The handle pins the opened file rather than its path. It does not prevent
123    // another process from truncating the file on Unix; see CalamineAdapter.
124    _file: File,
125    map: memmap2::Mmap,
126}
127
128#[derive(Clone)]
129enum SharedXlsxReader {
130    Bytes {
131        bytes: Arc<[u8]>,
132        position: u64,
133    },
134    File(SharedFileCursor),
135    #[cfg(any(unix, windows))]
136    Mapped {
137        source: Arc<MappedXlsxSource>,
138        position: u64,
139    },
140}
141
142/// Selects the retained backing source used for an XLSX filesystem path.
143///
144/// This policy applies only to path-based Calamine XLSX loads. Byte and reader
145/// inputs always retain one shared immutable byte allocation.
146#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
147pub enum XlsxPathSource {
148    /// Retain one opened file and serialize physical seeks and reads while
149    /// giving each XLSX consumer an independent logical cursor.
150    ///
151    /// This mutation-safe option is the default. Renaming or replacing the
152    /// pathname is safe because the opened handle is retained.
153    #[default]
154    SharedFile,
155    /// Retain the opened file and map it read-only.
156    ///
157    /// Mapping is attempted explicitly and errors are returned without falling
158    /// back to [`SharedFile`](Self::SharedFile). The caller guarantees that the
159    /// opened underlying file/inode is not destructively modified or truncated
160    /// for the adapter's lifetime. Violating this contract can terminate the
161    /// process; in particular, Unix may deliver `SIGBUS` when a mapped page past
162    /// a new end-of-file is accessed. Renaming or replacing the pathname is safe
163    /// because the original opened handle remains retained.
164    DirectMmap,
165}
166
167fn seek_position(position: u64, len: u64, from: SeekFrom) -> std::io::Result<u64> {
168    let next = match from {
169        SeekFrom::Start(next) => Some(next),
170        SeekFrom::Current(offset) => position.checked_add_signed(offset),
171        SeekFrom::End(offset) => len.checked_add_signed(offset),
172    };
173    next.ok_or_else(|| std::io::Error::new(std::io::ErrorKind::InvalidInput, "invalid seek"))
174}
175
176fn read_slice(source: &[u8], position: &mut u64, buf: &mut [u8]) -> std::io::Result<usize> {
177    let start = usize::try_from(*position).unwrap_or(usize::MAX);
178    if start >= source.len() {
179        return Ok(0);
180    }
181    let read = buf.len().min(source.len() - start);
182    buf[..read].copy_from_slice(&source[start..start + read]);
183    *position = position
184        .checked_add(read as u64)
185        .ok_or_else(|| std::io::Error::other("reader position overflow"))?;
186    Ok(read)
187}
188
189impl SharedXlsxReader {
190    fn from_bytes(data: Vec<u8>) -> Self {
191        Self::Bytes {
192            bytes: Arc::from(data),
193            position: 0,
194        }
195    }
196
197    fn from_file(file: File, policy: XlsxPathSource) -> Result<Self, std::io::Error> {
198        let len = file.metadata()?.len();
199        match policy {
200            XlsxPathSource::SharedFile => {
201                Ok(Self::File(SharedFileCursor::new(Arc::new(SharedFile {
202                    file: Mutex::new(file),
203                    len,
204                }))))
205            }
206            XlsxPathSource::DirectMmap => {
207                #[cfg(any(unix, windows))]
208                {
209                    if len == 0 {
210                        return Err(std::io::Error::new(
211                            std::io::ErrorKind::InvalidInput,
212                            "cannot memory-map an empty XLSX file",
213                        ));
214                    }
215                    // SAFETY: the map is read-only and its source handle remains
216                    // alive. The caller accepts the mutation/truncation contract
217                    // documented on `XlsxPathSource::DirectMmap`.
218                    let map = unsafe { memmap2::MmapOptions::new().map(&file) }?;
219                    Ok(Self::Mapped {
220                        source: Arc::new(MappedXlsxSource { _file: file, map }),
221                        position: 0,
222                    })
223                }
224                #[cfg(not(any(unix, windows)))]
225                {
226                    let _ = (file, len);
227                    Err(std::io::Error::new(
228                        std::io::ErrorKind::Unsupported,
229                        "DirectMmap XLSX path loading is unsupported on this target",
230                    ))
231                }
232            }
233        }
234    }
235
236    fn reader(&self) -> Self {
237        match self {
238            Self::Bytes { bytes, .. } => Self::Bytes {
239                bytes: Arc::clone(bytes),
240                position: 0,
241            },
242            Self::File(cursor) => Self::File(SharedFileCursor::new(Arc::clone(&cursor.source))),
243            #[cfg(any(unix, windows))]
244            Self::Mapped { source, .. } => Self::Mapped {
245                source: Arc::clone(source),
246                position: 0,
247            },
248        }
249    }
250
251    #[cfg(test)]
252    fn bytes_backing_ptr(&self) -> Option<*const u8> {
253        match self {
254            Self::Bytes { bytes, .. } => Some(bytes.as_ptr()),
255            _ => None,
256        }
257    }
258
259    #[cfg(test)]
260    fn strong_count(&self) -> usize {
261        match self {
262            Self::Bytes { bytes, .. } => Arc::strong_count(bytes),
263            Self::File(cursor) => Arc::strong_count(&cursor.source),
264            #[cfg(any(unix, windows))]
265            Self::Mapped { source, .. } => Arc::strong_count(source),
266        }
267    }
268}
269
270impl Read for SharedXlsxReader {
271    fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
272        match self {
273            Self::Bytes { bytes, position } => read_slice(bytes, position, buf),
274            Self::File(cursor) => {
275                if buf.is_empty() || cursor.position >= cursor.source.len {
276                    return Ok(0);
277                }
278                if let Some(read) = cursor.read_buffered(buf) {
279                    return Ok(read);
280                }
281                let remaining = cursor.source.len - cursor.position;
282                let limit = buf
283                    .len()
284                    .min(usize::try_from(remaining).unwrap_or(usize::MAX));
285                cursor.read_physical(buf, limit)
286            }
287            #[cfg(any(unix, windows))]
288            Self::Mapped { source, position } => read_slice(&source.map, position, buf),
289        }
290    }
291}
292
293impl Seek for SharedXlsxReader {
294    fn seek(&mut self, from: SeekFrom) -> std::io::Result<u64> {
295        let position = match self {
296            Self::Bytes { bytes, position } => {
297                *position = seek_position(*position, bytes.len() as u64, from)?;
298                *position
299            }
300            Self::File(cursor) => {
301                cursor.position = seek_position(cursor.position, cursor.source.len, from)?;
302                cursor.position
303            }
304            #[cfg(any(unix, windows))]
305            Self::Mapped { source, position } => {
306                *position = seek_position(*position, source.map.len() as u64, from)?;
307                *position
308            }
309        };
310        Ok(position)
311    }
312}
313
314/// A bounded, cooperative cancellation layer for Calamine's ZIP reads.
315///
316/// `ErrorKind::Interrupted` is deliberately not used: standard library helpers
317/// such as `read_to_end` retry it automatically. Limiting each forwarded read
318/// also bounds the time before a concurrently-signalled token is observed when
319/// the backing source is an in-memory byte slice.
320struct CancellableReader {
321    inner: SharedXlsxReader,
322    cancel: Option<CancelToken>,
323}
324
325impl CancellableReader {
326    const MAX_READ: usize = 64 * 1024;
327
328    fn new(inner: SharedXlsxReader, cancel: Option<CancelToken>) -> Self {
329        Self { inner, cancel }
330    }
331
332    fn checkpoint(&self) -> std::io::Result<()> {
333        if self.cancel.as_ref().is_some_and(CancelToken::is_cancelled) {
334            return Err(std::io::Error::other("calamine load cancelled"));
335        }
336        Ok(())
337    }
338}
339
340impl Read for CancellableReader {
341    fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
342        self.checkpoint()?;
343        let limit = buf.len().min(Self::MAX_READ);
344        self.inner.read(&mut buf[..limit])
345    }
346}
347
348impl Seek for CancellableReader {
349    fn seek(&mut self, from: SeekFrom) -> std::io::Result<u64> {
350        self.checkpoint()?;
351        self.inner.seek(from)
352    }
353}
354
355struct CalamineWorkbook(Xlsx<CancellableReader>);
356
357impl CalamineWorkbook {
358    fn worksheet_range(&mut self, sheet: &str) -> Result<Range<Data>, calamine::Error> {
359        self.0.worksheet_range(sheet).map_err(Into::into)
360    }
361
362    fn worksheet_formula(&mut self, sheet: &str) -> Result<Range<String>, calamine::Error> {
363        self.0.worksheet_formula(sheet).map_err(Into::into)
364    }
365}
366
367struct DebugTimer {
368    #[cfg(not(target_arch = "wasm32"))]
369    started: std::time::Instant,
370}
371
372struct DenseState {
373    aib: IngestBuilder,
374    row_vals: Vec<LiteralValue>,
375    current_row0: usize,
376    rows_appended: usize,
377    row_started: bool,
378}
379
380#[derive(Clone, Copy)]
381struct WorkbookSpoolUsage {
382    bytes: u64,
383    files: u32,
384}
385
386struct StreamWorksheetOptions {
387    chunk_rows: usize,
388    debug: bool,
389    workbook_spool_usage: WorkbookSpoolUsage,
390    shadow_relocation_comparator: Option<ShadowRelocationComparator>,
391}
392
393struct FormulaStaging {
394    parse_cache: rustc_hash::FxHashMap<String, Option<formualizer_eval::engine::AstNodeId>>,
395    formulas: Vec<FormulaIngestRecord>,
396    observed: usize,
397    handed_to_engine: usize,
398}
399
400impl FormulaStaging {
401    fn new() -> Self {
402        let mut parse_cache = rustc_hash::FxHashMap::default();
403        parse_cache.reserve(4096);
404        Self {
405            parse_cache,
406            formulas: Vec::new(),
407            observed: 0,
408            handed_to_engine: 0,
409        }
410    }
411}
412
413struct StreamedSheet {
414    arrow_sheet: formualizer_eval::arrow_store::ArrowSheet,
415    dimensions: (usize, usize),
416    max_col_seen: usize,
417    used_sparse_fallback: bool,
418    value_cells_observed: usize,
419    values_handed_to_engine: usize,
420    formulas_observed: usize,
421    formulas_handed_to_engine: usize,
422    formulas: Vec<FormulaIngestRecord>,
423    formula_source_report: FormulaCompressedSourceReport,
424    compressed_families: Vec<SourceFormulaFamily>,
425    partitioned_families: Vec<PartitionedSourceFormulaFamily>,
426    direct_preparation: Option<FormulaCompressedPreparation>,
427    deferred_package: Option<DeferredFormulaPackage>,
428    shared_formula_tags: usize,
429    formula_spool_bytes: u64,
430    formula_spool_spilled: bool,
431    stream_millis: u128,
432}
433
434#[inline]
435fn data_ref_to_literal(value: &DataRef<'_>, date_system: DateSystem) -> Option<LiteralValue> {
436    match value {
437        DataRef::Empty => None,
438        DataRef::String(s) if s.is_empty() => None,
439        DataRef::SharedString("") => None,
440        DataRef::String(s) => Some(LiteralValue::Text(s.clone())),
441        DataRef::SharedString(s) => Some(LiteralValue::Text((*s).to_string())),
442        DataRef::Float(f) => Some(LiteralValue::Number(*f)),
443        DataRef::Int(i) => Some(LiteralValue::Number(*i as f64)),
444        DataRef::Bool(b) => Some(LiteralValue::Boolean(*b)),
445        DataRef::Error(e) => Some(LiteralValue::Error(ExcelError::new(
446            match CalamineAdapter::calamine_error_code(e) {
447                1 => ExcelErrorKind::Null,
448                2 => ExcelErrorKind::Ref,
449                3 => ExcelErrorKind::Name,
450                4 => ExcelErrorKind::Value,
451                5 => ExcelErrorKind::Div,
452                6 => ExcelErrorKind::Na,
453                7 => ExcelErrorKind::Num,
454                _ => ExcelErrorKind::Error,
455            },
456        ))),
457        DataRef::DateTime(dt) => Some(
458            LiteralValue::try_from_serial_number_for(date_system, dt.as_f64())
459                .unwrap_or_else(LiteralValue::Error),
460        ),
461        DataRef::DateTimeIso(s) => Some(LiteralValue::Text(s.clone())),
462        DataRef::DurationIso(s) => Some(LiteralValue::Text(s.clone())),
463    }
464}
465
466#[inline]
467fn data_ref_to_overlay(value: &DataRef<'_>) -> Option<OverlayValue> {
468    match value {
469        DataRef::Empty => None,
470        DataRef::String(s) if s.is_empty() => None,
471        DataRef::SharedString("") => None,
472        DataRef::String(s) => Some(OverlayValue::Text(Arc::from(s.as_str()))),
473        DataRef::SharedString(s) => Some(OverlayValue::Text(Arc::from(*s))),
474        DataRef::Float(f) => Some(OverlayValue::Number(*f)),
475        DataRef::Int(i) => Some(OverlayValue::Number(*i as f64)),
476        DataRef::Bool(b) => Some(OverlayValue::Boolean(*b)),
477        DataRef::Error(e) => Some(OverlayValue::Error(CalamineAdapter::calamine_error_code(e))),
478        DataRef::DateTime(dt) => Some(OverlayValue::Number(dt.as_f64())),
479        DataRef::DateTimeIso(s) => Some(OverlayValue::Text(Arc::from(s.as_str()))),
480        DataRef::DurationIso(s) => Some(OverlayValue::Text(Arc::from(s.as_str()))),
481    }
482}
483
484fn data_ref_format(value: &DataRef<'_>) -> Option<formualizer_eval::format::FormatId> {
485    match value {
486        DataRef::DateTime(dt) if dt.is_duration() => {
487            Some(formualizer_eval::format::FormatId::DURATION)
488        }
489        DataRef::DateTime(dt) if (0.0..1.0).contains(&dt.as_f64()) => {
490            Some(formualizer_eval::format::FormatId::TIME)
491        }
492        DataRef::DateTime(dt) if dt.as_f64().fract().abs() > f64::EPSILON => {
493            Some(formualizer_eval::format::FormatId::DATETIME)
494        }
495        DataRef::DateTime(_) => Some(formualizer_eval::format::FormatId::DATE),
496        _ => None,
497    }
498}
499
500impl DebugTimer {
501    fn start() -> Self {
502        Self {
503            #[cfg(not(target_arch = "wasm32"))]
504            started: std::time::Instant::now(),
505        }
506    }
507
508    fn elapsed_millis(&self) -> u128 {
509        #[cfg(not(target_arch = "wasm32"))]
510        {
511            self.started.elapsed().as_millis()
512        }
513        #[cfg(target_arch = "wasm32")]
514        {
515            0
516        }
517    }
518}
519
520type ShadowRelocationComparator = Arc<dyn Fn(&ASTNode, &ASTNode) -> bool + Send + Sync>;
521
522/// Read-only XLSX adapter backed by one shared source acquisition.
523///
524/// [`SpreadsheetReader::open_path`] retains one opened file and uses serialized
525/// file I/O with independent logical cursors. [`Self::open_path_with_source`]
526/// can explicitly request a read-only memory map instead. Byte and reader inputs
527/// use shared immutable owned bytes. See [`XlsxPathSource::DirectMmap`] for the
528/// mapped-file safety contract.
529pub struct CalamineAdapter {
530    workbook: RwLock<CalamineWorkbook>,
531    source: SharedXlsxReader,
532    /// Present only for adapters opened through [`Self::open_bytes_cancellable`].
533    cancel: Option<CancelToken>,
534    loaded_sheets: HashSet<String>,
535    cached_names: Option<Vec<String>>,
536    /// Calamine's already-parsed `(name, formula)` pairs, captured at open time.
537    ///
538    /// Held outside `workbook` on purpose: [`Self::lazy_defined_names`] must not
539    /// acquire the `workbook` lock, because `parking_lot::RwLock` is not
540    /// reentrant and any caller holding the write lock would self-deadlock.
541    calamine_defined_names: Vec<(String, String)>,
542    defined_names: OnceLock<Vec<DefinedName>>,
543    external_link_targets: OnceLock<BTreeMap<u32, String>>,
544    calc_settings: OnceLock<Option<CalcSettings>>,
545    load_stats: AdapterLoadStats,
546    shadow_relocation_comparator: Option<ShadowRelocationComparator>,
547    #[cfg(test)]
548    lazy_scan_counts: LazyScanCounts,
549    #[cfg(test)]
550    stream_row_checkpoint_hook: Option<Arc<dyn Fn() + Send + Sync>>,
551}
552
553#[cfg(test)]
554#[derive(Default)]
555struct LazyScanCounts {
556    external_links: std::sync::atomic::AtomicUsize,
557    calc_settings: std::sync::atomic::AtomicUsize,
558    defined_names: std::sync::atomic::AtomicUsize,
559}
560
561impl CalamineAdapter {
562    const EXCEL_MAX_ROWS: u32 = 1_048_576;
563
564    /// Opens one XLSX path using the requested retained backing source.
565    ///
566    /// [`XlsxPathSource::SharedFile`] matches [`SpreadsheetReader::open_path`].
567    /// [`XlsxPathSource::DirectMmap`] performs an actual read-only mapping on
568    /// Unix and Windows and returns the mapping error without fallback. Other
569    /// targets return a `calamine::Error::Io` whose I/O kind is `Unsupported`.
570    pub fn open_path_with_source<P: AsRef<Path>>(
571        path: P,
572        source: XlsxPathSource,
573    ) -> Result<Self, calamine::Error> {
574        let file = File::open(path).map_err(calamine::Error::Io)?;
575        let source = SharedXlsxReader::from_file(file, source).map_err(calamine::Error::Io)?;
576        Self::from_shared_source(source, None)
577    }
578
579    /// Opens XLSX bytes with cooperative cancellation for parsing and later
580    /// [`EngineLoadStream::stream_into_engine`] work.
581    ///
582    /// Cancellation is reported as a non-`Interrupted` I/O error wrapped in
583    /// [`calamine::Error`], so callers can inspect the supplied token and map
584    /// it to their own typed cancellation result.
585    pub fn open_bytes_cancellable(
586        bytes: Vec<u8>,
587        cancel: CancelToken,
588    ) -> Result<Self, calamine::Error> {
589        Self::from_shared_source(SharedXlsxReader::from_bytes(bytes), Some(cancel))
590    }
591
592    #[doc(hidden)]
593    pub fn set_shadow_relocation_comparator_for_test(
594        &mut self,
595        comparator: impl Fn(&ASTNode, &ASTNode) -> bool + Send + Sync + 'static,
596    ) {
597        self.shadow_relocation_comparator = Some(Arc::new(comparator));
598    }
599
600    fn shadow_relocation_matches(
601        comparator: &ShadowRelocationComparator,
602        family: &SourceFormulaFamily,
603        coord0: SourceCoord,
604        expanded_formula: &str,
605    ) -> bool {
606        let expanded_formula = format!("={}", expanded_formula.trim_start_matches('='));
607        let anchor_formula = format!("={}", family.anchor_text.trim_start_matches('='));
608        let Ok(expanded) = formualizer_parse::parser::parse(&expanded_formula) else {
609            return false;
610        };
611        let Ok(anchor) = formualizer_parse::parser::parse(&anchor_formula) else {
612            return false;
613        };
614        let Ok(relocated) =
615            formualizer_eval::formula_plane::structural::relocate_ast_for_template_placement(
616                &anchor,
617                i64::from(coord0.row) - i64::from(family.anchor_coord0.row),
618                i64::from(coord0.col) - i64::from(family.anchor_coord0.col),
619            )
620        else {
621            return false;
622        };
623        comparator(&expanded, &relocated)
624    }
625    const EXCEL_MAX_COLS: u32 = 16_384;
626
627    fn stage_formula<C: EvaluationContext>(
628        engine: &mut EvalEngine<C>,
629        sheet: &str,
630        position: (u32, u32),
631        formula: &str,
632        debug: bool,
633        staging: &mut FormulaStaging,
634    ) -> Result<(), calamine::Error> {
635        let excel_row = position.0 + 1;
636        let excel_col = position.1 + 1;
637        let normalized = if formula.starts_with('=') {
638            formula.to_string()
639        } else {
640            format!("={formula}")
641        };
642        if debug && staging.observed < 16 {
643            eprintln!("[fz][load] formula observed at R{excel_row}C{excel_col}");
644        }
645        if engine.config.defer_graph_building {
646            engine.stage_formula_text(sheet, excel_row, excel_col, normalized);
647            staging.handed_to_engine += 1;
648        } else {
649            let ast_id = if let Some(cached) = staging.parse_cache.get(&normalized) {
650                *cached
651            } else {
652                let parsed = match formualizer_parse::parser::parse(&normalized) {
653                    Ok(parsed) => Some(parsed),
654                    Err(error) => engine
655                        .handle_formula_parse_error(
656                            sheet,
657                            excel_row,
658                            excel_col,
659                            &normalized,
660                            error.to_string(),
661                        )
662                        .map_err(|error| {
663                            calamine::Error::Io(std::io::Error::other(error.to_string()))
664                        })?,
665                };
666                let ast_id = parsed.as_ref().map(|ast| engine.intern_formula_ast(ast));
667                staging.parse_cache.insert(normalized.clone(), ast_id);
668                ast_id
669            };
670            if let Some(ast_id) = ast_id {
671                staging.formulas.push(FormulaIngestRecord::new(
672                    excel_row,
673                    excel_col,
674                    ast_id,
675                    Some(Arc::<str>::from(normalized)),
676                ));
677                staging.handed_to_engine += 1;
678            }
679        }
680        staging.observed += 1;
681        Ok(())
682    }
683
684    fn stream_worksheet<RS, C>(
685        workbook: &mut Xlsx<RS>,
686        sheet: &str,
687        engine: &mut EvalEngine<C>,
688        sheet_instance: u32,
689        options: StreamWorksheetOptions,
690        cancel: Option<&CancelToken>,
691        #[cfg(test)] row_checkpoint_hook: Option<&(dyn Fn() + Send + Sync)>,
692    ) -> Result<StreamedSheet, calamine::Error>
693    where
694        RS: Read + Seek,
695        C: EvaluationContext,
696    {
697        Self::cancellation_checkpoint(cancel)?;
698        let timer = DebugTimer::start();
699        let StreamWorksheetOptions {
700            chunk_rows,
701            debug,
702            workbook_spool_usage,
703            shadow_relocation_comparator,
704        } = options;
705        let mut reader = workbook
706            .worksheet_cells_reader(sheet)
707            .map_err(calamine::Error::Xlsx)?;
708        let declared = reader.dimensions();
709        let mut dims_rows = (declared.end.0 as usize + 1).max(1);
710        let mut dims_cols = (declared.end.1 as usize + 1).max(1);
711        enforce_sheet_dimension_limits(
712            "calamine",
713            sheet,
714            dims_rows as u32,
715            dims_cols as u32,
716            engine.workbook_load_limits(),
717        )
718        .map_err(|error| calamine::Error::Io(std::io::Error::other(error.to_string())))?;
719
720        let force_sparse_from_start = (dims_rows as u64).saturating_mul(dims_cols as u64)
721            > engine.workbook_load_limits().max_sheet_logical_cells;
722        let mut dense = (!force_sparse_from_start).then(|| DenseState {
723            aib: IngestBuilder::new(sheet, dims_cols, chunk_rows, engine.config.date_system),
724            row_vals: vec![LiteralValue::Empty; dims_cols],
725            current_row0: 0,
726            rows_appended: 0,
727            row_started: false,
728        });
729        let mut sparse = force_sparse_from_start.then(|| {
730            formualizer_eval::arrow_store::ArrowSheet::new_sparse_with_date_system(
731                sheet,
732                dims_cols,
733                dims_rows,
734                chunk_rows,
735                engine.config.date_system,
736            )
737        });
738        let mut used_sparse_fallback = force_sparse_from_start;
739        let mut max_row_seen = 0usize;
740        let mut max_col_seen = 0usize;
741        let mut value_cells_observed = 0usize;
742        let mut values_handed_to_engine = 0usize;
743        let mut formula_staging = FormulaStaging::new();
744        let mut formula_count = 0usize;
745        let mut deferred_source_coordinates = engine.config.defer_graph_building.then(Vec::new);
746        let mut formula_evidence = MonotonicFormulaEvidence::new();
747        let spool_limits = engine.workbook_load_limits();
748        let workbook_bytes_remaining = spool_limits
749            .max_formula_spool_bytes_per_workbook
750            .saturating_sub(workbook_spool_usage.bytes);
751        let spill_files_remaining = spool_limits
752            .max_formula_spool_files_per_workbook
753            .saturating_sub(workbook_spool_usage.files);
754        let mut formula_spool = HybridFormulaReplaySpool::new(FormulaSpoolLimits {
755            sheet_bytes: spool_limits.max_formula_spool_bytes_per_sheet,
756            workbook_bytes_remaining,
757            workbook_bytes_used: workbook_spool_usage.bytes,
758            memory_prefix_bytes: spool_limits.formula_spool_memory_prefix_bytes,
759            memory_only_bytes: spool_limits.max_formula_spool_memory_bytes,
760            allow_disk: spool_limits.formula_spool_disk_policy
761                == FormulaSpoolDiskPolicy::NativeSpill,
762            spill_files_remaining,
763            spill_files_limit: spool_limits.max_formula_spool_files_per_workbook,
764        });
765        let mut last_formula_coord = None;
766        let mut shared_formula_tags = 0usize;
767        let mut last_cancel_row = None;
768
769        while let Some(record) = reader
770            .next_cell_with_formula_metadata()
771            .map_err(calamine::Error::Xlsx)?
772        {
773            let (row0, col0) = record.pos;
774            let row = row0 as usize;
775            let col = col0 as usize;
776            if last_cancel_row != Some(row) {
777                #[cfg(test)]
778                if let Some(hook) = row_checkpoint_hook {
779                    hook();
780                }
781                Self::cancellation_checkpoint(cancel)?;
782                last_cancel_row = Some(row);
783            }
784            if row >= dims_rows || col >= dims_cols {
785                dims_rows = dims_rows.max(row + 1);
786                dims_cols = dims_cols.max(col + 1);
787                enforce_sheet_dimension_limits(
788                    "calamine",
789                    sheet,
790                    dims_rows as u32,
791                    dims_cols as u32,
792                    engine.workbook_load_limits(),
793                )
794                .map_err(|error| calamine::Error::Io(std::io::Error::other(error.to_string())))?;
795            }
796            max_row_seen = max_row_seen.max(row);
797            max_col_seen = max_col_seen.max(col);
798
799            let has_formula = record.formula.is_some();
800            if let Some(metadata) = record.formula {
801                if u64::try_from(value_cells_observed)
802                    .unwrap_or(u64::MAX)
803                    .saturating_add(u64::try_from(formula_count).unwrap_or(u64::MAX))
804                    .saturating_add(1)
805                    > engine.workbook_load_limits().max_sheet_logical_cells
806                {
807                    return Err(calamine::Error::Io(std::io::Error::other(format!(
808                        "Workbook load budget exceeded in calamine for sheet {sheet}: observed populated cell count exceeds configured logical-cell budget of {}",
809                        engine.workbook_load_limits().max_sheet_logical_cells
810                    ))));
811                }
812                let coord0 = SourceCoord {
813                    row: row0,
814                    col: col0,
815                };
816                let source_sequence = formula_count as u64;
817                match metadata {
818                    XlsxFormulaMetadata::Normal { formula } => {
819                        if let Some(coordinates) = deferred_source_coordinates.as_mut() {
820                            coordinates.push((coord0, None));
821                        }
822                        formula_evidence.observe_ordered(
823                            coord0,
824                            SourceFormulaOrder::new(source_sequence),
825                            EvidenceRecord::Ordinary,
826                        );
827                        formula_spool.append(SpoolFormulaRecord::Ordinary {
828                            sequence: source_sequence,
829                            coord0,
830                            text: &formula,
831                        })
832                    }
833                    XlsxFormulaMetadata::Shared {
834                        shared_index,
835                        range,
836                        formula,
837                    } => {
838                        shared_formula_tags += 1;
839                        let declared_range = range.map(|range| SourceRect {
840                            start: SourceCoord {
841                                row: range.start.0,
842                                col: range.start.1,
843                            },
844                            end: SourceCoord {
845                                row: range.end.0,
846                                col: range.end.1,
847                            },
848                        });
849                        let family = SourceFamilyId {
850                            sheet_instance,
851                            source_index: shared_index,
852                        };
853                        if let Some(coordinates) = deferred_source_coordinates.as_mut() {
854                            coordinates.push((coord0, Some(family)));
855                        }
856                        formula_evidence.observe_ordered(
857                            coord0,
858                            SourceFormulaOrder::new(source_sequence),
859                            EvidenceRecord::Anchor {
860                                family,
861                                range: declared_range,
862                                text: &formula,
863                            },
864                        );
865                        formula_spool.append(SpoolFormulaRecord::SharedAnchor {
866                            sequence: source_sequence,
867                            coord0,
868                            shared_index,
869                            declared_range,
870                            text: &formula,
871                        })
872                    }
873                    XlsxFormulaMetadata::SharedDerived { shared_index } => {
874                        shared_formula_tags += 1;
875                        let family = SourceFamilyId {
876                            sheet_instance,
877                            source_index: shared_index,
878                        };
879                        if let Some(coordinates) = deferred_source_coordinates.as_mut() {
880                            coordinates.push((coord0, Some(family)));
881                        }
882                        formula_evidence.observe_ordered(
883                            coord0,
884                            SourceFormulaOrder::new(source_sequence),
885                            EvidenceRecord::Descendant { family },
886                        );
887                        formula_spool.append(SpoolFormulaRecord::SharedDescendant {
888                            sequence: source_sequence,
889                            coord0,
890                            shared_index,
891                        })
892                    }
893                    _ => {
894                        if let Some(coordinates) = deferred_source_coordinates.as_mut() {
895                            coordinates.push((coord0, None));
896                        }
897                        formula_evidence.observe_ordered(
898                            coord0,
899                            SourceFormulaOrder::new(source_sequence),
900                            EvidenceRecord::Unsupported,
901                        );
902                        formula_spool.append(SpoolFormulaRecord::Unsupported {
903                            sequence: source_sequence,
904                            coord0,
905                        })
906                    }
907                }
908                .map_err(|error| calamine::Error::Io(std::io::Error::other(error.to_string())))?;
909                last_formula_coord = Some((row, col));
910                if let Some(state) = dense.as_mut()
911                    && state.row_started
912                    && state.current_row0 == row
913                    && col < state.row_vals.len()
914                {
915                    state.row_vals[col] = LiteralValue::Empty;
916                }
917                formula_count += 1;
918            }
919
920            // Preserve existing KeepCachedValue behavior: a formula's cached
921            // value is not handed to the value plane.
922            if has_formula {
923                continue;
924            }
925            let Some(literal) = data_ref_to_literal(&record.value, engine.config.date_system)
926            else {
927                continue;
928            };
929            value_cells_observed += 1;
930            if u64::try_from(value_cells_observed)
931                .unwrap_or(u64::MAX)
932                .saturating_add(u64::try_from(formula_count).unwrap_or(u64::MAX))
933                > engine.workbook_load_limits().max_sheet_logical_cells
934            {
935                return Err(calamine::Error::Io(std::io::Error::other(format!(
936                    "Workbook load budget exceeded in calamine for sheet {sheet}: observed populated cell count exceeds configured logical-cell budget of {}",
937                    engine.workbook_load_limits().max_sheet_logical_cells
938                ))));
939            }
940            if last_formula_coord == Some((row, col)) {
941                continue;
942            }
943
944            if let Some(arrow_sheet) = sparse.as_mut() {
945                if let Some(value) = data_ref_to_overlay(&record.value) {
946                    arrow_sheet.set_sparse_overlay_value(row, col, value);
947                    arrow_sheet.set_sparse_overlay_format(row, col, data_ref_format(&record.value));
948                    values_handed_to_engine += 1;
949                }
950                continue;
951            }
952
953            let state = dense.as_mut().expect("dense or sparse ingest mode");
954            let non_monotonic = state.row_started && row < state.current_row0;
955            let col_overflow = col >= state.row_vals.len();
956            let gap_rows = if state.row_started {
957                row.saturating_sub(state.current_row0)
958            } else {
959                row
960            };
961            let large_gap = gap_rows > 128;
962            let would_exceed_dense_budget =
963                u64::try_from(state.rows_appended.saturating_mul(state.row_vals.len()))
964                    .unwrap_or(u64::MAX)
965                    > engine.workbook_load_limits().max_sheet_logical_cells;
966            if non_monotonic || col_overflow || large_gap || would_exceed_dense_budget {
967                let mut state = dense.take().expect("dense state present");
968                if state.row_started && state.current_row0 == state.rows_appended {
969                    state.aib.append_row(&state.row_vals).map_err(|error| {
970                        calamine::Error::Io(std::io::Error::other(error.to_string()))
971                    })?;
972                    state.rows_appended += 1;
973                }
974                let mut arrow_sheet = state.aib.finish();
975                arrow_sheet.ensure_row_capacity(dims_rows.max(row + 1));
976                if col >= arrow_sheet.columns.len() {
977                    arrow_sheet.insert_columns(
978                        arrow_sheet.columns.len(),
979                        col + 1 - arrow_sheet.columns.len(),
980                    );
981                }
982                if let Some(value) = data_ref_to_overlay(&record.value) {
983                    arrow_sheet.set_sparse_overlay_value(row, col, value);
984                    arrow_sheet.set_sparse_overlay_format(row, col, data_ref_format(&record.value));
985                    values_handed_to_engine += 1;
986                }
987                sparse = Some(arrow_sheet);
988                used_sparse_fallback = true;
989                continue;
990            }
991
992            if !state.row_started {
993                while state.rows_appended < row {
994                    state
995                        .aib
996                        .append_row(&vec![LiteralValue::Empty; state.row_vals.len()])
997                        .map_err(|error| {
998                            calamine::Error::Io(std::io::Error::other(error.to_string()))
999                        })?;
1000                    state.rows_appended += 1;
1001                }
1002                state.current_row0 = row;
1003                state.row_started = true;
1004            } else if row > state.current_row0 {
1005                state.aib.append_row(&state.row_vals).map_err(|error| {
1006                    calamine::Error::Io(std::io::Error::other(error.to_string()))
1007                })?;
1008                state.rows_appended += 1;
1009                state.row_vals.fill(LiteralValue::Empty);
1010                while state.rows_appended < row {
1011                    state
1012                        .aib
1013                        .append_row(&vec![LiteralValue::Empty; state.row_vals.len()])
1014                        .map_err(|error| {
1015                            calamine::Error::Io(std::io::Error::other(error.to_string()))
1016                        })?;
1017                    state.rows_appended += 1;
1018                }
1019                state.current_row0 = row;
1020            }
1021            state.row_vals[col] = literal;
1022            values_handed_to_engine += 1;
1023        }
1024
1025        // Replay and validate the complete sheet-local source stream before any
1026        // formula staging, parsing, or graph mutation. The Arrow result is also
1027        // still local and is installed by the caller only after this succeeds.
1028        let compressed_evidence = formula_evidence.finish();
1029        let mut formula_source_report = compressed_evidence.report;
1030        let mut compressed_families = compressed_evidence.families;
1031        let partitioned_families = compressed_evidence
1032            .fragmented
1033            .into_iter()
1034            .map(|proposal| {
1035                let mut legacy_members: Vec<_> = proposal
1036                    .fallback_members
1037                    .into_iter()
1038                    .map(|coord| PartitionLegacyMember {
1039                        coord,
1040                        kind: PartitionLegacyMemberKind::SharedFamilyMember,
1041                    })
1042                    .collect();
1043                let mut ordinary_exceptions = 0u64;
1044                let mut holes = 0u64;
1045                for exclusion in proposal.exclusions {
1046                    match exclusion {
1047                        compressed_evidence::SourceExclusion::Hole(_) => {
1048                            holes = holes.saturating_add(1);
1049                        }
1050                        compressed_evidence::SourceExclusion::OrdinaryFormula(coord) => {
1051                            ordinary_exceptions = ordinary_exceptions.saturating_add(1);
1052                            legacy_members.push(PartitionLegacyMember {
1053                                coord,
1054                                kind: PartitionLegacyMemberKind::OrdinaryException,
1055                            });
1056                        }
1057                    }
1058                }
1059                let legacy_members = ExplicitPartitionLegacyMembers::try_new(legacy_members)
1060                    .map_err(|reason| calamine::Error::Io(std::io::Error::other(reason)))?;
1061                Ok(PartitionedSourceFormulaFamily {
1062                    source_id: proposal.source_id,
1063                    source_order: proposal.source_order,
1064                    template_origin0: proposal.anchor_coord0,
1065                    template_text: proposal.anchor_text,
1066                    declared: proposal.declared,
1067                    surviving_member_count: proposal.member_count,
1068                    fragments: proposal.fragments,
1069                    legacy_members,
1070                    reconciliation: PartitionReconciliation {
1071                        shared_members: proposal.member_count,
1072                        ordinary_exceptions,
1073                        holes,
1074                    },
1075                })
1076            })
1077            .collect::<Result<Vec<_>, calamine::Error>>()?;
1078        let deferred_source_coordinates = deferred_source_coordinates
1079            .unwrap_or_default()
1080            .into_iter()
1081            .map(|(coord, _)| coord)
1082            .collect::<Vec<_>>();
1083        let formula_spool_bytes = if formula_count == 0 {
1084            0
1085        } else {
1086            formula_spool.encoded_bytes()
1087        };
1088        let formula_spool_spilled = formula_spool.spilled();
1089        formula_source_report.source_formula_records_spooled = formula_count as u64;
1090        formula_source_report.source_spool_encoded_bytes = formula_spool_bytes;
1091        formula_source_report.source_spool_peak_memory_bytes = formula_spool.peak_memory_bytes();
1092        formula_source_report.source_spool_spilled_bytes = if formula_spool_spilled {
1093            formula_spool_bytes
1094        } else {
1095            0
1096        };
1097        formula_source_report.source_spool_spill_files = u64::from(formula_spool_spilled);
1098        let _formula_spool_storage = formula_spool.storage_kind();
1099        debug_assert!(formula_count == 0 || formula_spool_bytes >= 5);
1100        let mut formula_spool = Some(formula_spool);
1101        let direct_preparation = if engine.config.formula_plane_mode
1102            == formualizer_eval::engine::FormulaPlaneMode::AuthoritativeExperimental
1103            && !engine.config.defer_graph_building
1104        {
1105            Self::cancellation_checkpoint(cancel)?;
1106            let replay: Box<dyn formualizer_eval::engine::DeferredFormulaReplay> =
1107                Box::new(CalamineDeferredFormulaReplay::new(
1108                    formula_spool.take().expect("eager formula spool available"),
1109                    sheet.to_string(),
1110                    sheet_instance,
1111                ));
1112            Some(
1113                engine
1114                    .source_formula_ingress()
1115                    .prepare_eager_proposals(
1116                        sheet,
1117                        &compressed_families,
1118                        &partitioned_families,
1119                        formula_count as u64,
1120                        replay,
1121                    )
1122                    .map_err(|e| calamine::Error::Io(std::io::Error::other(e.to_string())))?,
1123            )
1124        } else {
1125            None
1126        };
1127        Self::cancellation_checkpoint(cancel)?;
1128        if direct_preparation.is_none() && !engine.config.defer_graph_building {
1129            formula_source_report.source_spool_replays = 1;
1130            let compare_shadow = engine.config.formula_plane_mode
1131                == formualizer_eval::engine::FormulaPlaneMode::Shadow;
1132            let mut relocation_mismatches = BTreeSet::new();
1133            replay_spool_per_cell_filtered_with_family(
1134                formula_spool
1135                    .as_mut()
1136                    .expect("replay formula spool available"),
1137                sheet,
1138                |_| false,
1139                |coord0, formula, shared_index| {
1140                    Self::cancellation_checkpoint(cancel)?;
1141                    if compare_shadow
1142                        && let (Some(comparator), Some(shared_index)) =
1143                            (shadow_relocation_comparator.as_ref(), shared_index)
1144                        && let Some(family) = compressed_families
1145                            .iter()
1146                            .find(|family| family.source_id.source_index == shared_index)
1147                        && !Self::shadow_relocation_matches(comparator, family, coord0, formula)
1148                    {
1149                        relocation_mismatches.insert(shared_index);
1150                    }
1151                    Self::stage_formula(
1152                        engine,
1153                        sheet,
1154                        (coord0.row, coord0.col),
1155                        formula,
1156                        debug,
1157                        &mut formula_staging,
1158                    )
1159                },
1160            )?;
1161            if !relocation_mismatches.is_empty() {
1162                compressed_families.retain(|family| {
1163                    !relocation_mismatches.contains(&family.source_id.source_index)
1164                });
1165            }
1166        }
1167
1168        if u64::try_from(value_cells_observed)
1169            .unwrap_or(u64::MAX)
1170            .saturating_add(u64::try_from(formula_count).unwrap_or(u64::MAX))
1171            > engine.workbook_load_limits().max_sheet_logical_cells
1172        {
1173            return Err(calamine::Error::Io(std::io::Error::other(format!(
1174                "Workbook load budget exceeded in calamine for sheet {sheet}: observed populated cell count exceeds configured logical-cell budget of {}",
1175                engine.workbook_load_limits().max_sheet_logical_cells
1176            ))));
1177        }
1178        enforce_sheet_dimension_limits(
1179            "calamine",
1180            sheet,
1181            dims_rows as u32,
1182            dims_cols as u32,
1183            engine.workbook_load_limits(),
1184        )
1185        .map_err(|error| calamine::Error::Io(std::io::Error::other(error.to_string())))?;
1186
1187        let mut arrow_sheet = if let Some(mut arrow_sheet) = sparse {
1188            arrow_sheet.ensure_row_capacity(dims_rows.max(max_row_seen + 1));
1189            arrow_sheet
1190        } else {
1191            let mut state = dense.take().expect("dense state present");
1192            if state.row_started {
1193                state.aib.append_row(&state.row_vals).map_err(|error| {
1194                    calamine::Error::Io(std::io::Error::other(error.to_string()))
1195                })?;
1196            }
1197            let mut arrow_sheet = state.aib.finish();
1198            arrow_sheet.ensure_row_capacity(dims_rows.max(max_row_seen + 1));
1199            arrow_sheet
1200        };
1201        if dims_cols > arrow_sheet.columns.len() {
1202            arrow_sheet.insert_columns(
1203                arrow_sheet.columns.len(),
1204                dims_cols - arrow_sheet.columns.len(),
1205            );
1206        }
1207        let deferred_package = engine.config.defer_graph_building.then(|| {
1208            DeferredFormulaPackage::new_with_source_coordinates(
1209                sheet.to_string(),
1210                formula_source_report.clone(),
1211                compressed_families.clone(),
1212                partitioned_families.clone(),
1213                deferred_source_coordinates,
1214                Box::new(CalamineDeferredFormulaReplay::new(
1215                    formula_spool
1216                        .take()
1217                        .expect("deferred formula spool available"),
1218                    sheet.to_string(),
1219                    sheet_instance,
1220                )),
1221            )
1222            .with_complete_coordinate_coverage()
1223        });
1224        Ok(StreamedSheet {
1225            arrow_sheet,
1226            dimensions: (dims_rows, dims_cols),
1227            max_col_seen,
1228            used_sparse_fallback,
1229            value_cells_observed,
1230            values_handed_to_engine,
1231            formulas_observed: formula_count,
1232            formulas_handed_to_engine: formula_count,
1233            formulas: formula_staging.formulas,
1234            formula_source_report,
1235            compressed_families,
1236            partitioned_families,
1237            direct_preparation,
1238            deferred_package,
1239            shared_formula_tags,
1240            formula_spool_bytes,
1241            formula_spool_spilled,
1242            stream_millis: timer.elapsed_millis(),
1243        })
1244    }
1245
1246    fn from_shared_source(
1247        source: SharedXlsxReader,
1248        cancel: Option<CancelToken>,
1249    ) -> Result<Self, calamine::Error> {
1250        let workbook: Xlsx<CancellableReader> =
1251            open_workbook_from_rs(CancellableReader::new(source.reader(), cancel.clone()))?;
1252        let sheet_names = workbook.sheet_names().to_vec();
1253        let calamine_defined_names = workbook.defined_names().to_vec();
1254
1255        Ok(Self {
1256            workbook: RwLock::new(CalamineWorkbook(workbook)),
1257            source,
1258            cancel,
1259            loaded_sheets: HashSet::new(),
1260            cached_names: Some(sheet_names),
1261            calamine_defined_names,
1262            defined_names: OnceLock::new(),
1263            external_link_targets: OnceLock::new(),
1264            calc_settings: OnceLock::new(),
1265            load_stats: AdapterLoadStats::default(),
1266            shadow_relocation_comparator: None,
1267            #[cfg(test)]
1268            lazy_scan_counts: LazyScanCounts::default(),
1269            #[cfg(test)]
1270            stream_row_checkpoint_hook: None,
1271        })
1272    }
1273
1274    fn cancellable_reader(&self) -> CancellableReader {
1275        CancellableReader::new(self.source.reader(), self.cancel.clone())
1276    }
1277
1278    fn checkpoint_cancel(&self) -> Result<(), calamine::Error> {
1279        if self.cancel.as_ref().is_some_and(CancelToken::is_cancelled) {
1280            return Err(calamine::Error::Io(std::io::Error::other(
1281                "calamine load cancelled",
1282            )));
1283        }
1284        Ok(())
1285    }
1286
1287    fn cancellation_checkpoint(cancel: Option<&CancelToken>) -> Result<(), calamine::Error> {
1288        if cancel.is_some_and(CancelToken::is_cancelled) {
1289            return Err(calamine::Error::Io(std::io::Error::other(
1290                "calamine load cancelled",
1291            )));
1292        }
1293        Ok(())
1294    }
1295
1296    fn lazy_external_link_targets(&self) -> &BTreeMap<u32, String> {
1297        self.external_link_targets.get_or_init(|| {
1298            #[cfg(test)]
1299            self.lazy_scan_counts
1300                .external_links
1301                .fetch_add(1, std::sync::atomic::Ordering::Relaxed);
1302            Self::scan_external_link_targets_from_reader(self.cancellable_reader())
1303        })
1304    }
1305
1306    fn lazy_calc_settings(&self) -> &Option<CalcSettings> {
1307        self.calc_settings.get_or_init(|| {
1308            #[cfg(test)]
1309            self.lazy_scan_counts
1310                .calc_settings
1311                .fetch_add(1, std::sync::atomic::Ordering::Relaxed);
1312            Self::scan_calc_settings_from_reader(self.cancellable_reader())
1313        })
1314    }
1315
1316    /// Resolves workbook/sheet-scoped defined names on first request.
1317    ///
1318    /// Deliberately lock-free with respect to [`Self::workbook`]: it reads only
1319    /// the shared source and the `calamine_defined_names` snapshot taken at open
1320    /// time, so it is safe to call while the `workbook` write lock is held.
1321    fn lazy_defined_names(&self) -> &Vec<DefinedName> {
1322        self.defined_names.get_or_init(|| {
1323            #[cfg(test)]
1324            self.lazy_scan_counts
1325                .defined_names
1326                .fetch_add(1, std::sync::atomic::Ordering::Relaxed);
1327            if self.calamine_defined_names.is_empty() {
1328                return Vec::new();
1329            }
1330
1331            let sheet_names = self.cached_names.as_deref().unwrap_or_default();
1332            let parsed =
1333                Self::scan_defined_names_from_reader(self.cancellable_reader(), sheet_names);
1334            if parsed.is_empty() {
1335                Self::fallback_defined_names(&self.calamine_defined_names, sheet_names)
1336            } else {
1337                parsed
1338            }
1339        })
1340    }
1341
1342    pub fn external_link_target(&self, index: u32) -> Option<&str> {
1343        self.lazy_external_link_targets()
1344            .get(&index)
1345            .map(String::as_str)
1346    }
1347
1348    fn normalize_open_ended_bounds(
1349        start_row: Option<u32>,
1350        start_col: Option<u32>,
1351        end_row: Option<u32>,
1352        end_col: Option<u32>,
1353    ) -> Option<(u32, u32, u32, u32)> {
1354        let mut sr = start_row;
1355        let mut sc = start_col;
1356        let mut er = end_row;
1357        let mut ec = end_col;
1358
1359        if sr.is_none() && er.is_none() {
1360            sr = Some(1);
1361            er = Some(Self::EXCEL_MAX_ROWS);
1362        }
1363        if sc.is_none() && ec.is_none() {
1364            sc = Some(1);
1365            ec = Some(Self::EXCEL_MAX_COLS);
1366        }
1367
1368        if sr.is_some() && er.is_none() {
1369            er = Some(Self::EXCEL_MAX_ROWS);
1370        }
1371        if er.is_some() && sr.is_none() {
1372            sr = Some(1);
1373        }
1374
1375        if sc.is_some() && ec.is_none() {
1376            ec = Some(Self::EXCEL_MAX_COLS);
1377        }
1378        if ec.is_some() && sc.is_none() {
1379            sc = Some(1);
1380        }
1381
1382        let sr = sr?;
1383        let sc = sc?;
1384        let er = er?;
1385        let ec = ec?;
1386
1387        if er < sr || ec < sc {
1388            return None;
1389        }
1390
1391        Some((sr, sc, er, ec))
1392    }
1393
1394    fn convert_defined_name(
1395        name: &str,
1396        raw_formula: &str,
1397        local_sheet_id: Option<usize>,
1398        sheet_names: &[String],
1399    ) -> Option<DefinedName> {
1400        let mut trimmed = raw_formula.trim();
1401        if let Some(rest) = trimmed.strip_prefix('=') {
1402            trimmed = rest.trim();
1403        }
1404        if trimmed.is_empty() || trimmed.contains(',') {
1405            return None;
1406        }
1407
1408        let reference = ReferenceType::from_string(trimmed).ok()?;
1409        let scope_sheet = local_sheet_id.and_then(|idx| sheet_names.get(idx).cloned());
1410        let scope = if scope_sheet.is_some() {
1411            DefinedNameScope::Sheet
1412        } else {
1413            DefinedNameScope::Workbook
1414        };
1415        let base_sheet = scope_sheet.as_deref();
1416
1417        let (sheet_name, start_row, start_col, end_row, end_col) = match reference {
1418            ReferenceType::Cell {
1419                sheet, row, col, ..
1420            } => {
1421                let sheet = sheet.or_else(|| base_sheet.map(|s| s.to_string()))?;
1422                (sheet, row, col, row, col)
1423            }
1424            ReferenceType::Range {
1425                sheet,
1426                start_row,
1427                start_col,
1428                end_row,
1429                end_col,
1430                ..
1431            } => {
1432                let (sr, sc, er, ec) =
1433                    Self::normalize_open_ended_bounds(start_row, start_col, end_row, end_col)?;
1434                let sheet = sheet.or_else(|| base_sheet.map(|s| s.to_string()))?;
1435                (sheet, sr, sc, er, ec)
1436            }
1437            _ => return None,
1438        };
1439
1440        let address = RangeAddress::new(sheet_name, start_row, start_col, end_row, end_col).ok()?;
1441
1442        Some(DefinedName {
1443            name: name.to_string(),
1444            scope,
1445            scope_sheet,
1446            definition: DefinedNameDefinition::Range { address },
1447        })
1448    }
1449
1450    fn decode_attr<R: BufRead>(
1451        reader: &XmlReader<R>,
1452        start: &BytesStart<'_>,
1453        key: &[u8],
1454    ) -> Option<String> {
1455        start
1456            .attributes()
1457            .filter_map(Result::ok)
1458            .find(|attr| attr.key == QName(key))
1459            .and_then(|attr| {
1460                attr.decode_and_unescape_value(reader.decoder())
1461                    .ok()
1462                    .map(|v| v.into_owned())
1463            })
1464    }
1465
1466    fn append_xml_entity(
1467        entity: &BytesRef<'_>,
1468        buffer: &mut String,
1469    ) -> Result<(), quick_xml::Error> {
1470        let decoded = entity.decode()?;
1471        match decoded.as_ref() {
1472            "lt" => buffer.push('<'),
1473            "gt" => buffer.push('>'),
1474            "amp" => buffer.push('&'),
1475            "apos" => buffer.push('\''),
1476            "quot" => buffer.push('"'),
1477            _ => {
1478                if let Some(ch) = entity.resolve_char_ref()? {
1479                    buffer.push(ch);
1480                } else {
1481                    return Err(quick_xml::Error::Escape(
1482                        quick_xml::escape::EscapeError::UnrecognizedEntity(
1483                            0..0,
1484                            format!("&{decoded};"),
1485                        ),
1486                    ));
1487                }
1488            }
1489        }
1490        Ok(())
1491    }
1492
1493    fn fallback_defined_names(
1494        calamine_defined_names: &[(String, String)],
1495        sheet_names: &[String],
1496    ) -> Vec<DefinedName> {
1497        let mut out = Vec::new();
1498        let mut seen: HashSet<(DefinedNameScope, Option<String>, String)> = HashSet::new();
1499
1500        for (name, formula) in calamine_defined_names {
1501            if let Some(converted) = Self::convert_defined_name(name, formula, None, sheet_names) {
1502                let key = (
1503                    converted.scope.clone(),
1504                    converted.scope_sheet.clone(),
1505                    converted.name.clone(),
1506                );
1507                if seen.insert(key) {
1508                    out.push(converted);
1509                }
1510            }
1511        }
1512
1513        out
1514    }
1515
1516    fn scan_defined_names_from_reader<R>(reader: R, sheet_names: &[String]) -> Vec<DefinedName>
1517    where
1518        R: Read + Seek,
1519    {
1520        let mut archive = match ZipArchive::new(reader) {
1521            Ok(a) => a,
1522            Err(_) => return Vec::new(),
1523        };
1524        let entry = match archive.by_name("xl/workbook.xml") {
1525            Ok(e) => e,
1526            Err(_) => return Vec::new(),
1527        };
1528
1529        // Calamine's public defined_names() surface flattens OOXML defined names to
1530        // (name, formula_text) and drops localSheetId. We recover only the scoped
1531        // defined-name metadata we need here with a targeted streaming pass over
1532        // workbook.xml, avoiding a full file String allocation or any sheet XML reparse.
1533        let mut xml = XmlReader::from_reader(BufReader::new(entry));
1534        xml.config_mut().trim_text(true);
1535
1536        let mut out = Vec::new();
1537        let mut seen: HashSet<(DefinedNameScope, Option<String>, String)> = HashSet::new();
1538        let mut buf = Vec::new();
1539        let mut inner_buf = Vec::new();
1540        let mut in_defined_names = false;
1541
1542        loop {
1543            buf.clear();
1544            match xml.read_event_into(&mut buf) {
1545                Ok(Event::Start(ref e)) if e.local_name().as_ref() == b"definedNames" => {
1546                    in_defined_names = true;
1547                }
1548                Ok(Event::End(ref e)) if e.local_name().as_ref() == b"definedNames" => {
1549                    break;
1550                }
1551                Ok(Event::Start(ref e))
1552                    if in_defined_names && e.local_name().as_ref() == b"definedName" =>
1553                {
1554                    let name = Self::decode_attr(&xml, e, b"name");
1555                    let local_sheet_id = Self::decode_attr(&xml, e, b"localSheetId")
1556                        .and_then(|v| v.parse::<usize>().ok());
1557                    let mut value = String::new();
1558
1559                    loop {
1560                        inner_buf.clear();
1561                        match xml.read_event_into(&mut inner_buf) {
1562                            Ok(Event::Text(t)) => match t.xml10_content() {
1563                                Ok(text) => value.push_str(&text),
1564                                Err(_) => return Vec::new(),
1565                            },
1566                            Ok(Event::GeneralRef(entity)) => {
1567                                if Self::append_xml_entity(&entity, &mut value).is_err() {
1568                                    return Vec::new();
1569                                }
1570                            }
1571                            Ok(Event::End(end)) if end.name() == e.name() => break,
1572                            Ok(Event::Eof) => return Vec::new(),
1573                            Err(_) => return Vec::new(),
1574                            _ => {}
1575                        }
1576                    }
1577
1578                    if let Some(name) = name
1579                        && let Some(converted) =
1580                            Self::convert_defined_name(&name, &value, local_sheet_id, sheet_names)
1581                    {
1582                        let key = (
1583                            converted.scope.clone(),
1584                            converted.scope_sheet.clone(),
1585                            converted.name.clone(),
1586                        );
1587                        if seen.insert(key) {
1588                            out.push(converted);
1589                        }
1590                    }
1591                }
1592                Ok(Event::Eof) => break,
1593                Err(_) => return Vec::new(),
1594                _ => {}
1595            }
1596        }
1597
1598        out
1599    }
1600
1601    fn scan_external_link_targets_from_reader<R>(reader: R) -> BTreeMap<u32, String>
1602    where
1603        R: Read + Seek,
1604    {
1605        let mut archive = match ZipArchive::new(reader) {
1606            Ok(a) => a,
1607            Err(_) => return BTreeMap::new(),
1608        };
1609
1610        fn extract_target(xml: &str) -> Option<String> {
1611            let key = "Target=\"";
1612            let start = xml.find(key)? + key.len();
1613            let end = xml[start..].find('"')? + start;
1614            Some(xml[start..end].to_string())
1615        }
1616
1617        let mut out = BTreeMap::new();
1618        for i in 0..archive.len() {
1619            let mut entry = match archive.by_index(i) {
1620                Ok(e) => e,
1621                Err(_) => continue,
1622            };
1623            let name = entry.name().to_string();
1624            let Some(rest) = name.strip_prefix("xl/externalLinks/_rels/externalLink") else {
1625                continue;
1626            };
1627            let Some(num_str) = rest.strip_suffix(".xml.rels") else {
1628                continue;
1629            };
1630            let Ok(idx) = num_str.parse::<u32>() else {
1631                continue;
1632            };
1633
1634            let mut xml = String::new();
1635            if entry.read_to_string(&mut xml).is_ok()
1636                && let Some(target) = extract_target(&xml)
1637            {
1638                out.insert(idx, target);
1639            }
1640        }
1641        out
1642    }
1643
1644    /// Parse the workbook-level `<calcPr>` settings (spec §9) straight from the
1645    /// `.xlsx` zip — calamine does not surface these. Reuses the shared
1646    /// `calc_pr` parser; returns `None` when `xl/workbook.xml` is missing or has
1647    /// no `<calcPr>` element.
1648    fn scan_calc_settings_from_reader<R>(reader: R) -> Option<CalcSettings>
1649    where
1650        R: Read + Seek,
1651    {
1652        let mut archive = ZipArchive::new(reader).ok()?;
1653        let mut entry = archive.by_name("xl/workbook.xml").ok()?;
1654        let mut xml = Vec::new();
1655        entry.read_to_end(&mut xml).ok()?;
1656        crate::calc_pr::parse_calc_pr(&xml)
1657    }
1658
1659    fn calamine_error_code(e: &calamine::CellErrorType) -> u8 {
1660        let kind = match e {
1661            calamine::CellErrorType::Div0 => ExcelErrorKind::Div,
1662            calamine::CellErrorType::NA => ExcelErrorKind::Na,
1663            calamine::CellErrorType::Name => ExcelErrorKind::Name,
1664            calamine::CellErrorType::Null => ExcelErrorKind::Null,
1665            calamine::CellErrorType::Num => ExcelErrorKind::Num,
1666            calamine::CellErrorType::Ref => ExcelErrorKind::Ref,
1667            calamine::CellErrorType::Value => ExcelErrorKind::Value,
1668            _ => ExcelErrorKind::Error,
1669        };
1670        map_error_code(kind)
1671    }
1672
1673    fn range_to_cells(
1674        range: &Range<Data>,
1675        formulas: Option<&Range<String>>,
1676        date_system: DateSystem,
1677    ) -> BTreeMap<(u32, u32), CellData> {
1678        let mut cells = BTreeMap::new();
1679
1680        // We use the cells() iterator which gives us actual positions
1681
1682        // Process values using actual positions
1683
1684        let start_row = range.start().unwrap_or_default().0 as usize;
1685        let start_col = range.start().unwrap_or_default().1 as usize;
1686
1687        for (row, col, val) in range.used_cells() {
1688            // Calamine uses 0-based indexing, convert to 1-based for Excel
1689            let excel_row = (row + start_row + 1) as u32;
1690            let excel_col = (col + start_col + 1) as u32;
1691
1692            // Convert value (skip empty cells and empty strings)
1693            let value = match val {
1694                Data::Empty => None,
1695                Data::String(s) if s.is_empty() => None, // Treat empty strings as no value
1696                Data::String(s) => Some(LiteralValue::Text(s.clone())),
1697                Data::Float(f) => Some(LiteralValue::Number(*f)),
1698                Data::Int(i) => Some(LiteralValue::Int(*i)),
1699                Data::Bool(b) => Some(LiteralValue::Boolean(*b)),
1700                Data::Error(e) => {
1701                    let kind = match e {
1702                        calamine::CellErrorType::Div0 => ExcelErrorKind::Div,
1703                        calamine::CellErrorType::NA => ExcelErrorKind::Na,
1704                        calamine::CellErrorType::Name => ExcelErrorKind::Name,
1705                        calamine::CellErrorType::Null => ExcelErrorKind::Null,
1706                        calamine::CellErrorType::Num => ExcelErrorKind::Num,
1707                        calamine::CellErrorType::Ref => ExcelErrorKind::Ref,
1708                        calamine::CellErrorType::Value => ExcelErrorKind::Value,
1709                        _ => ExcelErrorKind::Value,
1710                    };
1711                    Some(LiteralValue::Error(ExcelError::new(kind)))
1712                }
1713                Data::DateTime(dt) => Some(
1714                    LiteralValue::try_from_serial_number_for(date_system, dt.as_f64())
1715                        .unwrap_or_else(LiteralValue::Error),
1716                ),
1717                Data::DateTimeIso(s) => Some(LiteralValue::Text(s.clone())),
1718                Data::DurationIso(s) => Some(LiteralValue::Text(s.clone())),
1719            };
1720
1721            if value.is_some() {
1722                cells.insert(
1723                    (excel_row, excel_col),
1724                    CellData {
1725                        value,
1726                        formula: None,
1727                        style: None,
1728                    },
1729                );
1730            }
1731        }
1732
1733        // Process formulas using their actual positions
1734        if let Some(frm_range) = formulas {
1735            let start_row = frm_range.start().unwrap_or_default().0 as usize;
1736            let start_col = frm_range.start().unwrap_or_default().1 as usize;
1737
1738            for (row, col, formula) in frm_range.used_cells() {
1739                if !formula.is_empty() {
1740                    // Convert to 1-based Excel coordinates
1741                    let excel_row = (row + start_row + 1) as u32;
1742                    let excel_col = (col + start_col + 1) as u32;
1743
1744                    // Ensure formula starts with '=' for proper parsing
1745                    let formula_with_eq = if formula.starts_with('=') {
1746                        formula.clone()
1747                    } else {
1748                        format!("={formula}")
1749                    };
1750
1751                    // Update existing cell or create new one with formula
1752                    cells
1753                        .entry((excel_row, excel_col))
1754                        .and_modify(|cell| cell.formula = Some(formula_with_eq.clone()))
1755                        .or_insert_with(|| CellData {
1756                            value: None,
1757                            formula: Some(formula_with_eq),
1758                            style: None,
1759                        });
1760                }
1761            }
1762        }
1763
1764        cells
1765    }
1766}
1767
1768impl SpreadsheetReader for CalamineAdapter {
1769    type Error = calamine::Error;
1770
1771    fn access_granularity(&self) -> AccessGranularity {
1772        AccessGranularity::Sheet
1773    }
1774
1775    fn capabilities(&self) -> BackendCaps {
1776        BackendCaps {
1777            read: true,
1778            formulas: true,
1779            named_ranges: true,
1780            lazy_loading: false,
1781            random_access: false,
1782            styles: false,
1783            bytes_input: true,
1784            // conservative defaults
1785            date_system_1904: false,
1786            merged_cells: false,
1787            rich_text: false,
1788            hyperlinks: false,
1789            data_validations: false,
1790            shared_formulas: false,
1791            ..Default::default()
1792        }
1793    }
1794
1795    fn sheet_names(&self) -> Result<Vec<String>, Self::Error> {
1796        Ok(self.cached_names.clone().unwrap_or_default())
1797    }
1798
1799    fn load_stats(&self) -> Option<AdapterLoadStats> {
1800        Some(self.load_stats.clone())
1801    }
1802
1803    fn defined_names(&mut self) -> Result<Vec<DefinedName>, Self::Error> {
1804        Ok(self.lazy_defined_names().clone())
1805    }
1806
1807    fn calc_settings(&self) -> Option<CalcSettings> {
1808        self.lazy_calc_settings().clone()
1809    }
1810
1811    fn open_path<P: AsRef<Path>>(path: P) -> Result<Self, Self::Error>
1812    where
1813        Self: Sized,
1814    {
1815        Self::open_path_with_source(path, XlsxPathSource::SharedFile)
1816    }
1817
1818    fn open_reader(mut reader: Box<dyn Read + Send + Sync>) -> Result<Self, Self::Error>
1819    where
1820        Self: Sized,
1821    {
1822        let mut data = Vec::new();
1823        reader.read_to_end(&mut data).map_err(calamine::Error::Io)?;
1824        Self::from_shared_source(SharedXlsxReader::from_bytes(data), None)
1825    }
1826
1827    fn open_bytes(data: Vec<u8>) -> Result<Self, Self::Error>
1828    where
1829        Self: Sized,
1830    {
1831        Self::from_shared_source(SharedXlsxReader::from_bytes(data), None)
1832    }
1833
1834    fn read_range(
1835        &mut self,
1836        sheet: &str,
1837        start: (u32, u32),
1838        end: (u32, u32),
1839    ) -> Result<BTreeMap<(u32, u32), CellData>, Self::Error> {
1840        // Calamine loads entire sheet; filter after read_sheet
1841        let data = self.read_sheet(sheet)?;
1842        Ok(data
1843            .cells
1844            .into_iter()
1845            .filter(|((r, c), _)| *r >= start.0 && *r <= end.0 && *c >= start.1 && *c <= end.1)
1846            .collect())
1847    }
1848
1849    fn read_sheet(&mut self, sheet: &str) -> Result<SheetData, Self::Error> {
1850        // Values
1851        let mut wb = self.workbook.write();
1852        let range = wb.worksheet_range(sheet)?;
1853        // Formulas (same dims as range, may be empty strings)
1854        let formulas = wb.worksheet_formula(sheet).ok();
1855
1856        let dims = (range.height() as u32, range.width() as u32);
1857        // Calamine's ordinary reader API does not currently expose the
1858        // workbook date system at this boundary. Keep that policy explicit so
1859        // future metadata support only changes this selection point.
1860        let cells = Self::range_to_cells(&range, formulas.as_ref(), DateSystem::Excel1900);
1861
1862        self.loaded_sheets.insert(sheet.to_string());
1863
1864        Ok(SheetData {
1865            cells,
1866            dimensions: Some(dims),
1867            tables: vec![],
1868            named_ranges: vec![],
1869            date_system_1904: false, // calamine XLSX currently doesn’t expose this
1870            merged_cells: Vec::<MergedRange>::new(),
1871            hidden: false,
1872            // Explicit fallback: calamine does not expose row visibility metadata.
1873            row_hidden_manual: vec![],
1874            // Explicit fallback: filter-hidden row state is unavailable via calamine.
1875            row_hidden_filter: vec![],
1876        })
1877    }
1878
1879    fn sheet_bounds(&self, sheet: &str) -> Option<(u32, u32)> {
1880        let mut wb = self.workbook.write();
1881        wb.worksheet_range(sheet)
1882            .ok()
1883            .map(|r| (r.height() as u32, r.width() as u32))
1884    }
1885
1886    fn is_loaded(&self, sheet: &str, _row: Option<u32>, _col: Option<u32>) -> bool {
1887        self.loaded_sheets.contains(sheet)
1888    }
1889}
1890
1891impl<R> EngineLoadStream<R> for CalamineAdapter
1892where
1893    R: EvaluationContext,
1894{
1895    type Error = calamine::Error;
1896
1897    fn stream_into_engine(&mut self, engine: &mut EvalEngine<R>) -> Result<(), Self::Error> {
1898        use formualizer_eval::engine::named_range::{NameScope, NamedDefinition};
1899        use formualizer_eval::reference::{CellRef, Coord};
1900
1901        #[cfg(feature = "tracing")]
1902        let _span_load = tracing::info_span!(
1903            "io_stream_into_engine",
1904            backend = "calamine",
1905            formula_records = true,
1906        )
1907        .entered();
1908
1909        // Calamine 0.36 streams cached values and formula metadata from each XLSX
1910        // cell record in one pass. FormulaPlane staging and authoritative family
1911        // grouping remain unchanged downstream.
1912        self.checkpoint_cancel()?;
1913        let cancel = self.cancel.clone();
1914        let debug = std::env::var("FZ_DEBUG_LOAD")
1915            .ok()
1916            .is_some_and(|v| v != "0");
1917        let t0 = DebugTimer::start();
1918        let names = self.sheet_names()?;
1919        if debug {
1920            eprintln!("[fz][load] calamine: {} sheets", names.len());
1921        }
1922        // Single seam for sheet registration across every backend: folds the
1923        // engine's seeded default sheet into the file's first sheet on a fresh
1924        // engine and rejects duplicate names (#332). Registration is one bulk
1925        // call now, so the former per-sheet `io_load_sheet` span becomes one
1926        // `io_load_sheets` span over the whole registration.
1927        #[cfg(feature = "tracing")]
1928        let _span_sheets =
1929            tracing::info_span!("io_load_sheets", sheet_count = names.len()).entered();
1930        engine
1931            .adopt_file_sheets(names.iter().map(|n| n.as_str()))
1932            .map_err(|e| calamine::Error::Io(std::io::Error::other(e.to_string())))?;
1933        #[cfg(feature = "tracing")]
1934        drop(_span_sheets);
1935
1936        let prev_index_mode = engine.config.sheet_index_mode;
1937        engine.set_sheet_index_mode(formualizer_eval::engine::SheetIndexMode::Lazy);
1938        let prev_range_limit = engine.config.range_expansion_limit;
1939        engine.config.range_expansion_limit = 0;
1940        let prev_first_load = engine.first_load_assume_new();
1941        engine.set_first_load_assume_new(true);
1942        engine.reset_ensure_touched();
1943
1944        let load_result = (|| -> Result<(), calamine::Error> {
1945            let chunk_rows: usize = 32 * 1024;
1946            let mut total_values = 0usize;
1947            let mut total_value_cells_observed = 0usize;
1948            let mut total_formulas = 0usize;
1949            let mut total_formula_handed_to_engine = 0usize;
1950            let mut total_shared_formula_tags = 0usize;
1951            let mut workbook_spool_bytes_used = 0u64;
1952            let mut workbook_spill_files_used = 0u32;
1953            let mut eager_formula_batches: Vec<(FormulaIngestBatch, FormulaCompressedSourceBatch)> =
1954                Vec::new();
1955            let mut eager_direct_batches: Vec<(
1956                FormulaIngestBatch,
1957                FormulaCompressedSourceReport,
1958                FormulaCompressedPreparation,
1959            )> = Vec::new();
1960
1961            for (sheet_instance, n) in names.iter().enumerate() {
1962                Self::cancellation_checkpoint(cancel.as_ref())?;
1963                let t_sheet = DebugTimer::start();
1964                if debug {
1965                    eprintln!("[fz][load] >> sheet '{n}'");
1966                }
1967                #[cfg(feature = "tracing")]
1968                let _span_sheet =
1969                    tracing::info_span!("io_populate_sheet", sheet = n.as_str()).entered();
1970
1971                let shadow_relocation_comparator =
1972                    self.shadow_relocation_comparator.as_ref().map(Arc::clone);
1973                #[cfg(test)]
1974                let row_checkpoint_hook = self.stream_row_checkpoint_hook.as_deref();
1975                let streamed = {
1976                    let mut workbook = self.workbook.write();
1977                    Self::stream_worksheet(
1978                        &mut workbook.0,
1979                        n,
1980                        engine,
1981                        sheet_instance as u32,
1982                        StreamWorksheetOptions {
1983                            chunk_rows,
1984                            debug,
1985                            workbook_spool_usage: WorkbookSpoolUsage {
1986                                bytes: workbook_spool_bytes_used,
1987                                files: workbook_spill_files_used,
1988                            },
1989                            shadow_relocation_comparator,
1990                        },
1991                        cancel.as_ref(),
1992                        #[cfg(test)]
1993                        row_checkpoint_hook,
1994                    )?
1995                };
1996                let StreamedSheet {
1997                    arrow_sheet: asheet,
1998                    dimensions: (dims_rows, dims_cols),
1999                    max_col_seen,
2000                    used_sparse_fallback,
2001                    value_cells_observed: sheet_value_cells_observed,
2002                    values_handed_to_engine,
2003                    formulas_observed: parsed_n,
2004                    formulas_handed_to_engine: formula_handed_to_engine,
2005                    formulas,
2006                    formula_source_report,
2007                    compressed_families,
2008                    partitioned_families,
2009                    direct_preparation,
2010                    deferred_package,
2011                    shared_formula_tags,
2012                    formula_spool_bytes,
2013                    formula_spool_spilled,
2014                    stream_millis,
2015                } = streamed;
2016                workbook_spool_bytes_used = workbook_spool_bytes_used
2017                    .checked_add(formula_spool_bytes)
2018                    .expect("spool workbook accounting was preflighted");
2019                if formula_spool_spilled {
2020                    workbook_spill_files_used = workbook_spill_files_used
2021                        .checked_add(1)
2022                        .expect("spool file accounting was preflighted");
2023                }
2024                total_values += values_handed_to_engine;
2025                total_value_cells_observed += sheet_value_cells_observed;
2026                total_shared_formula_tags += shared_formula_tags;
2027
2028                let store = engine.sheet_store_mut();
2029                if let Some(pos) = store.sheets.iter().position(|s| s.name.as_ref() == n) {
2030                    store.sheets[pos] = asheet;
2031                } else {
2032                    store.sheets.push(asheet);
2033                }
2034
2035                if engine.config.defer_graph_building {
2036                    if let Some(package) = deferred_package {
2037                        engine.source_formula_ingress().stage_deferred(package);
2038                    }
2039                } else if !formulas.is_empty() || formula_source_report.source_formula_events != 0 {
2040                    let batch = FormulaIngestBatch::new(n.clone(), formulas);
2041                    if let Some(preparation) = direct_preparation {
2042                        eager_direct_batches.push((batch, formula_source_report, preparation));
2043                    } else {
2044                        eager_formula_batches.push((
2045                            batch,
2046                            FormulaCompressedSourceBatch::with_proposals(
2047                                n.clone(),
2048                                formula_source_report,
2049                                compressed_families,
2050                                partitioned_families,
2051                            ),
2052                        ));
2053                    }
2054                }
2055
2056                total_formulas += parsed_n;
2057                total_formula_handed_to_engine += formula_handed_to_engine;
2058                if debug {
2059                    eprintln!(
2060                        "[fz][load]    streamed rows={} cols={} max_record_col={} sparse_fallback={} values={} formulas={} in {} ms",
2061                        dims_rows,
2062                        dims_cols,
2063                        max_col_seen + 1,
2064                        used_sparse_fallback,
2065                        sheet_value_cells_observed,
2066                        parsed_n,
2067                        stream_millis,
2068                    );
2069                    eprintln!(
2070                        "[fz][load] << sheet '{}' staged in {} ms",
2071                        n,
2072                        t_sheet.elapsed_millis()
2073                    );
2074                }
2075                self.loaded_sheets.insert(n.to_string());
2076
2077                let row_hidden_manual: &[u32] = &[];
2078                let row_hidden_filter: &[u32] = &[];
2079                for row in row_hidden_manual {
2080                    engine
2081                        .set_row_hidden(
2082                            n,
2083                            *row,
2084                            true,
2085                            formualizer_eval::engine::RowVisibilitySource::Manual,
2086                        )
2087                        .map_err(|e| calamine::Error::Io(std::io::Error::other(e.to_string())))?;
2088                }
2089                for row in row_hidden_filter {
2090                    engine
2091                        .set_row_hidden(
2092                            n,
2093                            *row,
2094                            true,
2095                            formualizer_eval::engine::RowVisibilitySource::Filter,
2096                        )
2097                        .map_err(|e| calamine::Error::Io(std::io::Error::other(e.to_string())))?;
2098                }
2099            }
2100
2101            if !engine.config.defer_graph_building && !eager_formula_batches.is_empty() {
2102                Self::cancellation_checkpoint(cancel.as_ref())?;
2103                engine
2104                    .source_formula_ingress()
2105                    .ingest_replay_batches(eager_formula_batches)
2106                    .map_err(|e| calamine::Error::Io(std::io::Error::other(e.to_string())))?;
2107                Self::cancellation_checkpoint(cancel.as_ref())?;
2108            }
2109            if !eager_direct_batches.is_empty() {
2110                Self::cancellation_checkpoint(cancel.as_ref())?;
2111                engine
2112                    .source_formula_ingress()
2113                    .finish_prepared(eager_direct_batches)
2114                    .map_err(|e| calamine::Error::Io(std::io::Error::other(e.to_string())))?;
2115                Self::cancellation_checkpoint(cancel.as_ref())?;
2116            }
2117
2118            {
2119                use rustc_hash::FxHashSet;
2120
2121                Self::cancellation_checkpoint(cancel.as_ref())?;
2122                let defined = self.defined_names()?;
2123                let mut seen: FxHashSet<(DefinedNameScope, Option<String>, String)> =
2124                    FxHashSet::default();
2125
2126                for dn in defined {
2127                    Self::cancellation_checkpoint(cancel.as_ref())?;
2128                    let key = (dn.scope.clone(), dn.scope_sheet.clone(), dn.name.clone());
2129                    if !seen.insert(key) {
2130                        continue;
2131                    }
2132
2133                    let scope = match dn.scope {
2134                        DefinedNameScope::Workbook => NameScope::Workbook,
2135                        DefinedNameScope::Sheet => {
2136                            let sheet_name = dn.scope_sheet.as_deref().ok_or_else(|| {
2137                                calamine::Error::Io(std::io::Error::other(format!(
2138                                    "sheet-scoped defined name `{}` missing scope_sheet",
2139                                    dn.name
2140                                )))
2141                            })?;
2142                            let sid = engine.sheet_id(sheet_name).ok_or_else(|| {
2143                                calamine::Error::Io(std::io::Error::other(format!(
2144                                    "scope sheet not found: {sheet_name}"
2145                                )))
2146                            })?;
2147                            NameScope::Sheet(sid)
2148                        }
2149                    };
2150
2151                    let definition = match dn.definition {
2152                        DefinedNameDefinition::Range { address } => {
2153                            let sheet_id = engine
2154                                .sheet_id(&address.sheet)
2155                                .or_else(|| engine.add_sheet(&address.sheet).ok())
2156                                .ok_or_else(|| {
2157                                    calamine::Error::Io(std::io::Error::other(format!(
2158                                        "sheet not found: {}",
2159                                        address.sheet
2160                                    )))
2161                                })?;
2162
2163                            let sr0 = address.start_row.saturating_sub(1);
2164                            let sc0 = address.start_col.saturating_sub(1);
2165                            let er0 = address.end_row.saturating_sub(1);
2166                            let ec0 = address.end_col.saturating_sub(1);
2167
2168                            let start_ref =
2169                                CellRef::new(sheet_id, Coord::new(sr0, sc0, true, true));
2170                            if sr0 == er0 && sc0 == ec0 {
2171                                NamedDefinition::Cell(start_ref)
2172                            } else {
2173                                let end_ref =
2174                                    CellRef::new(sheet_id, Coord::new(er0, ec0, true, true));
2175                                let range_ref =
2176                                    formualizer_eval::reference::RangeRef::new(start_ref, end_ref);
2177                                NamedDefinition::Range(range_ref)
2178                            }
2179                        }
2180                        DefinedNameDefinition::Literal { value } => NamedDefinition::Literal(value),
2181                    };
2182
2183                    engine
2184                        .define_name(&dn.name, definition, scope)
2185                        .map_err(|e| calamine::Error::Io(std::io::Error::other(e.to_string())))?;
2186                }
2187            }
2188
2189            if debug {
2190                eprintln!(
2191                    "[fz][load] done: values={}, formulas={}, total={} ms",
2192                    total_values,
2193                    total_formulas,
2194                    t0.elapsed_millis(),
2195                );
2196            }
2197            for n in &names {
2198                engine.finalize_sheet_index(n);
2199            }
2200
2201            self.load_stats = AdapterLoadStats {
2202                formula_cells_observed: Some(total_formulas as u64),
2203                value_cells_observed: Some(total_value_cells_observed as u64),
2204                value_slots_handed_to_engine: Some(total_values as u64),
2205                formula_cells_handed_to_engine: Some(total_formula_handed_to_engine as u64),
2206                shared_formula_tags_observed: Some(total_shared_formula_tags as u64),
2207            };
2208            Ok(())
2209        })();
2210
2211        // Restore every temporary engine setting even when parsing, limits, or
2212        // graph ingest exits early.
2213        engine.set_first_load_assume_new(prev_first_load);
2214        engine.reset_ensure_touched();
2215        engine.set_sheet_index_mode(prev_index_mode);
2216        engine.config.range_expansion_limit = prev_range_limit;
2217        load_result
2218    }
2219}
2220
2221#[cfg(test)]
2222mod tests {
2223    use super::*;
2224    use std::io::Cursor;
2225
2226    fn metadata_fixture() -> Vec<u8> {
2227        metadata_fixture_with_padding(0)
2228    }
2229
2230    /// `padding` inserts a stored filler part immediately before the worksheet,
2231    /// pushing the worksheet's bytes past any readahead window that earlier
2232    /// parts warmed. Used to test behaviour at offsets that cannot be cached.
2233    fn metadata_fixture_with_padding(padding: usize) -> Vec<u8> {
2234        use std::io::Write;
2235        use zip::write::SimpleFileOptions;
2236        use zip::{CompressionMethod, ZipWriter};
2237
2238        let mut writer = ZipWriter::new(Cursor::new(Vec::new()));
2239        let options = SimpleFileOptions::default().compression_method(CompressionMethod::Deflated);
2240        let entries = [
2241            (
2242                "[Content_Types].xml",
2243                r#"<?xml version="1.0"?><Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types"><Default Extension="rels" ContentType="application/vnd.openxmlformats-package.relationships+xml"/><Default Extension="xml" ContentType="application/xml"/><Override PartName="/xl/workbook.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet.main+xml"/><Override PartName="/xl/worksheets/sheet1.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml"/></Types>"#,
2244            ),
2245            (
2246                "_rels/.rels",
2247                r#"<?xml version="1.0"?><Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships"><Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument" Target="xl/workbook.xml"/></Relationships>"#,
2248            ),
2249            (
2250                "xl/workbook.xml",
2251                r#"<?xml version="1.0"?><workbook xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships"><sheets><sheet name="Data" sheetId="1" r:id="rId1"/></sheets><definedNames><definedName name="GlobalData">Data!$A$1</definedName><definedName name="LocalData" localSheetId="0">$A$1</definedName></definedNames><calcPr iterate="1" iterateCount="17" iterateDelta="0.01" calcMode="auto"/></workbook>"#,
2252            ),
2253            (
2254                "xl/_rels/workbook.xml.rels",
2255                r#"<?xml version="1.0"?><Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships"><Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/worksheet" Target="worksheets/sheet1.xml"/></Relationships>"#,
2256            ),
2257            (
2258                "xl/worksheets/sheet1.xml",
2259                r#"<?xml version="1.0"?><worksheet xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main"><dimension ref="A1:B1"/><sheetData><row r="1"><c r="A1"><v>42</v></c><c r="B1"><f>A1*2</f><v>84</v></c></row></sheetData></worksheet>"#,
2260            ),
2261            (
2262                "xl/externalLinks/_rels/externalLink7.xml.rels",
2263                r#"<?xml version="1.0"?><Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships"><Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/externalLinkPath" Target="file:///tmp/source.xlsx" TargetMode="External"/></Relationships>"#,
2264            ),
2265        ];
2266        for (name, xml) in entries {
2267            if padding > 0 && name == "xl/worksheets/sheet1.xml" {
2268                writer
2269                    .start_file(
2270                        "xl/media/pad.bin",
2271                        SimpleFileOptions::default().compression_method(CompressionMethod::Stored),
2272                    )
2273                    .unwrap();
2274                writer
2275                    .write_all(&(0..=255_u8).cycle().take(padding).collect::<Vec<_>>())
2276                    .unwrap();
2277            }
2278            writer.start_file(name, options).unwrap();
2279            writer.write_all(xml.as_bytes()).unwrap();
2280        }
2281        writer.finish().unwrap().into_inner()
2282    }
2283
2284    #[test]
2285    fn shared_bytes_have_one_backing_allocation() {
2286        let adapter = CalamineAdapter::open_bytes(metadata_fixture()).unwrap();
2287        let pointer = adapter.source.bytes_backing_ptr().unwrap();
2288
2289        // One Arc is retained by the adapter for scanners and one by calamine's
2290        // cursor. Metadata cursors are cheap temporary Arc clones.
2291        assert_eq!(adapter.source.strong_count(), 2);
2292        assert_eq!(
2293            adapter.external_link_target(7),
2294            Some("file:///tmp/source.xlsx")
2295        );
2296        assert_eq!(adapter.source.bytes_backing_ptr(), Some(pointer));
2297        assert_eq!(adapter.source.strong_count(), 2);
2298    }
2299
2300    #[test]
2301    fn shared_file_cursors_keep_independent_positions_under_concurrency() {
2302        use std::io::Write;
2303
2304        let mut file = tempfile::tempfile().unwrap();
2305        let data: Vec<u8> = (0..=255).cycle().take(16 * 1024).collect();
2306        file.write_all(&data).unwrap();
2307        let source = SharedXlsxReader::File(SharedFileCursor::new(Arc::new(SharedFile {
2308            file: Mutex::new(file),
2309            len: data.len() as u64,
2310        })));
2311
2312        std::thread::scope(|scope| {
2313            for thread_index in 0..8_u64 {
2314                let mut reader = source.reader();
2315                let data = &data;
2316                scope.spawn(move || {
2317                    for round in 0..200_u64 {
2318                        let offset = (thread_index * 997 + round * 43) % 16_000;
2319                        reader.seek(SeekFrom::Start(offset)).unwrap();
2320                        let mut actual = [0_u8; 31];
2321                        reader.read_exact(&mut actual).unwrap();
2322                        assert_eq!(&actual, &data[offset as usize..offset as usize + 31]);
2323                        assert_eq!(reader.stream_position().unwrap(), offset + 31);
2324                    }
2325                });
2326            }
2327        });
2328    }
2329
2330    /// Truncation under the safe default must degrade to ordinary I/O results,
2331    /// never to a mapped-memory fault. The fixture is padded past the readahead
2332    /// window so the worksheet lives at an offset no cursor can have cached;
2333    /// bytes already inside a window may still be served after truncation,
2334    /// which is a staleness widening, not a memory-safety hazard.
2335    #[cfg(unix)]
2336    #[test]
2337    fn truncation_after_safe_open_returns_normal_fallbacks_and_errors() {
2338        let file = tempfile::NamedTempFile::new().unwrap();
2339        std::fs::write(
2340            file.path(),
2341            metadata_fixture_with_padding(4 * SHARED_FILE_READAHEAD),
2342        )
2343        .unwrap();
2344        let mut adapter = CalamineAdapter::open_path(file.path()).unwrap();
2345        assert!(matches!(adapter.source, SharedXlsxReader::File(_)));
2346
2347        std::fs::OpenOptions::new()
2348            .write(true)
2349            .open(file.path())
2350            .unwrap()
2351            .set_len(0)
2352            .unwrap();
2353
2354        assert_eq!(adapter.external_link_target(7), None);
2355        assert_eq!(adapter.calc_settings(), None);
2356        assert!(adapter.read_sheet("Data").is_err());
2357    }
2358
2359    #[cfg(any(unix, windows))]
2360    #[test]
2361    fn runtime_direct_mmap_uses_mapping_for_nonempty_path() {
2362        let file = tempfile::NamedTempFile::new().unwrap();
2363        std::fs::write(file.path(), metadata_fixture()).unwrap();
2364        let adapter =
2365            CalamineAdapter::open_path_with_source(file.path(), XlsxPathSource::DirectMmap)
2366                .unwrap();
2367        assert!(matches!(adapter.source, SharedXlsxReader::Mapped { .. }));
2368    }
2369
2370    #[test]
2371    fn default_open_path_uses_shared_file() {
2372        let file = tempfile::NamedTempFile::new().unwrap();
2373        std::fs::write(file.path(), metadata_fixture()).unwrap();
2374        let adapter = CalamineAdapter::open_path(file.path()).unwrap();
2375        assert_eq!(XlsxPathSource::default(), XlsxPathSource::SharedFile);
2376        assert!(matches!(adapter.source, SharedXlsxReader::File(_)));
2377    }
2378
2379    /// The legacy `mmap` Cargo feature is a compatibility alias and must not
2380    /// select load behaviour. This test only compiles under
2381    /// `--features calamine,mmap`, so the no-op is pinned rather than assumed.
2382    #[cfg(feature = "mmap")]
2383    #[test]
2384    fn legacy_mmap_feature_does_not_select_path_behaviour() {
2385        let file = tempfile::NamedTempFile::new().unwrap();
2386        std::fs::write(file.path(), metadata_fixture()).unwrap();
2387
2388        assert_eq!(XlsxPathSource::default(), XlsxPathSource::SharedFile);
2389        let default_adapter = CalamineAdapter::open_path(file.path()).unwrap();
2390        assert!(matches!(default_adapter.source, SharedXlsxReader::File(_)));
2391        let requested_default =
2392            CalamineAdapter::open_path_with_source(file.path(), XlsxPathSource::default()).unwrap();
2393        assert!(matches!(
2394            requested_default.source,
2395            SharedXlsxReader::File(_)
2396        ));
2397
2398        // Mapping remains reachable only through the explicit opt-in.
2399        let mapped =
2400            CalamineAdapter::open_path_with_source(file.path(), XlsxPathSource::DirectMmap)
2401                .unwrap();
2402        assert!(matches!(mapped.source, SharedXlsxReader::Mapped { .. }));
2403    }
2404
2405    /// The readahead window is keyed by absolute offset, so backward and
2406    /// forward seeks inside it must not re-read the file or desynchronise.
2407    #[test]
2408    fn shared_file_readahead_serves_seeks_within_the_window() {
2409        use std::io::Write;
2410
2411        let mut file = tempfile::tempfile().unwrap();
2412        let data: Vec<u8> = (0..=255).cycle().take(4 * SHARED_FILE_READAHEAD).collect();
2413        file.write_all(&data).unwrap();
2414        let source = SharedXlsxReader::File(SharedFileCursor::new(Arc::new(SharedFile {
2415            file: Mutex::new(file),
2416            len: data.len() as u64,
2417        })));
2418
2419        let mut reader = source.reader();
2420        for offset in [0_u64, 4096, 1, 64, 4095, 8192, 200_000, 199_999] {
2421            reader.seek(SeekFrom::Start(offset)).unwrap();
2422            let mut actual = [0_u8; 17];
2423            reader.read_exact(&mut actual).unwrap();
2424            assert_eq!(&actual, &data[offset as usize..offset as usize + 17]);
2425            assert_eq!(reader.stream_position().unwrap(), offset + 17);
2426        }
2427
2428        // A request larger than the window bypasses it and still reads exactly.
2429        reader.seek(SeekFrom::Start(3)).unwrap();
2430        let mut large = vec![0_u8; 2 * SHARED_FILE_READAHEAD];
2431        reader.read_exact(&mut large).unwrap();
2432        assert_eq!(&large[..], &data[3..3 + 2 * SHARED_FILE_READAHEAD]);
2433    }
2434
2435    #[cfg(any(unix, windows))]
2436    #[test]
2437    fn direct_mmap_mapping_failure_is_returned_without_fallback() {
2438        let file = tempfile::NamedTempFile::new().unwrap();
2439        let error = SharedXlsxReader::from_file(file.reopen().unwrap(), XlsxPathSource::DirectMmap)
2440            .err()
2441            .expect("empty files cannot be mapped");
2442        assert_eq!(error.kind(), std::io::ErrorKind::InvalidInput);
2443
2444        let error = CalamineAdapter::open_path_with_source(file.path(), XlsxPathSource::DirectMmap)
2445            .err()
2446            .expect("the public API must return the mapping error");
2447        assert!(
2448            matches!(error, calamine::Error::Io(error) if error.kind() == std::io::ErrorKind::InvalidInput)
2449        );
2450    }
2451
2452    #[test]
2453    fn metadata_scans_are_lazy_and_run_at_most_once() {
2454        use std::sync::atomic::Ordering;
2455
2456        let mut adapter = CalamineAdapter::open_bytes(metadata_fixture()).unwrap();
2457        assert_eq!(
2458            adapter
2459                .lazy_scan_counts
2460                .external_links
2461                .load(Ordering::Relaxed),
2462            0
2463        );
2464        assert_eq!(
2465            adapter
2466                .lazy_scan_counts
2467                .calc_settings
2468                .load(Ordering::Relaxed),
2469            0
2470        );
2471        assert_eq!(
2472            adapter
2473                .lazy_scan_counts
2474                .defined_names
2475                .load(Ordering::Relaxed),
2476            0
2477        );
2478
2479        std::thread::scope(|scope| {
2480            let adapter = &adapter;
2481            for _ in 0..4 {
2482                scope.spawn(move || {
2483                    assert_eq!(
2484                        adapter.external_link_target(7),
2485                        Some("file:///tmp/source.xlsx")
2486                    );
2487                    assert_eq!(adapter.calc_settings().unwrap().iterate_count, Some(17));
2488                });
2489            }
2490        });
2491        assert_eq!(adapter.defined_names().unwrap().len(), 2);
2492        assert_eq!(adapter.defined_names().unwrap().len(), 2);
2493
2494        assert_eq!(
2495            adapter
2496                .lazy_scan_counts
2497                .external_links
2498                .load(Ordering::Relaxed),
2499            1
2500        );
2501        assert_eq!(
2502            adapter
2503                .lazy_scan_counts
2504                .calc_settings
2505                .load(Ordering::Relaxed),
2506            1
2507        );
2508        assert_eq!(
2509            adapter
2510                .lazy_scan_counts
2511                .defined_names
2512                .load(Ordering::Relaxed),
2513            1
2514        );
2515    }
2516
2517    #[test]
2518    fn path_and_bytes_metadata_are_identical() {
2519        let bytes = metadata_fixture();
2520        let file = tempfile::NamedTempFile::new().unwrap();
2521        std::fs::write(file.path(), &bytes).unwrap();
2522        let mut from_path = CalamineAdapter::open_path(file.path()).unwrap();
2523        let mut from_bytes = CalamineAdapter::open_bytes(bytes).unwrap();
2524
2525        assert_eq!(
2526            from_path.sheet_names().unwrap(),
2527            from_bytes.sheet_names().unwrap()
2528        );
2529        assert_eq!(
2530            from_path.defined_names().unwrap(),
2531            from_bytes.defined_names().unwrap()
2532        );
2533        assert_eq!(from_path.calc_settings(), from_bytes.calc_settings());
2534        assert_eq!(
2535            from_path.external_link_target(7),
2536            from_bytes.external_link_target(7)
2537        );
2538    }
2539
2540    #[cfg(any(unix, windows))]
2541    #[test]
2542    fn shared_file_and_direct_mmap_have_full_semantic_parity() {
2543        use crate::{LoadStrategy, Workbook, WorkbookConfig};
2544
2545        #[derive(Debug, PartialEq)]
2546        struct Observed {
2547            sheet_names: Vec<String>,
2548            defined_names: Vec<DefinedName>,
2549            calc_settings: Option<CalcSettings>,
2550            external_target: Option<String>,
2551            value: Option<LiteralValue>,
2552            formula: Option<String>,
2553            evaluated: LiteralValue,
2554        }
2555
2556        fn observe(path: &Path, policy: XlsxPathSource) -> Observed {
2557            let mut adapter = CalamineAdapter::open_path_with_source(path, policy).unwrap();
2558            let sheet_names = adapter.sheet_names().unwrap();
2559            let defined_names = adapter.defined_names().unwrap();
2560            let calc_settings = adapter.calc_settings();
2561            let external_target = adapter.external_link_target(7).map(str::to_string);
2562            let mut workbook =
2563                Workbook::from_reader(adapter, LoadStrategy::EagerAll, WorkbookConfig::ephemeral())
2564                    .unwrap();
2565            Observed {
2566                sheet_names,
2567                defined_names,
2568                calc_settings,
2569                external_target,
2570                value: workbook.get_value("Data", 1, 1),
2571                formula: workbook.get_formula("Data", 1, 2),
2572                evaluated: workbook.evaluate_cell("Data", 1, 2).unwrap(),
2573            }
2574        }
2575
2576        let file = tempfile::NamedTempFile::new().unwrap();
2577        std::fs::write(file.path(), metadata_fixture()).unwrap();
2578        let shared = observe(file.path(), XlsxPathSource::SharedFile);
2579        let mapped = observe(file.path(), XlsxPathSource::DirectMmap);
2580
2581        assert_eq!(shared, mapped);
2582        assert_eq!(shared.sheet_names, ["Data"]);
2583        assert_eq!(shared.defined_names.len(), 2);
2584        assert!(
2585            shared
2586                .defined_names
2587                .iter()
2588                .any(|name| name.name == "GlobalData" && name.scope_sheet.is_none())
2589        );
2590        assert!(
2591            shared
2592                .defined_names
2593                .iter()
2594                .any(|name| name.name == "LocalData" && name.scope_sheet.as_deref() == Some("Data"))
2595        );
2596        assert_eq!(shared.calc_settings.unwrap().iterate_count, Some(17));
2597        assert_eq!(
2598            shared.external_target.as_deref(),
2599            Some("file:///tmp/source.xlsx")
2600        );
2601        assert_eq!(shared.value, Some(LiteralValue::Number(42.0)));
2602        assert_eq!(shared.formula.as_deref(), Some("=A1 * 2"));
2603        assert_eq!(shared.evaluated, LiteralValue::Number(84.0));
2604    }
2605
2606    #[cfg(unix)]
2607    #[test]
2608    fn path_is_not_reopened_for_lazy_metadata_or_sheet_reads() {
2609        let directory = tempfile::tempdir().unwrap();
2610        let path = directory.path().join("source.xlsx");
2611        std::fs::write(&path, metadata_fixture()).unwrap();
2612        let mut adapter = CalamineAdapter::open_path(&path).unwrap();
2613        std::fs::remove_file(&path).unwrap();
2614
2615        assert_eq!(
2616            adapter.external_link_target(7),
2617            Some("file:///tmp/source.xlsx")
2618        );
2619        assert_eq!(adapter.calc_settings().unwrap().iterate_count, Some(17));
2620        assert_eq!(adapter.defined_names().unwrap().len(), 2);
2621        assert_eq!(
2622            adapter.read_sheet("Data").unwrap().cells[&(1, 1)].value,
2623            Some(LiteralValue::Number(42.0))
2624        );
2625    }
2626
2627    #[test]
2628    fn new_calamine_error_variants_preserve_generic_error_semantics() {
2629        let error = calamine::CellErrorType::GettingData;
2630        assert!(matches!(
2631            data_ref_to_literal(&DataRef::Error(error.clone()), DateSystem::Excel1900),
2632            Some(LiteralValue::Error(ref value)) if value.kind == ExcelErrorKind::Error
2633        ));
2634        assert!(matches!(
2635            data_ref_to_overlay(&DataRef::Error(error)),
2636            Some(OverlayValue::Error(8))
2637        ));
2638    }
2639
2640    #[test]
2641    fn cancellable_open_rejects_pre_cancelled_token_without_interrupted_error() {
2642        let token = CancelToken::new();
2643        token.cancel();
2644        let mut reader = CancellableReader::new(
2645            SharedXlsxReader::from_bytes(metadata_fixture()),
2646            Some(token.clone()),
2647        );
2648        let read_error = reader.read(&mut [0_u8; 1]).expect_err("read must stop");
2649        assert_ne!(read_error.kind(), std::io::ErrorKind::Interrupted);
2650
2651        let error = match CalamineAdapter::open_bytes_cancellable(metadata_fixture(), token) {
2652            Ok(_) => panic!("pre-cancelled open must stop before parsing"),
2653            Err(error) => error,
2654        };
2655        assert!(
2656            !error.to_string().is_empty(),
2657            "cancellation must remain a reportable Calamine error"
2658        );
2659    }
2660
2661    #[test]
2662    fn cancellable_stream_stops_at_a_row_checkpoint() {
2663        let token = CancelToken::new();
2664        let mut adapter =
2665            CalamineAdapter::open_bytes_cancellable(metadata_fixture(), token.clone())
2666                .expect("open uncancelled workbook");
2667        adapter.stream_row_checkpoint_hook = Some(Arc::new(move || token.cancel()));
2668        let context = formualizer_eval::test_workbook::TestWorkbook::new();
2669        let mut engine = EvalEngine::new(context, Default::default());
2670
2671        let error = adapter
2672            .stream_into_engine(&mut engine)
2673            .expect_err("row checkpoint must observe cancellation");
2674        assert!(matches!(
2675            error,
2676            calamine::Error::Io(ref error) if error.kind() != std::io::ErrorKind::Interrupted
2677        ));
2678    }
2679
2680    #[test]
2681    fn uncancelled_cancellable_open_matches_open_bytes() {
2682        let bytes = metadata_fixture();
2683        let plain = CalamineAdapter::open_bytes(bytes.clone()).expect("plain open");
2684        let cancellable = CalamineAdapter::open_bytes_cancellable(bytes, CancelToken::new())
2685            .expect("uncancelled cancellable open");
2686        assert_eq!(
2687            plain.sheet_names().unwrap(),
2688            cancellable.sheet_names().unwrap()
2689        );
2690        assert_eq!(
2691            plain.calamine_defined_names,
2692            cancellable.calamine_defined_names
2693        );
2694    }
2695
2696    #[test]
2697    fn dateish_backend_signal_maps_date_time_datetime_and_duration() {
2698        use calamine::{ExcelDateTime, ExcelDateTimeType};
2699        use formualizer_eval::format::FormatId;
2700
2701        let date = DataRef::DateTime(ExcelDateTime::new(
2702            45_583.0,
2703            ExcelDateTimeType::DateTime,
2704            false,
2705        ));
2706        let time = DataRef::DateTime(ExcelDateTime::new(0.5, ExcelDateTimeType::DateTime, false));
2707        let datetime = DataRef::DateTime(ExcelDateTime::new(
2708            45_583.5,
2709            ExcelDateTimeType::DateTime,
2710            false,
2711        ));
2712        let duration =
2713            DataRef::DateTime(ExcelDateTime::new(1.5, ExcelDateTimeType::TimeDelta, false));
2714
2715        assert_eq!(data_ref_format(&date), Some(FormatId::DATE));
2716        assert_eq!(data_ref_format(&time), Some(FormatId::TIME));
2717        assert_eq!(data_ref_format(&datetime), Some(FormatId::DATETIME));
2718        assert_eq!(data_ref_format(&duration), Some(FormatId::DURATION));
2719    }
2720}