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        // Three chunks' worth of rep/def levels (1 rep bit + 1 def bit each), so the
1107        // planner must split the page. See #6184.
1108        let levels_per_chunk =
1109            crate::encodings::logical::primitive::miniblock::max_repdef_levels_per_chunk(2);
1110        let num_rows = (levels_per_chunk * 3) as u32;
1111        let num_non_empty = 100u32;
1112        let strings_per_list = 10;
1113
1114        let items_builder = StringBuilder::new();
1115        let mut list_builder = ListBuilder::new(items_builder);
1116
1117        // Spread non-empty lists evenly across the range
1118        let step = num_rows / num_non_empty;
1119        let mut next_non_empty = step / 2;
1120
1121        for i in 0..num_rows {
1122            if i == next_non_empty {
1123                let vals: Vec<Option<&str>> = (0..strings_per_list)
1124                    .map(|j| match j % 4 {
1125                        0 => Some("a"),
1126                        1 => Some("bb"),
1127                        2 => Some("ccc"),
1128                        _ => Some("d"),
1129                    })
1130                    .collect();
1131                list_builder.append_value(vals);
1132                next_non_empty = next_non_empty.saturating_add(step);
1133            } else {
1134                list_builder.append_value([] as [Option<&str>; 0]);
1135            }
1136        }
1137        let list_array = list_builder.finish();
1138
1139        let mut field_metadata = HashMap::new();
1140        field_metadata.insert(
1141            STRUCTURAL_ENCODING_META_KEY.to_string(),
1142            structural_encoding.into(),
1143        );
1144
1145        let test_cases = TestCases::default()
1146            .with_range(0..1000)
1147            .with_range(0..num_rows as u64)
1148            .with_indices(vec![0, (step / 2) as u64, num_rows as u64 - 1])
1149            .with_dense_encodings();
1150        check_round_trip_encoding_of_data(vec![Arc::new(list_array)], &test_cases, field_metadata)
1151            .await;
1152    }
1153
1154    #[test_log::test(tokio::test)]
1155    async fn test_sparse_boolean_list_uses_miniblock() {
1156        // Redacted reproduction from a production schema shape containing ARRAY(BOOLEAN).
1157        // The field names are not relevant; the failure requires sparse list structure
1158        // with a 1-bit Boolean leaf value.
1159        let num_rows = 200_000usize;
1160        let num_non_empty = 10usize;
1161        let booleans_per_list = 8usize;
1162        let step = num_rows / num_non_empty;
1163
1164        let mut offsets = Vec::with_capacity(num_rows + 1);
1165        let mut values = Vec::with_capacity(num_non_empty * booleans_per_list);
1166        offsets.push(0i32);
1167
1168        let mut next_non_empty = step / 2;
1169        for row in 0..num_rows {
1170            if row == next_non_empty {
1171                values.extend((0..booleans_per_list).map(|idx| idx % 2 == 0));
1172                next_non_empty += step;
1173            }
1174            offsets.push(values.len() as i32);
1175        }
1176
1177        let items = BooleanArray::from(values);
1178        let list_array = ListArray::new(
1179            Arc::new(Field::new("item", DataType::Boolean, true)),
1180            OffsetBuffer::new(ScalarBuffer::from(offsets)),
1181            Arc::new(items),
1182            None,
1183        );
1184
1185        let test_cases = TestCases::default()
1186            .with_range(0..1000)
1187            .with_range(0..num_rows as u64)
1188            .with_indices(vec![0, (step / 2) as u64, num_rows as u64 - 1])
1189            .with_dense_encodings();
1190        let list_array = Arc::new(list_array) as ArrayRef;
1191        let pages = encode_v22_pages(list_array.clone()).await;
1192        assert_split_miniblock_layout(&pages, false);
1193        check_round_trip_encoding_of_data(vec![list_array], &test_cases, HashMap::new()).await;
1194    }
1195
1196    #[test_log::test(tokio::test)]
1197    async fn test_sparse_boolean_list_with_long_empty_prefix() {
1198        let empty_prefix_rows = 70_000usize;
1199        let trailing_empty_rows = 9usize;
1200        let booleans_per_list = 8usize;
1201        let num_rows = empty_prefix_rows + 1 + trailing_empty_rows;
1202
1203        let mut offsets = Vec::with_capacity(num_rows + 1);
1204        offsets.extend(std::iter::repeat_n(0i32, empty_prefix_rows + 1));
1205        let values = (0..booleans_per_list)
1206            .map(|idx| idx % 2 == 0)
1207            .collect::<Vec<_>>();
1208        offsets.push(values.len() as i32);
1209        offsets.extend(std::iter::repeat_n(
1210            values.len() as i32,
1211            trailing_empty_rows,
1212        ));
1213
1214        let items = BooleanArray::from(values);
1215        let list_array = ListArray::new(
1216            Arc::new(Field::new("item", DataType::Boolean, true)),
1217            OffsetBuffer::new(ScalarBuffer::from(offsets)),
1218            Arc::new(items),
1219            None,
1220        );
1221
1222        let test_cases = TestCases::default()
1223            .with_range(0..num_rows as u64)
1224            .with_indices(vec![0, empty_prefix_rows as u64, num_rows as u64 - 1])
1225            .with_dense_encodings();
1226        let list_array = Arc::new(list_array) as ArrayRef;
1227        let pages = encode_v22_pages(list_array.clone()).await;
1228        assert_split_miniblock_layout(&pages, true);
1229        check_round_trip_encoding_of_data(vec![list_array], &test_cases, HashMap::new()).await;
1230    }
1231
1232    #[test_log::test(tokio::test)]
1233    async fn test_sparse_boolean_list_with_long_null_prefix() {
1234        let null_prefix_rows = 70_000usize;
1235        let trailing_empty_rows = 9usize;
1236        let booleans_per_list = 8usize;
1237        let num_rows = null_prefix_rows + 1 + trailing_empty_rows;
1238
1239        let mut offsets = Vec::with_capacity(num_rows + 1);
1240        offsets.extend(std::iter::repeat_n(0i32, null_prefix_rows + 1));
1241        let values = (0..booleans_per_list)
1242            .map(|idx| idx % 2 == 0)
1243            .collect::<Vec<_>>();
1244        offsets.push(values.len() as i32);
1245        offsets.extend(std::iter::repeat_n(
1246            values.len() as i32,
1247            trailing_empty_rows,
1248        ));
1249        let validity = BooleanBuffer::from_iter((0..num_rows).map(|row| row >= null_prefix_rows));
1250
1251        let items = BooleanArray::from(values);
1252        let list_array = ListArray::new(
1253            Arc::new(Field::new("item", DataType::Boolean, true)),
1254            OffsetBuffer::new(ScalarBuffer::from(offsets)),
1255            Arc::new(items),
1256            Some(NullBuffer::new(validity)),
1257        );
1258
1259        let test_cases = TestCases::default()
1260            .with_range(0..num_rows as u64)
1261            .with_indices(vec![0, null_prefix_rows as u64, num_rows as u64 - 1])
1262            .with_dense_encodings();
1263        let list_array = Arc::new(list_array) as ArrayRef;
1264        let pages = encode_v22_pages(list_array.clone()).await;
1265        assert_split_miniblock_layout(&pages, true);
1266        check_round_trip_encoding_of_data(vec![list_array], &test_cases, HashMap::new()).await;
1267    }
1268
1269    #[test_log::test(tokio::test)]
1270    async fn test_sparse_boolean_list_with_amortized_long_empty_prefix() {
1271        let empty_prefix_rows = 62_000usize;
1272        let booleans_per_list = 8_192usize;
1273        let num_rows = empty_prefix_rows + 1;
1274
1275        let mut offsets = Vec::with_capacity(num_rows + 1);
1276        offsets.extend(std::iter::repeat_n(0i32, empty_prefix_rows + 1));
1277        let values = (0..booleans_per_list)
1278            .map(|idx| idx % 2 == 0)
1279            .collect::<Vec<_>>();
1280        offsets.push(values.len() as i32);
1281
1282        let items = BooleanArray::from(values);
1283        let list_array = ListArray::new(
1284            Arc::new(Field::new("item", DataType::Boolean, true)),
1285            OffsetBuffer::new(ScalarBuffer::from(offsets)),
1286            Arc::new(items),
1287            None,
1288        );
1289
1290        let test_cases = TestCases::default()
1291            .with_range(0..num_rows as u64)
1292            .with_indices(vec![0, empty_prefix_rows as u64])
1293            .with_dense_encodings();
1294        let list_array = Arc::new(list_array) as ArrayRef;
1295        let pages = encode_v22_pages(list_array.clone()).await;
1296        assert_split_miniblock_layout(&pages, true);
1297        check_round_trip_encoding_of_data(vec![list_array], &test_cases, HashMap::new()).await;
1298    }
1299
1300    #[test_log::test(tokio::test)]
1301    async fn test_nested_sparse_boolean_list_fails_without_panic() {
1302        let empty_inner_lists = 70_000usize;
1303        let booleans_per_list = 8usize;
1304
1305        let mut inner_offsets = vec![0i32; empty_inner_lists + 1];
1306        let values = (0..booleans_per_list)
1307            .map(|idx| idx % 2 == 0)
1308            .collect::<Vec<_>>();
1309        inner_offsets.push(values.len() as i32);
1310
1311        let inner_items = BooleanArray::from(values);
1312        let inner_list = ListArray::new(
1313            Arc::new(Field::new("item", DataType::Boolean, true)),
1314            OffsetBuffer::new(ScalarBuffer::from(inner_offsets)),
1315            Arc::new(inner_items),
1316            None,
1317        );
1318        let outer_list = ListArray::new(
1319            Arc::new(Field::new("item", inner_list.data_type().clone(), true)),
1320            OffsetBuffer::new(ScalarBuffer::from(vec![0i32, empty_inner_lists as i32 + 1])),
1321            Arc::new(inner_list),
1322            None,
1323        );
1324
1325        let err = try_encode_v22_pages(Arc::new(outer_list))
1326            .await
1327            .unwrap_err();
1328        assert!(
1329            err.to_string().contains("Mini-block cannot encode"),
1330            "unexpected error: {err}"
1331        );
1332    }
1333
1334    #[test_log::test(tokio::test)]
1335    async fn test_nested_sparse_string_single_row_falls_back_to_fullzip() {
1336        let empty_inner_lists = 70_000usize;
1337
1338        let mut inner_offsets = vec![0i32; empty_inner_lists + 1];
1339        inner_offsets.push(1);
1340        inner_offsets.push(2);
1341
1342        let mut strings = StringBuilder::new();
1343        strings.append_value("value");
1344        strings.append_value("other");
1345        let inner_items = strings.finish();
1346        let inner_list = ListArray::new(
1347            Arc::new(Field::new("item", DataType::Utf8, true)),
1348            OffsetBuffer::new(ScalarBuffer::from(inner_offsets)),
1349            Arc::new(inner_items),
1350            None,
1351        );
1352        let outer_list = ListArray::new(
1353            Arc::new(Field::new("item", inner_list.data_type().clone(), true)),
1354            OffsetBuffer::new(ScalarBuffer::from(vec![0i32, empty_inner_lists as i32 + 2])),
1355            Arc::new(inner_list),
1356            None,
1357        );
1358
1359        let outer_list = Arc::new(outer_list) as ArrayRef;
1360        let pages = encode_v22_pages(outer_list.clone()).await;
1361        assert_has_fullzip_layout(&pages);
1362
1363        let test_cases = TestCases::default()
1364            .with_range(0..1)
1365            .with_indices(vec![0])
1366            .with_dense_encodings();
1367        check_round_trip_encoding_of_data(vec![outer_list], &test_cases, HashMap::new()).await;
1368    }
1369
1370    /// Builds the HNSW-flush repro shape: a dense prefix where every row has
1371    /// `NEIGHBORS_PER_ROW` distinct values, followed by a long tail of empty
1372    /// lists. Mirrors `HNSW::schema()` `__neighbors` / `__dists` columns:
1373    /// dense level-0 lists, then ~6x as many mostly-empty higher-level rows.
1374    fn make_hnsw_shaped_list_u32() -> ListArray {
1375        const DENSE_ROWS: u32 = 40_000;
1376        const NEIGHBORS_PER_ROW: u32 = 32;
1377        const EMPTY_TAIL_ROWS: u32 = 240_000;
1378
1379        let mut list_builder = ListBuilder::new(UInt32Builder::new());
1380        let mut next_val: u32 = 0;
1381        for _ in 0..DENSE_ROWS {
1382            for _ in 0..NEIGHBORS_PER_ROW {
1383                list_builder.values().append_value(next_val);
1384                next_val = next_val.wrapping_add(1);
1385            }
1386            list_builder.append(true);
1387        }
1388        for _ in 0..EMPTY_TAIL_ROWS {
1389            list_builder.append(true);
1390        }
1391        list_builder.finish()
1392    }
1393
1394    /// Reproduces the HNSW-flush shape at v2.2 on the auto path (no
1395    /// `STRUCTURAL_ENCODING` metadata): a dense level-0 prefix followed by a
1396    /// long tail of empty lists. The global levels/values ratio looks dense,
1397    /// so this used to encode as a single mini-block page whose final chunk
1398    /// absorbed every trailing empty list and overflowed the per-chunk `u16`
1399    /// `num_levels`, corrupting the read. The structural page planner now
1400    /// splits on top-level row boundaries: the dense prefix stays on
1401    /// mini-block pages and the empty tail becomes structural-only pages, so
1402    /// the round-trip is lossless without falling back to full-zip.
1403    #[test_log::test(tokio::test)]
1404    async fn test_list_hnsw_shape_splits_to_miniblock_v2_2() {
1405        let list_array = make_hnsw_shaped_list_u32();
1406        let dense_rows: u64 = 40_000;
1407        let total_rows = list_array.len() as u64;
1408
1409        let test_cases = TestCases::default()
1410            .with_range(0..1000)
1411            .with_range(dense_rows.saturating_sub(8)..(dense_rows + 8))
1412            .with_range(0..total_rows)
1413            .with_indices(vec![0, dense_rows - 1, dense_rows, total_rows - 1])
1414            .with_encoding(TestEncoding::StructuralU32);
1415        let list_array = Arc::new(list_array) as ArrayRef;
1416        let pages = encode_v22_pages(list_array.clone()).await;
1417        assert_split_miniblock_layout(&pages, true);
1418        check_round_trip_encoding_of_data(vec![list_array], &test_cases, HashMap::new()).await;
1419    }
1420
1421    /// Companion to the auto-path test: even when the user explicitly requests
1422    /// `STRUCTURAL_ENCODING_MINIBLOCK`, the structural page planner splits the
1423    /// HNSW shape so every emitted page fits the mini-block per-chunk budget.
1424    /// The request is honored (the dense prefix stays on mini-block pages
1425    /// rather than being forced to full-zip) and the round-trip is lossless.
1426    #[test_log::test(tokio::test)]
1427    async fn test_forced_miniblock_hnsw_shape_splits_to_miniblock_v2_2() {
1428        let list_array = make_hnsw_shaped_list_u32();
1429        let total_rows = list_array.len() as u64;
1430
1431        let mut field_metadata = HashMap::new();
1432        field_metadata.insert(
1433            STRUCTURAL_ENCODING_META_KEY.to_string(),
1434            STRUCTURAL_ENCODING_MINIBLOCK.into(),
1435        );
1436
1437        let test_cases = TestCases::default()
1438            .with_range(0..total_rows)
1439            .with_encoding(TestEncoding::StructuralU32);
1440        let list_array = Arc::new(list_array) as ArrayRef;
1441        let pages = try_encode_v22_pages_with_metadata(list_array.clone(), field_metadata.clone())
1442            .await
1443            .unwrap();
1444        assert_split_miniblock_layout(&pages, true);
1445        check_round_trip_encoding_of_data(vec![list_array], &test_cases, field_metadata).await;
1446    }
1447}