Skip to main content

parquet/arrow/arrow_writer/
byte_array.rs

1// Licensed to the Apache Software Foundation (ASF) under one
2// or more contributor license agreements.  See the NOTICE file
3// distributed with this work for additional information
4// regarding copyright ownership.  The ASF licenses this file
5// to you under the Apache License, Version 2.0 (the
6// "License"); you may not use this file except in compliance
7// with the License.  You may obtain a copy of the License at
8//
9//   http://www.apache.org/licenses/LICENSE-2.0
10//
11// Unless required by applicable law or agreed to in writing,
12// software distributed under the License is distributed on an
13// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14// KIND, either express or implied.  See the License for the
15// specific language governing permissions and limitations
16// under the License.
17
18use crate::basic::Encoding;
19use crate::bloom_filter::Sbbf;
20use crate::column::writer::encoder::{
21    ColumnValueEncoder, DataPageValues, DictionaryPage, create_bloom_filter,
22};
23use crate::data_type::{AsBytes, ByteArray, Int32Type};
24use crate::encodings::encoding::{DeltaBitPackEncoder, Encoder};
25use crate::encodings::rle::RleEncoder;
26use crate::errors::{ParquetError, Result};
27use crate::file::properties::{EnabledStatistics, WriterProperties, WriterVersion};
28use crate::geospatial::accumulator::{GeoStatsAccumulator, try_new_geo_stats_accumulator};
29use crate::geospatial::statistics::GeospatialStatistics;
30use crate::schema::types::ColumnDescPtr;
31use crate::util::bit_util::num_required_bits;
32use crate::util::interner::{Interner, Storage};
33use crate::util::prefix::common_prefix_length;
34use arrow_array::types::ByteArrayType;
35use arrow_array::{
36    Array, ArrayAccessor, BinaryArray, BinaryViewArray, DictionaryArray, FixedSizeBinaryArray,
37    GenericByteArray, LargeBinaryArray, LargeStringArray, StringArray, StringViewArray,
38};
39use arrow_buffer::{ArrowNativeType, Buffer};
40use arrow_schema::DataType;
41
42macro_rules! downcast_dict_impl {
43    ($array:ident, $key:ident, $val:ident, $op:expr $(, $arg:expr)*) => {{
44        $op($array
45            .as_any()
46            .downcast_ref::<DictionaryArray<arrow_array::types::$key>>()
47            .unwrap()
48            .downcast_dict::<$val>()
49            .unwrap()$(, $arg)*)
50    }};
51}
52
53macro_rules! downcast_dict_op {
54    ($key_type:expr, $val:ident, $array:ident, $op:expr $(, $arg:expr)*) => {
55        match $key_type.as_ref() {
56            DataType::UInt8 => downcast_dict_impl!($array, UInt8Type, $val, $op$(, $arg)*),
57            DataType::UInt16 => downcast_dict_impl!($array, UInt16Type, $val, $op$(, $arg)*),
58            DataType::UInt32 => downcast_dict_impl!($array, UInt32Type, $val, $op$(, $arg)*),
59            DataType::UInt64 => downcast_dict_impl!($array, UInt64Type, $val, $op$(, $arg)*),
60            DataType::Int8 => downcast_dict_impl!($array, Int8Type, $val, $op$(, $arg)*),
61            DataType::Int16 => downcast_dict_impl!($array, Int16Type, $val, $op$(, $arg)*),
62            DataType::Int32 => downcast_dict_impl!($array, Int32Type, $val, $op$(, $arg)*),
63            DataType::Int64 => downcast_dict_impl!($array, Int64Type, $val, $op$(, $arg)*),
64            _ => unreachable!(),
65        }
66    };
67}
68
69macro_rules! downcast_op {
70    ($data_type:expr, $array:ident, $op:expr $(, $arg:expr)*) => {
71        match $data_type {
72            DataType::Utf8 => $op($array.as_any().downcast_ref::<StringArray>().unwrap()$(, $arg)*),
73            DataType::LargeUtf8 => {
74                $op($array.as_any().downcast_ref::<LargeStringArray>().unwrap()$(, $arg)*)
75            }
76            DataType::Utf8View => $op($array.as_any().downcast_ref::<StringViewArray>().unwrap()$(, $arg)*),
77            DataType::Binary => {
78                $op($array.as_any().downcast_ref::<BinaryArray>().unwrap()$(, $arg)*)
79            }
80            DataType::LargeBinary => {
81                $op($array.as_any().downcast_ref::<LargeBinaryArray>().unwrap()$(, $arg)*)
82            }
83            DataType::BinaryView => {
84                $op($array.as_any().downcast_ref::<BinaryViewArray>().unwrap()$(, $arg)*)
85            }
86            DataType::Dictionary(key, value) => match value.as_ref() {
87                DataType::Utf8 => downcast_dict_op!(key, StringArray, $array, $op$(, $arg)*),
88                DataType::LargeUtf8 => {
89                    downcast_dict_op!(key, LargeStringArray, $array, $op$(, $arg)*)
90                }
91                DataType::Binary => downcast_dict_op!(key, BinaryArray, $array, $op$(, $arg)*),
92                DataType::LargeBinary => {
93                    downcast_dict_op!(key, LargeBinaryArray, $array, $op$(, $arg)*)
94                }
95                DataType::FixedSizeBinary(_) => {
96                    downcast_dict_op!(key, FixedSizeBinaryArray, $array, $op$(, $arg)*)
97                }
98                d => unreachable!("cannot downcast {} dictionary value to byte array", d),
99            },
100            d => unreachable!("cannot downcast {} to byte array", d),
101        }
102    };
103}
104
105/// A fallback encoder, i.e. non-dictionary, for [`ByteArray`]
106struct FallbackEncoder {
107    encoder: FallbackEncoderImpl,
108    num_values: usize,
109    variable_length_bytes: i64,
110}
111
112/// The fallback encoder in use
113///
114/// Note: DeltaBitPackEncoder is boxed as it is rather large
115enum FallbackEncoderImpl {
116    Plain {
117        buffer: Vec<u8>,
118    },
119    DeltaLength {
120        buffer: Vec<u8>,
121        lengths: Box<DeltaBitPackEncoder<Int32Type>>,
122    },
123    Delta {
124        buffer: Vec<u8>,
125        last_value: Vec<u8>,
126        prefix_lengths: Box<DeltaBitPackEncoder<Int32Type>>,
127        suffix_lengths: Box<DeltaBitPackEncoder<Int32Type>>,
128    },
129}
130
131impl FallbackEncoder {
132    /// Create the fallback encoder for the given [`ColumnDescPtr`] and [`WriterProperties`]
133    fn new(descr: &ColumnDescPtr, props: &WriterProperties) -> Result<Self> {
134        // Set either main encoder or fallback encoder.
135        let encoding =
136            props
137                .encoding(descr.path())
138                .unwrap_or_else(|| match props.writer_version() {
139                    WriterVersion::PARQUET_1_0 => Encoding::PLAIN,
140                    WriterVersion::PARQUET_2_0 => Encoding::DELTA_BYTE_ARRAY,
141                });
142
143        let encoder = match encoding {
144            Encoding::PLAIN => FallbackEncoderImpl::Plain { buffer: vec![] },
145            Encoding::DELTA_LENGTH_BYTE_ARRAY => FallbackEncoderImpl::DeltaLength {
146                buffer: vec![],
147                lengths: Box::new(DeltaBitPackEncoder::new()),
148            },
149            Encoding::DELTA_BYTE_ARRAY => FallbackEncoderImpl::Delta {
150                buffer: vec![],
151                last_value: vec![],
152                prefix_lengths: Box::new(DeltaBitPackEncoder::new()),
153                suffix_lengths: Box::new(DeltaBitPackEncoder::new()),
154            },
155            _ => {
156                return Err(general_err!(
157                    "unsupported encoding {} for byte array",
158                    encoding
159                ));
160            }
161        };
162
163        Ok(Self {
164            encoder,
165            num_values: 0,
166            variable_length_bytes: 0,
167        })
168    }
169
170    /// Encode `values` to the in-progress page
171    fn encode<T>(&mut self, values: T, indices: impl ExactSizeIterator<Item = usize>)
172    where
173        T: ArrayAccessor + Copy,
174        T::Item: AsRef<[u8]>,
175    {
176        self.num_values += indices.len();
177        match &mut self.encoder {
178            FallbackEncoderImpl::Plain { buffer } => {
179                for idx in indices {
180                    let value = values.value(idx);
181                    let value = value.as_ref();
182                    buffer.extend_from_slice((value.len() as u32).as_bytes());
183                    buffer.extend_from_slice(value);
184                    self.variable_length_bytes += value.len() as i64;
185                }
186            }
187            FallbackEncoderImpl::DeltaLength { buffer, lengths } => {
188                for idx in indices {
189                    let value = values.value(idx);
190                    let value = value.as_ref();
191                    lengths.put(&[value.len() as i32]).unwrap();
192                    buffer.extend_from_slice(value);
193                    self.variable_length_bytes += value.len() as i64;
194                }
195            }
196            FallbackEncoderImpl::Delta {
197                buffer,
198                last_value,
199                prefix_lengths,
200                suffix_lengths,
201            } => {
202                for idx in indices {
203                    let value = values.value(idx);
204                    let value = value.as_ref();
205
206                    let prefix_length = common_prefix_length(last_value, value);
207                    let suffix_length = value.len() - prefix_length;
208
209                    last_value.clear();
210                    last_value.extend_from_slice(value);
211
212                    buffer.extend_from_slice(&value[prefix_length..]);
213                    prefix_lengths.put(&[prefix_length as i32]).unwrap();
214                    suffix_lengths.put(&[suffix_length as i32]).unwrap();
215                    self.variable_length_bytes += value.len() as i64;
216                }
217            }
218        }
219    }
220
221    /// Returns an estimate of the data page size in bytes
222    ///
223    /// This includes:
224    /// <already_written_encoded_byte_size> + <estimated_encoded_size_of_unflushed_bytes>
225    fn estimated_data_page_size(&self) -> usize {
226        match &self.encoder {
227            FallbackEncoderImpl::Plain { buffer, .. } => buffer.len(),
228            FallbackEncoderImpl::DeltaLength { buffer, lengths } => {
229                buffer.len() + lengths.estimated_data_encoded_size()
230            }
231            FallbackEncoderImpl::Delta {
232                buffer,
233                prefix_lengths,
234                suffix_lengths,
235                ..
236            } => {
237                buffer.len()
238                    + prefix_lengths.estimated_data_encoded_size()
239                    + suffix_lengths.estimated_data_encoded_size()
240            }
241        }
242    }
243
244    fn flush_data_page(
245        &mut self,
246        min_value: Option<ByteArray>,
247        max_value: Option<ByteArray>,
248    ) -> Result<DataPageValues<ByteArray>> {
249        let (buf, encoding) = match &mut self.encoder {
250            FallbackEncoderImpl::Plain { buffer } => (std::mem::take(buffer), Encoding::PLAIN),
251            FallbackEncoderImpl::DeltaLength { buffer, lengths } => {
252                let lengths = lengths.flush_buffer()?;
253
254                let mut out = Vec::with_capacity(lengths.len() + buffer.len());
255                out.extend_from_slice(&lengths);
256                out.extend_from_slice(buffer);
257                buffer.clear();
258                (out, Encoding::DELTA_LENGTH_BYTE_ARRAY)
259            }
260            FallbackEncoderImpl::Delta {
261                buffer,
262                prefix_lengths,
263                suffix_lengths,
264                last_value,
265            } => {
266                let prefix_lengths = prefix_lengths.flush_buffer()?;
267                let suffix_lengths = suffix_lengths.flush_buffer()?;
268
269                let mut out =
270                    Vec::with_capacity(prefix_lengths.len() + suffix_lengths.len() + buffer.len());
271                out.extend_from_slice(&prefix_lengths);
272                out.extend_from_slice(&suffix_lengths);
273                out.extend_from_slice(buffer);
274                buffer.clear();
275                last_value.clear();
276                (out, Encoding::DELTA_BYTE_ARRAY)
277            }
278        };
279
280        // Capture value of variable_length_bytes and reset for next page
281        let variable_length_bytes = Some(self.variable_length_bytes);
282        self.variable_length_bytes = 0;
283
284        Ok(DataPageValues {
285            buf: buf.into(),
286            num_values: std::mem::take(&mut self.num_values),
287            encoding,
288            min_value,
289            max_value,
290            nan_count: None,
291            variable_length_bytes,
292        })
293    }
294}
295
296/// [`Storage`] for the [`Interner`] used by [`DictEncoder`]
297#[derive(Debug, Default)]
298struct ByteArrayStorage {
299    /// Encoded dictionary data
300    page: Vec<u8>,
301
302    values: Vec<std::ops::Range<usize>>,
303}
304
305impl Storage for ByteArrayStorage {
306    type Key = u64;
307    type Value = [u8];
308
309    fn get(&self, idx: Self::Key) -> &Self::Value {
310        &self.page[self.values[idx as usize].clone()]
311    }
312
313    fn push(&mut self, value: &Self::Value) -> Self::Key {
314        let key = self.values.len();
315
316        self.page.reserve(4 + value.len());
317        self.page.extend_from_slice((value.len() as u32).as_bytes());
318
319        let start = self.page.len();
320        self.page.extend_from_slice(value);
321        self.values.push(start..self.page.len());
322
323        key as u64
324    }
325
326    #[allow(dead_code)] // not used in parquet_derive, so is dead there
327    fn estimated_memory_size(&self) -> usize {
328        self.page.capacity() * std::mem::size_of::<u8>()
329            + self.values.capacity() * std::mem::size_of::<std::ops::Range<usize>>()
330    }
331}
332
333/// A dictionary encoder for byte array data
334#[derive(Debug, Default)]
335struct DictEncoder {
336    interner: Interner<ByteArrayStorage>,
337    indices: Vec<u64>,
338    variable_length_bytes: i64,
339}
340
341impl DictEncoder {
342    /// Encode `values` to the in-progress page
343    fn encode<T>(&mut self, values: T, indices: impl ExactSizeIterator<Item = usize>)
344    where
345        T: ArrayAccessor + Copy,
346        T::Item: AsRef<[u8]>,
347    {
348        self.indices.reserve(indices.len());
349
350        for idx in indices {
351            let value = values.value(idx);
352            let interned = self.interner.intern(value.as_ref());
353            self.indices.push(interned);
354            self.variable_length_bytes += value.as_ref().len() as i64;
355        }
356    }
357
358    fn bit_width(&self) -> u8 {
359        let length = self.interner.storage().values.len();
360        num_required_bits(length.saturating_sub(1) as u64)
361    }
362
363    fn estimated_memory_size(&self) -> usize {
364        self.interner.estimated_memory_size() + self.indices.capacity() * std::mem::size_of::<u64>()
365    }
366
367    fn estimated_data_page_size(&self) -> usize {
368        let bit_width = self.bit_width();
369        1 + RleEncoder::max_buffer_size(bit_width, self.indices.len())
370    }
371
372    fn estimated_dict_page_size(&self) -> usize {
373        self.interner.storage().page.len()
374    }
375
376    fn flush_dict_page(self) -> DictionaryPage {
377        let storage = self.interner.into_inner();
378
379        DictionaryPage {
380            buf: storage.page.into(),
381            num_values: storage.values.len(),
382            is_sorted: false,
383        }
384    }
385
386    fn flush_data_page(
387        &mut self,
388        min_value: Option<ByteArray>,
389        max_value: Option<ByteArray>,
390    ) -> DataPageValues<ByteArray> {
391        let num_values = self.indices.len();
392        let buffer_len = self.estimated_data_page_size();
393        let mut buffer = Vec::with_capacity(buffer_len);
394        buffer.push(self.bit_width());
395
396        let mut encoder = RleEncoder::new_from_buf(self.bit_width(), buffer);
397        for index in &self.indices {
398            encoder.put(*index)
399        }
400
401        self.indices.clear();
402
403        // Capture value of variable_length_bytes and reset for next page
404        let variable_length_bytes = Some(self.variable_length_bytes);
405        self.variable_length_bytes = 0;
406
407        DataPageValues {
408            buf: encoder.consume().into(),
409            num_values,
410            encoding: Encoding::RLE_DICTIONARY,
411            min_value,
412            max_value,
413            nan_count: None,
414            variable_length_bytes,
415        }
416    }
417}
418
419pub struct ByteArrayEncoder {
420    fallback: FallbackEncoder,
421    dict_encoder: Option<DictEncoder>,
422    statistics_enabled: EnabledStatistics,
423    min_value: Option<ByteArray>,
424    max_value: Option<ByteArray>,
425    bloom_filter: Option<Sbbf>,
426    bloom_filter_target_fpp: f64,
427    geo_stats_accumulator: Option<Box<dyn GeoStatsAccumulator>>,
428}
429
430impl ColumnValueEncoder for ByteArrayEncoder {
431    type T = ByteArray;
432    type Values = dyn Array;
433    fn flush_bloom_filter(&mut self) -> Option<Sbbf> {
434        let mut sbbf = self.bloom_filter.take()?;
435        sbbf.fold_to_target_fpp(self.bloom_filter_target_fpp);
436        Some(sbbf)
437    }
438
439    fn try_new(descr: &ColumnDescPtr, props: &WriterProperties) -> Result<Self>
440    where
441        Self: Sized,
442    {
443        let dictionary = props
444            .dictionary_enabled(descr.path())
445            .then(DictEncoder::default);
446
447        let fallback = FallbackEncoder::new(descr, props)?;
448
449        let (bloom_filter, bloom_filter_target_fpp) = create_bloom_filter(props, descr)?;
450
451        let statistics_enabled = props.statistics_enabled(descr.path());
452
453        let geo_stats_accumulator = try_new_geo_stats_accumulator(descr);
454
455        Ok(Self {
456            fallback,
457            statistics_enabled,
458            bloom_filter,
459            bloom_filter_target_fpp,
460            dict_encoder: dictionary,
461            min_value: None,
462            max_value: None,
463            geo_stats_accumulator,
464        })
465    }
466
467    fn write(&mut self, _values: &Self::Values, _offset: usize, _len: usize) -> Result<()> {
468        unreachable!("should call write_gather instead")
469    }
470
471    fn write_gather(&mut self, values: &Self::Values, indices: &[usize]) -> Result<()> {
472        downcast_op!(
473            values.data_type(),
474            values,
475            encode,
476            indices.iter().copied(),
477            self
478        );
479        Ok(())
480    }
481
482    fn count_values_within_byte_budget_gather(
483        values: &Self::Values,
484        indices: &[usize],
485        byte_budget: usize,
486    ) -> Option<usize> {
487        // `ByteArrayEncoder` only ever writes via `write_gather`, so this
488        // is the relevant method.
489        //
490        // Two-stage walk for the simple offset-buffer byte array types:
491        //   1. If indices are contiguous, compute the total payload in
492        //      O(1) via a single subtraction on the offsets buffer.
493        //      When the total fits the budget — the overwhelmingly
494        //      common "small values" case — return immediately.
495        //   2. Otherwise, walk per-value byte sizes from the offsets
496        //      buffer (still cheap, no slice/UTF-8 construction) and
497        //      exit at the first value that pushes the cumulative sum
498        //      past the budget. This bounds skewed distributions: an
499        //      outlier value is caught wherever it lands in the chunk.
500        let count = match values.data_type() {
501            DataType::Utf8 => count_within_budget_offsets(
502                values.as_any().downcast_ref::<StringArray>().unwrap(),
503                indices,
504                byte_budget,
505            ),
506            DataType::LargeUtf8 => count_within_budget_offsets(
507                values.as_any().downcast_ref::<LargeStringArray>().unwrap(),
508                indices,
509                byte_budget,
510            ),
511            DataType::Binary => count_within_budget_offsets(
512                values.as_any().downcast_ref::<BinaryArray>().unwrap(),
513                indices,
514                byte_budget,
515            ),
516            DataType::LargeBinary => count_within_budget_offsets(
517                values.as_any().downcast_ref::<LargeBinaryArray>().unwrap(),
518                indices,
519                byte_budget,
520            ),
521            // View arrays carry each value's length in the low 32 bits of
522            // its u128 view word, so lengths are scannable without touching
523            // any data buffer — and the common small-value case skips even
524            // that scan via an O(1) conservative bound.
525            DataType::Utf8View => {
526                let array = values.as_any().downcast_ref::<StringViewArray>().unwrap();
527                count_within_budget_views(
528                    array.views(),
529                    indices,
530                    byte_budget,
531                    max_view_value_len(array.data_buffers()),
532                )
533            }
534            DataType::BinaryView => {
535                let array = values.as_any().downcast_ref::<BinaryViewArray>().unwrap();
536                count_within_budget_views(
537                    array.views(),
538                    indices,
539                    byte_budget,
540                    max_view_value_len(array.data_buffers()),
541                )
542            }
543            // The values in an arrow dictionary are already small and
544            // deduplicated, so there is nothing to bound — treat every
545            // chunk as fitting and stay on the batched path. (A per-value
546            // walk through dict keys on every chunk also measured ~+30-80%
547            // slower than `main`.)
548            DataType::Dictionary(_, _) => indices.len(),
549            // Every byte-array type `ByteArrayEncoder` is constructed for
550            // has an explicit arm above. A `Dictionary(value = FixedSizeBinary)`
551            // column hits the `Dictionary(_, _)` arm (its `values.data_type()`
552            // is `Dictionary`), and a bare `FixedSizeBinary` column is routed
553            // to the generic column writer, never this encoder — so no other
554            // type can reach here.
555            data_type => unreachable!("ByteArrayEncoder cannot be constructed for {data_type:?}"),
556        };
557        Some(count)
558    }
559
560    fn num_values(&self) -> usize {
561        match &self.dict_encoder {
562            Some(encoder) => encoder.indices.len(),
563            None => self.fallback.num_values,
564        }
565    }
566
567    fn has_dictionary(&self) -> bool {
568        self.dict_encoder.is_some()
569    }
570
571    fn estimated_memory_size(&self) -> usize {
572        let encoder_size = match &self.dict_encoder {
573            Some(encoder) => encoder.estimated_memory_size(),
574            // For the FallbackEncoder, these unflushed bytes are already encoded.
575            // Therefore, the size should be the same as estimated_data_page_size.
576            None => self.fallback.estimated_data_page_size(),
577        };
578
579        let bloom_filter_size = self
580            .bloom_filter
581            .as_ref()
582            .map(|bf| bf.estimated_memory_size())
583            .unwrap_or_default();
584
585        let stats_size = self.min_value.as_ref().map(|v| v.len()).unwrap_or_default()
586            + self.max_value.as_ref().map(|v| v.len()).unwrap_or_default();
587
588        encoder_size + bloom_filter_size + stats_size
589    }
590
591    fn estimated_dict_page_size(&self) -> Option<usize> {
592        Some(self.dict_encoder.as_ref()?.estimated_dict_page_size())
593    }
594
595    /// Returns an estimate of the data page size in bytes
596    ///
597    /// This includes:
598    /// <already_written_encoded_byte_size> + <estimated_encoded_size_of_unflushed_bytes>
599    fn estimated_data_page_size(&self) -> usize {
600        match &self.dict_encoder {
601            Some(encoder) => encoder.estimated_data_page_size(),
602            None => self.fallback.estimated_data_page_size(),
603        }
604    }
605
606    fn flush_dict_page(&mut self) -> Result<Option<DictionaryPage>> {
607        match self.dict_encoder.take() {
608            Some(encoder) => {
609                if !encoder.indices.is_empty() {
610                    return Err(general_err!(
611                        "Must flush data pages before flushing dictionary"
612                    ));
613                }
614
615                Ok(Some(encoder.flush_dict_page()))
616            }
617            _ => Ok(None),
618        }
619    }
620
621    fn flush_data_page(&mut self) -> Result<DataPageValues<ByteArray>> {
622        let min_value = self.min_value.take();
623        let max_value = self.max_value.take();
624
625        match &mut self.dict_encoder {
626            Some(encoder) => Ok(encoder.flush_data_page(min_value, max_value)),
627            _ => self.fallback.flush_data_page(min_value, max_value),
628        }
629    }
630
631    fn flush_geospatial_statistics(&mut self) -> Option<Box<GeospatialStatistics>> {
632        self.geo_stats_accumulator.as_mut().map(|a| a.finish())?
633    }
634}
635
636/// Encodes the provided `values` and `indices` to `encoder`
637///
638/// This is a free function so it can be used with `downcast_op!`
639fn encode<T, I>(values: T, indices: I, encoder: &mut ByteArrayEncoder)
640where
641    T: ArrayAccessor + Copy,
642    T::Item: Copy + Ord + AsRef<[u8]>,
643    I: ExactSizeIterator<Item = usize> + Clone,
644{
645    if encoder.statistics_enabled != EnabledStatistics::None {
646        if let Some(accumulator) = encoder.geo_stats_accumulator.as_mut() {
647            update_geo_stats_accumulator(accumulator.as_mut(), values, indices.clone());
648        } else if let Some((min, max)) = compute_min_max(values, indices.clone()) {
649            if encoder.min_value.as_ref().is_none_or(|m| m > &min) {
650                encoder.min_value = Some(min);
651            }
652
653            if encoder.max_value.as_ref().is_none_or(|m| m < &max) {
654                encoder.max_value = Some(max);
655            }
656        }
657    }
658
659    // encode the values into bloom filter if enabled
660    if let Some(bloom_filter) = &mut encoder.bloom_filter {
661        for idx in indices.clone() {
662            bloom_filter.insert(values.value(idx).as_ref());
663        }
664    }
665
666    match &mut encoder.dict_encoder {
667        Some(dict_encoder) => dict_encoder.encode(values, indices),
668        None => encoder.fallback.encode(values, indices),
669    }
670}
671
672/// Upper bound on any single value's byte length in a view array.
673fn max_view_value_len(buffers: &[Buffer]) -> usize {
674    /// Bytes that fit inline in a u128 view word (the rest is len + prefix).
675    const MAX_INLINE_VIEW_LEN: usize = 12;
676    // An out-of-line view's data is a contiguous slice of exactly one data
677    // buffer, so it cannot exceed the largest buffer; inline views hold at
678    // most `MAX_INLINE_VIEW_LEN`. Loose (a value is usually far smaller than
679    // a whole buffer) but O(number of buffers) and always sound.
680    buffers
681        .iter()
682        .map(|b| b.len())
683        .max()
684        .unwrap_or(0)
685        .max(MAX_INLINE_VIEW_LEN)
686}
687
688/// Number of leading `indices` whose cumulative plain-encoded size fits
689/// `byte_budget` (boundary value included), for view arrays (`Utf8View`,
690/// `BinaryView`).
691fn count_within_budget_views(
692    views: &[u128],
693    indices: &[usize],
694    byte_budget: usize,
695    max_value_len: usize,
696) -> usize {
697    // Each plain-encoded BYTE_ARRAY value carries a 4-byte length prefix, so
698    // the budget is compared against `value_len + size_of::<u32>()` — the
699    // bytes actually written to the page, not just the payload.
700    //
701    // Stage 1: O(1) conservative bound. View arrays have no prefix-sum
702    // offsets buffer, so the exact span subtraction used by
703    // `count_within_budget_offsets` is unavailable; instead bound every
704    // value by `max_value_len`. Skips the walk for the common small-value
705    // case (what view arrays are built for, and where there is nothing to
706    // bound).
707    let per_value = max_value_len + std::mem::size_of::<u32>();
708    if indices.len().saturating_mul(per_value) <= byte_budget {
709        return indices.len();
710    }
711    // Stage 2: exact per-value scan, reading each length from the low 32
712    // bits of its u128 view word (no data-buffer dereference).
713    let mut cum: usize = 0;
714    for (i, idx) in indices.iter().enumerate() {
715        let len = (views[*idx] as u32) as usize;
716        cum = cum.saturating_add(len + std::mem::size_of::<u32>());
717        if cum > byte_budget {
718            return i + 1;
719        }
720    }
721    indices.len()
722}
723
724/// Number of leading `indices` whose cumulative plain-encoded size fits
725/// `byte_budget` (boundary value included), for offset-buffer byte arrays
726/// (`Utf8`/`LargeUtf8`/`Binary`/`LargeBinary`).
727///
728/// `indices` are assumed sorted ascending — they always are here, since
729/// they come from `non_null_indices`, which is built in array order.
730fn count_within_budget_offsets<T: ByteArrayType>(
731    values: &GenericByteArray<T>,
732    indices: &[usize],
733    byte_budget: usize,
734) -> usize {
735    if indices.is_empty() {
736        return 0;
737    }
738    let n = indices.len();
739    let first = indices[0];
740    let last = indices[n - 1];
741    let offsets = values.value_offsets();
742    // Each plain-encoded value carries a 4-byte length prefix on the page.
743    let prefix_overhead = std::mem::size_of::<u32>();
744
745    // Stage 1: O(1) span upper bound. The span `offsets[last+1] -
746    // offsets[first]` covers every array position in `[first, last]`, a
747    // superset of `indices` — and the skipped positions in a nullable
748    // column are nulls with zero offset delta, so the span still equals the
749    // exact payload. If it fits the budget, every value fits. Covers the
750    // common small-value case for both non-null and (sparse) nullable
751    // columns.
752    if last >= first {
753        let payload = (offsets[last + 1] - offsets[first]).as_usize();
754        if payload + n * prefix_overhead <= byte_budget {
755            return n;
756        }
757    }
758
759    // Stage 2: scan per-index lengths from the offsets buffer.
760    let mut cum: usize = 0;
761    for (i, idx) in indices.iter().enumerate() {
762        let len = (offsets[idx + 1] - offsets[*idx]).as_usize() + prefix_overhead;
763        cum = cum.saturating_add(len);
764        if cum > byte_budget {
765            return i + 1;
766        }
767    }
768    n
769}
770
771/// Computes the min and max for the provided array and indices
772///
773/// This is a free function so it can be used with `downcast_op!`
774fn compute_min_max<T>(
775    array: T,
776    mut valid: impl Iterator<Item = usize>,
777) -> Option<(ByteArray, ByteArray)>
778where
779    T: ArrayAccessor,
780    T::Item: Copy + Ord + AsRef<[u8]>,
781{
782    let first_idx = valid.next()?;
783
784    let first_val = array.value(first_idx);
785    let mut min = first_val;
786    let mut max = first_val;
787    for idx in valid {
788        let val = array.value(idx);
789        min = min.min(val);
790        max = max.max(val);
791    }
792    Some((min.as_ref().to_vec().into(), max.as_ref().to_vec().into()))
793}
794
795/// Updates geospatial statistics for the provided array and indices
796fn update_geo_stats_accumulator<T>(
797    bounder: &mut dyn GeoStatsAccumulator,
798    array: T,
799    valid: impl Iterator<Item = usize>,
800) where
801    T: ArrayAccessor,
802    T::Item: Copy + Ord + AsRef<[u8]>,
803{
804    if bounder.is_valid() {
805        for idx in valid {
806            let val = array.value(idx);
807            bounder.update_wkb(val.as_ref());
808        }
809    }
810}