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
46const SHARED_FILE_READAHEAD: usize = 64 * 1024;
58
59#[derive(Clone)]
60struct SharedFileCursor {
61 source: Arc<SharedFile>,
62 position: u64,
63 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 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 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 _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#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
147pub enum XlsxPathSource {
148 #[default]
154 SharedFile,
155 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 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
314struct 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
522pub struct CalamineAdapter {
530 workbook: RwLock<CalamineWorkbook>,
531 source: SharedXlsxReader,
532 cancel: Option<CancelToken>,
534 loaded_sheets: HashSet<String>,
535 cached_names: Option<Vec<String>>,
536 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 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 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 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 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 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 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 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 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 let excel_row = (row + start_row + 1) as u32;
1690 let excel_col = (col + start_col + 1) as u32;
1691
1692 let value = match val {
1694 Data::Empty => None,
1695 Data::String(s) if s.is_empty() => None, 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 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 let excel_row = (row + start_row + 1) as u32;
1742 let excel_col = (col + start_col + 1) as u32;
1743
1744 let formula_with_eq = if formula.starts_with('=') {
1746 formula.clone()
1747 } else {
1748 format!("={formula}")
1749 };
1750
1751 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 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 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 let mut wb = self.workbook.write();
1852 let range = wb.worksheet_range(sheet)?;
1853 let formulas = wb.worksheet_formula(sheet).ok();
1855
1856 let dims = (range.height() as u32, range.width() as u32);
1857 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, merged_cells: Vec::<MergedRange>::new(),
1871 hidden: false,
1872 row_hidden_manual: vec![],
1874 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 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 #[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 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 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 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 #[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 #[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 let mapped =
2400 CalamineAdapter::open_path_with_source(file.path(), XlsxPathSource::DirectMmap)
2401 .unwrap();
2402 assert!(matches!(mapped.source, SharedXlsxReader::Mapped { .. }));
2403 }
2404
2405 #[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 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}