Skip to main content

formualizer_eval/engine/
range_view.rs

1use crate::arrow_store;
2use crate::arrow_store::IngestBuilder;
3use crate::engine::CancelToken;
4use crate::stripes::NumericChunk;
5use arrow_array::Array;
6use arrow_schema::DataType;
7use formualizer_common::{CoercionPolicy, DateSystem, ExcelError, LiteralValue};
8use std::sync::Arc;
9
10#[cfg(test)]
11pub(crate) mod range_work {
12    use std::cell::Cell;
13
14    #[derive(Clone, Copy, Debug, Default, serde::Serialize)]
15    pub(crate) struct Work {
16        pub iterators: usize,
17        pub search_probes: usize,
18        pub candidates: usize,
19        pub segments: usize,
20        pub segment_rows: usize,
21        pub generic_columns: usize,
22        pub null_arrays: usize,
23        pub null_slots: usize,
24        pub provider_requests: [usize; 4],
25        pub provider_builds: [usize; 4],
26        pub provider_slots: [usize; 4],
27        pub error_pieces: usize,
28        pub error_piece_max_rows: usize,
29    }
30
31    thread_local! {
32        static WORK: Cell<Option<Work>> = const { Cell::new(None) };
33    }
34
35    pub(crate) fn begin() {
36        WORK.with(|work| work.set(Some(Work::default())));
37    }
38
39    pub(crate) fn take() -> Work {
40        WORK.with(|work| work.replace(None).unwrap_or_default())
41    }
42
43    #[inline]
44    pub(crate) fn record(f: impl FnOnce(&mut Work)) {
45        WORK.with(|work| {
46            if let Some(mut value) = work.get() {
47                f(&mut value);
48                work.set(Some(value));
49            }
50        });
51    }
52}
53
54#[derive(Clone)]
55pub enum RangeBacking<'a> {
56    Borrowed(&'a arrow_store::ArrowSheet),
57    Owned(Arc<arrow_store::ArrowSheet>),
58}
59
60/// Unified view over a 2D range with efficient traversal utilities.
61/// Phase 4: Arrow-only backing.
62#[derive(Clone)]
63pub struct RangeView<'a> {
64    backing: RangeBacking<'a>,
65    sr: usize,
66    sc: usize,
67    er: usize,
68    ec: usize,
69    rows: usize,
70    cols: usize,
71    cancel_token: Option<CancelToken>,
72}
73
74impl<'a> core::fmt::Debug for RangeView<'a> {
75    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
76        f.debug_struct("RangeView")
77            .field("rows", &self.rows)
78            .field("cols", &self.cols)
79            .field("kind", &self.kind_probe())
80            .finish()
81    }
82}
83
84#[derive(Copy, Clone, Debug, Eq, PartialEq)]
85pub enum RangeKind {
86    Empty,
87    NumericOnly,
88    TextOnly,
89    Mixed,
90}
91
92pub struct ChunkCol {
93    pub numbers: Option<arrow_array::ArrayRef>,
94    pub booleans: Option<arrow_array::ArrayRef>,
95    pub text: Option<arrow_array::ArrayRef>,
96    pub errors: Option<arrow_array::ArrayRef>,
97    pub type_tag: arrow_array::ArrayRef,
98}
99
100pub struct ChunkSlice {
101    pub row_start: usize, // relative to view top
102    pub row_len: usize,
103    pub cols: Vec<ChunkCol>,
104}
105
106struct RowSegment {
107    chunk_idx: usize,
108    chunk_offset: usize,
109    row_start: usize,
110    row_len: usize,
111}
112
113struct RowSegmentIterator<'a> {
114    view: &'a RangeView<'a>,
115    chunks: Option<core::ops::Range<usize>>,
116}
117
118impl Iterator for RowSegmentIterator<'_> {
119    type Item = Result<RowSegment, ExcelError>;
120
121    fn next(&mut self) -> Option<Self::Item> {
122        if self
123            .view
124            .cancel_token
125            .as_ref()
126            .is_some_and(CancelToken::is_cancelled)
127        {
128            return Some(Err(ExcelError::new(
129                formualizer_common::ExcelErrorKind::Cancelled,
130            )));
131        }
132        let sheet = self.view.sheet();
133        let starts = &sheet.chunk_starts;
134        let sheet_rows = sheet.nrows as usize;
135        let row_end = self.view.er.min(sheet_rows.saturating_sub(1));
136        let chunks = self.chunks.get_or_insert_with(|| {
137            // Match row coverage independently of logical column/zero-sized subview dimensions.
138            if sheet_rows == 0 || starts.is_empty() || self.view.sr > row_end {
139                return 0..0;
140            }
141            let first = starts
142                .partition_point(|&start| {
143                    #[cfg(test)]
144                    range_work::record(|w| w.search_probes += 1);
145                    start <= self.view.sr
146                })
147                .saturating_sub(1);
148            let end = starts.partition_point(|&start| {
149                #[cfg(test)]
150                range_work::record(|w| w.search_probes += 1);
151                start <= row_end
152            });
153            first..end
154        });
155        for ci in chunks.by_ref() {
156            #[cfg(test)]
157            range_work::record(|w| w.candidates += 1);
158            let start = starts[ci];
159            let end = starts.get(ci + 1).copied().unwrap_or(sheet_rows);
160            let len = end.saturating_sub(start);
161            if len == 0 {
162                continue;
163            }
164            let lo = start.max(self.view.sr);
165            let hi = (start + len - 1).min(row_end);
166            if lo > hi {
167                continue;
168            }
169            let row_len = hi - lo + 1;
170            #[cfg(test)]
171            range_work::record(|w| {
172                w.segments += 1;
173                w.segment_rows += row_len;
174            });
175            return Some(Ok(RowSegment {
176                chunk_idx: ci,
177                chunk_offset: lo - start,
178                row_start: lo - self.view.sr,
179                row_len,
180            }));
181        }
182        None
183    }
184}
185
186pub struct RowChunkIterator<'a> {
187    segments: RowSegmentIterator<'a>,
188}
189
190impl Iterator for RowChunkIterator<'_> {
191    type Item = Result<ChunkSlice, ExcelError>;
192
193    fn next(&mut self) -> Option<Self::Item> {
194        let segment = match self.segments.next()? {
195            Ok(segment) => segment,
196            Err(error) => return Some(Err(error)),
197        };
198        let view = self.segments.view;
199        let sheet = view.sheet();
200        let ci = segment.chunk_idx;
201        let rel_off = segment.chunk_offset;
202        let seg_len = segment.row_len;
203        let mut cols = Vec::with_capacity(view.cols);
204        for col_idx in view.sc..=view.ec {
205            #[cfg(test)]
206            range_work::record(|w| w.generic_columns += 1);
207            if col_idx >= sheet.columns.len() {
208                #[cfg(test)]
209                range_work::record(|w| {
210                    w.null_arrays += 4;
211                    w.null_slots += 4 * seg_len;
212                });
213                let numbers = Some(arrow_array::new_null_array(&DataType::Float64, seg_len));
214                let booleans = Some(arrow_array::new_null_array(&DataType::Boolean, seg_len));
215                let text = Some(arrow_array::new_null_array(&DataType::Utf8, seg_len));
216                let errors = Some(arrow_array::new_null_array(&DataType::UInt8, seg_len));
217                let type_tag: arrow_array::ArrayRef =
218                    Arc::new(arrow_array::UInt8Array::from(vec![
219                        arrow_store::TypeTag::Empty
220                            as u8;
221                        seg_len
222                    ]));
223                cols.push(ChunkCol {
224                    numbers,
225                    booleans,
226                    text,
227                    errors,
228                    type_tag,
229                });
230            } else {
231                let col = &sheet.columns[col_idx];
232                let Some(ch) = col.chunk(ci) else {
233                    #[cfg(test)]
234                    range_work::record(|w| {
235                        w.null_arrays += 4;
236                        w.null_slots += 4 * seg_len;
237                    });
238                    let numbers = Some(arrow_array::new_null_array(&DataType::Float64, seg_len));
239                    let booleans = Some(arrow_array::new_null_array(&DataType::Boolean, seg_len));
240                    let text = Some(arrow_array::new_null_array(&DataType::Utf8, seg_len));
241                    let errors = Some(arrow_array::new_null_array(&DataType::UInt8, seg_len));
242                    let type_tag: arrow_array::ArrayRef =
243                        Arc::new(arrow_array::UInt8Array::from(vec![
244                            arrow_store::TypeTag::Empty
245                                as u8;
246                            seg_len
247                        ]));
248                    cols.push(ChunkCol {
249                        numbers,
250                        booleans,
251                        text,
252                        errors,
253                        type_tag,
254                    });
255                    continue;
256                };
257
258                let numbers_base: arrow_array::ArrayRef = ch.numbers_or_null();
259                let booleans_base: arrow_array::ArrayRef = ch.booleans_or_null();
260                let text_base: arrow_array::ArrayRef = ch.text_or_null();
261                let errors_base: arrow_array::ArrayRef = ch.errors_or_null();
262
263                let numbers = Some(numbers_base.slice(rel_off, seg_len));
264                let booleans = Some(booleans_base.slice(rel_off, seg_len));
265                let text = Some(text_base.slice(rel_off, seg_len));
266                let errors = Some(errors_base.slice(rel_off, seg_len));
267                let type_tag: arrow_array::ArrayRef = Arc::new(ch.type_tag.slice(rel_off, seg_len));
268                cols.push(ChunkCol {
269                    numbers,
270                    booleans,
271                    text,
272                    errors,
273                    type_tag,
274                });
275            }
276        }
277        Some(Ok(ChunkSlice {
278            row_start: segment.row_start,
279            row_len: seg_len,
280            cols,
281        }))
282    }
283}
284
285impl<'a> RangeView<'a> {
286    pub(crate) fn new(
287        backing: RangeBacking<'a>,
288        sr: usize,
289        sc: usize,
290        er: usize,
291        ec: usize,
292        rows: usize,
293        cols: usize,
294    ) -> Self {
295        Self {
296            backing,
297            sr,
298            sc,
299            er,
300            ec,
301            rows,
302            cols,
303            cancel_token: None,
304        }
305    }
306
307    /// Attaches a shared cancellation handle to cancellation-aware range walks.
308    ///
309    /// Cloning a [`CancelToken`] shares its signal without allocating. Retrieve
310    /// a context token once before a hot loop and poll
311    /// [`CancelToken::is_cancelled`] periodically.
312    #[must_use]
313    pub fn with_cancel_token(mut self, token: Option<CancelToken>) -> Self {
314        self.cancel_token = token;
315        self
316    }
317
318    #[inline]
319    pub fn sheet(&self) -> &arrow_store::ArrowSheet {
320        match &self.backing {
321            RangeBacking::Borrowed(s) => s,
322            RangeBacking::Owned(s) => s,
323        }
324    }
325
326    pub fn from_owned_rows(
327        rows: Vec<Vec<LiteralValue>>,
328        date_system: DateSystem,
329    ) -> RangeView<'static> {
330        Self::try_from_owned_rows(rows, date_system, None)
331            .expect("uncancelled RangeView conversion")
332    }
333
334    pub(crate) fn try_from_owned_rows(
335        rows: Vec<Vec<LiteralValue>>,
336        date_system: DateSystem,
337        cancel_token: Option<CancelToken>,
338    ) -> Result<RangeView<'static>, ExcelError> {
339        let nrows = rows.len();
340        let ncols = rows.iter().map(|r| r.len()).max().unwrap_or(0);
341
342        let chunk_rows = 32 * 1024;
343        let mut ib = IngestBuilder::new("__tmp", ncols, chunk_rows, date_system);
344
345        for mut r in rows {
346            if cancel_token.as_ref().is_some_and(CancelToken::is_cancelled) {
347                return Err(ExcelError::new(
348                    formualizer_common::ExcelErrorKind::Cancelled,
349                ));
350            }
351            r.resize(ncols, LiteralValue::Empty);
352            ib.append_row(&r).expect("append_row for RangeView");
353        }
354
355        let sheet = Arc::new(ib.finish());
356
357        if nrows == 0 || ncols == 0 {
358            return Ok(RangeView {
359                backing: RangeBacking::Owned(sheet),
360                sr: 1,
361                sc: 1,
362                er: 0,
363                ec: 0,
364                rows: 0,
365                cols: 0,
366                cancel_token,
367            });
368        }
369
370        Ok(RangeView {
371            backing: RangeBacking::Owned(sheet),
372            sr: 0,
373            sc: 0,
374            er: nrows - 1,
375            ec: ncols - 1,
376            rows: nrows,
377            cols: ncols,
378            cancel_token,
379        })
380    }
381
382    pub fn dims(&self) -> (usize, usize) {
383        (self.rows, self.cols)
384    }
385
386    pub fn expand_to(&self, rows: usize, cols: usize) -> RangeView<'a> {
387        let er = self.sr + rows.saturating_sub(1);
388        let ec = self.sc + cols.saturating_sub(1);
389        RangeView {
390            backing: match &self.backing {
391                RangeBacking::Borrowed(s) => RangeBacking::Borrowed(s),
392                RangeBacking::Owned(s) => RangeBacking::Owned(s.clone()),
393            },
394            sr: self.sr,
395            sc: self.sc,
396            er,
397            ec,
398            rows,
399            cols,
400            cancel_token: self.cancel_token.clone(),
401        }
402    }
403
404    pub fn sub_view(&self, rs: usize, cs: usize, rows: usize, cols: usize) -> RangeView<'a> {
405        let abs_sr = self.sr + rs;
406        let abs_sc = self.sc + cs;
407        let er = abs_sr + rows.saturating_sub(1);
408        let ec = abs_sc + cols.saturating_sub(1);
409        RangeView {
410            backing: match &self.backing {
411                RangeBacking::Borrowed(s) => RangeBacking::Borrowed(s),
412                RangeBacking::Owned(s) => RangeBacking::Owned(s.clone()),
413            },
414            sr: abs_sr,
415            sc: abs_sc,
416            er,
417            ec,
418            rows,
419            cols,
420            cancel_token: self.cancel_token.clone(),
421        }
422    }
423
424    #[inline]
425    pub fn is_empty(&self) -> bool {
426        self.rows == 0 || self.cols == 0
427    }
428
429    /// Absolute 0-based start row of this view.
430    pub fn start_row(&self) -> usize {
431        self.sr
432    }
433    /// Absolute 0-based end row of this view (inclusive).
434    pub fn end_row(&self) -> usize {
435        self.er
436    }
437    /// Absolute 0-based start column of this view.
438    pub fn start_col(&self) -> usize {
439        self.sc
440    }
441    /// Absolute 0-based end column of this view (inclusive).
442    pub fn end_col(&self) -> usize {
443        self.ec
444    }
445    /// Whether this view reads a workbook sheet, as opposed to rows owned by
446    /// the view itself (array results, and names bound to a constant or a
447    /// formula), whose backing is a temporary sheet with no cell addresses.
448    pub(crate) fn is_sheet_backed(&self) -> bool {
449        matches!(self.backing, RangeBacking::Borrowed(_))
450    }
451
452    /// Owning sheet name.
453    pub fn sheet_name(&self) -> &str {
454        &self.sheet().name
455    }
456
457    pub fn kind_probe(&self) -> RangeKind {
458        if self.is_empty() {
459            return RangeKind::Empty;
460        }
461
462        let mut has_num = false;
463        let mut has_text = false;
464
465        for r in 0..self.rows {
466            for c in 0..self.cols {
467                match self.get_cell(r, c) {
468                    LiteralValue::Empty => {}
469                    LiteralValue::Number(_) | LiteralValue::Int(_) => has_num = true,
470                    LiteralValue::Text(_) => has_text = true,
471                    _ => return RangeKind::Mixed,
472                }
473                if has_num && has_text {
474                    return RangeKind::Mixed;
475                }
476            }
477        }
478
479        match (has_num, has_text) {
480            (false, false) => RangeKind::Empty,
481            (true, false) => RangeKind::NumericOnly,
482            (false, true) => RangeKind::TextOnly,
483            (true, true) => RangeKind::Mixed,
484        }
485    }
486
487    pub fn as_1x1(&self) -> Option<LiteralValue> {
488        if self.rows == 1 && self.cols == 1 {
489            Some(self.get_cell(0, 0))
490        } else {
491            None
492        }
493    }
494
495    /// Get a specific cell by row and column index (0-based).
496    /// Returns Empty for out-of-bounds access.
497    pub fn get_cell(&self, row: usize, col: usize) -> LiteralValue {
498        if row >= self.rows || col >= self.cols {
499            return LiteralValue::Empty;
500        }
501        let abs_row = self.sr + row;
502        let abs_col = self.sc + col;
503        let sheet = self.sheet();
504        let sheet_rows = sheet.nrows as usize;
505        if abs_row >= sheet_rows {
506            return LiteralValue::Empty;
507        }
508        if abs_col >= sheet.columns.len() {
509            return LiteralValue::Empty;
510        }
511        let col_ref = &sheet.columns[abs_col];
512        // Locate chunk by binary searching start offsets
513        let chunk_starts = &sheet.chunk_starts;
514        let ch_idx = match chunk_starts.binary_search(&abs_row) {
515            Ok(i) => i,
516            Err(0) => 0,
517            Err(i) => i - 1,
518        };
519        let Some(ch) = col_ref.chunk(ch_idx) else {
520            return LiteralValue::Empty;
521        };
522        let row_start = chunk_starts[ch_idx];
523        let in_off = abs_row - row_start;
524        // Overlay takes precedence: user edits over computed over base.
525        let cascade = arrow_store::OverlayCascade::new(&ch.overlay, &ch.computed_overlay);
526        if let Some(ov) = cascade.get_scalar(in_off) {
527            return ov.to_literal_for(sheet.date_system);
528        }
529        // Read tag and route to lane
530        let tag_u8 = ch.type_tag.value(in_off);
531        match arrow_store::TypeTag::from_u8(tag_u8) {
532            arrow_store::TypeTag::Empty => LiteralValue::Empty,
533            arrow_store::TypeTag::Number => {
534                if let Some(arr) = &ch.numbers {
535                    if arr.is_null(in_off) {
536                        return LiteralValue::Empty;
537                    }
538                    LiteralValue::Number(arr.value(in_off))
539                } else {
540                    LiteralValue::Empty
541                }
542            }
543            arrow_store::TypeTag::DateTime | arrow_store::TypeTag::Duration => {
544                if let Some(arr) = &ch.numbers {
545                    if arr.is_null(in_off) {
546                        LiteralValue::Empty
547                    } else {
548                        LiteralValue::Number(arr.value(in_off))
549                    }
550                } else {
551                    LiteralValue::Empty
552                }
553            }
554            arrow_store::TypeTag::Boolean => {
555                if let Some(arr) = &ch.booleans {
556                    if arr.is_null(in_off) {
557                        return LiteralValue::Empty;
558                    }
559                    LiteralValue::Boolean(arr.value(in_off))
560                } else {
561                    LiteralValue::Empty
562                }
563            }
564            arrow_store::TypeTag::Text => {
565                if let Some(arr) = &ch.text {
566                    if arr.is_null(in_off) {
567                        return LiteralValue::Empty;
568                    }
569                    let sa = arr
570                        .as_any()
571                        .downcast_ref::<arrow_array::StringArray>()
572                        .unwrap();
573                    LiteralValue::Text(sa.value(in_off).to_string())
574                } else {
575                    LiteralValue::Empty
576                }
577            }
578            arrow_store::TypeTag::Error => {
579                if let Some(arr) = &ch.errors {
580                    if arr.is_null(in_off) {
581                        return LiteralValue::Empty;
582                    }
583                    let kind = arrow_store::unmap_error_code(arr.value(in_off));
584                    LiteralValue::Error(ExcelError::new(kind))
585                } else {
586                    LiteralValue::Empty
587                }
588            }
589            arrow_store::TypeTag::Pending => LiteralValue::Pending,
590        }
591    }
592
593    /// Iterate overlapping chunks by row segment.
594    pub fn iter_row_chunks(&self) -> RowChunkIterator<'_> {
595        RowChunkIterator {
596            segments: self.iter_row_segments(),
597        }
598    }
599
600    fn iter_row_segments(&self) -> RowSegmentIterator<'_> {
601        #[cfg(test)]
602        range_work::record(|w| w.iterators += 1);
603        RowSegmentIterator {
604            view: self,
605            chunks: None,
606        }
607    }
608
609    /// Row-major cell traversal.
610    pub fn for_each_cell(
611        &self,
612        f: &mut dyn FnMut(&LiteralValue) -> Result<(), ExcelError>,
613    ) -> Result<(), ExcelError> {
614        for res in self.iter_row_chunks() {
615            let cs = res?;
616            for r in 0..cs.row_len {
617                for c in 0..self.cols {
618                    let tmp = self.get_cell(cs.row_start + r, c);
619                    f(&tmp)?;
620                }
621            }
622        }
623        Ok(())
624    }
625
626    /// Visit each row as a borrowed slice (buffered).
627    pub fn for_each_row(
628        &self,
629        f: &mut dyn FnMut(&[LiteralValue]) -> Result<(), ExcelError>,
630    ) -> Result<(), ExcelError> {
631        let mut buf: Vec<LiteralValue> = Vec::with_capacity(self.cols);
632        for r in 0..self.rows {
633            buf.clear();
634            for c in 0..self.cols {
635                buf.push(self.get_cell(r, c));
636            }
637            f(&buf[..])?;
638        }
639        Ok(())
640    }
641
642    /// Visit each column as a contiguous slice (buffered).
643    pub fn for_each_col(
644        &self,
645        f: &mut dyn FnMut(&[LiteralValue]) -> Result<(), ExcelError>,
646    ) -> Result<(), ExcelError> {
647        let mut col_buf: Vec<LiteralValue> = Vec::with_capacity(self.rows);
648        for c in 0..self.cols {
649            col_buf.clear();
650            for r in 0..self.rows {
651                col_buf.push(self.get_cell(r, c));
652            }
653            f(&col_buf[..])?;
654        }
655        Ok(())
656    }
657
658    /// Get a numeric value at a specific cell, with coercion.
659    /// Returns None for empty cells or non-coercible values.
660    pub fn get_cell_numeric(&self, row: usize, col: usize, policy: CoercionPolicy) -> Option<f64> {
661        if row >= self.rows || col >= self.cols {
662            return None;
663        }
664
665        let val = self.get_cell(row, col);
666        pack_numeric(&val, policy).ok().flatten()
667    }
668
669    /// Numeric chunk iteration with coercion policy.
670    pub fn numbers_chunked(
671        &self,
672        policy: CoercionPolicy,
673        min_chunk: usize,
674        f: &mut dyn FnMut(NumericChunk) -> Result<(), ExcelError>,
675    ) -> Result<(), ExcelError> {
676        // Fast path for Arrow numbers lane when policy allows ignoring non-numeric cells in ranges (standard Excel behavior for SUM/AVERAGE/etc over ranges)
677        if matches!(policy, CoercionPolicy::NumberStrict) {
678            for res in self.numbers_slices() {
679                let (_, _, cols) = res?;
680                for col in cols {
681                    if col.null_count() < col.len() {
682                        let data = col.values();
683                        // If there are nulls, we need to handle them.
684                        // Currently NumericChunk doesn't have a perfect way to represent sparse Arrow slices
685                        // without copying if we want a contiguous f64 slice.
686                        // For now, we can just provide the raw data and the validity mask if it exists.
687
688                        let validity = if col.null_count() > 0 {
689                            // Extract validity mask.
690                            // Note: This is still slightly awkward with the current NumericChunk design.
691                            None // TODO: Implement validity mask propagation
692                        } else {
693                            None
694                        };
695
696                        if col.null_count() == 0 {
697                            f(NumericChunk { data, validity })?;
698                        } else {
699                            // Fallback for nulls: iterate and push to a small buffer
700                            let mut buf = Vec::with_capacity(col.len());
701                            for i in 0..col.len() {
702                                if !col.is_null(i) {
703                                    buf.push(col.value(i));
704                                }
705                            }
706                            if !buf.is_empty() {
707                                f(NumericChunk {
708                                    data: &buf,
709                                    validity: None,
710                                })?;
711                            }
712                        }
713                    }
714                }
715            }
716            return Ok(());
717        }
718
719        let min_chunk = min_chunk.max(1);
720        let mut buf: Vec<f64> = Vec::with_capacity(min_chunk);
721        let mut flush = |buf: &mut Vec<f64>| -> Result<(), ExcelError> {
722            if buf.is_empty() {
723                return Ok(());
724            }
725            // SAFETY: read-only borrow for callback duration
726            let ptr = buf.as_ptr();
727            let len = buf.len();
728            let slice = unsafe { std::slice::from_raw_parts(ptr, len) };
729            let chunk = NumericChunk {
730                data: slice,
731                validity: None,
732            };
733            f(chunk)?;
734            buf.clear();
735            Ok(())
736        };
737
738        self.for_each_cell(&mut |v| {
739            if let Some(n) = pack_numeric(v, policy)? {
740                buf.push(n);
741                if buf.len() >= min_chunk {
742                    flush(&mut buf)?;
743                }
744            }
745            Ok(())
746        })?;
747        flush(&mut buf)?;
748
749        Ok(())
750    }
751
752    /// Typed numeric slices per row-segment: (row_start, row_len, per-column Float64 arrays)
753    pub fn numbers_slices(
754        &self,
755    ) -> impl Iterator<Item = Result<(usize, usize, Vec<Arc<arrow_array::Float64Array>>), ExcelError>> + '_
756    {
757        self.iter_row_segments().map(move |res| {
758            let segment = res?;
759            let mut out_cols = Vec::with_capacity(self.cols);
760            let sheet = self.sheet();
761            for col_idx in self.sc..=self.ec {
762                let Some(ch) = sheet
763                    .columns
764                    .get(col_idx)
765                    .and_then(|col| col.chunk(segment.chunk_idx))
766                else {
767                    #[cfg(test)]
768                    range_work::record(|w| {
769                        w.null_arrays += 1;
770                        w.null_slots += segment.row_len;
771                    });
772                    out_cols.push(Arc::new(arrow_array::Float64Array::new_null(
773                        segment.row_len,
774                    )));
775                    continue;
776                };
777                let base = ch
778                    .numbers_or_null()
779                    .slice(segment.chunk_offset, segment.row_len);
780                let range = segment.chunk_offset..segment.chunk_offset + segment.row_len;
781                let cascade = arrow_store::OverlayCascade::new(&ch.overlay, &ch.computed_overlay);
782                out_cols.push(if cascade.has_any_in_range(range.clone()) {
783                    ch.merged_numbers(range.clone())
784                        .unwrap_or_else(|| cascade.select_numbers(range, &base))
785                } else {
786                    Arc::new(base)
787                });
788            }
789            Ok((segment.row_start, segment.row_len, out_cols))
790        })
791    }
792
793    /// Typed boolean slices per row-segment, overlay-aware via zip.
794    pub fn booleans_slices(
795        &self,
796    ) -> impl Iterator<Item = Result<(usize, usize, Vec<Arc<arrow_array::BooleanArray>>), ExcelError>> + '_
797    {
798        self.iter_row_chunks().map(move |res| {
799            let cs = res?;
800            let mut out_cols: Vec<Arc<arrow_array::BooleanArray>> =
801                Vec::with_capacity(cs.cols.len());
802            let sheet = self.sheet();
803            let chunk_starts = &sheet.chunk_starts;
804
805            for (local_c, col_idx) in (self.sc..=self.ec).enumerate() {
806                let base = cs.cols[local_c]
807                    .booleans
808                    .as_ref()
809                    .expect("booleans lane exists")
810                    .clone();
811                let base_ba = base
812                    .as_any()
813                    .downcast_ref::<arrow_array::BooleanArray>()
814                    .unwrap()
815                    .clone();
816                let base_arc = Arc::new(base_ba);
817
818                // Identify chunk and overlay segment
819                let abs_seg_start = self.sr + cs.row_start;
820                let ch_idx = match chunk_starts.binary_search(&abs_seg_start) {
821                    Ok(i) => i,
822                    Err(0) => 0,
823                    Err(i) => i - 1,
824                };
825                if col_idx >= sheet.columns.len() {
826                    out_cols.push(base_arc);
827                    continue;
828                }
829                let col = &sheet.columns[col_idx];
830                let Some(ch) = col.chunk(ch_idx) else {
831                    out_cols.push(base_arc);
832                    continue;
833                };
834                let rel_off = (self.sr + cs.row_start) - chunk_starts[ch_idx];
835                let seg_range = rel_off..(rel_off + cs.row_len);
836                let cascade = arrow_store::OverlayCascade::new(&ch.overlay, &ch.computed_overlay);
837                if cascade.has_any_in_range(seg_range.clone()) {
838                    let base_ba = base
839                        .as_any()
840                        .downcast_ref::<arrow_array::BooleanArray>()
841                        .unwrap();
842                    out_cols.push(cascade.select_booleans(seg_range, base_ba));
843                } else {
844                    out_cols.push(base_arc);
845                }
846            }
847            Ok((cs.row_start, cs.row_len, out_cols))
848        })
849    }
850
851    /// Text slices per row-segment (erased as ArrayRef for Utf8 today; future Dict/View support).
852    pub fn text_slices(
853        &self,
854    ) -> impl Iterator<Item = Result<(usize, usize, Vec<arrow_array::ArrayRef>), ExcelError>> + '_
855    {
856        self.iter_row_chunks().map(move |res| {
857            let cs = res?;
858            let mut out_cols: Vec<arrow_array::ArrayRef> = Vec::with_capacity(cs.cols.len());
859            let sheet = self.sheet();
860            let chunk_starts = &sheet.chunk_starts;
861
862            for (local_c, col_idx) in (self.sc..=self.ec).enumerate() {
863                let base = cs.cols[local_c]
864                    .text
865                    .as_ref()
866                    .expect("text lane exists")
867                    .clone();
868                let abs_seg_start = self.sr + cs.row_start;
869                let ch_idx = match chunk_starts.binary_search(&abs_seg_start) {
870                    Ok(i) => i,
871                    Err(0) => 0,
872                    Err(i) => i - 1,
873                };
874                if col_idx >= sheet.columns.len() {
875                    out_cols.push(base.clone());
876                    continue;
877                }
878                let col = &sheet.columns[col_idx];
879                let Some(ch) = col.chunk(ch_idx) else {
880                    out_cols.push(base.clone());
881                    continue;
882                };
883                let rel_off = (self.sr + cs.row_start) - chunk_starts[ch_idx];
884                let seg_range = rel_off..(rel_off + cs.row_len);
885                let cascade = arrow_store::OverlayCascade::new(&ch.overlay, &ch.computed_overlay);
886                if cascade.has_any_in_range(seg_range.clone()) {
887                    let base_sa = base
888                        .as_any()
889                        .downcast_ref::<arrow_array::StringArray>()
890                        .unwrap();
891                    out_cols.push(cascade.select_text(seg_range, base_sa));
892                } else {
893                    out_cols.push(base.clone());
894                }
895            }
896            Ok((cs.row_start, cs.row_len, out_cols))
897        })
898    }
899
900    /// Typed lowered text slices per row-segment, overlay-aware via zip.
901    pub fn lowered_text_slices(
902        &self,
903    ) -> impl Iterator<Item = Result<(usize, usize, Vec<Arc<arrow_array::StringArray>>), ExcelError>> + '_
904    {
905        self.iter_row_chunks().map(move |res| {
906            let cs = res?;
907            let mut out_cols: Vec<Arc<arrow_array::StringArray>> =
908                Vec::with_capacity(cs.cols.len());
909            let sheet = self.sheet();
910            let chunk_starts = &sheet.chunk_starts;
911
912            for (local_c, col_idx) in (self.sc..=self.ec).enumerate() {
913                // Identify chunk
914                let abs_seg_start = self.sr + cs.row_start;
915                let ch_idx = match chunk_starts.binary_search(&abs_seg_start) {
916                    Ok(i) => i,
917                    Err(0) => 0,
918                    Err(i) => i - 1,
919                };
920                if col_idx >= sheet.columns.len() {
921                    out_cols.push(Arc::new(arrow_array::StringArray::new_null(cs.row_len)));
922                    continue;
923                }
924                let col = &sheet.columns[col_idx];
925                let Some(ch) = col.chunk(ch_idx) else {
926                    out_cols.push(Arc::new(arrow_array::StringArray::new_null(cs.row_len)));
927                    continue;
928                };
929                let rel_off = (self.sr + cs.row_start) - chunk_starts[ch_idx];
930                let seg_range = rel_off..(rel_off + cs.row_len);
931
932                let base_lowered = ch.text_lower_or_null();
933                let base_seg = base_lowered.slice(rel_off, cs.row_len);
934                let base_sa = base_seg
935                    .as_any()
936                    .downcast_ref::<arrow_array::StringArray>()
937                    .expect("lowered slice downcast");
938
939                let cascade = arrow_store::OverlayCascade::new(&ch.overlay, &ch.computed_overlay);
940                if cascade.has_any_in_range(seg_range.clone()) {
941                    out_cols.push(cascade.select_lowered_text(seg_range, base_sa));
942                } else {
943                    out_cols.push(Arc::new(base_sa.clone()));
944                }
945            }
946            Ok((cs.row_start, cs.row_len, out_cols))
947        })
948    }
949
950    /// Typed error-code slices per row-segment.
951    pub fn errors_slices(
952        &self,
953    ) -> impl Iterator<Item = Result<(usize, usize, Vec<Arc<arrow_array::UInt8Array>>), ExcelError>> + '_
954    {
955        self.iter_row_segments().map(move |res| {
956            let segment = res?;
957            let mut out_cols = Vec::with_capacity(self.cols);
958            let sheet = self.sheet();
959            for col_idx in self.sc..=self.ec {
960                let Some(ch) = sheet
961                    .columns
962                    .get(col_idx)
963                    .and_then(|col| col.chunk(segment.chunk_idx))
964                else {
965                    #[cfg(test)]
966                    range_work::record(|w| {
967                        w.null_arrays += 1;
968                        w.null_slots += segment.row_len;
969                    });
970                    out_cols.push(Arc::new(arrow_array::UInt8Array::new_null(segment.row_len)));
971                    continue;
972                };
973                let base = ch
974                    .errors_or_null()
975                    .slice(segment.chunk_offset, segment.row_len);
976                let range = segment.chunk_offset..segment.chunk_offset + segment.row_len;
977                let cascade = arrow_store::OverlayCascade::new(&ch.overlay, &ch.computed_overlay);
978                out_cols.push(if cascade.has_any_in_range(range.clone()) {
979                    ch.merged_errors(range.clone())
980                        .unwrap_or_else(|| cascade.select_errors(range, &base))
981                } else {
982                    Arc::new(base)
983                });
984            }
985            Ok((segment.row_start, segment.row_len, out_cols))
986        })
987    }
988
989    /// Visits overlay-merged error lanes in bounded pieces: one column and at
990    /// most `max_rows` rows each, in column-major order within each row
991    /// segment. `visit` returns `Ok(true)` to stop early; the method then
992    /// returns `Ok(true)`.
993    ///
994    /// Unlike [`Self::errors_slices`], preparing a piece never builds more
995    /// than `max_rows` slots: it does not consult the whole-chunk merged-lane
996    /// cache, does not materialize whole-chunk null lanes, and skips columns
997    /// and chunks without an error lane or overlay entries in the piece
998    /// (they hold no errors). The view's cancellation token is checked before
999    /// every piece is prepared, so a cancelled walk returns
1000    /// `Err(Cancelled)` rather than a partial clean result.
1001    pub(crate) fn try_for_each_error_piece(
1002        &self,
1003        max_rows: usize,
1004        visit: &mut dyn FnMut(&arrow_array::UInt8Array) -> Result<bool, ExcelError>,
1005    ) -> Result<bool, ExcelError> {
1006        let max_rows = max_rows.max(1);
1007        let cancelled = || {
1008            self.cancel_token
1009                .as_ref()
1010                .is_some_and(CancelToken::is_cancelled)
1011        };
1012        let sheet = self.sheet();
1013        for segment in self.iter_row_segments() {
1014            let segment = segment?;
1015            for col_idx in self.sc..=self.ec {
1016                let Some(ch) = sheet
1017                    .columns
1018                    .get(col_idx)
1019                    .and_then(|col| col.chunk(segment.chunk_idx))
1020                else {
1021                    continue;
1022                };
1023                let cascade = arrow_store::OverlayCascade::new(&ch.overlay, &ch.computed_overlay);
1024                let mut done = 0usize;
1025                while done < segment.row_len {
1026                    if cancelled() {
1027                        return Err(ExcelError::new(
1028                            formualizer_common::ExcelErrorKind::Cancelled,
1029                        ));
1030                    }
1031                    let len = (segment.row_len - done).min(max_rows);
1032                    let start = segment.chunk_offset + done;
1033                    let range = start..start + len;
1034                    done += len;
1035                    #[cfg(test)]
1036                    range_work::record(|w| {
1037                        w.error_pieces += 1;
1038                        w.error_piece_max_rows = w.error_piece_max_rows.max(len);
1039                    });
1040                    let lane = if cascade.has_any_in_range(range.clone()) {
1041                        let base = match &ch.errors {
1042                            Some(errors) => errors.slice(start, len),
1043                            None => arrow_array::UInt8Array::new_null(len),
1044                        };
1045                        cascade.select_errors(range, &base)
1046                    } else if let Some(errors) = &ch.errors {
1047                        Arc::new(errors.slice(start, len))
1048                    } else {
1049                        continue;
1050                    };
1051                    if visit(&lane)? {
1052                        return Ok(true);
1053                    }
1054                }
1055            }
1056        }
1057        Ok(false)
1058    }
1059
1060    /// Typed type-tag slices per row-segment.
1061    pub fn type_tags_slices(
1062        &self,
1063    ) -> impl Iterator<Item = Result<(usize, usize, Vec<Arc<arrow_array::UInt8Array>>), ExcelError>> + '_
1064    {
1065        self.iter_row_chunks().map(move |res| {
1066            let cs = res?;
1067            let mut out_cols: Vec<Arc<arrow_array::UInt8Array>> = Vec::with_capacity(cs.cols.len());
1068            let sheet = self.sheet();
1069            let chunk_starts = &sheet.chunk_starts;
1070
1071            for (local_c, col_idx) in (self.sc..=self.ec).enumerate() {
1072                let base = cs.cols[local_c].type_tag.clone();
1073                let base_ta = base
1074                    .as_any()
1075                    .downcast_ref::<arrow_array::UInt8Array>()
1076                    .unwrap()
1077                    .clone();
1078                let base_arc = Arc::new(base_ta);
1079
1080                let abs_seg_start = self.sr + cs.row_start;
1081                let ch_idx = match chunk_starts.binary_search(&abs_seg_start) {
1082                    Ok(i) => i,
1083                    Err(0) => 0,
1084                    Err(i) => i - 1,
1085                };
1086                if col_idx >= sheet.columns.len() {
1087                    out_cols.push(base_arc);
1088                    continue;
1089                }
1090                let col = &sheet.columns[col_idx];
1091                let Some(ch) = col.chunk(ch_idx) else {
1092                    out_cols.push(base_arc);
1093                    continue;
1094                };
1095                let rel_off = (self.sr + cs.row_start) - chunk_starts[ch_idx];
1096                let seg_range = rel_off..(rel_off + cs.row_len);
1097                let cascade = arrow_store::OverlayCascade::new(&ch.overlay, &ch.computed_overlay);
1098                if cascade.has_any_in_range(seg_range.clone()) {
1099                    let base_ta = base
1100                        .as_any()
1101                        .downcast_ref::<arrow_array::UInt8Array>()
1102                        .unwrap();
1103                    out_cols.push(cascade.select_type_tags(seg_range, base_ta));
1104                } else {
1105                    out_cols.push(base_arc);
1106                }
1107            }
1108            Ok((cs.row_start, cs.row_len, out_cols))
1109        })
1110    }
1111
1112    /// Build per-column concatenated lowered text arrays for this view.
1113    /// Uses per-chunk lowered cache for base text and merges overlays via zip_select.
1114    pub fn lowered_text_columns(&self) -> Vec<arrow_array::ArrayRef> {
1115        use crate::compute_prelude::concat_arrays;
1116
1117        let mut out: Vec<arrow_array::ArrayRef> = Vec::with_capacity(self.cols);
1118        if self.rows == 0 || self.cols == 0 {
1119            return out;
1120        }
1121        let sheet = self.sheet();
1122        let chunk_starts = &sheet.chunk_starts;
1123        // Clamp to physically materialized sheet rows; this view may be logically larger (e.g. A:A).
1124        let sheet_rows = sheet.nrows as usize;
1125        if sheet_rows == 0 || self.sr >= sheet_rows {
1126            for _ in 0..self.cols {
1127                out.push(arrow_array::new_null_array(&DataType::Utf8, 0));
1128            }
1129            return out;
1130        }
1131        let row_end = self.er.min(sheet_rows.saturating_sub(1));
1132        let physical_len = row_end.saturating_sub(self.sr) + 1;
1133        for col_idx in self.sc..=self.ec {
1134            let mut segs: Vec<arrow_array::ArrayRef> = Vec::new();
1135            if col_idx >= sheet.columns.len() {
1136                // OOB: nulls across rows
1137                segs.push(arrow_array::new_null_array(&DataType::Utf8, physical_len));
1138            } else {
1139                let col_ref = &sheet.columns[col_idx];
1140                for (ci, &start) in chunk_starts.iter().enumerate() {
1141                    let chunk_end = chunk_starts
1142                        .get(ci + 1)
1143                        .copied()
1144                        .unwrap_or(sheet.nrows as usize);
1145                    let len = chunk_end.saturating_sub(start);
1146                    if len == 0 {
1147                        continue;
1148                    }
1149                    let end = start + len - 1;
1150                    let is = start.max(self.sr);
1151                    let ie = end.min(row_end);
1152                    if is > ie {
1153                        continue;
1154                    }
1155                    let seg_len = ie - is + 1;
1156                    let rel_off = is - start;
1157                    if let Some(ch) = col_ref.chunk(ci) {
1158                        // Overlay-aware lowered segment
1159                        let seg_range = rel_off..(rel_off + seg_len);
1160                        let cascade =
1161                            arrow_store::OverlayCascade::new(&ch.overlay, &ch.computed_overlay);
1162                        if cascade.has_any_in_range(seg_range.clone()) {
1163                            let base_lowered = ch.text_lower_or_null();
1164                            let base_seg = base_lowered.slice(rel_off, seg_len);
1165                            let base_sa = base_seg
1166                                .as_any()
1167                                .downcast_ref::<arrow_array::StringArray>()
1168                                .expect("lowered slice downcast");
1169                            segs.push(cascade.select_lowered_text(seg_range, base_sa));
1170                        } else {
1171                            // No overlay: slice from lowered base
1172                            let lowered = ch.text_lower_or_null();
1173                            segs.push(lowered.slice(rel_off, seg_len));
1174                        }
1175                    } else {
1176                        segs.push(arrow_array::new_null_array(&DataType::Utf8, seg_len));
1177                    }
1178                }
1179            }
1180            // Ensure concat has at least one segment (can happen on sparse/empty sheets).
1181            if segs.is_empty() {
1182                segs.push(arrow_array::new_null_array(&DataType::Utf8, physical_len));
1183            }
1184            // Concat segments for this column
1185            let anys: Vec<&dyn arrow_array::Array> = segs
1186                .iter()
1187                .map(|a| a.as_ref() as &dyn arrow_array::Array)
1188                .collect();
1189            let conc = concat_arrays(&anys).expect("concat lowered segments");
1190            out.push(conc);
1191        }
1192        out
1193    }
1194
1195    /// Slice typed float arrays for a specific row interval (relative to view).
1196    pub fn slice_numbers(
1197        &self,
1198        rel_start: usize,
1199        len: usize,
1200    ) -> Vec<Option<Arc<arrow_array::Float64Array>>> {
1201        let abs_start = self.sr + rel_start;
1202        let abs_end = abs_start + len;
1203        let sheet = self.sheet();
1204        let chunk_starts = &sheet.chunk_starts;
1205
1206        let mut out_cols = Vec::with_capacity(self.cols);
1207        for col_idx in self.sc..=self.ec {
1208            if col_idx >= sheet.columns.len() {
1209                out_cols.push(None);
1210                continue;
1211            }
1212            let col = &sheet.columns[col_idx];
1213
1214            let start_ch_idx = match chunk_starts.binary_search(&abs_start) {
1215                Ok(i) => i,
1216                Err(0) => 0,
1217                Err(i) => i - 1,
1218            };
1219
1220            let mut segments: Vec<Arc<arrow_array::Float64Array>> = Vec::new();
1221            let mut null_only = true;
1222
1223            let mut curr = abs_start;
1224            let mut remaining = len;
1225            let mut ch_idx = start_ch_idx;
1226
1227            while remaining > 0 && ch_idx < chunk_starts.len() {
1228                let ch_start = chunk_starts[ch_idx];
1229                let ch_end = chunk_starts
1230                    .get(ch_idx + 1)
1231                    .copied()
1232                    .unwrap_or(sheet.nrows as usize);
1233                let ch_len = ch_end.saturating_sub(ch_start);
1234                if ch_len == 0 {
1235                    ch_idx += 1;
1236                    continue;
1237                }
1238
1239                let overlap_start = curr.max(ch_start);
1240                let overlap_end = ch_end.min(abs_end);
1241
1242                if overlap_start < overlap_end {
1243                    let seg_len = overlap_end - overlap_start;
1244                    let rel_off_in_chunk = overlap_start - ch_start;
1245
1246                    if let Some(ch) = col.chunk(ch_idx) {
1247                        let base_nums_arc = ch.numbers_or_null();
1248                        let base_nums = base_nums_arc.as_ref();
1249
1250                        let seg_range = rel_off_in_chunk..(rel_off_in_chunk + seg_len);
1251                        let cascade =
1252                            arrow_store::OverlayCascade::new(&ch.overlay, &ch.computed_overlay);
1253
1254                        let final_arr = if cascade.has_any_in_range(seg_range.clone()) {
1255                            let base_slice = base_nums.slice(rel_off_in_chunk, seg_len);
1256                            let base_fa = base_slice
1257                                .as_any()
1258                                .downcast_ref::<arrow_array::Float64Array>()
1259                                .unwrap();
1260                            cascade.select_numbers(seg_range, base_fa).as_ref().clone()
1261                        } else {
1262                            let sl = base_nums.slice(rel_off_in_chunk, seg_len);
1263                            sl.as_any()
1264                                .downcast_ref::<arrow_array::Float64Array>()
1265                                .unwrap()
1266                                .clone()
1267                        };
1268
1269                        if final_arr.null_count() < final_arr.len() {
1270                            null_only = false;
1271                        }
1272                        segments.push(Arc::new(final_arr));
1273                    } else {
1274                        segments.push(Arc::new(arrow_array::Float64Array::new_null(seg_len)));
1275                    }
1276                    curr += seg_len;
1277                    remaining -= seg_len;
1278                }
1279                ch_idx += 1;
1280            }
1281
1282            if remaining > 0 {
1283                segments.push(Arc::new(arrow_array::Float64Array::new_null(remaining)));
1284            }
1285
1286            if segments.len() == 1 {
1287                if null_only && segments[0].null_count() == segments[0].len() {
1288                    out_cols.push(None);
1289                } else {
1290                    out_cols.push(Some(segments.pop().unwrap()));
1291                }
1292            } else {
1293                let refs: Vec<&dyn Array> =
1294                    segments.iter().map(|a| a.as_ref() as &dyn Array).collect();
1295                let c = crate::compute_prelude::concat_arrays(&refs).expect("concat slice");
1296                let fa = c
1297                    .as_any()
1298                    .downcast_ref::<arrow_array::Float64Array>()
1299                    .unwrap()
1300                    .clone();
1301                out_cols.push(Some(Arc::new(fa)));
1302            }
1303        }
1304        out_cols
1305    }
1306
1307    /// Slice typed lowered text arrays for a specific row interval (relative to view).
1308    pub fn slice_lowered_text(
1309        &self,
1310        rel_start: usize,
1311        len: usize,
1312    ) -> Vec<Option<Arc<arrow_array::StringArray>>> {
1313        let abs_start = self.sr + rel_start;
1314        let abs_end = abs_start + len;
1315        let sheet = self.sheet();
1316        let chunk_starts = &sheet.chunk_starts;
1317
1318        let mut out_cols = Vec::with_capacity(self.cols);
1319        for col_idx in self.sc..=self.ec {
1320            if col_idx >= sheet.columns.len() {
1321                out_cols.push(None);
1322                continue;
1323            }
1324            let col = &sheet.columns[col_idx];
1325            let start_ch_idx = match chunk_starts.binary_search(&abs_start) {
1326                Ok(i) => i,
1327                Err(0) => 0,
1328                Err(i) => i - 1,
1329            };
1330
1331            let mut segments: Vec<Arc<arrow_array::StringArray>> = Vec::new();
1332            let mut null_only = true;
1333
1334            let mut curr = abs_start;
1335            let mut remaining = len;
1336            let mut ch_idx = start_ch_idx;
1337
1338            while remaining > 0 && ch_idx < chunk_starts.len() {
1339                let ch_start = chunk_starts[ch_idx];
1340                let ch_end = chunk_starts
1341                    .get(ch_idx + 1)
1342                    .copied()
1343                    .unwrap_or(sheet.nrows as usize);
1344                let ch_len = ch_end.saturating_sub(ch_start);
1345                if ch_len == 0 {
1346                    ch_idx += 1;
1347                    continue;
1348                }
1349
1350                let overlap_start = curr.max(ch_start);
1351                let overlap_end = ch_end.min(abs_end);
1352
1353                if overlap_start < overlap_end {
1354                    let seg_len = overlap_end - overlap_start;
1355                    let rel_off_in_chunk = overlap_start - ch_start;
1356
1357                    if let Some(ch) = col.chunk(ch_idx) {
1358                        let base_lowered = ch.text_lower_or_null();
1359                        let seg_range = rel_off_in_chunk..(rel_off_in_chunk + seg_len);
1360                        let cascade =
1361                            arrow_store::OverlayCascade::new(&ch.overlay, &ch.computed_overlay);
1362
1363                        let final_arr = if cascade.has_any_in_range(seg_range.clone()) {
1364                            let base_slice = base_lowered.slice(rel_off_in_chunk, seg_len);
1365                            let base_sa = base_slice
1366                                .as_any()
1367                                .downcast_ref::<arrow_array::StringArray>()
1368                                .unwrap();
1369                            cascade
1370                                .select_lowered_text(seg_range, base_sa)
1371                                .as_ref()
1372                                .clone()
1373                        } else {
1374                            let sl = base_lowered.slice(rel_off_in_chunk, seg_len);
1375                            sl.as_any()
1376                                .downcast_ref::<arrow_array::StringArray>()
1377                                .unwrap()
1378                                .clone()
1379                        };
1380
1381                        if final_arr.null_count() < final_arr.len() {
1382                            null_only = false;
1383                        }
1384                        segments.push(Arc::new(final_arr));
1385                    } else {
1386                        segments.push(Arc::new(arrow_array::StringArray::new_null(seg_len)));
1387                    }
1388                    curr += seg_len;
1389                    remaining -= seg_len;
1390                }
1391                ch_idx += 1;
1392            }
1393
1394            if remaining > 0 {
1395                segments.push(Arc::new(arrow_array::StringArray::new_null(remaining)));
1396            }
1397
1398            if segments.len() == 1 {
1399                if null_only && segments[0].null_count() == segments[0].len() {
1400                    out_cols.push(None);
1401                } else {
1402                    out_cols.push(Some(segments.pop().unwrap()));
1403                }
1404            } else {
1405                let refs: Vec<&dyn Array> =
1406                    segments.iter().map(|a| a.as_ref() as &dyn Array).collect();
1407                let c = crate::compute_prelude::concat_arrays(&refs).expect("concat text");
1408                let sa = c
1409                    .as_any()
1410                    .downcast_ref::<arrow_array::StringArray>()
1411                    .unwrap()
1412                    .clone();
1413                out_cols.push(Some(Arc::new(sa)));
1414            }
1415        }
1416        out_cols
1417    }
1418}
1419
1420#[inline]
1421fn pack_numeric(v: &LiteralValue, policy: CoercionPolicy) -> Result<Option<f64>, ExcelError> {
1422    match policy {
1423        CoercionPolicy::NumberLenientText => match v {
1424            LiteralValue::Error(e) => Err(e.clone()),
1425            LiteralValue::Empty => Ok(None),
1426            other => Ok(crate::coercion::to_number_lenient(other).ok()),
1427        },
1428        CoercionPolicy::NumberStrict => match v {
1429            LiteralValue::Error(e) => Err(e.clone()),
1430            LiteralValue::Empty => Ok(None),
1431            other => Ok(crate::coercion::to_number_strict(other).ok()),
1432        },
1433        _ => match v {
1434            LiteralValue::Error(e) => Err(e.clone()),
1435            _ => Ok(None),
1436        },
1437    }
1438}
1439
1440#[cfg(test)]
1441mod tests {
1442    use super::*;
1443
1444    #[test]
1445    fn owned_rows_numeric_chunking() {
1446        let data: Vec<Vec<LiteralValue>> = vec![
1447            vec![
1448                LiteralValue::Number(1.0),
1449                LiteralValue::Text("x".into()),
1450                LiteralValue::Number(3.0),
1451            ],
1452            vec![
1453                LiteralValue::Boolean(true),
1454                LiteralValue::Empty,
1455                LiteralValue::Number(2.5),
1456            ],
1457        ];
1458        let view = RangeView::from_owned_rows(data, DateSystem::Excel1900);
1459        let mut sum = 0.0f64;
1460        view.numbers_chunked(CoercionPolicy::NumberLenientText, 2, &mut |chunk| {
1461            for &n in chunk.data {
1462                sum += n;
1463            }
1464            Ok(())
1465        })
1466        .unwrap();
1467        assert!((sum - 7.5).abs() < 1e-9);
1468    }
1469
1470    #[test]
1471    fn as_1x1_works() {
1472        let view = RangeView::from_owned_rows(
1473            vec![vec![LiteralValue::Number(7.0)]],
1474            DateSystem::Excel1900,
1475        );
1476        assert_eq!(view.as_1x1(), Some(LiteralValue::Number(7.0)));
1477    }
1478
1479    #[test]
1480    fn pre_cancelled_token_stops_owned_row_construction() {
1481        let token = CancelToken::new();
1482        token.cancel();
1483
1484        let error = RangeView::try_from_owned_rows(
1485            vec![vec![LiteralValue::Number(1.0)]],
1486            DateSystem::Excel1900,
1487            Some(token),
1488        )
1489        .unwrap_err();
1490
1491        assert_eq!(error.kind, formualizer_common::ExcelErrorKind::Cancelled);
1492    }
1493
1494    #[test]
1495    fn pre_cancelled_token_stops_row_chunk_iteration() {
1496        let token = CancelToken::new();
1497        token.cancel();
1498        let view = RangeView::from_owned_rows(
1499            vec![vec![LiteralValue::Number(1.0)]],
1500            DateSystem::Excel1900,
1501        )
1502        .with_cancel_token(Some(token));
1503
1504        let Some(Err(error)) = view.iter_row_chunks().next() else {
1505            panic!("pre-cancelled chunk iteration should return cancellation");
1506        };
1507
1508        assert_eq!(error.kind, formualizer_common::ExcelErrorKind::Cancelled);
1509    }
1510}
1511#[cfg(test)]
1512mod bounded_projection_tests {
1513    use super::*;
1514    use crate::arrow_store::{ArrowSheet, OverlayFragment, OverlayValue};
1515    use arrow_array::{Float64Array, UInt8Array};
1516    use formualizer_common::ExcelErrorKind;
1517
1518    // Deliberately retain the exhaustive baseline intersection algorithm as an oracle.
1519    fn segments(view: &RangeView<'_>) -> Vec<(usize, usize, usize, usize)> {
1520        let sheet = view.sheet();
1521        let mut out = Vec::new();
1522        let row_end = view.er.min((sheet.nrows as usize).saturating_sub(1));
1523        for (ci, &start) in sheet.chunk_starts.iter().enumerate() {
1524            let end = sheet
1525                .chunk_starts
1526                .get(ci + 1)
1527                .copied()
1528                .unwrap_or(sheet.nrows as usize);
1529            let len = end.saturating_sub(start);
1530            if len == 0 {
1531                continue;
1532            }
1533            let lo = start.max(view.sr);
1534            let hi = (start + len - 1).min(row_end);
1535            if lo <= hi {
1536                out.push((ci, lo - start, lo - view.sr, hi - lo + 1));
1537            }
1538        }
1539        out
1540    }
1541
1542    fn assert_projection(view: &RangeView<'_>) {
1543        let expected = segments(view);
1544        let generic: Vec<_> = view.iter_row_chunks().map(Result::unwrap).collect();
1545        assert_eq!(
1546            generic
1547                .iter()
1548                .map(|s| (s.row_start, s.row_len))
1549                .collect::<Vec<_>>(),
1550            expected.iter().map(|s| (s.2, s.3)).collect::<Vec<_>>()
1551        );
1552        let numbers: Vec<_> = view.numbers_slices().map(Result::unwrap).collect();
1553        let errors: Vec<_> = view.errors_slices().map(Result::unwrap).collect();
1554        assert_eq!(numbers.len(), expected.len());
1555        assert_eq!(errors.len(), expected.len());
1556        for (seg, &(ci, offset, row_start, len)) in expected.iter().enumerate() {
1557            assert_eq!((numbers[seg].0, numbers[seg].1), (row_start, len));
1558            assert_eq!((errors[seg].0, errors[seg].1), (row_start, len));
1559            let columns = (view.sc..=view.ec).count();
1560            assert_eq!(generic[seg].cols.len(), columns);
1561            assert_eq!(numbers[seg].2.len(), columns);
1562            assert_eq!(errors[seg].2.len(), columns);
1563            for (c, absolute) in (view.sc..=view.ec).enumerate() {
1564                let (base_n, base_e) =
1565                    match view.sheet().columns.get(absolute).and_then(|c| c.chunk(ci)) {
1566                        Some(ch) => {
1567                            // Access physical lanes directly, without the optimized cursor/providers.
1568                            let n = ch
1569                                .numbers
1570                                .as_ref()
1571                                .map(|a| a.slice(offset, len))
1572                                .unwrap_or_else(|| Float64Array::new_null(len));
1573                            let e = ch
1574                                .errors
1575                                .as_ref()
1576                                .map(|a| a.slice(offset, len))
1577                                .unwrap_or_else(|| UInt8Array::new_null(len));
1578                            let cascade =
1579                                arrow_store::OverlayCascade::new(&ch.overlay, &ch.computed_overlay);
1580                            (
1581                                cascade.select_numbers(offset..offset + len, &n),
1582                                cascade.select_errors(offset..offset + len, &e),
1583                            )
1584                        }
1585                        None => (
1586                            Arc::new(Float64Array::new_null(len)),
1587                            Arc::new(UInt8Array::new_null(len)),
1588                        ),
1589                    };
1590                let actual_n = &numbers[seg].2[c];
1591                let actual_e = &errors[seg].2[c];
1592                assert_eq!(actual_n.len(), len);
1593                assert_eq!(actual_e.len(), len);
1594                for i in 0..len {
1595                    assert_eq!(actual_n.is_null(i), base_n.is_null(i));
1596                    assert_eq!(actual_e.is_null(i), base_e.is_null(i));
1597                    if !base_n.is_null(i) {
1598                        assert_eq!(actual_n.value(i).to_bits(), base_n.value(i).to_bits());
1599                    }
1600                    if !base_e.is_null(i) {
1601                        assert_eq!(actual_e.value(i), base_e.value(i));
1602                    }
1603                }
1604            }
1605        }
1606    }
1607
1608    fn fixture(chunk_rows: usize) -> ArrowSheet {
1609        let mut ingest = IngestBuilder::new("S", 3, chunk_rows, DateSystem::Excel1900);
1610        for row in 0..11 {
1611            ingest
1612                .append_row(&[
1613                    LiteralValue::Number(row as f64),
1614                    if row % 3 == 0 {
1615                        LiteralValue::Error(ExcelError::new(ExcelErrorKind::Div))
1616                    } else {
1617                        LiteralValue::Empty
1618                    },
1619                    LiteralValue::Text(format!("r{row}")),
1620                ])
1621                .unwrap();
1622        }
1623        let mut sheet = ingest.finish();
1624        sheet.ensure_row_capacity(29);
1625        for (row, value) in [
1626            (1, OverlayValue::Empty),
1627            (3, OverlayValue::Text(Arc::from("mask"))),
1628            (
1629                7,
1630                OverlayValue::Error(arrow_store::map_error_code(ExcelErrorKind::Na)),
1631            ),
1632            (22, OverlayValue::Number(17.0)),
1633            (28, OverlayValue::Pending),
1634        ] {
1635            let (ci, off) = sheet.chunk_of_row(row).unwrap();
1636            let chunk = sheet.ensure_column_chunk_mut(0, ci).unwrap();
1637            chunk.computed_overlay.set(off, OverlayValue::Number(99.0));
1638            chunk.overlay.set(off, value);
1639        }
1640        for (row, user) in [(0, OverlayValue::Empty), (8, OverlayValue::Number(17.0))] {
1641            let (ci, off) = sheet.chunk_of_row(row).unwrap();
1642            let ch = sheet.ensure_column_chunk_mut(1, ci).unwrap();
1643            ch.computed_overlay.set(
1644                off,
1645                OverlayValue::Error(arrow_store::map_error_code(ExcelErrorKind::Na)),
1646            );
1647            ch.overlay.set(off, user);
1648        }
1649        sheet
1650    }
1651
1652    #[test]
1653    fn range_projection_matches_independent_oracle_for_all_small_bounds() {
1654        for chunk_rows in [1, 3, 8, 32] {
1655            let sheet = fixture(chunk_rows);
1656            for sr in 0..=31 {
1657                for er in 0..=31 {
1658                    for (sc, ec) in [(0, 0), (1, 2), (0, 4), (4, 5), (2, 1)] {
1659                        assert_projection(&sheet.range_view(sr, sc, er, ec));
1660                    }
1661                }
1662            }
1663            let base = sheet.range_view(2, 1, 10, 2);
1664            for rows in [0, 1, 9, 40] {
1665                for cols in [0, 1, 4] {
1666                    assert_projection(&base.sub_view(1, 1, rows, cols));
1667                    assert_projection(&base.expand_to(rows, cols));
1668                }
1669            }
1670        }
1671        let empty = IngestBuilder::new("S", 1, 4, DateSystem::Excel1900).finish();
1672        assert_projection(&empty.range_view(0, 0, 7, 3));
1673    }
1674
1675    #[test]
1676    fn range_projection_preserves_dense_run_and_partial_overlay_paths() {
1677        for run in [false, true] {
1678            let mut sheet = fixture(32);
1679            let chunk = &mut sheet.columns[0].chunks[0];
1680            chunk.overlay.clear();
1681            chunk.computed_overlay.clear();
1682            let values = vec![OverlayValue::Number(7.0); 29];
1683            let fragment = if run {
1684                OverlayFragment::run_range(0, values)
1685            } else {
1686                OverlayFragment::dense_range(0, values)
1687            }
1688            .unwrap();
1689            chunk.computed_overlay.apply_fragment(fragment);
1690            let view = sheet.range_view(2, 0, 9, 0);
1691            arrow_store::reset_overlay_select_stats();
1692            let numeric = view.numbers_slices().next().unwrap().unwrap();
1693            assert_eq!(numeric.2[0].value(0), 7.0);
1694            let stats = arrow_store::snapshot_overlay_select_stats();
1695            assert_eq!(stats.zip_select_calls, 0);
1696            assert_eq!(stats.direct_dense_slices, usize::from(!run));
1697            assert_eq!(stats.direct_run_materializations, usize::from(run));
1698            assert_projection(&view);
1699            let chunk = &mut sheet.columns[0].chunks[0];
1700            for (off, value) in [
1701                (3, OverlayValue::Empty),
1702                (4, OverlayValue::Pending),
1703                (5, OverlayValue::DateTime(45000.25)),
1704                (6, OverlayValue::Duration(0.5)),
1705                (7, OverlayValue::Boolean(true)),
1706                (
1707                    8,
1708                    OverlayValue::Error(arrow_store::map_error_code(ExcelErrorKind::Ref)),
1709                ),
1710                (9, OverlayValue::Text(Arc::from("not numeric"))),
1711            ] {
1712                chunk.overlay.set(off, value);
1713            }
1714            assert_projection(&sheet.range_view(2, 0, 10, 3));
1715        }
1716    }
1717
1718    #[test]
1719    fn range_discovery_work_is_bounded_and_projection_is_lane_local() {
1720        let mut ingest = IngestBuilder::new("S", 1, 256, DateSystem::Excel1900);
1721        for _ in 0..256 {
1722            ingest.append_row(&[LiteralValue::Number(1.0)]).unwrap();
1723        }
1724        let mut sheet = ingest.finish();
1725        sheet.ensure_row_capacity(256 * 4096);
1726        for start in [0, 256 * 2048 + 1, 256 * 4096 - 8, 256 * 4096 + 8] {
1727            let view = sheet.range_view(start, 0, start + 7, 2);
1728            range_work::begin();
1729            let generic: Vec<_> = view.iter_row_chunks().collect();
1730            let work = range_work::take();
1731            assert_eq!(work.candidates, generic.len());
1732            assert_eq!(work.segments, generic.len());
1733            assert!(work.search_probes <= 32);
1734            range_work::begin();
1735            let _: Vec<_> = view.numbers_slices().collect();
1736            let numeric = range_work::take();
1737            assert_eq!(numeric.generic_columns, 0);
1738            assert_eq!(numeric.provider_requests[1..], [0, 0, 0]);
1739            assert!(numeric.null_arrays <= 3);
1740            range_work::begin();
1741            let _: Vec<_> = view.errors_slices().collect();
1742            let errors = range_work::take();
1743            assert_eq!(errors.generic_columns, 0);
1744            assert_eq!(errors.provider_requests[0], 0);
1745            assert_eq!(errors.provider_requests[1], 0);
1746            assert_eq!(errors.provider_requests[3], 0);
1747        }
1748        let view = sheet.range_view(0, 0, sheet.nrows as usize - 1, 0);
1749        range_work::begin();
1750        assert!(view.iter_row_chunks().next().unwrap().is_ok());
1751        let work = range_work::take();
1752        assert_eq!(work.candidates, 1);
1753        assert_eq!(work.segments, 1);
1754    }
1755
1756    #[test]
1757    fn range_cold_numeric_does_not_initialize_unused_null_providers() {
1758        let mut ingest = IngestBuilder::new("S", 1, 32768, DateSystem::Excel1900);
1759        for _ in 0..32768 {
1760            ingest.append_row(&[LiteralValue::Number(1.0)]).unwrap();
1761        }
1762        let sheet = ingest.finish();
1763        let view = sheet.range_view(1, 0, 8, 0);
1764        range_work::begin();
1765        view.numbers_slices().next().unwrap().unwrap();
1766        let numeric = range_work::take();
1767        assert_eq!(numeric.provider_requests, [1, 0, 0, 0]);
1768        assert_eq!(numeric.provider_builds, [0; 4]);
1769        range_work::begin();
1770        view.errors_slices().next().unwrap().unwrap();
1771        let errors = range_work::take();
1772        assert_eq!(errors.provider_requests, [0, 0, 1, 0]);
1773        assert_eq!(errors.provider_builds, [0, 0, 1, 0]);
1774        assert_eq!(errors.provider_slots, [0, 0, 32768, 0]);
1775    }
1776
1777    /// One 32,768-row chunk: column 0 numbers only, column 1 a dense computed
1778    /// overlay of `#DIV/0!` with `#N/A` at row 30,000, column 2 a single user
1779    /// `#REF!` point at row 20,000.
1780    fn dense_error_sheet() -> ArrowSheet {
1781        let rows = 32_768;
1782        let mut ingest = IngestBuilder::new("S", 3, rows, DateSystem::Excel1900);
1783        for row in 0..rows {
1784            ingest
1785                .append_row(&[
1786                    LiteralValue::Number(row as f64),
1787                    LiteralValue::Number(1.0),
1788                    LiteralValue::Empty,
1789                ])
1790                .unwrap();
1791        }
1792        let mut sheet = ingest.finish();
1793        let mut dense =
1794            vec![OverlayValue::Error(arrow_store::map_error_code(ExcelErrorKind::Div)); rows];
1795        dense[30_000] = OverlayValue::Error(arrow_store::map_error_code(ExcelErrorKind::Na));
1796        sheet
1797            .ensure_column_chunk_mut(1, 0)
1798            .unwrap()
1799            .computed_overlay
1800            .apply_fragment(OverlayFragment::dense_range(0, dense).unwrap());
1801        sheet.ensure_column_chunk_mut(2, 0).unwrap().overlay.set(
1802            20_000,
1803            OverlayValue::Error(arrow_store::map_error_code(ExcelErrorKind::Ref)),
1804        );
1805        sheet
1806    }
1807
1808    fn error_codes_by_slices(view: &RangeView<'_>) -> Vec<u8> {
1809        let mut out = Vec::new();
1810        for segment in view.errors_slices() {
1811            for lane in segment.unwrap().2 {
1812                out.extend(lane.iter().flatten());
1813            }
1814        }
1815        out.sort_unstable();
1816        out
1817    }
1818
1819    #[test]
1820    fn error_pieces_are_bounded_and_match_errors_slices() {
1821        let sheet = dense_error_sheet();
1822        for (sr, er) in [(0, 32_767), (5, 30_001), (29_999, 30_000)] {
1823            let view = sheet.range_view(sr, 0, er, 2);
1824            let mut codes = Vec::new();
1825            range_work::begin();
1826            let stopped = view
1827                .try_for_each_error_piece(4096, &mut |lane| {
1828                    assert!(lane.len() <= 4096);
1829                    codes.extend(lane.iter().flatten());
1830                    Ok(false)
1831                })
1832                .unwrap();
1833            let work = range_work::take();
1834            assert!(!stopped);
1835            codes.sort_unstable();
1836            assert_eq!(codes, error_codes_by_slices(&view), "rows {sr}..={er}");
1837            assert!(work.error_piece_max_rows <= 4096);
1838            // No whole-chunk null error lane is materialized for columns
1839            // without an error lane.
1840            assert_eq!(work.provider_builds[2], 0);
1841        }
1842    }
1843
1844    #[test]
1845    fn error_pieces_stop_early_and_observe_cancellation_between_pieces() {
1846        let sheet = dense_error_sheet();
1847        // Dense overlay: cancellation raised while visiting a piece stops
1848        // preparation of the next one.
1849        let token = CancelToken::new();
1850        let view = sheet
1851            .range_view(0, 1, 32_767, 1)
1852            .with_cancel_token(Some(token.clone()));
1853        let mut visits = 0;
1854        range_work::begin();
1855        let error = view
1856            .try_for_each_error_piece(4096, &mut |_| {
1857                visits += 1;
1858                if visits == 2 {
1859                    token.cancel();
1860                }
1861                Ok(false)
1862            })
1863            .unwrap_err();
1864        let work = range_work::take();
1865        assert_eq!(error.kind, ExcelErrorKind::Cancelled);
1866        assert_eq!(visits, 2);
1867        assert_eq!(work.error_pieces, 2);
1868
1869        // Early stop reports a match without preparing further pieces.
1870        let view = sheet.range_view(0, 1, 32_767, 1);
1871        range_work::begin();
1872        assert!(
1873            view.try_for_each_error_piece(4096, &mut |_| Ok(true))
1874                .unwrap()
1875        );
1876        assert_eq!(range_work::take().error_pieces, 1);
1877
1878        // Wide single-row view: every column is a piece, and cancellation is
1879        // observed between columns.
1880        let mut ingest = IngestBuilder::new("W", 64, 8, DateSystem::Excel1900);
1881        ingest
1882            .append_row(&vec![
1883                LiteralValue::Error(ExcelError::new(
1884                    ExcelErrorKind::Div
1885                ));
1886                64
1887            ])
1888            .unwrap();
1889        let wide = ingest.finish();
1890        let token = CancelToken::new();
1891        let view = wide
1892            .range_view(0, 0, 0, 63)
1893            .with_cancel_token(Some(token.clone()));
1894        let mut visits = 0;
1895        let error = view
1896            .try_for_each_error_piece(4096, &mut |_| {
1897                visits += 1;
1898                token.cancel();
1899                Ok(false)
1900            })
1901            .unwrap_err();
1902        assert_eq!(error.kind, ExcelErrorKind::Cancelled);
1903        assert_eq!(visits, 1);
1904    }
1905
1906    #[test]
1907    fn range_cancellation_keeps_empty_exhausted_and_between_segment_errors() {
1908        let sheet = fixture(3);
1909        for bounds in [(0, 0, 28, 0), (30, 0, 35, 0), (3, 0, 1, 0), (0, 2, 5, 1)] {
1910            let token = CancelToken::new();
1911            let view = sheet
1912                .range_view(bounds.0, bounds.1, bounds.2, bounds.3)
1913                .with_cancel_token(Some(token.clone()));
1914            let mut generic = view.iter_row_chunks();
1915            let mut numeric = view.numbers_slices();
1916            let mut errors = view.errors_slices();
1917            token.cancel();
1918            for _ in 0..2 {
1919                assert_eq!(
1920                    generic.next().unwrap().err().unwrap().kind,
1921                    ExcelErrorKind::Cancelled
1922                );
1923                assert_eq!(
1924                    numeric.next().unwrap().err().unwrap().kind,
1925                    ExcelErrorKind::Cancelled
1926                );
1927                assert_eq!(
1928                    errors.next().unwrap().err().unwrap().kind,
1929                    ExcelErrorKind::Cancelled
1930                );
1931            }
1932        }
1933        let token = CancelToken::new();
1934        let view = sheet
1935            .range_view(0, 0, 28, 0)
1936            .with_cancel_token(Some(token.clone()));
1937        let mut numeric = view.numbers_slices();
1938        assert!(numeric.next().unwrap().is_ok());
1939        token.cancel();
1940        assert_eq!(
1941            numeric.next().unwrap().err().unwrap().kind,
1942            ExcelErrorKind::Cancelled
1943        );
1944        let token = CancelToken::new();
1945        let view = sheet
1946            .range_view(30, 0, 35, 0)
1947            .with_cancel_token(Some(token.clone()));
1948        let mut numeric = view.numbers_slices();
1949        assert!(numeric.next().is_none());
1950        token.cancel();
1951        assert_eq!(
1952            numeric.next().unwrap().err().unwrap().kind,
1953            ExcelErrorKind::Cancelled
1954        );
1955    }
1956
1957    #[test]
1958    fn range_projection_keeps_numeric_bits_and_engine_error_order() {
1959        let values = [
1960            0.0,
1961            -0.0,
1962            f64::INFINITY,
1963            f64::NEG_INFINITY,
1964            f64::from_bits(0x7ff8000000000001),
1965        ];
1966        let view = RangeView::from_owned_rows(
1967            values
1968                .iter()
1969                .map(|n| vec![LiteralValue::Number(*n)])
1970                .collect(),
1971            DateSystem::Excel1900,
1972        );
1973        assert_projection(&view);
1974        let slice = view.numbers_slices().next().unwrap().unwrap().2.remove(0);
1975        for (i, n) in values.iter().enumerate() {
1976            assert_eq!(slice.value(i).to_bits(), n.to_bits());
1977        }
1978        for chunk_rows in [1, 2] {
1979            let mut engine = crate::engine::Engine::new(
1980                crate::test_workbook::TestWorkbook::new(),
1981                crate::engine::EvalConfig {
1982                    arrow_storage_enabled: true,
1983                    delta_overlay_enabled: true,
1984                    write_formula_overlay_enabled: true,
1985                    enable_parallel: false,
1986                    ..Default::default()
1987                },
1988            );
1989            let mut ingest = engine.begin_bulk_ingest_arrow();
1990            ingest.add_sheet("Source", 2, chunk_rows);
1991            ingest
1992                .append_row(
1993                    "Source",
1994                    &[
1995                        LiteralValue::Number(1.0),
1996                        LiteralValue::Error(ExcelError::new(ExcelErrorKind::Na)),
1997                    ],
1998                )
1999                .unwrap();
2000            ingest
2001                .append_row(
2002                    "Source",
2003                    &[
2004                        LiteralValue::Error(ExcelError::new(ExcelErrorKind::Div)),
2005                        LiteralValue::Number(2.0),
2006                    ],
2007                )
2008                .unwrap();
2009            ingest.finish().unwrap();
2010            engine
2011                .set_cell_formula(
2012                    "Result",
2013                    1,
2014                    1,
2015                    formualizer_parse::parser::parse("=SUM(Source!A1:B2)").unwrap(),
2016                )
2017                .unwrap();
2018            engine.evaluate_all().unwrap();
2019            let expected = if chunk_rows == 1 {
2020                ExcelErrorKind::Na
2021            } else {
2022                ExcelErrorKind::Div
2023            };
2024            assert!(
2025                matches!(engine.get_cell_value("Result", 1, 1), Some(LiteralValue::Error(e)) if e.kind == expected)
2026            );
2027        }
2028    }
2029
2030    #[test]
2031    fn range_arrays_outlive_owned_view_and_new_views_follow_structural_edits() {
2032        let numbers = {
2033            let view = RangeView::from_owned_rows(
2034                vec![vec![LiteralValue::Number(-0.0)]],
2035                DateSystem::Excel1900,
2036            );
2037            view.numbers_slices().next().unwrap().unwrap().2.remove(0)
2038        };
2039        assert_eq!(numbers.value(0).to_bits(), (-0.0f64).to_bits());
2040        let errors = {
2041            let view = RangeView::from_owned_rows(
2042                vec![vec![LiteralValue::Error(ExcelError::new(
2043                    ExcelErrorKind::Div,
2044                ))]],
2045                DateSystem::Excel1900,
2046            );
2047            view.errors_slices().next().unwrap().unwrap().2.remove(0)
2048        };
2049        assert_eq!(
2050            errors.value(0),
2051            arrow_store::map_error_code(ExcelErrorKind::Div)
2052        );
2053        let mut sheet = fixture(3);
2054        let chunks_before: usize = sheet.columns.iter().map(|c| c.total_chunk_count()).sum();
2055        assert_projection(&sheet.range_view(0, 0, 40, 4));
2056        assert_eq!(
2057            sheet
2058                .columns
2059                .iter()
2060                .map(|c| c.total_chunk_count())
2061                .sum::<usize>(),
2062            chunks_before
2063        );
2064        sheet.insert_rows(4, 2);
2065        assert_projection(&sheet.range_view(1, 0, 35, 4));
2066        sheet.delete_rows(2, 1);
2067        assert_projection(&sheet.range_view(0, 0, 35, 4));
2068        sheet.ensure_row_capacity(50);
2069        assert_projection(&sheet.range_view(0, 0, 55, 4));
2070        sheet.insert_columns(1, 1);
2071        assert_projection(&sheet.range_view(0, 0, 55, 4));
2072        sheet.delete_columns(0, 1);
2073        assert_projection(&sheet.range_view(0, 0, 55, 4));
2074    }
2075}