Skip to main content

lance_encoding/encodings/logical/
list.rs

1// SPDX-License-Identifier: Apache-2.0
2// SPDX-FileCopyrightText: Copyright The Lance Authors
3
4use std::{ops::Range, sync::Arc};
5
6use arrow_array::{Array, ArrayRef, LargeListArray, ListArray, cast::AsArray, make_array};
7use arrow_schema::DataType;
8use futures::future::BoxFuture;
9use lance_arrow::deepcopy::deep_copy_nulls;
10use lance_arrow::list::ListArrayExt;
11use lance_core::Result;
12
13use crate::{
14    decoder::{
15        DecodedArray, FilterExpression, ScheduledScanLine, SchedulerContext,
16        StructuralDecodeArrayTask, StructuralFieldDecoder, StructuralFieldScheduler,
17        StructuralSchedulingJob,
18    },
19    encoder::{EncodeTask, FieldEncoder, OutOfLineBuffers},
20    repdef::RepDefBuilder,
21};
22
23/// A structural encoder for list fields
24///
25/// The list's offsets are added to the rep/def builder
26/// and the list array's values are passed to the child encoder
27///
28/// The values will have any garbage values removed and will be trimmed
29/// to only include the values that are actually used.
30pub struct ListStructuralEncoder {
31    keep_original_array: bool,
32    child: Box<dyn FieldEncoder>,
33}
34
35impl ListStructuralEncoder {
36    pub fn new(keep_original_array: bool, child: Box<dyn FieldEncoder>) -> Self {
37        Self {
38            keep_original_array,
39            child,
40        }
41    }
42}
43
44impl FieldEncoder for ListStructuralEncoder {
45    fn maybe_encode(
46        &mut self,
47        array: ArrayRef,
48        external_buffers: &mut OutOfLineBuffers,
49        mut repdef: RepDefBuilder,
50        row_number: u64,
51        num_rows: u64,
52    ) -> Result<Vec<EncodeTask>> {
53        let values = if let Some(list_arr) = array.as_list_opt::<i32>() {
54            let has_garbage_values = if self.keep_original_array {
55                repdef.add_offsets(list_arr.offsets().clone(), array.nulls().cloned())
56            } else {
57                // there is no need to deep copy offsets, because offset buffers will be cast to a common type (i64).
58                repdef.add_offsets(list_arr.offsets().clone(), deep_copy_nulls(array.nulls()))
59            };
60            if has_garbage_values {
61                list_arr.filter_garbage_nulls().trimmed_values()
62            } else {
63                list_arr.trimmed_values()
64            }
65        } else if let Some(list_arr) = array.as_list_opt::<i64>() {
66            let has_garbage_values = if self.keep_original_array {
67                repdef.add_offsets(list_arr.offsets().clone(), array.nulls().cloned())
68            } else {
69                repdef.add_offsets(list_arr.offsets().clone(), deep_copy_nulls(array.nulls()))
70            };
71            if has_garbage_values {
72                list_arr.filter_garbage_nulls().trimmed_values()
73            } else {
74                list_arr.trimmed_values()
75            }
76        } else {
77            panic!("List encoder used for non-list data")
78        };
79        self.child
80            .maybe_encode(values, external_buffers, repdef, row_number, num_rows)
81    }
82
83    fn flush(&mut self, external_buffers: &mut OutOfLineBuffers) -> Result<Vec<EncodeTask>> {
84        self.child.flush(external_buffers)
85    }
86
87    fn num_columns(&self) -> u32 {
88        self.child.num_columns()
89    }
90
91    fn finish(
92        &mut self,
93        external_buffers: &mut OutOfLineBuffers,
94    ) -> BoxFuture<'_, Result<Vec<crate::encoder::EncodedColumn>>> {
95        self.child.finish(external_buffers)
96    }
97}
98
99#[derive(Debug)]
100pub struct StructuralListScheduler {
101    child: Box<dyn StructuralFieldScheduler>,
102}
103
104impl StructuralListScheduler {
105    pub fn new(child: Box<dyn StructuralFieldScheduler>) -> Self {
106        Self { child }
107    }
108}
109
110impl StructuralFieldScheduler for StructuralListScheduler {
111    fn schedule_ranges<'a>(
112        &'a self,
113        ranges: &[Range<u64>],
114        filter: &FilterExpression,
115    ) -> Result<Box<dyn StructuralSchedulingJob + 'a>> {
116        let child = self.child.schedule_ranges(ranges, filter)?;
117
118        Ok(Box::new(StructuralListSchedulingJob::new(child)))
119    }
120
121    fn initialize<'a>(
122        &'a mut self,
123        filter: &'a FilterExpression,
124        context: &'a SchedulerContext,
125    ) -> BoxFuture<'a, Result<()>> {
126        self.child.initialize(filter, context)
127    }
128}
129
130/// Scheduling job for list data
131///
132/// Scheduling is handled by the primitive encoder and nothing special
133/// happens here.
134#[derive(Debug)]
135struct StructuralListSchedulingJob<'a> {
136    child: Box<dyn StructuralSchedulingJob + 'a>,
137}
138
139impl<'a> StructuralListSchedulingJob<'a> {
140    fn new(child: Box<dyn StructuralSchedulingJob + 'a>) -> Self {
141        Self { child }
142    }
143}
144
145impl StructuralSchedulingJob for StructuralListSchedulingJob<'_> {
146    fn schedule_next(&mut self, context: &mut SchedulerContext) -> Result<Vec<ScheduledScanLine>> {
147        self.child.schedule_next(context)
148    }
149}
150
151#[derive(Debug)]
152pub struct StructuralListDecoder {
153    child: Box<dyn StructuralFieldDecoder>,
154    data_type: DataType,
155}
156
157impl StructuralListDecoder {
158    pub fn new(child: Box<dyn StructuralFieldDecoder>, data_type: DataType) -> Self {
159        Self { child, data_type }
160    }
161}
162
163impl StructuralFieldDecoder for StructuralListDecoder {
164    fn accept_page(&mut self, child: crate::decoder::LoadedPageShard) -> Result<()> {
165        self.child.accept_page(child)
166    }
167
168    fn drain(&mut self, num_rows: u64) -> Result<Box<dyn StructuralDecodeArrayTask>> {
169        let child_task = self.child.drain(num_rows)?;
170        Ok(Box::new(StructuralListDecodeTask::new(
171            child_task,
172            self.data_type.clone(),
173        )))
174    }
175
176    fn data_type(&self) -> &DataType {
177        &self.data_type
178    }
179}
180
181#[derive(Debug)]
182struct StructuralListDecodeTask {
183    child_task: Box<dyn StructuralDecodeArrayTask>,
184    data_type: DataType,
185}
186
187impl StructuralListDecodeTask {
188    fn new(child_task: Box<dyn StructuralDecodeArrayTask>, data_type: DataType) -> Self {
189        Self {
190            child_task,
191            data_type,
192        }
193    }
194}
195
196impl StructuralDecodeArrayTask for StructuralListDecodeTask {
197    fn decode(self: Box<Self>) -> Result<DecodedArray> {
198        let DecodedArray {
199            array,
200            mut repdef,
201            data_size,
202        } = self.child_task.decode()?;
203        match &self.data_type {
204            DataType::List(child_field) => {
205                let (offsets, validity) = repdef.unravel_offsets::<i32>()?;
206                let array = if !child_field.is_nullable() && array.null_count() == array.len() {
207                    make_array(array.into_data().into_builder().nulls(None).build()?)
208                } else {
209                    array
210                };
211                let list_array = ListArray::try_new(child_field.clone(), offsets, array, validity)?;
212
213                Ok(DecodedArray {
214                    array: Arc::new(list_array),
215                    repdef,
216                    data_size,
217                })
218            }
219            DataType::LargeList(child_field) => {
220                let (offsets, validity) = repdef.unravel_offsets::<i64>()?;
221                let list_array =
222                    LargeListArray::try_new(child_field.clone(), offsets, array, validity)?;
223                Ok(DecodedArray {
224                    array: Arc::new(list_array),
225                    repdef,
226                    data_size,
227                })
228            }
229            _ => panic!("List decoder did not have a list field"),
230        }
231    }
232}
233
234#[cfg(test)]
235mod tests {
236
237    use std::{collections::HashMap, sync::Arc};
238
239    use crate::constants::{
240        STRUCTURAL_ENCODING_FULLZIP, STRUCTURAL_ENCODING_META_KEY, STRUCTURAL_ENCODING_MINIBLOCK,
241    };
242    use arrow_array::{
243        Array, ArrayRef, BooleanArray, DictionaryArray, LargeStringArray, ListArray, StructArray,
244        UInt8Array, UInt64Array,
245        builder::{
246            Int32Builder, Int64Builder, LargeListBuilder, ListBuilder, StringBuilder, UInt32Builder,
247        },
248    };
249
250    use arrow_buffer::{BooleanBuffer, NullBuffer, OffsetBuffer, ScalarBuffer};
251    use arrow_schema::{DataType, Field, Fields};
252    use rstest::rstest;
253
254    use crate::testing::{
255        TestCases, TestEncoding, check_basic_random, check_round_trip_encoding_of_data,
256        create_test_field_encoder, test_encoding_strategy,
257    };
258
259    fn make_list_type(inner_type: DataType) -> DataType {
260        DataType::List(Arc::new(Field::new("item", inner_type, true)))
261    }
262
263    fn make_large_list_type(inner_type: DataType) -> DataType {
264        DataType::LargeList(Arc::new(Field::new("item", inner_type, true)))
265    }
266
267    async fn try_encode_v22_pages(
268        array: ArrayRef,
269    ) -> lance_core::Result<Vec<crate::encoder::EncodedPage>> {
270        try_encode_v22_pages_with_metadata(array, HashMap::new()).await
271    }
272
273    async fn try_encode_v22_pages_with_metadata(
274        array: ArrayRef,
275        field_metadata: HashMap<String, String>,
276    ) -> lance_core::Result<Vec<crate::encoder::EncodedPage>> {
277        let arrow_field =
278            Field::new("", array.data_type().clone(), true).with_metadata(field_metadata);
279        let lance_field = lance_core::datatypes::Field::try_from(&arrow_field).unwrap();
280        let encoding_strategy = test_encoding_strategy(TestEncoding::StructuralU32);
281        let mut column_index_seq = crate::encoder::ColumnIndexSequence::default();
282        let encoding_options = crate::encoder::EncodingOptions::default();
283        let mut encoder = create_test_field_encoder(
284            encoding_strategy.as_ref(),
285            &lance_field,
286            &mut column_index_seq,
287            &encoding_options,
288        )
289        .unwrap();
290        let mut external_buffers =
291            crate::encoder::OutOfLineBuffers::new(0, crate::encoder::MIN_PAGE_BUFFER_ALIGNMENT);
292        let num_rows = array.len() as u64;
293        let mut pages = Vec::new();
294        for task in encoder
295            .maybe_encode(
296                array,
297                &mut external_buffers,
298                crate::repdef::RepDefBuilder::default(),
299                0,
300                num_rows,
301            )
302            .unwrap()
303        {
304            pages.push(task.await?);
305        }
306        for task in encoder.flush(&mut external_buffers).unwrap() {
307            pages.push(task.await?);
308        }
309        Ok(pages)
310    }
311
312    async fn encode_v22_pages(array: ArrayRef) -> Vec<crate::encoder::EncodedPage> {
313        try_encode_v22_pages(array).await.unwrap()
314    }
315
316    fn assert_split_miniblock_layout(
317        pages: &[crate::encoder::EncodedPage],
318        expect_structural_only_page: bool,
319    ) {
320        let mut miniblock_pages = 0;
321        let mut fullzip_pages = 0;
322        let mut structural_only_pages = 0;
323
324        for page in pages {
325            let crate::decoder::PageEncoding::Structural(layout) = &page.description else {
326                continue;
327            };
328            match layout.layout.as_ref().unwrap() {
329                crate::format::pb21::page_layout::Layout::MiniBlockLayout(_) => {
330                    miniblock_pages += 1;
331                }
332                crate::format::pb21::page_layout::Layout::FullZipLayout(_) => {
333                    fullzip_pages += 1;
334                }
335                crate::format::pb21::page_layout::Layout::ConstantLayout(layout) => {
336                    if layout.inline_value.is_none()
337                        && (layout.num_rep_values > 0 || layout.num_def_values > 0)
338                    {
339                        structural_only_pages += 1;
340                    }
341                }
342                crate::format::pb21::page_layout::Layout::BlobLayout(_) => {}
343                crate::format::pb21::page_layout::Layout::SparseLayout(_) => {}
344            }
345        }
346
347        assert!(
348            miniblock_pages > 0,
349            "expected leaf values to remain on mini-block pages"
350        );
351        assert_eq!(
352            fullzip_pages, 0,
353            "split list pages should not fall back to full-zip"
354        );
355        if expect_structural_only_page {
356            assert!(
357                structural_only_pages > 0,
358                "expected at least one structural-only page"
359            );
360        }
361    }
362
363    fn assert_has_fullzip_layout(pages: &[crate::encoder::EncodedPage]) {
364        let has_fullzip = pages.iter().any(|page| {
365            let crate::decoder::PageEncoding::Structural(layout) = &page.description else {
366                return false;
367            };
368            matches!(
369                layout.layout.as_ref().unwrap(),
370                crate::format::pb21::page_layout::Layout::FullZipLayout(_)
371            )
372        });
373        assert!(has_fullzip, "expected at least one full-zip page");
374    }
375
376    #[rstest]
377    #[test_log::test(tokio::test)]
378    async fn test_list(
379        #[values(STRUCTURAL_ENCODING_MINIBLOCK, STRUCTURAL_ENCODING_FULLZIP)]
380        structural_encoding: &str,
381    ) {
382        let mut field_metadata = HashMap::new();
383        field_metadata.insert(
384            STRUCTURAL_ENCODING_META_KEY.to_string(),
385            structural_encoding.into(),
386        );
387        let field =
388            Field::new("", make_list_type(DataType::Int32), true).with_metadata(field_metadata);
389        check_basic_random(field).await;
390    }
391
392    #[rstest]
393    #[test_log::test(tokio::test)]
394    async fn test_deeply_nested_lists(
395        #[values(STRUCTURAL_ENCODING_MINIBLOCK, STRUCTURAL_ENCODING_FULLZIP)]
396        structural_encoding: &str,
397    ) {
398        let mut field_metadata = HashMap::new();
399        field_metadata.insert(
400            STRUCTURAL_ENCODING_META_KEY.to_string(),
401            structural_encoding.into(),
402        );
403        let field = Field::new("item", DataType::Int32, true).with_metadata(field_metadata);
404        for _ in 0..5 {
405            let field = Field::new("", make_list_type(field.data_type().clone()), true);
406            check_basic_random(field).await;
407        }
408    }
409
410    #[test_log::test(tokio::test)]
411    async fn test_large_list() {
412        let field = Field::new("", make_large_list_type(DataType::Int32), true);
413        check_basic_random(field).await;
414    }
415
416    #[test_log::test(tokio::test)]
417    async fn test_nested_strings() {
418        let field = Field::new("", make_list_type(DataType::Utf8), true);
419        check_basic_random(field).await;
420    }
421
422    #[test_log::test(tokio::test)]
423    async fn test_nested_list() {
424        let field = Field::new("", make_list_type(make_list_type(DataType::Int32)), true);
425        check_basic_random(field).await;
426    }
427
428    /// Regression test: a `List<List<Float32>>` column written as MULTIPLE
429    /// batches (chunks) whose flattened leaf values cross a value-page boundary
430    /// fails to decode with "Max offset N exceeds length of values M" (Arrow
431    /// error raised by `ListArray::try_new` in `StructuralListDecodeTask::decode`).
432    ///
433    /// The trigger (verified against the production file and pylance 7.0.0b12 /
434    /// 7.0.0 / 9.0.0-beta.10) requires ALL of:
435    ///   1. >= 2 list layers (`List<List<..>>`),
436    ///   2. a leaf large enough to be chunked into multiple value pages,
437    ///   3. the column written as more than one batch.
438    /// A single batch of the identical data round-trips fine — which is why the
439    /// earlier single-chunk version of this test (and the small `test_nested_list`
440    /// cases) did not catch it. Found in production on the gaming TransNet
441    /// `dino_embedding_per_frame` column (rectangular 3 x 768 float per row).
442    ///
443    /// Each element of the `vec![..]` passed to `check_round_trip_encoding_of_data`
444    /// is encoded as a separate batch (its own `RepDefBuilder`), so we split the
445    /// rows into two chunks to exercise the multi-batch repdef accumulation path.
446    #[rstest]
447    #[test_log::test(tokio::test)]
448    async fn test_multipage_nested_float_list(
449        #[values(STRUCTURAL_ENCODING_MINIBLOCK, STRUCTURAL_ENCODING_FULLZIP)]
450        structural_encoding: &str,
451    ) {
452        use arrow_array::Float32Array;
453
454        // Production shape: 3 inner lists per row, 768 floats each.
455        let inner_per_row: usize = 3;
456        let inner_len: usize = 768;
457        // Two chunks (batches) -> two pages; a read batch that spans the page
458        // boundary is where the multi-page outer-offset bug triggered. A single
459        // [2731] chunk (one page) decodes fine, which is why this needs >= 2.
460        let chunk_rows: &[usize] = &[1366, 1365];
461
462        let make_chunk = |start_row: usize, num_rows: usize| -> Arc<dyn Array> {
463            let total_inner = num_rows * inner_per_row;
464            let total_values = total_inner * inner_len;
465            let values = Float32Array::from(
466                (0..total_values)
467                    .map(|i| (start_row + i) as f32)
468                    .collect::<Vec<_>>(),
469            );
470            let inner_offsets = ScalarBuffer::<i32>::from(
471                (0..=total_inner)
472                    .map(|i| (i * inner_len) as i32)
473                    .collect::<Vec<_>>(),
474            );
475            let inner_list = ListArray::new(
476                Arc::new(Field::new("item", DataType::Float32, true)),
477                OffsetBuffer::new(inner_offsets),
478                Arc::new(values),
479                None,
480            );
481            let outer_offsets = ScalarBuffer::<i32>::from(
482                (0..=num_rows)
483                    .map(|i| (i * inner_per_row) as i32)
484                    .collect::<Vec<_>>(),
485            );
486            Arc::new(ListArray::new(
487                Arc::new(Field::new(
488                    "item",
489                    DataType::List(Arc::new(Field::new("item", DataType::Float32, true))),
490                    true,
491                )),
492                OffsetBuffer::new(outer_offsets),
493                Arc::new(inner_list),
494                None,
495            ))
496        };
497
498        let mut start = 0;
499        let chunks: Vec<Arc<dyn Array>> = chunk_rows
500            .iter()
501            .map(|&n| {
502                let c = make_chunk(start, n);
503                start += n;
504                c
505            })
506            .collect();
507
508        let mut field_metadata = HashMap::new();
509        field_metadata.insert(
510            STRUCTURAL_ENCODING_META_KEY.to_string(),
511            structural_encoding.into(),
512        );
513
514        let test_cases = TestCases::default().with_structural_encodings();
515        check_round_trip_encoding_of_data(chunks, &test_cases, field_metadata).await;
516    }
517
518    #[test_log::test(tokio::test)]
519    async fn test_list_struct_list() {
520        let struct_type = DataType::Struct(Fields::from(vec![Field::new(
521            "inner_str",
522            DataType::Utf8,
523            false,
524        )]));
525
526        let field = Field::new("", make_list_type(struct_type), true);
527        check_basic_random(field).await;
528    }
529
530    #[test_log::test(tokio::test)]
531    async fn test_list_struct_empty() {
532        let fields = Fields::from(vec![Field::new("inner", DataType::UInt64, true)]);
533        let items = UInt64Array::from(Vec::<u64>::new());
534        let structs = StructArray::new(fields, vec![Arc::new(items)], None);
535        let offsets = OffsetBuffer::new(ScalarBuffer::<i32>::from(vec![0; 2 * 1024 * 1024 + 1]));
536        let lists = ListArray::new(
537            Arc::new(Field::new("item", structs.data_type().clone(), true)),
538            offsets,
539            Arc::new(structs),
540            None,
541        );
542
543        check_round_trip_encoding_of_data(
544            vec![Arc::new(lists)],
545            &TestCases::default(),
546            HashMap::new(),
547        )
548        .await;
549    }
550
551    #[rstest]
552    #[test_log::test(tokio::test)]
553    async fn test_simple_list(
554        #[values(STRUCTURAL_ENCODING_MINIBLOCK, STRUCTURAL_ENCODING_FULLZIP)]
555        structural_encoding: &str,
556    ) {
557        let items_builder = Int32Builder::new();
558        let mut list_builder = ListBuilder::new(items_builder);
559        list_builder.append_value([Some(1), Some(2), Some(3)]);
560        list_builder.append_value([Some(4), Some(5)]);
561        list_builder.append_null();
562        list_builder.append_value([Some(6), Some(7), Some(8)]);
563        let list_array = list_builder.finish();
564
565        let mut field_metadata = HashMap::new();
566        field_metadata.insert(
567            STRUCTURAL_ENCODING_META_KEY.to_string(),
568            structural_encoding.into(),
569        );
570
571        let test_cases = TestCases::default()
572            .with_range(0..2)
573            .with_range(0..3)
574            .with_range(1..3)
575            .with_indices(vec![1, 3])
576            .with_indices(vec![2]);
577        check_round_trip_encoding_of_data(vec![Arc::new(list_array)], &test_cases, field_metadata)
578            .await;
579    }
580
581    #[rstest]
582    #[test_log::test(tokio::test)]
583    async fn test_simple_nested_list_ends_with_null(
584        #[values(STRUCTURAL_ENCODING_MINIBLOCK, STRUCTURAL_ENCODING_FULLZIP)]
585        structural_encoding: &str,
586    ) {
587        use arrow_array::Int32Array;
588
589        let values = Int32Array::from(vec![1, 2, 3, 4, 5]);
590        let inner_offsets = ScalarBuffer::<i32>::from(vec![0, 1, 2, 3, 4, 5, 5]);
591        let inner_validity = BooleanBuffer::from(vec![true, true, true, true, true, false]);
592        let outer_offsets = ScalarBuffer::<i32>::from(vec![0, 1, 2, 3, 4, 5, 6, 6]);
593        let outer_validity = BooleanBuffer::from(vec![true, true, true, true, true, true, false]);
594
595        let inner_list = ListArray::new(
596            Arc::new(Field::new("item", DataType::Int32, true)),
597            OffsetBuffer::new(inner_offsets),
598            Arc::new(values),
599            Some(NullBuffer::new(inner_validity)),
600        );
601        let outer_list = ListArray::new(
602            Arc::new(Field::new(
603                "item",
604                DataType::List(Arc::new(Field::new("item", DataType::Int32, true))),
605                true,
606            )),
607            OffsetBuffer::new(outer_offsets),
608            Arc::new(inner_list),
609            Some(NullBuffer::new(outer_validity)),
610        );
611
612        let mut field_metadata = HashMap::new();
613        field_metadata.insert(
614            STRUCTURAL_ENCODING_META_KEY.to_string(),
615            structural_encoding.into(),
616        );
617
618        let test_cases = TestCases::default()
619            .with_range(0..2)
620            .with_range(0..3)
621            .with_range(5..7)
622            .with_indices(vec![1, 6])
623            .with_indices(vec![6])
624            .with_structural_encodings();
625        check_round_trip_encoding_of_data(vec![Arc::new(outer_list)], &test_cases, field_metadata)
626            .await;
627    }
628
629    #[rstest]
630    #[test_log::test(tokio::test)]
631    async fn test_simple_string_list(
632        #[values(STRUCTURAL_ENCODING_MINIBLOCK, STRUCTURAL_ENCODING_FULLZIP)]
633        structural_encoding: &str,
634    ) {
635        let items_builder = StringBuilder::new();
636        let mut list_builder = ListBuilder::new(items_builder);
637        list_builder.append_value([Some("a"), Some("bc"), Some("def")]);
638        list_builder.append_value([Some("gh"), None]);
639        list_builder.append_null();
640        list_builder.append_value([Some("ijk"), Some("lmnop"), Some("qrs")]);
641        let list_array = list_builder.finish();
642
643        let mut field_metadata = HashMap::new();
644        field_metadata.insert(
645            STRUCTURAL_ENCODING_META_KEY.to_string(),
646            structural_encoding.into(),
647        );
648
649        let test_cases = TestCases::default()
650            .with_range(0..2)
651            .with_range(0..3)
652            .with_range(1..3)
653            .with_indices(vec![1, 3])
654            .with_indices(vec![2])
655            .with_structural_encodings();
656        check_round_trip_encoding_of_data(vec![Arc::new(list_array)], &test_cases, field_metadata)
657            .await;
658    }
659
660    #[rstest]
661    #[test_log::test(tokio::test)]
662    async fn test_simple_string_list_no_null(
663        #[values(STRUCTURAL_ENCODING_MINIBLOCK, STRUCTURAL_ENCODING_FULLZIP)]
664        structural_encoding: &str,
665    ) {
666        let items_builder = StringBuilder::new();
667        let mut list_builder = ListBuilder::new(items_builder);
668        list_builder.append_value([Some("a"), Some("bc"), Some("def")]);
669        list_builder.append_value([Some("gh"), Some("zxy")]);
670        list_builder.append_value([Some("gh"), Some("z")]);
671        list_builder.append_value([Some("ijk"), Some("lmnop"), Some("qrs")]);
672        let list_array = list_builder.finish();
673
674        let mut field_metadata = HashMap::new();
675        field_metadata.insert(
676            STRUCTURAL_ENCODING_META_KEY.to_string(),
677            structural_encoding.into(),
678        );
679
680        let test_cases = TestCases::default()
681            .with_range(0..2)
682            .with_range(0..3)
683            .with_range(1..3)
684            .with_indices(vec![1, 3])
685            .with_indices(vec![2])
686            .with_structural_encodings();
687        check_round_trip_encoding_of_data(vec![Arc::new(list_array)], &test_cases, field_metadata)
688            .await;
689    }
690
691    #[rstest]
692    #[test_log::test(tokio::test)]
693    async fn test_simple_sliced_list(
694        #[values(STRUCTURAL_ENCODING_MINIBLOCK, STRUCTURAL_ENCODING_FULLZIP)]
695        structural_encoding: &str,
696    ) {
697        let items_builder = Int32Builder::new();
698        let mut list_builder = ListBuilder::new(items_builder);
699        list_builder.append_value([Some(1), Some(2), Some(3)]);
700        list_builder.append_value([Some(4), Some(5)]);
701        list_builder.append_null();
702        list_builder.append_value([Some(6), Some(7), Some(8)]);
703        let list_array = list_builder.finish();
704
705        let list_array = list_array.slice(1, 2);
706
707        let mut field_metadata = HashMap::new();
708        field_metadata.insert(
709            STRUCTURAL_ENCODING_META_KEY.to_string(),
710            structural_encoding.into(),
711        );
712
713        let test_cases = TestCases::default()
714            .with_range(0..2)
715            .with_range(1..2)
716            .with_indices(vec![0])
717            .with_indices(vec![1])
718            .with_structural_encodings();
719        check_round_trip_encoding_of_data(vec![Arc::new(list_array)], &test_cases, field_metadata)
720            .await;
721    }
722
723    #[test_log::test(tokio::test)]
724    async fn test_simple_list_dict() {
725        let values = LargeStringArray::from_iter_values(["a", "bb", "ccc"]);
726        let indices = UInt8Array::from(vec![0, 1, 2, 0, 1, 2, 0, 1, 2]);
727        let dict_array = DictionaryArray::new(indices, Arc::new(values));
728        let offsets = OffsetBuffer::new(ScalarBuffer::<i32>::from(vec![0, 3, 5, 6, 9]));
729        let list_array = ListArray::new(
730            Arc::new(Field::new("item", dict_array.data_type().clone(), true)),
731            offsets,
732            Arc::new(dict_array),
733            None,
734        );
735
736        let test_cases = TestCases::default()
737            .with_range(0..2)
738            .with_range(1..3)
739            .with_range(2..4)
740            .with_indices(vec![1])
741            .with_indices(vec![2]);
742        check_round_trip_encoding_of_data(
743            vec![Arc::new(list_array)],
744            &test_cases,
745            HashMap::default(),
746        )
747        .await;
748    }
749
750    #[test_log::test(tokio::test)]
751    async fn test_simple_list_all_null() {
752        let items = UInt64Array::from(vec![0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]);
753        let offsets = ScalarBuffer::<i32>::from(vec![0, 5, 8, 10]);
754        let offsets = OffsetBuffer::new(offsets);
755        let list_validity = NullBuffer::new(BooleanBuffer::from(vec![false, false, false]));
756
757        // The list array is nullable but the items are not.  Then, all lists are null.
758        let list_arr = ListArray::new(
759            Arc::new(Field::new("item", DataType::UInt64, false)),
760            offsets,
761            Arc::new(items),
762            Some(list_validity),
763        );
764
765        let test_cases = TestCases::default()
766            .with_range(0..3)
767            .with_range(1..2)
768            .with_indices(vec![1])
769            .with_indices(vec![2])
770            .with_structural_encodings();
771        check_round_trip_encoding_of_data(
772            vec![Arc::new(list_arr)],
773            &test_cases,
774            HashMap::default(),
775        )
776        .await;
777    }
778
779    #[rstest]
780    #[test_log::test(tokio::test)]
781    async fn test_list_with_garbage_nulls(
782        #[values(STRUCTURAL_ENCODING_MINIBLOCK, STRUCTURAL_ENCODING_FULLZIP)]
783        structural_encoding: &str,
784    ) {
785        // In Arrow, list nulls are allowed to be non-empty, with masked garbage values
786        // Here we make a list with a null row in the middle with 3 garbage values
787        let items = UInt64Array::from(vec![0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]);
788        let offsets = ScalarBuffer::<i32>::from(vec![0, 5, 8, 10]);
789        let offsets = OffsetBuffer::new(offsets);
790        let list_validity = NullBuffer::new(BooleanBuffer::from(vec![true, false, true]));
791        let list_arr = ListArray::new(
792            Arc::new(Field::new("item", DataType::UInt64, true)),
793            offsets,
794            Arc::new(items),
795            Some(list_validity),
796        );
797
798        let mut field_metadata = HashMap::new();
799        field_metadata.insert(
800            STRUCTURAL_ENCODING_META_KEY.to_string(),
801            structural_encoding.into(),
802        );
803
804        let test_cases = TestCases::default()
805            .with_range(0..3)
806            .with_range(1..2)
807            .with_indices(vec![1])
808            .with_indices(vec![2])
809            .with_structural_encodings();
810        check_round_trip_encoding_of_data(vec![Arc::new(list_arr)], &test_cases, field_metadata)
811            .await;
812    }
813
814    #[rstest]
815    #[test_log::test(tokio::test)]
816    async fn test_simple_two_page_list(
817        #[values(STRUCTURAL_ENCODING_MINIBLOCK, STRUCTURAL_ENCODING_FULLZIP)]
818        structural_encoding: &str,
819    ) {
820        // This is a simple pre-defined list that spans two pages.  This test is useful for
821        // debugging the repetition index
822
823        let items_builder = Int64Builder::new();
824        let mut list_builder = ListBuilder::new(items_builder);
825        for i in 0..512 {
826            list_builder.append_value([Some(i), Some(i * 2)]);
827        }
828        let list_array_1 = list_builder.finish();
829
830        let items_builder = Int64Builder::new();
831        let mut list_builder = ListBuilder::new(items_builder);
832        for i in 0..512 {
833            let i = i + 512;
834            list_builder.append_value([Some(i), Some(i * 2)]);
835        }
836        let list_array_2 = list_builder.finish();
837
838        let mut metadata = HashMap::new();
839        metadata.insert(
840            STRUCTURAL_ENCODING_META_KEY.to_string(),
841            structural_encoding.into(),
842        );
843
844        let test_cases = TestCases::default()
845            .with_structural_encodings()
846            .with_page_sizes(vec![100])
847            .with_range(800..900);
848        check_round_trip_encoding_of_data(
849            vec![Arc::new(list_array_1), Arc::new(list_array_2)],
850            &test_cases,
851            metadata,
852        )
853        .await;
854    }
855
856    #[test_log::test(tokio::test)]
857    async fn test_simple_large_list() {
858        let items_builder = Int32Builder::new();
859        let mut list_builder = LargeListBuilder::new(items_builder);
860        list_builder.append_value([Some(1), Some(2), Some(3)]);
861        list_builder.append_value([Some(4), Some(5)]);
862        list_builder.append_null();
863        list_builder.append_value([Some(6), Some(7), Some(8)]);
864        let list_array = list_builder.finish();
865
866        let test_cases = TestCases::default()
867            .with_range(0..2)
868            .with_range(0..3)
869            .with_range(1..3)
870            .with_indices(vec![1, 3]);
871        check_round_trip_encoding_of_data(vec![Arc::new(list_array)], &test_cases, HashMap::new())
872            .await;
873    }
874
875    #[rstest]
876    #[test_log::test(tokio::test)]
877    async fn test_empty_lists(
878        #[values(STRUCTURAL_ENCODING_MINIBLOCK, STRUCTURAL_ENCODING_FULLZIP)]
879        structural_encoding: &str,
880    ) {
881        let mut field_metadata = HashMap::new();
882        field_metadata.insert(
883            STRUCTURAL_ENCODING_META_KEY.to_string(),
884            structural_encoding.into(),
885        );
886
887        // Scenario 1: Some lists are empty
888
889        let values = [vec![Some(1), Some(2), Some(3)], vec![], vec![None]];
890        // Test empty list at beginning, middle, and end
891        for order in [[0, 1, 2], [1, 0, 2], [2, 0, 1]] {
892            let items_builder = Int32Builder::new();
893            let mut list_builder = ListBuilder::new(items_builder);
894            for idx in order {
895                list_builder.append_value(values[idx].clone());
896            }
897            let list_array = Arc::new(list_builder.finish());
898            let test_cases = TestCases::default()
899                .with_indices(vec![1])
900                .with_indices(vec![0])
901                .with_indices(vec![2])
902                .with_indices(vec![0, 1]);
903            check_round_trip_encoding_of_data(
904                vec![list_array.clone()],
905                &test_cases,
906                field_metadata.clone(),
907            )
908            .await;
909            let test_cases = test_cases.with_batch_size(1);
910            check_round_trip_encoding_of_data(
911                vec![list_array],
912                &test_cases,
913                field_metadata.clone(),
914            )
915            .await;
916        }
917
918        // Scenario 2: All lists are empty
919
920        // When encoding a list of empty lists there are no items to encode
921        // which is strange and we want to ensure we handle it
922        let items_builder = Int32Builder::new();
923        let mut list_builder = ListBuilder::new(items_builder);
924        list_builder.append(true);
925        list_builder.append_null();
926        list_builder.append(true);
927        let list_array = Arc::new(list_builder.finish());
928
929        let test_cases = TestCases::default().with_range(0..2).with_indices(vec![1]);
930        check_round_trip_encoding_of_data(
931            vec![list_array.clone()],
932            &test_cases,
933            field_metadata.clone(),
934        )
935        .await;
936        let test_cases = test_cases.with_batch_size(1);
937        check_round_trip_encoding_of_data(vec![list_array], &test_cases, field_metadata.clone())
938            .await;
939
940        // Scenario 2B: All lists are empty (but now with strings)
941
942        // When encoding a list of empty lists there are no items to encode
943        // which is strange and we want to ensure we handle it
944        let items_builder = StringBuilder::new();
945        let mut list_builder = ListBuilder::new(items_builder);
946        list_builder.append(true);
947        list_builder.append_null();
948        list_builder.append(true);
949        let list_array = Arc::new(list_builder.finish());
950
951        let test_cases = TestCases::default().with_range(0..2).with_indices(vec![1]);
952        check_round_trip_encoding_of_data(
953            vec![list_array.clone()],
954            &test_cases,
955            field_metadata.clone(),
956        )
957        .await;
958        let test_cases = test_cases.with_batch_size(1);
959        check_round_trip_encoding_of_data(vec![list_array], &test_cases, field_metadata.clone())
960            .await;
961
962        // Scenario 3: All lists are null
963
964        let items_builder = Int32Builder::new();
965        let mut list_builder = ListBuilder::new(items_builder);
966        list_builder.append_null();
967        list_builder.append_null();
968        list_builder.append_null();
969        let list_array = Arc::new(list_builder.finish());
970
971        let test_cases = TestCases::default().with_range(0..2).with_indices(vec![1]);
972        check_round_trip_encoding_of_data(
973            vec![list_array.clone()],
974            &test_cases,
975            field_metadata.clone(),
976        )
977        .await;
978        let test_cases = test_cases.with_batch_size(1);
979        check_round_trip_encoding_of_data(vec![list_array], &test_cases, field_metadata.clone())
980            .await;
981
982        // Scenario 4: All lists are null and inside a struct (only valid for 2.1 since 2.0 doesn't
983        // support null structs)
984        let items_builder = Int32Builder::new();
985        let mut list_builder = ListBuilder::new(items_builder);
986        list_builder.append_null();
987        list_builder.append_null();
988        list_builder.append_null();
989        let list_array = Arc::new(list_builder.finish());
990
991        let struct_validity = NullBuffer::new(BooleanBuffer::from(vec![true, false, true]));
992        let struct_array = Arc::new(StructArray::new(
993            Fields::from(vec![Field::new(
994                "lists",
995                list_array.data_type().clone(),
996                true,
997            )]),
998            vec![list_array],
999            Some(struct_validity),
1000        ));
1001
1002        let test_cases = TestCases::default()
1003            .with_range(0..2)
1004            .with_indices(vec![1])
1005            .with_structural_encodings();
1006        check_round_trip_encoding_of_data(
1007            vec![struct_array.clone()],
1008            &test_cases,
1009            field_metadata.clone(),
1010        )
1011        .await;
1012        let test_cases = test_cases.with_batch_size(1);
1013        check_round_trip_encoding_of_data(vec![struct_array], &test_cases, field_metadata.clone())
1014            .await;
1015    }
1016
1017    #[test_log::test(tokio::test)]
1018    async fn test_empty_list_list() {
1019        let items_builder = Int32Builder::new();
1020        let list_builder = ListBuilder::new(items_builder);
1021        let mut outer_list_builder = ListBuilder::new(list_builder);
1022        outer_list_builder.append_null();
1023        outer_list_builder.append_null();
1024        outer_list_builder.append_null();
1025        let list_array = Arc::new(outer_list_builder.finish());
1026
1027        let test_cases = TestCases::default().with_structural_encodings();
1028        check_round_trip_encoding_of_data(vec![list_array], &test_cases, HashMap::new()).await;
1029    }
1030
1031    #[test_log::test(tokio::test)]
1032    #[ignore] // This test is quite slow in debug mode
1033    async fn test_jumbo_list() {
1034        // This is an overflow test.  We have a list of lists where each list
1035        // has 1Mi items.  We encode 5000 of these lists and so we have over 4Gi in the
1036        // offsets range
1037        let items = BooleanArray::new_null(1024 * 1024);
1038        let offsets = OffsetBuffer::new(ScalarBuffer::from(vec![0, 1024 * 1024]));
1039        let list_arr = Arc::new(ListArray::new(
1040            Arc::new(Field::new("item", DataType::Boolean, true)),
1041            offsets,
1042            Arc::new(items),
1043            None,
1044        )) as ArrayRef;
1045        let arrs = vec![list_arr; 5000];
1046
1047        // We can't validate because our validation relies on concatenating all input arrays
1048        let test_cases = TestCases::default().without_validation();
1049        check_round_trip_encoding_of_data(arrs, &test_cases, HashMap::new()).await;
1050    }
1051
1052    // Regression test for issue with ListArray encoding when crossing 1024 value boundary
1053    // This test reproduces the bug where rows_avail assertion fails in schedule_instructions
1054    // when encoding a ListArray with specific size patterns that cross the 1024 value boundary
1055    #[tokio::test]
1056    async fn test_fuzz_issue_4466() {
1057        // This specific pattern of list sizes triggers the bug when total values cross 1024
1058        // 94 lists total 1009 values (passes), 95 lists total 1025 values (fails)
1059        let list_sizes = vec![
1060            13, 18, 12, 7, 14, 12, 6, 13, 18, 8, // 0-9: 119 values
1061            6, 11, 17, 12, 8, 19, 5, 6, 10, 13, // 10-19: 107 values
1062            8, 6, 10, 4, 8, 16, 14, 12, 18, 9, // 20-29: 105 values
1063            17, 8, 14, 18, 15, 3, 2, 4, 5, 1, // 30-39: 82 values
1064            3, 13, 1, 2, 10, 4, 10, 18, 7, 14, // 40-49: 75 values
1065            18, 13, 9, 17, 3, 13, 10, 14, 8, 19, // 50-59: 125 values
1066            17, 10, 5, 11, 6, 15, 10, 18, 18, 20, // 60-69: 130 values
1067            16, 11, 12, 15, 7, 9, 3, 10, 20, 5, // 70-79: 102 values
1068            2, 3, 17, 4, 8, 12, 15, 6, 3, 20, // 80-89: 90 values
1069            15, 20, 1, 19, 16, // 90-94: 71 values
1070        ];
1071
1072        // Build the ListArray
1073        let mut list_builder = ListBuilder::new(Int32Builder::new());
1074        let mut total_values = 0;
1075
1076        for size in &list_sizes {
1077            for i in 0..*size {
1078                list_builder.values().append_value(i);
1079            }
1080            list_builder.append(true);
1081            total_values += size;
1082        }
1083
1084        let list_array = Arc::new(list_builder.finish());
1085
1086        // Verify we have the expected number of values
1087        assert_eq!(list_array.len(), 95);
1088        assert_eq!(total_values, 1025);
1089
1090        // This should trigger the assertion failure at primitive.rs:1362
1091        // debug_assert!(rows_avail > 0)
1092        let test_cases = TestCases::default().with_structural_encodings();
1093
1094        // The bug manifests when encoding this specific pattern
1095        // Expected: successful round-trip encoding
1096        // Actual: panic at primitive.rs:1362 - assertion failed: rows_avail > 0
1097        check_round_trip_encoding_of_data(vec![list_array], &test_cases, HashMap::new()).await;
1098    }
1099
1100    #[rstest]
1101    #[test_log::test(tokio::test)]
1102    async fn test_sparse_large_string_list(
1103        #[values(STRUCTURAL_ENCODING_MINIBLOCK, STRUCTURAL_ENCODING_FULLZIP)]
1104        structural_encoding: &str,
1105    ) {
1106        // 2.5 million rows, mostly empty lists. ~100 lists have 10 short strings each.
1107        let num_rows = 2_500_000u32;
1108        let num_non_empty = 100u32;
1109        let strings_per_list = 10;
1110
1111        let items_builder = StringBuilder::new();
1112        let mut list_builder = ListBuilder::new(items_builder);
1113
1114        // Spread non-empty lists evenly across the range
1115        let step = num_rows / num_non_empty;
1116        let mut next_non_empty = step / 2;
1117
1118        for i in 0..num_rows {
1119            if i == next_non_empty {
1120                let vals: Vec<Option<&str>> = (0..strings_per_list)
1121                    .map(|j| match j % 4 {
1122                        0 => Some("a"),
1123                        1 => Some("bb"),
1124                        2 => Some("ccc"),
1125                        _ => Some("d"),
1126                    })
1127                    .collect();
1128                list_builder.append_value(vals);
1129                next_non_empty = next_non_empty.saturating_add(step);
1130            } else {
1131                list_builder.append_value([] as [Option<&str>; 0]);
1132            }
1133        }
1134        let list_array = list_builder.finish();
1135
1136        let mut field_metadata = HashMap::new();
1137        field_metadata.insert(
1138            STRUCTURAL_ENCODING_META_KEY.to_string(),
1139            structural_encoding.into(),
1140        );
1141
1142        let test_cases = TestCases::default()
1143            .with_range(0..1000)
1144            .with_range(0..num_rows as u64)
1145            .with_indices(vec![0, (step / 2) as u64, num_rows as u64 - 1])
1146            .with_dense_encodings();
1147        check_round_trip_encoding_of_data(vec![Arc::new(list_array)], &test_cases, field_metadata)
1148            .await;
1149    }
1150
1151    #[test_log::test(tokio::test)]
1152    async fn test_sparse_boolean_list_uses_miniblock() {
1153        // Redacted reproduction from a production schema shape containing ARRAY(BOOLEAN).
1154        // The field names are not relevant; the failure requires sparse list structure
1155        // with a 1-bit Boolean leaf value.
1156        let num_rows = 200_000usize;
1157        let num_non_empty = 10usize;
1158        let booleans_per_list = 8usize;
1159        let step = num_rows / num_non_empty;
1160
1161        let mut offsets = Vec::with_capacity(num_rows + 1);
1162        let mut values = Vec::with_capacity(num_non_empty * booleans_per_list);
1163        offsets.push(0i32);
1164
1165        let mut next_non_empty = step / 2;
1166        for row in 0..num_rows {
1167            if row == next_non_empty {
1168                values.extend((0..booleans_per_list).map(|idx| idx % 2 == 0));
1169                next_non_empty += step;
1170            }
1171            offsets.push(values.len() as i32);
1172        }
1173
1174        let items = BooleanArray::from(values);
1175        let list_array = ListArray::new(
1176            Arc::new(Field::new("item", DataType::Boolean, true)),
1177            OffsetBuffer::new(ScalarBuffer::from(offsets)),
1178            Arc::new(items),
1179            None,
1180        );
1181
1182        let test_cases = TestCases::default()
1183            .with_range(0..1000)
1184            .with_range(0..num_rows as u64)
1185            .with_indices(vec![0, (step / 2) as u64, num_rows as u64 - 1])
1186            .with_dense_encodings();
1187        let list_array = Arc::new(list_array) as ArrayRef;
1188        let pages = encode_v22_pages(list_array.clone()).await;
1189        assert_split_miniblock_layout(&pages, false);
1190        check_round_trip_encoding_of_data(vec![list_array], &test_cases, HashMap::new()).await;
1191    }
1192
1193    #[test_log::test(tokio::test)]
1194    async fn test_sparse_boolean_list_with_long_empty_prefix() {
1195        let empty_prefix_rows = 70_000usize;
1196        let trailing_empty_rows = 9usize;
1197        let booleans_per_list = 8usize;
1198        let num_rows = empty_prefix_rows + 1 + trailing_empty_rows;
1199
1200        let mut offsets = Vec::with_capacity(num_rows + 1);
1201        offsets.extend(std::iter::repeat_n(0i32, empty_prefix_rows + 1));
1202        let values = (0..booleans_per_list)
1203            .map(|idx| idx % 2 == 0)
1204            .collect::<Vec<_>>();
1205        offsets.push(values.len() as i32);
1206        offsets.extend(std::iter::repeat_n(
1207            values.len() as i32,
1208            trailing_empty_rows,
1209        ));
1210
1211        let items = BooleanArray::from(values);
1212        let list_array = ListArray::new(
1213            Arc::new(Field::new("item", DataType::Boolean, true)),
1214            OffsetBuffer::new(ScalarBuffer::from(offsets)),
1215            Arc::new(items),
1216            None,
1217        );
1218
1219        let test_cases = TestCases::default()
1220            .with_range(0..num_rows as u64)
1221            .with_indices(vec![0, empty_prefix_rows as u64, num_rows as u64 - 1])
1222            .with_dense_encodings();
1223        let list_array = Arc::new(list_array) as ArrayRef;
1224        let pages = encode_v22_pages(list_array.clone()).await;
1225        assert_split_miniblock_layout(&pages, true);
1226        check_round_trip_encoding_of_data(vec![list_array], &test_cases, HashMap::new()).await;
1227    }
1228
1229    #[test_log::test(tokio::test)]
1230    async fn test_sparse_boolean_list_with_long_null_prefix() {
1231        let null_prefix_rows = 70_000usize;
1232        let trailing_empty_rows = 9usize;
1233        let booleans_per_list = 8usize;
1234        let num_rows = null_prefix_rows + 1 + trailing_empty_rows;
1235
1236        let mut offsets = Vec::with_capacity(num_rows + 1);
1237        offsets.extend(std::iter::repeat_n(0i32, null_prefix_rows + 1));
1238        let values = (0..booleans_per_list)
1239            .map(|idx| idx % 2 == 0)
1240            .collect::<Vec<_>>();
1241        offsets.push(values.len() as i32);
1242        offsets.extend(std::iter::repeat_n(
1243            values.len() as i32,
1244            trailing_empty_rows,
1245        ));
1246        let validity = BooleanBuffer::from_iter((0..num_rows).map(|row| row >= null_prefix_rows));
1247
1248        let items = BooleanArray::from(values);
1249        let list_array = ListArray::new(
1250            Arc::new(Field::new("item", DataType::Boolean, true)),
1251            OffsetBuffer::new(ScalarBuffer::from(offsets)),
1252            Arc::new(items),
1253            Some(NullBuffer::new(validity)),
1254        );
1255
1256        let test_cases = TestCases::default()
1257            .with_range(0..num_rows as u64)
1258            .with_indices(vec![0, null_prefix_rows as u64, num_rows as u64 - 1])
1259            .with_dense_encodings();
1260        let list_array = Arc::new(list_array) as ArrayRef;
1261        let pages = encode_v22_pages(list_array.clone()).await;
1262        assert_split_miniblock_layout(&pages, true);
1263        check_round_trip_encoding_of_data(vec![list_array], &test_cases, HashMap::new()).await;
1264    }
1265
1266    #[test_log::test(tokio::test)]
1267    async fn test_sparse_boolean_list_with_amortized_long_empty_prefix() {
1268        let empty_prefix_rows = 62_000usize;
1269        let booleans_per_list = 8_192usize;
1270        let num_rows = empty_prefix_rows + 1;
1271
1272        let mut offsets = Vec::with_capacity(num_rows + 1);
1273        offsets.extend(std::iter::repeat_n(0i32, empty_prefix_rows + 1));
1274        let values = (0..booleans_per_list)
1275            .map(|idx| idx % 2 == 0)
1276            .collect::<Vec<_>>();
1277        offsets.push(values.len() as i32);
1278
1279        let items = BooleanArray::from(values);
1280        let list_array = ListArray::new(
1281            Arc::new(Field::new("item", DataType::Boolean, true)),
1282            OffsetBuffer::new(ScalarBuffer::from(offsets)),
1283            Arc::new(items),
1284            None,
1285        );
1286
1287        let test_cases = TestCases::default()
1288            .with_range(0..num_rows as u64)
1289            .with_indices(vec![0, empty_prefix_rows as u64])
1290            .with_dense_encodings();
1291        let list_array = Arc::new(list_array) as ArrayRef;
1292        let pages = encode_v22_pages(list_array.clone()).await;
1293        assert_split_miniblock_layout(&pages, true);
1294        check_round_trip_encoding_of_data(vec![list_array], &test_cases, HashMap::new()).await;
1295    }
1296
1297    #[test_log::test(tokio::test)]
1298    async fn test_nested_sparse_boolean_list_fails_without_panic() {
1299        let empty_inner_lists = 70_000usize;
1300        let booleans_per_list = 8usize;
1301
1302        let mut inner_offsets = vec![0i32; empty_inner_lists + 1];
1303        let values = (0..booleans_per_list)
1304            .map(|idx| idx % 2 == 0)
1305            .collect::<Vec<_>>();
1306        inner_offsets.push(values.len() as i32);
1307
1308        let inner_items = BooleanArray::from(values);
1309        let inner_list = ListArray::new(
1310            Arc::new(Field::new("item", DataType::Boolean, true)),
1311            OffsetBuffer::new(ScalarBuffer::from(inner_offsets)),
1312            Arc::new(inner_items),
1313            None,
1314        );
1315        let outer_list = ListArray::new(
1316            Arc::new(Field::new("item", inner_list.data_type().clone(), true)),
1317            OffsetBuffer::new(ScalarBuffer::from(vec![0i32, empty_inner_lists as i32 + 1])),
1318            Arc::new(inner_list),
1319            None,
1320        );
1321
1322        let err = try_encode_v22_pages(Arc::new(outer_list))
1323            .await
1324            .unwrap_err();
1325        assert!(
1326            err.to_string().contains("Mini-block cannot encode"),
1327            "unexpected error: {err}"
1328        );
1329    }
1330
1331    #[test_log::test(tokio::test)]
1332    async fn test_nested_sparse_string_single_row_falls_back_to_fullzip() {
1333        let empty_inner_lists = 70_000usize;
1334
1335        let mut inner_offsets = vec![0i32; empty_inner_lists + 1];
1336        inner_offsets.push(1);
1337        inner_offsets.push(2);
1338
1339        let mut strings = StringBuilder::new();
1340        strings.append_value("value");
1341        strings.append_value("other");
1342        let inner_items = strings.finish();
1343        let inner_list = ListArray::new(
1344            Arc::new(Field::new("item", DataType::Utf8, true)),
1345            OffsetBuffer::new(ScalarBuffer::from(inner_offsets)),
1346            Arc::new(inner_items),
1347            None,
1348        );
1349        let outer_list = ListArray::new(
1350            Arc::new(Field::new("item", inner_list.data_type().clone(), true)),
1351            OffsetBuffer::new(ScalarBuffer::from(vec![0i32, empty_inner_lists as i32 + 2])),
1352            Arc::new(inner_list),
1353            None,
1354        );
1355
1356        let outer_list = Arc::new(outer_list) as ArrayRef;
1357        let pages = encode_v22_pages(outer_list.clone()).await;
1358        assert_has_fullzip_layout(&pages);
1359
1360        let test_cases = TestCases::default()
1361            .with_range(0..1)
1362            .with_indices(vec![0])
1363            .with_dense_encodings();
1364        check_round_trip_encoding_of_data(vec![outer_list], &test_cases, HashMap::new()).await;
1365    }
1366
1367    /// Builds the HNSW-flush repro shape: a dense prefix where every row has
1368    /// `NEIGHBORS_PER_ROW` distinct values, followed by a long tail of empty
1369    /// lists. Mirrors `HNSW::schema()` `__neighbors` / `__dists` columns:
1370    /// dense level-0 lists, then ~6x as many mostly-empty higher-level rows.
1371    fn make_hnsw_shaped_list_u32() -> ListArray {
1372        const DENSE_ROWS: u32 = 40_000;
1373        const NEIGHBORS_PER_ROW: u32 = 32;
1374        const EMPTY_TAIL_ROWS: u32 = 240_000;
1375
1376        let mut list_builder = ListBuilder::new(UInt32Builder::new());
1377        let mut next_val: u32 = 0;
1378        for _ in 0..DENSE_ROWS {
1379            for _ in 0..NEIGHBORS_PER_ROW {
1380                list_builder.values().append_value(next_val);
1381                next_val = next_val.wrapping_add(1);
1382            }
1383            list_builder.append(true);
1384        }
1385        for _ in 0..EMPTY_TAIL_ROWS {
1386            list_builder.append(true);
1387        }
1388        list_builder.finish()
1389    }
1390
1391    /// Reproduces the HNSW-flush shape at v2.2 on the auto path (no
1392    /// `STRUCTURAL_ENCODING` metadata): a dense level-0 prefix followed by a
1393    /// long tail of empty lists. The global levels/values ratio looks dense,
1394    /// so this used to encode as a single mini-block page whose final chunk
1395    /// absorbed every trailing empty list and overflowed the per-chunk `u16`
1396    /// `num_levels`, corrupting the read. The structural page planner now
1397    /// splits on top-level row boundaries: the dense prefix stays on
1398    /// mini-block pages and the empty tail becomes structural-only pages, so
1399    /// the round-trip is lossless without falling back to full-zip.
1400    #[test_log::test(tokio::test)]
1401    async fn test_list_hnsw_shape_splits_to_miniblock_v2_2() {
1402        let list_array = make_hnsw_shaped_list_u32();
1403        let dense_rows: u64 = 40_000;
1404        let total_rows = list_array.len() as u64;
1405
1406        let test_cases = TestCases::default()
1407            .with_range(0..1000)
1408            .with_range(dense_rows.saturating_sub(8)..(dense_rows + 8))
1409            .with_range(0..total_rows)
1410            .with_indices(vec![0, dense_rows - 1, dense_rows, total_rows - 1])
1411            .with_encoding(TestEncoding::StructuralU32);
1412        let list_array = Arc::new(list_array) as ArrayRef;
1413        let pages = encode_v22_pages(list_array.clone()).await;
1414        assert_split_miniblock_layout(&pages, true);
1415        check_round_trip_encoding_of_data(vec![list_array], &test_cases, HashMap::new()).await;
1416    }
1417
1418    /// Companion to the auto-path test: even when the user explicitly requests
1419    /// `STRUCTURAL_ENCODING_MINIBLOCK`, the structural page planner splits the
1420    /// HNSW shape so every emitted page fits the mini-block per-chunk budget.
1421    /// The request is honored (the dense prefix stays on mini-block pages
1422    /// rather than being forced to full-zip) and the round-trip is lossless.
1423    #[test_log::test(tokio::test)]
1424    async fn test_forced_miniblock_hnsw_shape_splits_to_miniblock_v2_2() {
1425        let list_array = make_hnsw_shaped_list_u32();
1426        let total_rows = list_array.len() as u64;
1427
1428        let mut field_metadata = HashMap::new();
1429        field_metadata.insert(
1430            STRUCTURAL_ENCODING_META_KEY.to_string(),
1431            STRUCTURAL_ENCODING_MINIBLOCK.into(),
1432        );
1433
1434        let test_cases = TestCases::default()
1435            .with_range(0..total_rows)
1436            .with_encoding(TestEncoding::StructuralU32);
1437        let list_array = Arc::new(list_array) as ArrayRef;
1438        let pages = try_encode_v22_pages_with_metadata(list_array.clone(), field_metadata.clone())
1439            .await
1440            .unwrap();
1441        assert_split_miniblock_layout(&pages, true);
1442        check_round_trip_encoding_of_data(vec![list_array], &test_cases, field_metadata).await;
1443    }
1444}