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, StringArray,
244        StructArray, 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 lance_datagen::{RowCount, Seed, array, gen_batch};
253    use rstest::rstest;
254
255    use crate::testing::{
256        TestCases, TestEncoding, check_round_trip_encoding_of_data, create_test_field_encoder,
257        test_encoding_strategy,
258    };
259
260    fn make_list_type(inner_type: DataType) -> DataType {
261        DataType::List(Arc::new(Field::new("item", inner_type, true)))
262    }
263
264    fn make_large_list_type(inner_type: DataType) -> DataType {
265        DataType::LargeList(Arc::new(Field::new("item", inner_type, true)))
266    }
267
268    #[derive(Clone, Copy)]
269    enum NullPattern {
270        None,
271        Mixed,
272        All,
273    }
274
275    async fn check_nested_type(
276        data_type: DataType,
277        null_pattern: NullPattern,
278        encoding: TestEncoding,
279    ) {
280        check_nested_type_with_metadata(data_type, null_pattern, encoding, HashMap::new()).await;
281    }
282
283    async fn check_nested_type_with_metadata(
284        data_type: DataType,
285        null_pattern: NullPattern,
286        encoding: TestEncoding,
287        field_metadata: HashMap<String, String>,
288    ) {
289        let null_rate = match null_pattern {
290            NullPattern::None => None,
291            NullPattern::Mixed => Some(0.5),
292            NullPattern::All => Some(1.0),
293        };
294        let make_batch = |seed, rows| {
295            let mut generator = gen_batch()
296                .with_seed(Seed::from(seed))
297                .anon_col(array::rand_type(&data_type));
298            if let Some(null_rate) = null_rate {
299                generator.with_random_nulls(null_rate);
300            }
301            generator
302                .into_batch_rows(RowCount::from(rows))
303                .unwrap()
304                .column(0)
305                .clone()
306        };
307
308        // Combine a non-zero-offset slice with an independently generated batch.
309        // This covers both offset rebasing and rep/def accumulation at the ingest
310        // boundary without repeating the full generic random-test matrix.
311        let first = make_batch(0, 513).slice(1, 512);
312        let second = make_batch(1, 513);
313        let test_cases = TestCases::default()
314            .with_page_sizes(vec![4096])
315            .with_encoding(encoding)
316            .with_batch_size(257)
317            .with_range(510..515)
318            .with_indices(vec![0, 511, 512, 1024]);
319
320        check_round_trip_encoding_of_data(vec![first, second], &test_cases, field_metadata).await;
321    }
322
323    async fn try_encode_v22_pages(
324        array: ArrayRef,
325    ) -> lance_core::Result<Vec<crate::encoder::EncodedPage>> {
326        try_encode_v22_pages_with_metadata(array, HashMap::new()).await
327    }
328
329    async fn try_encode_v22_pages_with_metadata(
330        array: ArrayRef,
331        field_metadata: HashMap<String, String>,
332    ) -> lance_core::Result<Vec<crate::encoder::EncodedPage>> {
333        let arrow_field =
334            Field::new("", array.data_type().clone(), true).with_metadata(field_metadata);
335        let lance_field = lance_core::datatypes::Field::try_from(&arrow_field).unwrap();
336        let encoding_strategy = test_encoding_strategy(TestEncoding::StructuralU32);
337        let mut column_index_seq = crate::encoder::ColumnIndexSequence::default();
338        let encoding_options = crate::encoder::EncodingOptions::default();
339        let mut encoder = create_test_field_encoder(
340            encoding_strategy.as_ref(),
341            &lance_field,
342            &mut column_index_seq,
343            &encoding_options,
344        )
345        .unwrap();
346        let mut external_buffers =
347            crate::encoder::OutOfLineBuffers::new(0, crate::encoder::MIN_PAGE_BUFFER_ALIGNMENT);
348        let num_rows = array.len() as u64;
349        let mut pages = Vec::new();
350        for task in encoder
351            .maybe_encode(
352                array,
353                &mut external_buffers,
354                crate::repdef::RepDefBuilder::default(),
355                0,
356                num_rows,
357            )
358            .unwrap()
359        {
360            pages.push(task.await?);
361        }
362        for task in encoder.flush(&mut external_buffers).unwrap() {
363            pages.push(task.await?);
364        }
365        Ok(pages)
366    }
367
368    async fn encode_v22_pages(array: ArrayRef) -> Vec<crate::encoder::EncodedPage> {
369        try_encode_v22_pages(array).await.unwrap()
370    }
371
372    fn assert_split_miniblock_layout(
373        pages: &[crate::encoder::EncodedPage],
374        min_miniblock_pages: usize,
375        expect_structural_only_page: bool,
376    ) {
377        let mut miniblock_pages = 0;
378        let mut fullzip_pages = 0;
379        let mut structural_only_pages = 0;
380
381        for page in pages {
382            let crate::decoder::PageEncoding::Structural(layout) = &page.description else {
383                continue;
384            };
385            match layout.layout.as_ref().unwrap() {
386                crate::format::pb21::page_layout::Layout::MiniBlockLayout(_) => {
387                    miniblock_pages += 1;
388                }
389                crate::format::pb21::page_layout::Layout::FullZipLayout(_) => {
390                    fullzip_pages += 1;
391                }
392                crate::format::pb21::page_layout::Layout::ConstantLayout(layout) => {
393                    if layout.inline_value.is_none()
394                        && (layout.num_rep_values > 0 || layout.num_def_values > 0)
395                    {
396                        structural_only_pages += 1;
397                    }
398                }
399                crate::format::pb21::page_layout::Layout::BlobLayout(_) => {}
400                crate::format::pb21::page_layout::Layout::SparseLayout(_) => {}
401            }
402        }
403
404        assert!(
405            miniblock_pages >= min_miniblock_pages,
406            "expected at least {min_miniblock_pages} mini-block pages, got {miniblock_pages}"
407        );
408        assert_eq!(
409            fullzip_pages, 0,
410            "split list pages should not fall back to full-zip"
411        );
412        assert_eq!(
413            structural_only_pages > 0,
414            expect_structural_only_page,
415            "structural-only page presence did not match expectation; got {structural_only_pages}"
416        );
417    }
418
419    fn assert_has_fullzip_layout(pages: &[crate::encoder::EncodedPage]) {
420        let has_fullzip = pages.iter().any(|page| {
421            let crate::decoder::PageEncoding::Structural(layout) = &page.description else {
422                return false;
423            };
424            matches!(
425                layout.layout.as_ref().unwrap(),
426                crate::format::pb21::page_layout::Layout::FullZipLayout(_)
427            )
428        });
429        assert!(has_fullzip, "expected at least one full-zip page");
430    }
431
432    #[rstest]
433    #[test_log::test(tokio::test)]
434    async fn test_list(
435        #[values(STRUCTURAL_ENCODING_MINIBLOCK, STRUCTURAL_ENCODING_FULLZIP)]
436        structural_encoding: &str,
437        #[values(NullPattern::None, NullPattern::Mixed, NullPattern::All)]
438        null_pattern: NullPattern,
439        #[values(
440            TestEncoding::Array,
441            TestEncoding::StructuralU16,
442            TestEncoding::StructuralU32,
443            TestEncoding::StructuralSparse
444        )]
445        encoding: TestEncoding,
446    ) {
447        let mut field_metadata = HashMap::new();
448        field_metadata.insert(
449            STRUCTURAL_ENCODING_META_KEY.to_string(),
450            structural_encoding.into(),
451        );
452        check_nested_type_with_metadata(
453            make_list_type(DataType::Int32),
454            null_pattern,
455            encoding,
456            field_metadata,
457        )
458        .await;
459    }
460
461    #[rstest]
462    #[test_log::test(tokio::test)]
463    async fn test_deeply_nested_lists(
464        #[values(STRUCTURAL_ENCODING_MINIBLOCK, STRUCTURAL_ENCODING_FULLZIP)]
465        structural_encoding: &str,
466        #[values(1, 2, 3, 4, 5)] depth: usize,
467        #[values(
468            TestEncoding::Array,
469            TestEncoding::StructuralU16,
470            TestEncoding::StructuralU32,
471            TestEncoding::StructuralSparse
472        )]
473        test_encoding: TestEncoding,
474    ) {
475        let mut data_type = DataType::Int32;
476        for _ in 0..depth {
477            data_type = make_list_type(data_type);
478        }
479
480        let mut generator = gen_batch()
481            .with_seed(Seed::from(depth as u64))
482            .anon_col(array::rand_type(&data_type));
483        generator.with_random_nulls(0.2);
484        let source = generator
485            .into_batch_rows(RowCount::from(1026))
486            .unwrap()
487            .column(0)
488            .clone();
489
490        // Two non-zero-offset slices cover nested offset rebasing across ingest
491        // batches. The selected range and indices straddle that batch boundary.
492        let data = vec![source.slice(1, 512), source.slice(513, 513)];
493        let test_cases = TestCases::default()
494            .with_page_sizes(vec![4096])
495            .with_encoding(test_encoding)
496            .with_batch_size(257)
497            .with_range(510..515)
498            .with_indices(vec![0, 511, 512, 1024]);
499        let field_metadata = HashMap::from([(
500            STRUCTURAL_ENCODING_META_KEY.to_string(),
501            structural_encoding.into(),
502        )]);
503
504        check_round_trip_encoding_of_data(data, &test_cases, field_metadata).await;
505    }
506
507    #[rstest]
508    #[test_log::test(tokio::test)]
509    async fn test_large_list(
510        #[values(NullPattern::None, NullPattern::Mixed, NullPattern::All)]
511        null_pattern: NullPattern,
512        #[values(
513            TestEncoding::Array,
514            TestEncoding::StructuralU16,
515            TestEncoding::StructuralU32,
516            TestEncoding::StructuralSparse
517        )]
518        encoding: TestEncoding,
519    ) {
520        check_nested_type(
521            make_large_list_type(DataType::Int32),
522            null_pattern,
523            encoding,
524        )
525        .await;
526    }
527
528    #[rstest]
529    #[test_log::test(tokio::test)]
530    async fn test_nested_strings(
531        #[values(NullPattern::None, NullPattern::Mixed, NullPattern::All)]
532        null_pattern: NullPattern,
533        #[values(
534            TestEncoding::Array,
535            TestEncoding::StructuralU16,
536            TestEncoding::StructuralU32,
537            TestEncoding::StructuralSparse
538        )]
539        encoding: TestEncoding,
540    ) {
541        check_nested_type(make_list_type(DataType::Utf8), null_pattern, encoding).await;
542    }
543
544    #[rstest]
545    #[test_log::test(tokio::test)]
546    async fn test_nested_list(
547        #[values(NullPattern::None, NullPattern::Mixed, NullPattern::All)]
548        null_pattern: NullPattern,
549        #[values(
550            TestEncoding::Array,
551            TestEncoding::StructuralU16,
552            TestEncoding::StructuralU32,
553            TestEncoding::StructuralSparse
554        )]
555        encoding: TestEncoding,
556    ) {
557        check_nested_type(
558            make_list_type(make_list_type(DataType::Int32)),
559            null_pattern,
560            encoding,
561        )
562        .await;
563    }
564
565    /// Regression test: a `List<List<Float32>>` column written as MULTIPLE
566    /// batches (chunks) whose flattened leaf values cross a value-page boundary
567    /// fails to decode with "Max offset N exceeds length of values M" (Arrow
568    /// error raised by `ListArray::try_new` in `StructuralListDecodeTask::decode`).
569    ///
570    /// The trigger (verified against the production file and pylance 7.0.0b12 /
571    /// 7.0.0 / 9.0.0-beta.10) requires ALL of:
572    ///   1. >= 2 list layers (`List<List<..>>`),
573    ///   2. a leaf large enough to be chunked into multiple value pages,
574    ///   3. the column written as more than one batch.
575    /// A single batch of the identical data round-trips fine — which is why the
576    /// earlier single-chunk version of this test (and the small `test_nested_list`
577    /// cases) did not catch it. Found in production on the gaming TransNet
578    /// `dino_embedding_per_frame` column (rectangular 3 x 768 float per row).
579    ///
580    /// Each element of the `vec![..]` passed to `check_round_trip_encoding_of_data`
581    /// is encoded as a separate batch (its own `RepDefBuilder`), so we split the
582    /// rows into two chunks to exercise the multi-batch repdef accumulation path.
583    #[rstest]
584    #[test_log::test(tokio::test)]
585    async fn test_multipage_nested_float_list(
586        #[values(STRUCTURAL_ENCODING_MINIBLOCK, STRUCTURAL_ENCODING_FULLZIP)]
587        structural_encoding: &str,
588        #[values(
589            TestEncoding::StructuralU16,
590            TestEncoding::StructuralU32,
591            TestEncoding::StructuralSparse
592        )]
593        test_encoding: TestEncoding,
594    ) {
595        use arrow_array::Float32Array;
596
597        // Production shape: 3 inner lists per row, 768 floats each.
598        let inner_per_row: usize = 3;
599        let inner_len: usize = 768;
600        // Each chunk contains ~1.38 MiB of leaf values, so both cross the 1 MiB
601        // value-page limit. A read batch that spans the ingest boundary is where
602        // the multi-page outer-offset bug triggered.
603        let chunk_rows: &[usize] = &[150, 149];
604
605        let make_chunk = |start_row: usize, num_rows: usize| -> Arc<dyn Array> {
606            let total_inner = num_rows * inner_per_row;
607            let total_values = total_inner * inner_len;
608            let values = Float32Array::from(
609                (0..total_values)
610                    .map(|i| (start_row + i) as f32)
611                    .collect::<Vec<_>>(),
612            );
613            let inner_offsets = ScalarBuffer::<i32>::from(
614                (0..=total_inner)
615                    .map(|i| (i * inner_len) as i32)
616                    .collect::<Vec<_>>(),
617            );
618            let inner_list = ListArray::new(
619                Arc::new(Field::new("item", DataType::Float32, true)),
620                OffsetBuffer::new(inner_offsets),
621                Arc::new(values),
622                None,
623            );
624            let outer_offsets = ScalarBuffer::<i32>::from(
625                (0..=num_rows)
626                    .map(|i| (i * inner_per_row) as i32)
627                    .collect::<Vec<_>>(),
628            );
629            Arc::new(ListArray::new(
630                Arc::new(Field::new(
631                    "item",
632                    DataType::List(Arc::new(Field::new("item", DataType::Float32, true))),
633                    true,
634                )),
635                OffsetBuffer::new(outer_offsets),
636                Arc::new(inner_list),
637                None,
638            ))
639        };
640
641        let mut start = 0;
642        let chunks: Vec<Arc<dyn Array>> = chunk_rows
643            .iter()
644            .map(|&n| {
645                let c = make_chunk(start, n);
646                start += n;
647                c
648            })
649            .collect();
650
651        let mut field_metadata = HashMap::new();
652        field_metadata.insert(
653            STRUCTURAL_ENCODING_META_KEY.to_string(),
654            structural_encoding.into(),
655        );
656
657        let test_cases = TestCases::default()
658            .with_page_sizes(vec![1024 * 1024])
659            .with_encoding(test_encoding)
660            .with_batch_size(151)
661            .with_range(148..152)
662            .with_indices(vec![0, 149, 150, 298]);
663        check_round_trip_encoding_of_data(chunks, &test_cases, field_metadata).await;
664    }
665
666    #[rstest]
667    #[test_log::test(tokio::test)]
668    async fn test_list_struct_list(
669        #[values(NullPattern::None, NullPattern::Mixed, NullPattern::All)]
670        null_pattern: NullPattern,
671        #[values(
672            TestEncoding::Array,
673            TestEncoding::StructuralU16,
674            TestEncoding::StructuralU32,
675            TestEncoding::StructuralSparse
676        )]
677        encoding: TestEncoding,
678    ) {
679        let struct_type = DataType::Struct(Fields::from(vec![Field::new(
680            "inner_str",
681            DataType::Utf8,
682            false,
683        )]));
684
685        check_nested_type(make_list_type(struct_type), null_pattern, encoding).await;
686    }
687
688    #[rstest]
689    #[test_log::test(tokio::test)]
690    async fn test_list_struct_empty(
691        #[values(
692            TestEncoding::Array,
693            TestEncoding::StructuralU16,
694            TestEncoding::StructuralU32,
695            TestEncoding::StructuralSparse
696        )]
697        encoding: TestEncoding,
698    ) {
699        let fields = Fields::from(vec![Field::new("inner", DataType::UInt64, true)]);
700        let items = UInt64Array::from(Vec::<u64>::new());
701        let structs = StructArray::new(fields, vec![Arc::new(items)], None);
702        // Exceed two 1 MiB offset pages so flushing the empty struct child is
703        // still exercised multiple times (the original #2762 regression).
704        let num_rows = 2 * 1024 * 1024 / size_of::<i32>() + 1;
705        let offsets = OffsetBuffer::new(ScalarBuffer::<i32>::from(vec![0; num_rows + 1]));
706        let lists = ListArray::new(
707            Arc::new(Field::new("item", structs.data_type().clone(), true)),
708            offsets,
709            Arc::new(structs),
710            None,
711        );
712
713        check_round_trip_encoding_of_data(
714            vec![Arc::new(lists)],
715            &TestCases::default()
716                .with_page_sizes(vec![1024 * 1024])
717                .with_encoding(encoding),
718            HashMap::new(),
719        )
720        .await;
721    }
722
723    #[rstest]
724    #[test_log::test(tokio::test)]
725    async fn test_simple_list(
726        #[values(STRUCTURAL_ENCODING_MINIBLOCK, STRUCTURAL_ENCODING_FULLZIP)]
727        structural_encoding: &str,
728    ) {
729        let items_builder = Int32Builder::new();
730        let mut list_builder = ListBuilder::new(items_builder);
731        list_builder.append_value([Some(1), Some(2), Some(3)]);
732        list_builder.append_value([Some(4), Some(5)]);
733        list_builder.append_null();
734        list_builder.append_value([Some(6), Some(7), Some(8)]);
735        let list_array = list_builder.finish();
736
737        let mut field_metadata = HashMap::new();
738        field_metadata.insert(
739            STRUCTURAL_ENCODING_META_KEY.to_string(),
740            structural_encoding.into(),
741        );
742
743        let test_cases = TestCases::default()
744            .with_range(0..2)
745            .with_range(0..3)
746            .with_range(1..3)
747            .with_indices(vec![1, 3])
748            .with_indices(vec![2]);
749        check_round_trip_encoding_of_data(vec![Arc::new(list_array)], &test_cases, field_metadata)
750            .await;
751    }
752
753    #[rstest]
754    #[test_log::test(tokio::test)]
755    async fn test_simple_nested_list_ends_with_null(
756        #[values(STRUCTURAL_ENCODING_MINIBLOCK, STRUCTURAL_ENCODING_FULLZIP)]
757        structural_encoding: &str,
758    ) {
759        use arrow_array::Int32Array;
760
761        let values = Int32Array::from(vec![1, 2, 3, 4, 5]);
762        let inner_offsets = ScalarBuffer::<i32>::from(vec![0, 1, 2, 3, 4, 5, 5]);
763        let inner_validity = BooleanBuffer::from(vec![true, true, true, true, true, false]);
764        let outer_offsets = ScalarBuffer::<i32>::from(vec![0, 1, 2, 3, 4, 5, 6, 6]);
765        let outer_validity = BooleanBuffer::from(vec![true, true, true, true, true, true, false]);
766
767        let inner_list = ListArray::new(
768            Arc::new(Field::new("item", DataType::Int32, true)),
769            OffsetBuffer::new(inner_offsets),
770            Arc::new(values),
771            Some(NullBuffer::new(inner_validity)),
772        );
773        let outer_list = ListArray::new(
774            Arc::new(Field::new(
775                "item",
776                DataType::List(Arc::new(Field::new("item", DataType::Int32, true))),
777                true,
778            )),
779            OffsetBuffer::new(outer_offsets),
780            Arc::new(inner_list),
781            Some(NullBuffer::new(outer_validity)),
782        );
783
784        let mut field_metadata = HashMap::new();
785        field_metadata.insert(
786            STRUCTURAL_ENCODING_META_KEY.to_string(),
787            structural_encoding.into(),
788        );
789
790        let test_cases = TestCases::default()
791            .with_range(0..2)
792            .with_range(0..3)
793            .with_range(5..7)
794            .with_indices(vec![1, 6])
795            .with_indices(vec![6])
796            .with_structural_encodings();
797        check_round_trip_encoding_of_data(vec![Arc::new(outer_list)], &test_cases, field_metadata)
798            .await;
799    }
800
801    #[rstest]
802    #[test_log::test(tokio::test)]
803    async fn test_simple_string_list(
804        #[values(STRUCTURAL_ENCODING_MINIBLOCK, STRUCTURAL_ENCODING_FULLZIP)]
805        structural_encoding: &str,
806    ) {
807        let items_builder = StringBuilder::new();
808        let mut list_builder = ListBuilder::new(items_builder);
809        list_builder.append_value([Some("a"), Some("bc"), Some("def")]);
810        list_builder.append_value([Some("gh"), None]);
811        list_builder.append_null();
812        list_builder.append_value([Some("ijk"), Some("lmnop"), Some("qrs")]);
813        let list_array = list_builder.finish();
814
815        let mut field_metadata = HashMap::new();
816        field_metadata.insert(
817            STRUCTURAL_ENCODING_META_KEY.to_string(),
818            structural_encoding.into(),
819        );
820
821        let test_cases = TestCases::default()
822            .with_range(0..2)
823            .with_range(0..3)
824            .with_range(1..3)
825            .with_indices(vec![1, 3])
826            .with_indices(vec![2])
827            .with_structural_encodings();
828        check_round_trip_encoding_of_data(vec![Arc::new(list_array)], &test_cases, field_metadata)
829            .await;
830    }
831
832    #[rstest]
833    #[test_log::test(tokio::test)]
834    async fn test_simple_string_list_no_null(
835        #[values(STRUCTURAL_ENCODING_MINIBLOCK, STRUCTURAL_ENCODING_FULLZIP)]
836        structural_encoding: &str,
837    ) {
838        let items_builder = StringBuilder::new();
839        let mut list_builder = ListBuilder::new(items_builder);
840        list_builder.append_value([Some("a"), Some("bc"), Some("def")]);
841        list_builder.append_value([Some("gh"), Some("zxy")]);
842        list_builder.append_value([Some("gh"), Some("z")]);
843        list_builder.append_value([Some("ijk"), Some("lmnop"), Some("qrs")]);
844        let list_array = list_builder.finish();
845
846        let mut field_metadata = HashMap::new();
847        field_metadata.insert(
848            STRUCTURAL_ENCODING_META_KEY.to_string(),
849            structural_encoding.into(),
850        );
851
852        let test_cases = TestCases::default()
853            .with_range(0..2)
854            .with_range(0..3)
855            .with_range(1..3)
856            .with_indices(vec![1, 3])
857            .with_indices(vec![2])
858            .with_structural_encodings();
859        check_round_trip_encoding_of_data(vec![Arc::new(list_array)], &test_cases, field_metadata)
860            .await;
861    }
862
863    #[rstest]
864    #[test_log::test(tokio::test)]
865    async fn test_simple_sliced_list(
866        #[values(STRUCTURAL_ENCODING_MINIBLOCK, STRUCTURAL_ENCODING_FULLZIP)]
867        structural_encoding: &str,
868    ) {
869        let items_builder = Int32Builder::new();
870        let mut list_builder = ListBuilder::new(items_builder);
871        list_builder.append_value([Some(1), Some(2), Some(3)]);
872        list_builder.append_value([Some(4), Some(5)]);
873        list_builder.append_null();
874        list_builder.append_value([Some(6), Some(7), Some(8)]);
875        let list_array = list_builder.finish();
876
877        let list_array = list_array.slice(1, 2);
878
879        let mut field_metadata = HashMap::new();
880        field_metadata.insert(
881            STRUCTURAL_ENCODING_META_KEY.to_string(),
882            structural_encoding.into(),
883        );
884
885        let test_cases = TestCases::default()
886            .with_range(0..2)
887            .with_range(1..2)
888            .with_indices(vec![0])
889            .with_indices(vec![1])
890            .with_structural_encodings();
891        check_round_trip_encoding_of_data(vec![Arc::new(list_array)], &test_cases, field_metadata)
892            .await;
893    }
894
895    #[test_log::test(tokio::test)]
896    async fn test_simple_list_dict() {
897        let values = LargeStringArray::from_iter_values(["a", "bb", "ccc"]);
898        let indices = UInt8Array::from(vec![0, 1, 2, 0, 1, 2, 0, 1, 2]);
899        let dict_array = DictionaryArray::new(indices, Arc::new(values));
900        let offsets = OffsetBuffer::new(ScalarBuffer::<i32>::from(vec![0, 3, 5, 6, 9]));
901        let list_array = ListArray::new(
902            Arc::new(Field::new("item", dict_array.data_type().clone(), true)),
903            offsets,
904            Arc::new(dict_array),
905            None,
906        );
907
908        let test_cases = TestCases::default()
909            .with_range(0..2)
910            .with_range(1..3)
911            .with_range(2..4)
912            .with_indices(vec![1])
913            .with_indices(vec![2]);
914        check_round_trip_encoding_of_data(
915            vec![Arc::new(list_array)],
916            &test_cases,
917            HashMap::default(),
918        )
919        .await;
920    }
921
922    #[test_log::test(tokio::test)]
923    async fn test_simple_list_all_null() {
924        let items = UInt64Array::from(vec![0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]);
925        let offsets = ScalarBuffer::<i32>::from(vec![0, 5, 8, 10]);
926        let offsets = OffsetBuffer::new(offsets);
927        let list_validity = NullBuffer::new(BooleanBuffer::from(vec![false, false, false]));
928
929        // The list array is nullable but the items are not.  Then, all lists are null.
930        let list_arr = ListArray::new(
931            Arc::new(Field::new("item", DataType::UInt64, false)),
932            offsets,
933            Arc::new(items),
934            Some(list_validity),
935        );
936
937        let test_cases = TestCases::default()
938            .with_range(0..3)
939            .with_range(1..2)
940            .with_indices(vec![1])
941            .with_indices(vec![2])
942            .with_structural_encodings();
943        check_round_trip_encoding_of_data(
944            vec![Arc::new(list_arr)],
945            &test_cases,
946            HashMap::default(),
947        )
948        .await;
949    }
950
951    #[rstest]
952    #[test_log::test(tokio::test)]
953    async fn test_list_with_garbage_nulls(
954        #[values(STRUCTURAL_ENCODING_MINIBLOCK, STRUCTURAL_ENCODING_FULLZIP)]
955        structural_encoding: &str,
956    ) {
957        // In Arrow, list nulls are allowed to be non-empty, with masked garbage values
958        // Here we make a list with a null row in the middle with 3 garbage values
959        let items = UInt64Array::from(vec![0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]);
960        let offsets = ScalarBuffer::<i32>::from(vec![0, 5, 8, 10]);
961        let offsets = OffsetBuffer::new(offsets);
962        let list_validity = NullBuffer::new(BooleanBuffer::from(vec![true, false, true]));
963        let list_arr = ListArray::new(
964            Arc::new(Field::new("item", DataType::UInt64, true)),
965            offsets,
966            Arc::new(items),
967            Some(list_validity),
968        );
969
970        let mut field_metadata = HashMap::new();
971        field_metadata.insert(
972            STRUCTURAL_ENCODING_META_KEY.to_string(),
973            structural_encoding.into(),
974        );
975
976        let test_cases = TestCases::default()
977            .with_range(0..3)
978            .with_range(1..2)
979            .with_indices(vec![1])
980            .with_indices(vec![2])
981            .with_structural_encodings();
982        check_round_trip_encoding_of_data(vec![Arc::new(list_arr)], &test_cases, field_metadata)
983            .await;
984    }
985
986    #[rstest]
987    #[test_log::test(tokio::test)]
988    async fn test_simple_two_page_list(
989        #[values(STRUCTURAL_ENCODING_MINIBLOCK, STRUCTURAL_ENCODING_FULLZIP)]
990        structural_encoding: &str,
991    ) {
992        // This is a simple pre-defined list that spans two pages.  This test is useful for
993        // debugging the repetition index
994
995        let items_builder = Int64Builder::new();
996        let mut list_builder = ListBuilder::new(items_builder);
997        for i in 0..512 {
998            list_builder.append_value([Some(i), Some(i * 2)]);
999        }
1000        let list_array_1 = list_builder.finish();
1001
1002        let items_builder = Int64Builder::new();
1003        let mut list_builder = ListBuilder::new(items_builder);
1004        for i in 0..512 {
1005            let i = i + 512;
1006            list_builder.append_value([Some(i), Some(i * 2)]);
1007        }
1008        let list_array_2 = list_builder.finish();
1009
1010        let mut metadata = HashMap::new();
1011        metadata.insert(
1012            STRUCTURAL_ENCODING_META_KEY.to_string(),
1013            structural_encoding.into(),
1014        );
1015
1016        let test_cases = TestCases::default()
1017            .with_structural_encodings()
1018            .with_page_sizes(vec![100])
1019            .with_range(800..900);
1020        check_round_trip_encoding_of_data(
1021            vec![Arc::new(list_array_1), Arc::new(list_array_2)],
1022            &test_cases,
1023            metadata,
1024        )
1025        .await;
1026    }
1027
1028    #[test_log::test(tokio::test)]
1029    async fn test_simple_large_list() {
1030        let items_builder = Int32Builder::new();
1031        let mut list_builder = LargeListBuilder::new(items_builder);
1032        list_builder.append_value([Some(1), Some(2), Some(3)]);
1033        list_builder.append_value([Some(4), Some(5)]);
1034        list_builder.append_null();
1035        list_builder.append_value([Some(6), Some(7), Some(8)]);
1036        let list_array = list_builder.finish();
1037
1038        let test_cases = TestCases::default()
1039            .with_range(0..2)
1040            .with_range(0..3)
1041            .with_range(1..3)
1042            .with_indices(vec![1, 3]);
1043        check_round_trip_encoding_of_data(vec![Arc::new(list_array)], &test_cases, HashMap::new())
1044            .await;
1045    }
1046
1047    #[rstest]
1048    #[test_log::test(tokio::test)]
1049    async fn test_empty_lists(
1050        #[values(STRUCTURAL_ENCODING_MINIBLOCK, STRUCTURAL_ENCODING_FULLZIP)]
1051        structural_encoding: &str,
1052    ) {
1053        let mut field_metadata = HashMap::new();
1054        field_metadata.insert(
1055            STRUCTURAL_ENCODING_META_KEY.to_string(),
1056            structural_encoding.into(),
1057        );
1058
1059        // Scenario 1: Some lists are empty
1060
1061        let values = [vec![Some(1), Some(2), Some(3)], vec![], vec![None]];
1062        // Test empty list at beginning, middle, and end
1063        for order in [[0, 1, 2], [1, 0, 2], [2, 0, 1]] {
1064            let items_builder = Int32Builder::new();
1065            let mut list_builder = ListBuilder::new(items_builder);
1066            for idx in order {
1067                list_builder.append_value(values[idx].clone());
1068            }
1069            let list_array = Arc::new(list_builder.finish());
1070            let test_cases = TestCases::default()
1071                .with_indices(vec![1])
1072                .with_indices(vec![0])
1073                .with_indices(vec![2])
1074                .with_indices(vec![0, 1]);
1075            check_round_trip_encoding_of_data(
1076                vec![list_array.clone()],
1077                &test_cases,
1078                field_metadata.clone(),
1079            )
1080            .await;
1081            let test_cases = test_cases.with_batch_size(1);
1082            check_round_trip_encoding_of_data(
1083                vec![list_array],
1084                &test_cases,
1085                field_metadata.clone(),
1086            )
1087            .await;
1088        }
1089
1090        // Scenario 2: All lists are empty
1091
1092        // When encoding a list of empty lists there are no items to encode
1093        // which is strange and we want to ensure we handle it
1094        let items_builder = Int32Builder::new();
1095        let mut list_builder = ListBuilder::new(items_builder);
1096        list_builder.append(true);
1097        list_builder.append_null();
1098        list_builder.append(true);
1099        let list_array = Arc::new(list_builder.finish());
1100
1101        let test_cases = TestCases::default().with_range(0..2).with_indices(vec![1]);
1102        check_round_trip_encoding_of_data(
1103            vec![list_array.clone()],
1104            &test_cases,
1105            field_metadata.clone(),
1106        )
1107        .await;
1108        let test_cases = test_cases.with_batch_size(1);
1109        check_round_trip_encoding_of_data(vec![list_array], &test_cases, field_metadata.clone())
1110            .await;
1111
1112        // Scenario 2B: All lists are empty (but now with strings)
1113
1114        // When encoding a list of empty lists there are no items to encode
1115        // which is strange and we want to ensure we handle it
1116        let items_builder = StringBuilder::new();
1117        let mut list_builder = ListBuilder::new(items_builder);
1118        list_builder.append(true);
1119        list_builder.append_null();
1120        list_builder.append(true);
1121        let list_array = Arc::new(list_builder.finish());
1122
1123        let test_cases = TestCases::default().with_range(0..2).with_indices(vec![1]);
1124        check_round_trip_encoding_of_data(
1125            vec![list_array.clone()],
1126            &test_cases,
1127            field_metadata.clone(),
1128        )
1129        .await;
1130        let test_cases = test_cases.with_batch_size(1);
1131        check_round_trip_encoding_of_data(vec![list_array], &test_cases, field_metadata.clone())
1132            .await;
1133
1134        // Scenario 3: All lists are null
1135
1136        let items_builder = Int32Builder::new();
1137        let mut list_builder = ListBuilder::new(items_builder);
1138        list_builder.append_null();
1139        list_builder.append_null();
1140        list_builder.append_null();
1141        let list_array = Arc::new(list_builder.finish());
1142
1143        let test_cases = TestCases::default().with_range(0..2).with_indices(vec![1]);
1144        check_round_trip_encoding_of_data(
1145            vec![list_array.clone()],
1146            &test_cases,
1147            field_metadata.clone(),
1148        )
1149        .await;
1150        let test_cases = test_cases.with_batch_size(1);
1151        check_round_trip_encoding_of_data(vec![list_array], &test_cases, field_metadata.clone())
1152            .await;
1153
1154        // Scenario 4: All lists are null and inside a struct (only valid for 2.1 since 2.0 doesn't
1155        // support null structs)
1156        let items_builder = Int32Builder::new();
1157        let mut list_builder = ListBuilder::new(items_builder);
1158        list_builder.append_null();
1159        list_builder.append_null();
1160        list_builder.append_null();
1161        let list_array = Arc::new(list_builder.finish());
1162
1163        let struct_validity = NullBuffer::new(BooleanBuffer::from(vec![true, false, true]));
1164        let struct_array = Arc::new(StructArray::new(
1165            Fields::from(vec![Field::new(
1166                "lists",
1167                list_array.data_type().clone(),
1168                true,
1169            )]),
1170            vec![list_array],
1171            Some(struct_validity),
1172        ));
1173
1174        let test_cases = TestCases::default()
1175            .with_range(0..2)
1176            .with_indices(vec![1])
1177            .with_structural_encodings();
1178        check_round_trip_encoding_of_data(
1179            vec![struct_array.clone()],
1180            &test_cases,
1181            field_metadata.clone(),
1182        )
1183        .await;
1184        let test_cases = test_cases.with_batch_size(1);
1185        check_round_trip_encoding_of_data(vec![struct_array], &test_cases, field_metadata.clone())
1186            .await;
1187    }
1188
1189    #[test_log::test(tokio::test)]
1190    async fn test_empty_list_list() {
1191        let items_builder = Int32Builder::new();
1192        let list_builder = ListBuilder::new(items_builder);
1193        let mut outer_list_builder = ListBuilder::new(list_builder);
1194        outer_list_builder.append_null();
1195        outer_list_builder.append_null();
1196        outer_list_builder.append_null();
1197        let list_array = Arc::new(outer_list_builder.finish());
1198
1199        let test_cases = TestCases::default().with_structural_encodings();
1200        check_round_trip_encoding_of_data(vec![list_array], &test_cases, HashMap::new()).await;
1201    }
1202
1203    #[test_log::test(tokio::test)]
1204    #[ignore] // This test is quite slow in debug mode
1205    async fn test_jumbo_list() {
1206        // This is an overflow test.  We have a list of lists where each list
1207        // has 1Mi items.  We encode 5000 of these lists and so we have over 4Gi in the
1208        // offsets range
1209        let items = BooleanArray::new_null(1024 * 1024);
1210        let offsets = OffsetBuffer::new(ScalarBuffer::from(vec![0, 1024 * 1024]));
1211        let list_arr = Arc::new(ListArray::new(
1212            Arc::new(Field::new("item", DataType::Boolean, true)),
1213            offsets,
1214            Arc::new(items),
1215            None,
1216        )) as ArrayRef;
1217        let arrs = vec![list_arr; 5000];
1218
1219        // We can't validate because our validation relies on concatenating all input arrays
1220        let test_cases = TestCases::default().without_validation();
1221        check_round_trip_encoding_of_data(arrs, &test_cases, HashMap::new()).await;
1222    }
1223
1224    // Regression test for issue with ListArray encoding when crossing 1024 value boundary
1225    // This test reproduces the bug where rows_avail assertion fails in schedule_instructions
1226    // when encoding a ListArray with specific size patterns that cross the 1024 value boundary
1227    #[tokio::test]
1228    async fn test_fuzz_issue_4466() {
1229        // This specific pattern of list sizes triggers the bug when total values cross 1024
1230        // 94 lists total 1009 values (passes), 95 lists total 1025 values (fails)
1231        let list_sizes = vec![
1232            13, 18, 12, 7, 14, 12, 6, 13, 18, 8, // 0-9: 119 values
1233            6, 11, 17, 12, 8, 19, 5, 6, 10, 13, // 10-19: 107 values
1234            8, 6, 10, 4, 8, 16, 14, 12, 18, 9, // 20-29: 105 values
1235            17, 8, 14, 18, 15, 3, 2, 4, 5, 1, // 30-39: 82 values
1236            3, 13, 1, 2, 10, 4, 10, 18, 7, 14, // 40-49: 75 values
1237            18, 13, 9, 17, 3, 13, 10, 14, 8, 19, // 50-59: 125 values
1238            17, 10, 5, 11, 6, 15, 10, 18, 18, 20, // 60-69: 130 values
1239            16, 11, 12, 15, 7, 9, 3, 10, 20, 5, // 70-79: 102 values
1240            2, 3, 17, 4, 8, 12, 15, 6, 3, 20, // 80-89: 90 values
1241            15, 20, 1, 19, 16, // 90-94: 71 values
1242        ];
1243
1244        // Build the ListArray
1245        let mut list_builder = ListBuilder::new(Int32Builder::new());
1246        let mut total_values = 0;
1247
1248        for size in &list_sizes {
1249            for i in 0..*size {
1250                list_builder.values().append_value(i);
1251            }
1252            list_builder.append(true);
1253            total_values += size;
1254        }
1255
1256        let list_array = Arc::new(list_builder.finish());
1257
1258        // Verify we have the expected number of values
1259        assert_eq!(list_array.len(), 95);
1260        assert_eq!(total_values, 1025);
1261
1262        // This should trigger the assertion failure at primitive.rs:1362
1263        // debug_assert!(rows_avail > 0)
1264        let test_cases = TestCases::default().with_structural_encodings();
1265
1266        // The bug manifests when encoding this specific pattern
1267        // Expected: successful round-trip encoding
1268        // Actual: panic at primitive.rs:1362 - assertion failed: rows_avail > 0
1269        check_round_trip_encoding_of_data(vec![list_array], &test_cases, HashMap::new()).await;
1270    }
1271
1272    #[rstest]
1273    #[test_log::test(tokio::test)]
1274    async fn test_sparse_large_string_list(
1275        #[values(STRUCTURAL_ENCODING_MINIBLOCK, STRUCTURAL_ENCODING_FULLZIP)]
1276        structural_encoding: &str,
1277    ) {
1278        // Three chunks' worth of rep/def levels (1 rep bit + 1 def bit each), so the
1279        // planner must split the page. See #6184.
1280        let levels_per_chunk =
1281            crate::encodings::logical::primitive::miniblock::max_repdef_levels_per_chunk(2);
1282        let num_rows = (levels_per_chunk * 3) as u32;
1283        let num_non_empty = 100u32;
1284        let strings_per_list = 10;
1285
1286        let items_builder = StringBuilder::new();
1287        let mut list_builder = ListBuilder::new(items_builder);
1288
1289        // Spread non-empty lists evenly across the range
1290        let step = num_rows / num_non_empty;
1291        let mut next_non_empty = step / 2;
1292
1293        for i in 0..num_rows {
1294            if i == next_non_empty {
1295                let vals: Vec<Option<&str>> = (0..strings_per_list)
1296                    .map(|j| match j % 4 {
1297                        0 => Some("a"),
1298                        1 => Some("bb"),
1299                        2 => Some("ccc"),
1300                        _ => Some("d"),
1301                    })
1302                    .collect();
1303                list_builder.append_value(vals);
1304                next_non_empty = next_non_empty.saturating_add(step);
1305            } else {
1306                list_builder.append_value([] as [Option<&str>; 0]);
1307            }
1308        }
1309        let list_array = list_builder.finish();
1310
1311        let mut field_metadata = HashMap::new();
1312        field_metadata.insert(
1313            STRUCTURAL_ENCODING_META_KEY.to_string(),
1314            structural_encoding.into(),
1315        );
1316
1317        let list_array = Arc::new(list_array) as ArrayRef;
1318        let pages = try_encode_v22_pages_with_metadata(list_array.clone(), field_metadata.clone())
1319            .await
1320            .unwrap();
1321        if structural_encoding == STRUCTURAL_ENCODING_MINIBLOCK {
1322            assert_split_miniblock_layout(&pages, 2, true);
1323        }
1324
1325        let chunk_boundary = levels_per_chunk;
1326        let test_cases = TestCases::default()
1327            .with_range(chunk_boundary - 2..chunk_boundary + 2)
1328            .with_indices(vec![
1329                0,
1330                (step / 2) as u64,
1331                chunk_boundary - 1,
1332                chunk_boundary,
1333                num_rows as u64 - 1,
1334            ])
1335            .with_batch_size(64 * 1024)
1336            .with_page_sizes(vec![1024 * 1024])
1337            .with_encoding(TestEncoding::StructuralU32);
1338        check_round_trip_encoding_of_data(vec![list_array], &test_cases, field_metadata).await;
1339    }
1340
1341    #[test_log::test(tokio::test)]
1342    async fn test_sparse_boolean_list_uses_miniblock() {
1343        // Redacted reproduction from a production schema shape containing ARRAY(BOOLEAN).
1344        // The field names are not relevant; the failure requires sparse list structure
1345        // with a 1-bit Boolean leaf value.
1346        let levels_per_chunk =
1347            crate::encodings::logical::primitive::miniblock::max_repdef_levels_per_chunk(2);
1348        // One row past the chunk limit forces a split. Keeping values at both ends ensures
1349        // both sides of that split remain mini-block pages instead of structural-only pages.
1350        let num_rows = (levels_per_chunk + 1) as usize;
1351        let booleans_per_list = 8usize;
1352
1353        let mut offsets = Vec::with_capacity(num_rows + 1);
1354        let mut values = Vec::with_capacity(2 * booleans_per_list);
1355        offsets.push(0i32);
1356
1357        for row in 0..num_rows {
1358            if row == 0 || row == num_rows - 1 {
1359                values.extend((0..booleans_per_list).map(|idx| idx % 2 == 0));
1360            }
1361            offsets.push(values.len() as i32);
1362        }
1363
1364        let items = BooleanArray::from(values);
1365        let list_array = ListArray::new(
1366            Arc::new(Field::new("item", DataType::Boolean, true)),
1367            OffsetBuffer::new(ScalarBuffer::from(offsets)),
1368            Arc::new(items),
1369            None,
1370        );
1371
1372        let test_cases = TestCases::default()
1373            .with_range(0..1000)
1374            .with_indices(vec![0, levels_per_chunk / 2, num_rows as u64 - 1])
1375            .with_batch_size(64 * 1024)
1376            .with_page_sizes(vec![1024 * 1024])
1377            .with_encoding(TestEncoding::StructuralU32);
1378        let list_array = Arc::new(list_array) as ArrayRef;
1379        let pages = encode_v22_pages(list_array.clone()).await;
1380        assert_split_miniblock_layout(&pages, 2, false);
1381        check_round_trip_encoding_of_data(vec![list_array], &test_cases, HashMap::new()).await;
1382    }
1383
1384    #[test_log::test(tokio::test)]
1385    async fn test_sparse_boolean_list_with_long_empty_prefix() {
1386        let empty_prefix_rows = 70_000usize;
1387        let trailing_empty_rows = 9usize;
1388        let booleans_per_list = 8usize;
1389        let num_rows = empty_prefix_rows + 1 + trailing_empty_rows;
1390
1391        let mut offsets = Vec::with_capacity(num_rows + 1);
1392        offsets.extend(std::iter::repeat_n(0i32, empty_prefix_rows + 1));
1393        let values = (0..booleans_per_list)
1394            .map(|idx| idx % 2 == 0)
1395            .collect::<Vec<_>>();
1396        offsets.push(values.len() as i32);
1397        offsets.extend(std::iter::repeat_n(
1398            values.len() as i32,
1399            trailing_empty_rows,
1400        ));
1401
1402        let items = BooleanArray::from(values);
1403        let list_array = ListArray::new(
1404            Arc::new(Field::new("item", DataType::Boolean, true)),
1405            OffsetBuffer::new(ScalarBuffer::from(offsets)),
1406            Arc::new(items),
1407            None,
1408        );
1409
1410        let test_cases = TestCases::default()
1411            .with_range(0..num_rows as u64)
1412            .with_indices(vec![0, empty_prefix_rows as u64, num_rows as u64 - 1])
1413            .with_dense_encodings();
1414        let list_array = Arc::new(list_array) as ArrayRef;
1415        let pages = encode_v22_pages(list_array.clone()).await;
1416        assert_split_miniblock_layout(&pages, 1, true);
1417        check_round_trip_encoding_of_data(vec![list_array], &test_cases, HashMap::new()).await;
1418    }
1419
1420    #[rstest]
1421    #[test_log::test(tokio::test)]
1422    async fn test_sparse_boolean_list_with_long_null_prefix(
1423        #[values(
1424            TestEncoding::Array,
1425            TestEncoding::StructuralU16,
1426            TestEncoding::StructuralU32
1427        )]
1428        encoding: TestEncoding,
1429    ) {
1430        let null_prefix_rows = 70_000usize;
1431        let trailing_empty_rows = 9usize;
1432        let booleans_per_list = 8usize;
1433        let num_rows = null_prefix_rows + 1 + trailing_empty_rows;
1434
1435        let mut offsets = Vec::with_capacity(num_rows + 1);
1436        offsets.extend(std::iter::repeat_n(0i32, null_prefix_rows + 1));
1437        let values = (0..booleans_per_list)
1438            .map(|idx| idx % 2 == 0)
1439            .collect::<Vec<_>>();
1440        offsets.push(values.len() as i32);
1441        offsets.extend(std::iter::repeat_n(
1442            values.len() as i32,
1443            trailing_empty_rows,
1444        ));
1445        let validity = BooleanBuffer::from_iter((0..num_rows).map(|row| row >= null_prefix_rows));
1446
1447        let items = BooleanArray::from(values);
1448        let list_array = ListArray::new(
1449            Arc::new(Field::new("item", DataType::Boolean, true)),
1450            OffsetBuffer::new(ScalarBuffer::from(offsets)),
1451            Arc::new(items),
1452            Some(NullBuffer::new(validity)),
1453        );
1454
1455        let test_cases = TestCases::default()
1456            .with_range(0..num_rows as u64)
1457            .with_indices(vec![0, null_prefix_rows as u64, num_rows as u64 - 1])
1458            .with_encoding(encoding);
1459        let list_array = Arc::new(list_array) as ArrayRef;
1460        let pages = encode_v22_pages(list_array.clone()).await;
1461        assert_split_miniblock_layout(&pages, 1, true);
1462        check_round_trip_encoding_of_data(vec![list_array], &test_cases, HashMap::new()).await;
1463    }
1464
1465    #[test_log::test(tokio::test)]
1466    async fn test_sparse_boolean_list_with_amortized_long_empty_prefix() {
1467        let empty_prefix_rows = 62_000usize;
1468        let booleans_per_list = 8_192usize;
1469        let num_rows = empty_prefix_rows + 1;
1470
1471        let mut offsets = Vec::with_capacity(num_rows + 1);
1472        offsets.extend(std::iter::repeat_n(0i32, empty_prefix_rows + 1));
1473        let values = (0..booleans_per_list)
1474            .map(|idx| idx % 2 == 0)
1475            .collect::<Vec<_>>();
1476        offsets.push(values.len() as i32);
1477
1478        let items = BooleanArray::from(values);
1479        let list_array = ListArray::new(
1480            Arc::new(Field::new("item", DataType::Boolean, true)),
1481            OffsetBuffer::new(ScalarBuffer::from(offsets)),
1482            Arc::new(items),
1483            None,
1484        );
1485
1486        let test_cases = TestCases::default()
1487            .with_range(0..num_rows as u64)
1488            .with_indices(vec![0, empty_prefix_rows as u64])
1489            .with_dense_encodings();
1490        let list_array = Arc::new(list_array) as ArrayRef;
1491        let pages = encode_v22_pages(list_array.clone()).await;
1492        assert_split_miniblock_layout(&pages, 1, true);
1493        check_round_trip_encoding_of_data(vec![list_array], &test_cases, HashMap::new()).await;
1494    }
1495
1496    fn unsplittable_nested_list(items: ArrayRef, empty_inner_lists: usize) -> ArrayRef {
1497        let mut inner_offsets = vec![0i32; empty_inner_lists + 1];
1498        inner_offsets.push(items.len() as i32);
1499        let inner_list = ListArray::new(
1500            Arc::new(Field::new("item", items.data_type().clone(), true)),
1501            OffsetBuffer::new(ScalarBuffer::from(inner_offsets)),
1502            items,
1503            None,
1504        );
1505        Arc::new(ListArray::new(
1506            Arc::new(Field::new("item", inner_list.data_type().clone(), true)),
1507            OffsetBuffer::new(ScalarBuffer::from(vec![0i32, empty_inner_lists as i32 + 1])),
1508            Arc::new(inner_list),
1509            None,
1510        ))
1511    }
1512
1513    #[rstest]
1514    #[case::boolean(Arc::new(BooleanArray::from(vec![true, false, true, false, true, false, true, false])))]
1515    #[case::string(Arc::new(StringArray::from(vec!["value", "other"])))]
1516    #[test_log::test(tokio::test)]
1517    async fn test_nested_sparse_single_row_falls_back_to_fullzip(#[case] items: ArrayRef) {
1518        let list = unsplittable_nested_list(items, 70_000);
1519        let pages = encode_v22_pages(list.clone()).await;
1520        assert_has_fullzip_layout(&pages);
1521
1522        let test_cases = TestCases::default()
1523            .with_range(0..1)
1524            .with_indices(vec![0])
1525            .with_dense_encodings();
1526        check_round_trip_encoding_of_data(vec![list], &test_cases, HashMap::new()).await;
1527    }
1528
1529    /// Builds the HNSW-flush repro shape: a dense prefix where every row has
1530    /// `NEIGHBORS_PER_ROW` distinct values, followed by a long tail of empty
1531    /// lists. Mirrors `HNSW::schema()` `__neighbors` / `__dists` columns:
1532    /// dense level-0 lists, then ~6x as many mostly-empty higher-level rows.
1533    fn make_hnsw_shaped_list_u32() -> ListArray {
1534        const DENSE_ROWS: u32 = 5_000;
1535        const NEIGHBORS_PER_ROW: u32 = 32;
1536        const EMPTY_TAIL_ROWS: u32 = 70_000;
1537
1538        let mut list_builder = ListBuilder::new(UInt32Builder::new());
1539        let mut next_val: u32 = 0;
1540        for _ in 0..DENSE_ROWS {
1541            for _ in 0..NEIGHBORS_PER_ROW {
1542                list_builder.values().append_value(next_val);
1543                next_val = next_val.wrapping_add(1);
1544            }
1545            list_builder.append(true);
1546        }
1547        for _ in 0..EMPTY_TAIL_ROWS {
1548            list_builder.append(true);
1549        }
1550        list_builder.finish()
1551    }
1552
1553    /// Reproduces the HNSW-flush shape at v2.2 on the auto path (no
1554    /// `STRUCTURAL_ENCODING` metadata): a dense level-0 prefix followed by a
1555    /// long tail of empty lists. The global levels/values ratio looks dense,
1556    /// so this used to encode as a single mini-block page whose final chunk
1557    /// absorbed every trailing empty list and overflowed the per-chunk `u16`
1558    /// `num_levels`, corrupting the read. The structural page planner now
1559    /// splits on top-level row boundaries: the dense prefix stays on
1560    /// mini-block pages and the empty tail becomes structural-only pages, so
1561    /// the round-trip is lossless without falling back to full-zip.
1562    #[test_log::test(tokio::test)]
1563    async fn test_list_hnsw_shape_splits_to_miniblock_v2_2() {
1564        let list_array = make_hnsw_shaped_list_u32();
1565        let dense_rows: u64 = 5_000;
1566        let total_rows = list_array.len() as u64;
1567
1568        let test_cases = TestCases::default()
1569            .with_range(0..1000)
1570            .with_range(dense_rows.saturating_sub(8)..(dense_rows + 8))
1571            .with_range(0..total_rows)
1572            .with_indices(vec![0, dense_rows - 1, dense_rows, total_rows - 1])
1573            .with_encoding(TestEncoding::StructuralU32);
1574        let list_array = Arc::new(list_array) as ArrayRef;
1575        let pages = encode_v22_pages(list_array.clone()).await;
1576        assert_split_miniblock_layout(&pages, 1, true);
1577        check_round_trip_encoding_of_data(vec![list_array], &test_cases, HashMap::new()).await;
1578    }
1579
1580    /// Companion to the auto-path test: even when the user explicitly requests
1581    /// `STRUCTURAL_ENCODING_MINIBLOCK`, the structural page planner splits the
1582    /// HNSW shape so every emitted page fits the mini-block per-chunk budget.
1583    /// The request is honored (the dense prefix stays on mini-block pages
1584    /// rather than being forced to full-zip) and the round-trip is lossless.
1585    #[test_log::test(tokio::test)]
1586    async fn test_forced_miniblock_hnsw_shape_splits_to_miniblock_v2_2() {
1587        let list_array = make_hnsw_shaped_list_u32();
1588        let total_rows = list_array.len() as u64;
1589
1590        let mut field_metadata = HashMap::new();
1591        field_metadata.insert(
1592            STRUCTURAL_ENCODING_META_KEY.to_string(),
1593            STRUCTURAL_ENCODING_MINIBLOCK.into(),
1594        );
1595
1596        let test_cases = TestCases::default()
1597            .with_range(0..total_rows)
1598            .with_encoding(TestEncoding::StructuralU32);
1599        let list_array = Arc::new(list_array) as ArrayRef;
1600        let pages = try_encode_v22_pages_with_metadata(list_array.clone(), field_metadata.clone())
1601            .await
1602            .unwrap();
1603        assert_split_miniblock_layout(&pages, 1, true);
1604        check_round_trip_encoding_of_data(vec![list_array], &test_cases, field_metadata).await;
1605    }
1606}