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