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::column::writer::{compare_greater_byte_array_decimals, is_decimal_descr};
24use crate::data_type::{AsBytes, ByteArray, Int32Type};
25use crate::encodings::encoding::{DeltaBitPackEncoder, Encoder};
26use crate::encodings::rle::RleEncoder;
27use crate::errors::{ParquetError, Result};
28use crate::file::properties::{
29    EnabledStatistics, ResolvedColumnProperties, WriterProperties, WriterVersion,
30};
31use crate::geospatial::accumulator::{GeoStatsAccumulator, try_new_geo_stats_accumulator};
32use crate::geospatial::statistics::GeospatialStatistics;
33use crate::schema::types::ColumnDescPtr;
34use crate::util::bit_util::num_required_bits;
35use crate::util::prefix::common_prefix_length;
36use arrow_array::types::ByteArrayType;
37use arrow_array::{
38    Array, ArrayAccessor, BinaryArray, BinaryViewArray, DictionaryArray, FixedSizeBinaryArray,
39    GenericByteArray, LargeBinaryArray, LargeStringArray, StringArray, StringViewArray,
40};
41use arrow_buffer::{ArrowNativeType, Buffer};
42use arrow_schema::DataType;
43
44macro_rules! downcast_dict_impl {
45    ($array:ident, $key:ident, $val:ident, $op:expr $(, $arg:expr)*) => {{
46        $op($array
47            .as_any()
48            .downcast_ref::<DictionaryArray<arrow_array::types::$key>>()
49            .unwrap()
50            .downcast_dict::<$val>()
51            .unwrap()$(, $arg)*)
52    }};
53}
54
55macro_rules! downcast_dict_op {
56    ($key_type:expr, $val:ident, $array:ident, $op:expr $(, $arg:expr)*) => {
57        match $key_type.as_ref() {
58            DataType::UInt8 => downcast_dict_impl!($array, UInt8Type, $val, $op$(, $arg)*),
59            DataType::UInt16 => downcast_dict_impl!($array, UInt16Type, $val, $op$(, $arg)*),
60            DataType::UInt32 => downcast_dict_impl!($array, UInt32Type, $val, $op$(, $arg)*),
61            DataType::UInt64 => downcast_dict_impl!($array, UInt64Type, $val, $op$(, $arg)*),
62            DataType::Int8 => downcast_dict_impl!($array, Int8Type, $val, $op$(, $arg)*),
63            DataType::Int16 => downcast_dict_impl!($array, Int16Type, $val, $op$(, $arg)*),
64            DataType::Int32 => downcast_dict_impl!($array, Int32Type, $val, $op$(, $arg)*),
65            DataType::Int64 => downcast_dict_impl!($array, Int64Type, $val, $op$(, $arg)*),
66            _ => unreachable!(),
67        }
68    };
69}
70
71macro_rules! downcast_op {
72    ($data_type:expr, $array:ident, $op:expr $(, $arg:expr)*) => {
73        match $data_type {
74            DataType::Utf8 => $op($array.as_any().downcast_ref::<StringArray>().unwrap()$(, $arg)*),
75            DataType::LargeUtf8 => {
76                $op($array.as_any().downcast_ref::<LargeStringArray>().unwrap()$(, $arg)*)
77            }
78            DataType::Utf8View => $op($array.as_any().downcast_ref::<StringViewArray>().unwrap()$(, $arg)*),
79            DataType::Binary => {
80                $op($array.as_any().downcast_ref::<BinaryArray>().unwrap()$(, $arg)*)
81            }
82            DataType::LargeBinary => {
83                $op($array.as_any().downcast_ref::<LargeBinaryArray>().unwrap()$(, $arg)*)
84            }
85            DataType::BinaryView => {
86                $op($array.as_any().downcast_ref::<BinaryViewArray>().unwrap()$(, $arg)*)
87            }
88            DataType::Dictionary(key, value) => match value.as_ref() {
89                DataType::Utf8 => downcast_dict_op!(key, StringArray, $array, $op$(, $arg)*),
90                DataType::LargeUtf8 => {
91                    downcast_dict_op!(key, LargeStringArray, $array, $op$(, $arg)*)
92                }
93                DataType::Utf8View => {
94                    downcast_dict_op!(key, StringViewArray, $array, $op$(, $arg)*)
95                }
96                DataType::Binary => downcast_dict_op!(key, BinaryArray, $array, $op$(, $arg)*),
97                DataType::LargeBinary => {
98                    downcast_dict_op!(key, LargeBinaryArray, $array, $op$(, $arg)*)
99                }
100                DataType::BinaryView => {
101                    downcast_dict_op!(key, BinaryViewArray, $array, $op$(, $arg)*)
102                }
103                DataType::FixedSizeBinary(_) => {
104                    downcast_dict_op!(key, FixedSizeBinaryArray, $array, $op$(, $arg)*)
105                }
106                d => unreachable!("cannot downcast {} dictionary value to byte array", d),
107            },
108            d => unreachable!("cannot downcast {} to byte array", d),
109        }
110    };
111}
112
113/// A fallback encoder, i.e. non-dictionary, for [`ByteArray`]
114struct FallbackEncoder {
115    encoder: FallbackEncoderImpl,
116    num_values: usize,
117    variable_length_bytes: i64,
118}
119
120/// The fallback encoder in use
121///
122/// Note: DeltaBitPackEncoder is boxed as it is rather large
123enum FallbackEncoderImpl {
124    Plain {
125        buffer: Vec<u8>,
126    },
127    DeltaLength {
128        buffer: Vec<u8>,
129        lengths: Box<DeltaBitPackEncoder<Int32Type>>,
130    },
131    Delta {
132        buffer: Vec<u8>,
133        last_value: Vec<u8>,
134        prefix_lengths: Box<DeltaBitPackEncoder<Int32Type>>,
135        suffix_lengths: Box<DeltaBitPackEncoder<Int32Type>>,
136    },
137}
138
139impl FallbackEncoder {
140    /// Create the fallback encoder for the given [`WriterProperties`] and the
141    /// column settings already resolved from them
142    fn new(props: &WriterProperties, column_props: &ResolvedColumnProperties) -> Result<Self> {
143        // Set either main encoder or fallback encoder.
144        let encoding = column_props
145            .encoding
146            .unwrap_or_else(|| match props.writer_version() {
147                WriterVersion::PARQUET_1_0 => Encoding::PLAIN,
148                WriterVersion::PARQUET_2_0 => Encoding::DELTA_BYTE_ARRAY,
149            });
150
151        let encoder = match encoding {
152            Encoding::PLAIN => FallbackEncoderImpl::Plain { buffer: vec![] },
153            Encoding::DELTA_LENGTH_BYTE_ARRAY => FallbackEncoderImpl::DeltaLength {
154                buffer: vec![],
155                lengths: Box::new(DeltaBitPackEncoder::new()),
156            },
157            Encoding::DELTA_BYTE_ARRAY => FallbackEncoderImpl::Delta {
158                buffer: vec![],
159                last_value: vec![],
160                prefix_lengths: Box::new(DeltaBitPackEncoder::new()),
161                suffix_lengths: Box::new(DeltaBitPackEncoder::new()),
162            },
163            _ => {
164                return Err(general_err!(
165                    "unsupported encoding {} for byte array",
166                    encoding
167                ));
168            }
169        };
170
171        Ok(Self {
172            encoder,
173            num_values: 0,
174            variable_length_bytes: 0,
175        })
176    }
177
178    /// Encode `values` to the in-progress page
179    fn encode<T>(&mut self, values: T, indices: impl ExactSizeIterator<Item = usize>)
180    where
181        T: ArrayAccessor + Copy,
182        T::Item: AsRef<[u8]>,
183    {
184        self.num_values += indices.len();
185        match &mut self.encoder {
186            FallbackEncoderImpl::Plain { buffer } => {
187                for idx in indices {
188                    let value = values.value(idx);
189                    let value = value.as_ref();
190                    buffer.extend_from_slice((value.len() as u32).as_bytes());
191                    buffer.extend_from_slice(value);
192                    self.variable_length_bytes += value.len() as i64;
193                }
194            }
195            FallbackEncoderImpl::DeltaLength { buffer, lengths } => {
196                for idx in indices {
197                    let value = values.value(idx);
198                    let value = value.as_ref();
199                    lengths.put(&[value.len() as i32]).unwrap();
200                    buffer.extend_from_slice(value);
201                    self.variable_length_bytes += value.len() as i64;
202                }
203            }
204            FallbackEncoderImpl::Delta {
205                buffer,
206                last_value,
207                prefix_lengths,
208                suffix_lengths,
209            } => {
210                for idx in indices {
211                    let value = values.value(idx);
212                    let value = value.as_ref();
213
214                    let prefix_length = common_prefix_length(last_value, value);
215                    let suffix_length = value.len() - prefix_length;
216
217                    last_value.clear();
218                    last_value.extend_from_slice(value);
219
220                    buffer.extend_from_slice(&value[prefix_length..]);
221                    prefix_lengths.put(&[prefix_length as i32]).unwrap();
222                    suffix_lengths.put(&[suffix_length as i32]).unwrap();
223                    self.variable_length_bytes += value.len() as i64;
224                }
225            }
226        }
227    }
228
229    /// Returns an estimate of the data page size in bytes
230    ///
231    /// This includes:
232    /// <already_written_encoded_byte_size> + <estimated_encoded_size_of_unflushed_bytes>
233    fn estimated_data_page_size(&self) -> usize {
234        match &self.encoder {
235            FallbackEncoderImpl::Plain { buffer, .. } => buffer.len(),
236            FallbackEncoderImpl::DeltaLength { buffer, lengths } => {
237                buffer.len() + lengths.estimated_data_encoded_size()
238            }
239            FallbackEncoderImpl::Delta {
240                buffer,
241                prefix_lengths,
242                suffix_lengths,
243                ..
244            } => {
245                buffer.len()
246                    + prefix_lengths.estimated_data_encoded_size()
247                    + suffix_lengths.estimated_data_encoded_size()
248            }
249        }
250    }
251
252    fn flush_data_page(
253        &mut self,
254        min_value: Option<ByteArray>,
255        max_value: Option<ByteArray>,
256    ) -> Result<DataPageValues<ByteArray>> {
257        let (buf, encoding) = match &mut self.encoder {
258            FallbackEncoderImpl::Plain { buffer } => (std::mem::take(buffer), Encoding::PLAIN),
259            FallbackEncoderImpl::DeltaLength { buffer, lengths } => {
260                let lengths = lengths.flush_buffer()?;
261
262                let mut out = Vec::with_capacity(lengths.len() + buffer.len());
263                out.extend_from_slice(&lengths);
264                out.extend_from_slice(buffer);
265                buffer.clear();
266                (out, Encoding::DELTA_LENGTH_BYTE_ARRAY)
267            }
268            FallbackEncoderImpl::Delta {
269                buffer,
270                prefix_lengths,
271                suffix_lengths,
272                last_value,
273            } => {
274                let prefix_lengths = prefix_lengths.flush_buffer()?;
275                let suffix_lengths = suffix_lengths.flush_buffer()?;
276
277                let mut out =
278                    Vec::with_capacity(prefix_lengths.len() + suffix_lengths.len() + buffer.len());
279                out.extend_from_slice(&prefix_lengths);
280                out.extend_from_slice(&suffix_lengths);
281                out.extend_from_slice(buffer);
282                buffer.clear();
283                last_value.clear();
284                (out, Encoding::DELTA_BYTE_ARRAY)
285            }
286        };
287
288        // Capture value of variable_length_bytes and reset for next page
289        let variable_length_bytes = Some(self.variable_length_bytes);
290        self.variable_length_bytes = 0;
291
292        Ok(DataPageValues {
293            buf: buf.into(),
294            num_values: std::mem::take(&mut self.num_values),
295            encoding,
296            min_value,
297            max_value,
298            nan_count: None,
299            variable_length_bytes,
300        })
301    }
302}
303
304/// Maximum size of a value that can be inlined in the hash table
305const SHORT_VALUE_LEN: usize = size_of::<u64>() - 1;
306
307/// Entry in the hash table
308#[derive(Debug, PartialEq, Eq, Hash, Clone, Copy)]
309struct Entry {
310    /// hash of the value (stored to avoid recomputing it in hash table check)
311    hash: u64,
312    /// if len =< [`SHORT_VALUE_LEN`]: the data inlined
313    /// if len > [`SHORT_VALUE_LEN`], the offset of where the data starts
314    offset_or_inline: OffsetOrInlineView,
315    index_in_dict: u64,
316}
317
318/// Either offset to the dictionary page bytes or inline bytes when the bytes are short enough
319/// to avoid doing the indirection
320///
321/// For inlined view:
322///   +------------+---------------+------------------------------+
323///   | 1 bit set  | len {7 bits}  | value {56 bits}              |
324///   +------------+---------------+------------------------------+
325///
326/// For non-inlined view:
327///   +--------------+--------------------------------------------+
328///   | 1 bit unset  | offset in dict {63 bits}                   |
329///   +--------------+--------------------------------------------+
330///
331/// the inline view contain it's length to avoid having another field for length in the entry
332/// since if the hashes are the same, most probably the length will be the same, so we can do the indirection check
333#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
334struct OffsetOrInlineView(u64);
335
336impl OffsetOrInlineView {
337    const INLINE_FLAG: u64 = 1 << 63;
338    const LEN_SHIFT: u32 = 56;
339
340    #[inline]
341    fn is_non_inlined(&self) -> bool {
342        (self.0 & Self::INLINE_FLAG) == 0
343    }
344
345    #[inline]
346    fn is_inlined(&self) -> bool {
347        !self.is_non_inlined()
348    }
349
350    #[inline]
351    fn new_inlined(value: &[u8]) -> Self {
352        debug_assert!(value.len() <= 7);
353
354        // Value bytes occupy the low 56 bits, byte 0 in the least significant position
355        let mut buf = [0u8; 8];
356        buf[..value.len()].copy_from_slice(value);
357        let packed = u64::from_le_bytes(buf);
358
359        Self(Self::INLINE_FLAG | ((value.len() as u64) << Self::LEN_SHIFT) | packed)
360    }
361
362    #[inline]
363    fn new_non_inlined(offset: u64) -> Self {
364        // `offset` is a position in the dictionary page `Vec`, whose length is at
365        // most `isize::MAX`, so it always fits in 63 bits and can't collide with
366        // `INLINE_FLAG`
367        // also, having the inline flag set for non-inlined values would mean the dictionary page is at least 9 exabytes.
368        debug_assert_eq!(
369            offset & Self::INLINE_FLAG,
370            0,
371            "dictionary page offset must fit in 63 bits"
372        );
373        let non_inlined_value = offset;
374        Self(non_inlined_value)
375    }
376
377    fn maybe_non_inlined_value(&self) -> Option<u64> {
378        if self.is_non_inlined() {
379            // Not inlined is when the first bit is unset, so the value
380            Some(self.0)
381        } else {
382            None
383        }
384    }
385}
386
387#[derive(Debug, Default)]
388struct ByteArrayInterner {
389    state: ahash::RandomState,
390    /// Underlying hash set for each distinct value
391    map: hashbrown::hash_table::HashTable<Entry>,
392    /// Encoded dictionary page: each value prefixed by its length as a `u32`
393    page: Vec<u8>,
394}
395
396impl ByteArrayInterner {
397    fn intern(&mut self, value: &[u8]) -> Result<u64> {
398        let hash = self.state.hash_one(value);
399
400        // value is "small" - can be inlined
401        let (found_entry, inline) = if let Some(inline) = maybe_inline_value(value) {
402            // is value is already present in the set?
403            let entry = self.map.find(hash, |header| {
404                // value is stored inline so no need to consult buffer
405                // (this is the "small string optimization")
406                //
407                // no need to compare the hashes since the hash is a function of the value which is compared here
408                let is_same = inline == header.offset_or_inline;
409
410                if is_same {
411                    debug_assert_eq!(
412                        header.hash, hash,
413                        "hashes must match if the values are the same"
414                    );
415                }
416
417                is_same
418            });
419
420            (entry, Some(inline))
421        } else {
422            // value is not "small"
423            // Check if the value is already present in the set
424            let entry = self.map.find(hash, |header| {
425                // compare hash
426                if header.hash != hash {
427                    return false;
428                }
429
430                // If the value is not inlined compare it
431                let Some(offset) = header.offset_or_inline.maybe_non_inlined_value() else {
432                    // that value is inline, so it must be different
433                    return false;
434                };
435
436                self.is_at(offset, value)
437            });
438
439            (entry, None)
440        };
441
442        if let Some(entry) = found_entry {
443            return Ok(entry.index_in_dict);
444        }
445
446        // if no existing entry, make a new one
447        self.insert(value, hash, inline)
448    }
449
450    /// Returns whether `value` is in the dictionary page at `offset`
451    #[inline(always)]
452    fn is_at(&self, offset: u64, value: &[u8]) -> bool {
453        let offset = offset as usize;
454        // SAFETY: every value in the page is preceded by its length
455        let stored_len = unsafe { self.page.get_unchecked(offset - 4..offset) };
456        // Rarely false, as the hashes matched, so the branch predicts well and
457        // the comparison's reads issue before the length is known
458        if u32::from_le_bytes(stored_len.try_into().unwrap()) as usize != value.len() {
459            return false;
460        }
461        // SAFETY: the value at `offset` has the length just checked
462        let existing = unsafe { self.page.get_unchecked(offset..offset + value.len()) };
463        existing == value
464    }
465
466    fn num_unique_values(&self) -> usize {
467        self.map.len()
468    }
469
470    /// Appends `value` to the dictionary, in the empty slot at `pos`
471    fn insert(
472        &mut self,
473        value: &[u8],
474        hash: u64,
475        inline: Option<OffsetOrInlineView>,
476    ) -> Result<u64> {
477        // If inline provided it should inlined
478        debug_assert!(inline.is_none_or(|i| i.is_inlined()));
479
480        // The dictionary page is PLAIN encoded, where each value is prefixed by
481        // its length as a 4-byte little endian integer so we need to reject that since we use unsafe access later
482        //
483        // From the parquet-format spec:
484        // > Encoding PLAIN:
485        // > [...] For the byte array type, it encodes the length as a 4-byte little
486        // > endian integer, followed by the bytes.
487        // >
488        // > Dictionary Encoding:
489        // > [...] Dictionary page format: the entries in the dictionary using the plain encoding.
490        // >
491        //
492        // Reference:
493        // - Plain encoding: https://github.com/apache/parquet-format/blob/53b8bd8576827080d4382fa457003f45b41cda6e/Encodings.md?plain=1#L72-L73
494        // - Plain dictionary encoding: https://github.com/apache/parquet-format/blob/53b8bd8576827080d4382fa457003f45b41cda6e/Encodings.md?plain=1#L83
495        if value.len() > u32::MAX as usize {
496            return Err(ParquetError::General(format!(
497                "Byte array value is too large to be written in dictionary: {} bytes",
498                value.len()
499            )));
500        }
501
502        let key = self.num_unique_values() as u64;
503        let len = value.len();
504
505        self.page.reserve(4 + value.len());
506        self.page.extend_from_slice(&(len as u32).to_le_bytes());
507        let offset = self.page.len() as u64;
508        self.page.extend_from_slice(value);
509
510        // Make the new entry
511        let new_header = Entry {
512            hash,
513            offset_or_inline: inline.unwrap_or_else(|| OffsetOrInlineView::new_non_inlined(offset)),
514            index_in_dict: key,
515        };
516        self.map
517            .insert_unique(hash, new_header, |header| header.hash);
518
519        Ok(key)
520    }
521
522    fn estimated_memory_size(&self) -> usize {
523        self.page.capacity() * std::mem::size_of::<u8>() + self.map.allocation_size()
524    }
525
526    fn iter(&self) -> impl Iterator<Item = &[u8]> {
527        ByteArrayInternerIterator {
528            values: &self.page,
529            current_index: 0,
530        }
531    }
532}
533
534/// Iterates the values of a [`ByteArrayInterner`]'s dictionary page
535///
536/// `values` must be a page built by [`ByteArrayInterner::insert`], which is a
537/// sequence of values each preceded by its exact length as a `u32`, and
538/// `current_index` must start at 0, so it is always at the start of a value
539struct ByteArrayInternerIterator<'a> {
540    values: &'a [u8],
541    current_index: usize,
542}
543
544impl<'a> Iterator for ByteArrayInternerIterator<'a> {
545    type Item = &'a [u8];
546
547    fn next(&mut self) -> Option<Self::Item> {
548        if self.current_index >= self.values.len() {
549            return None;
550        }
551
552        // SAFETY: `current_index` is at the start of a value and is not past
553        // the end of the page, so the value's 4-byte length prefix is in bounds
554        let len_bytes = unsafe {
555            self.values
556                .get_unchecked(self.current_index..self.current_index + 4)
557        };
558        let len = u32::from_le_bytes(len_bytes.try_into().unwrap()) as usize;
559
560        let start = self.current_index + 4;
561        let end = start + len;
562
563        // SAFETY: `insert` writes exactly `len` bytes after the prefix, and
564        // rejects values whose length does not fit in a `u32`, so the prefix is
565        // never truncated and `start..end` is within the page
566        let value = unsafe { self.values.get_unchecked(start..end) };
567
568        self.current_index = end;
569
570        Some(value)
571    }
572}
573
574/// A dictionary encoder for byte array data
575#[derive(Debug, Default)]
576struct DictEncoder {
577    interner: ByteArrayInterner,
578    indices: Vec<u64>,
579    variable_length_bytes: i64,
580}
581
582impl DictEncoder {
583    /// Encode `values` to the in-progress page
584    fn encode<T>(&mut self, values: T, indices: impl ExactSizeIterator<Item = usize>) -> Result<()>
585    where
586        T: ArrayAccessor + Copy,
587        T::Item: AsRef<[u8]>,
588    {
589        self.indices.reserve(indices.len());
590
591        for idx in indices {
592            let value = values.value(idx);
593            let interned = self.interner.intern(value.as_ref())?;
594            self.indices.push(interned);
595            self.variable_length_bytes += value.as_ref().len() as i64;
596        }
597
598        Ok(())
599    }
600
601    fn bit_width(&self) -> u8 {
602        let length = self.interner.num_unique_values();
603        num_required_bits(length.saturating_sub(1) as u64)
604    }
605
606    fn estimated_memory_size(&self) -> usize {
607        self.interner.estimated_memory_size() + self.indices.capacity() * std::mem::size_of::<u64>()
608    }
609
610    fn estimated_data_page_size(&self) -> usize {
611        let bit_width = self.bit_width();
612        1 + RleEncoder::max_buffer_size(bit_width, self.indices.len())
613    }
614
615    fn estimated_dict_page_size(&self) -> usize {
616        self.interner.page.len()
617    }
618
619    fn flush_dict_page(self) -> DictionaryPage {
620        let unique_values = self.interner.num_unique_values();
621        let bytes = self.interner.page;
622
623        DictionaryPage {
624            buf: bytes.into(),
625            num_values: unique_values,
626            is_sorted: false,
627        }
628    }
629
630    fn flush_data_page(
631        &mut self,
632        min_value: Option<ByteArray>,
633        max_value: Option<ByteArray>,
634    ) -> DataPageValues<ByteArray> {
635        let num_values = self.indices.len();
636        let buffer_len = self.estimated_data_page_size();
637        let mut buffer = Vec::with_capacity(buffer_len);
638        buffer.push(self.bit_width());
639
640        let mut encoder = RleEncoder::new_from_buf(self.bit_width(), buffer);
641        encoder.put_batch(&self.indices);
642
643        self.indices.clear();
644
645        // Capture value of variable_length_bytes and reset for next page
646        let variable_length_bytes = Some(self.variable_length_bytes);
647        self.variable_length_bytes = 0;
648
649        DataPageValues {
650            buf: encoder.consume().into(),
651            num_values,
652            encoding: Encoding::RLE_DICTIONARY,
653            min_value,
654            max_value,
655            nan_count: None,
656            variable_length_bytes,
657        }
658    }
659}
660
661fn maybe_inline_value(value: &[u8]) -> Option<OffsetOrInlineView> {
662    if value.len() <= SHORT_VALUE_LEN {
663        Some(OffsetOrInlineView::new_inlined(value))
664    } else {
665        None
666    }
667}
668
669pub struct ByteArrayEncoder {
670    fallback: FallbackEncoder,
671    dict_encoder: Option<DictEncoder>,
672    statistics_enabled: EnabledStatistics,
673    min_value: Option<ByteArray>,
674    max_value: Option<ByteArray>,
675    bloom_filter: Option<Sbbf>,
676    bloom_filter_target_fpp: f64,
677    geo_stats_accumulator: Option<Box<dyn GeoStatsAccumulator>>,
678    is_decimal: bool,
679}
680
681impl ColumnValueEncoder for ByteArrayEncoder {
682    type T = ByteArray;
683    type Values = dyn Array;
684    fn flush_bloom_filter(&mut self) -> Option<Sbbf> {
685        let mut sbbf = self.bloom_filter.take()?;
686        sbbf.fold_to_target_fpp(self.bloom_filter_target_fpp);
687        Some(sbbf)
688    }
689
690    fn try_new(
691        descr: &ColumnDescPtr,
692        props: &WriterProperties,
693        column_props: &ResolvedColumnProperties,
694    ) -> Result<Self>
695    where
696        Self: Sized,
697    {
698        let dictionary = column_props.dictionary_enabled.then(DictEncoder::default);
699
700        let fallback = FallbackEncoder::new(props, column_props)?;
701
702        let (bloom_filter, bloom_filter_target_fpp) = create_bloom_filter(column_props)?;
703
704        let statistics_enabled = column_props.statistics_enabled;
705
706        let geo_stats_accumulator = try_new_geo_stats_accumulator(descr);
707
708        let is_decimal = is_decimal_descr(descr.get_basic_info());
709
710        Ok(Self {
711            fallback,
712            statistics_enabled,
713            bloom_filter,
714            bloom_filter_target_fpp,
715            dict_encoder: dictionary,
716            min_value: None,
717            max_value: None,
718            geo_stats_accumulator,
719            is_decimal,
720        })
721    }
722
723    fn write(&mut self, _values: &Self::Values, _offset: usize, _len: usize) -> Result<()> {
724        unreachable!("should call write_gather instead")
725    }
726
727    fn write_gather(&mut self, values: &Self::Values, indices: &[usize]) -> Result<()> {
728        downcast_op!(
729            values.data_type(),
730            values,
731            encode,
732            indices.iter().copied(),
733            self
734        )
735    }
736
737    fn count_values_within_byte_budget_gather(
738        values: &Self::Values,
739        indices: &[usize],
740        byte_budget: usize,
741    ) -> Option<usize> {
742        // `ByteArrayEncoder` only ever writes via `write_gather`, so this
743        // is the relevant method.
744        //
745        // Two-stage walk for the simple offset-buffer byte array types:
746        //   1. If indices are contiguous, compute the total payload in
747        //      O(1) via a single subtraction on the offsets buffer.
748        //      When the total fits the budget — the overwhelmingly
749        //      common "small values" case — return immediately.
750        //   2. Otherwise, walk per-value byte sizes from the offsets
751        //      buffer (still cheap, no slice/UTF-8 construction) and
752        //      exit at the first value that pushes the cumulative sum
753        //      past the budget. This bounds skewed distributions: an
754        //      outlier value is caught wherever it lands in the chunk.
755        let count = match values.data_type() {
756            DataType::Utf8 => count_within_budget_offsets(
757                values.as_any().downcast_ref::<StringArray>().unwrap(),
758                indices,
759                byte_budget,
760            ),
761            DataType::LargeUtf8 => count_within_budget_offsets(
762                values.as_any().downcast_ref::<LargeStringArray>().unwrap(),
763                indices,
764                byte_budget,
765            ),
766            DataType::Binary => count_within_budget_offsets(
767                values.as_any().downcast_ref::<BinaryArray>().unwrap(),
768                indices,
769                byte_budget,
770            ),
771            DataType::LargeBinary => count_within_budget_offsets(
772                values.as_any().downcast_ref::<LargeBinaryArray>().unwrap(),
773                indices,
774                byte_budget,
775            ),
776            // View arrays carry each value's length in the low 32 bits of
777            // its u128 view word, so lengths are scannable without touching
778            // any data buffer — and the common small-value case skips even
779            // that scan via an O(1) conservative bound.
780            DataType::Utf8View => {
781                let array = values.as_any().downcast_ref::<StringViewArray>().unwrap();
782                count_within_budget_views(
783                    array.views(),
784                    indices,
785                    byte_budget,
786                    max_view_value_len(array.data_buffers()),
787                )
788            }
789            DataType::BinaryView => {
790                let array = values.as_any().downcast_ref::<BinaryViewArray>().unwrap();
791                count_within_budget_views(
792                    array.views(),
793                    indices,
794                    byte_budget,
795                    max_view_value_len(array.data_buffers()),
796                )
797            }
798            // The values in an arrow dictionary are already small and
799            // deduplicated, so there is nothing to bound — treat every
800            // chunk as fitting and stay on the batched path. (A per-value
801            // walk through dict keys on every chunk also measured ~+30-80%
802            // slower than `main`.)
803            DataType::Dictionary(_, _) => indices.len(),
804            // Every byte-array type `ByteArrayEncoder` is constructed for
805            // has an explicit arm above. A `Dictionary(value = FixedSizeBinary)`
806            // column hits the `Dictionary(_, _)` arm (its `values.data_type()`
807            // is `Dictionary`), and a bare `FixedSizeBinary` column is routed
808            // to the generic column writer, never this encoder — so no other
809            // type can reach here.
810            data_type => unreachable!("ByteArrayEncoder cannot be constructed for {data_type:?}"),
811        };
812        Some(count)
813    }
814
815    fn num_values(&self) -> usize {
816        match &self.dict_encoder {
817            Some(encoder) => encoder.indices.len(),
818            None => self.fallback.num_values,
819        }
820    }
821
822    fn has_dictionary(&self) -> bool {
823        self.dict_encoder.is_some()
824    }
825
826    fn compresses_against_previous_value(&self) -> bool {
827        // While dictionary encoding is active the data page holds RLE
828        // indices, which carry no cross-value state; only the DELTA_BYTE_ARRAY
829        // fallback shares prefixes with the preceding value.
830        self.dict_encoder.is_none()
831            && matches!(self.fallback.encoder, FallbackEncoderImpl::Delta { .. })
832    }
833
834    fn estimated_memory_size(&self) -> usize {
835        let encoder_size = match &self.dict_encoder {
836            Some(encoder) => encoder.estimated_memory_size(),
837            // For the FallbackEncoder, these unflushed bytes are already encoded.
838            // Therefore, the size should be the same as estimated_data_page_size.
839            None => self.fallback.estimated_data_page_size(),
840        };
841
842        let bloom_filter_size = self
843            .bloom_filter
844            .as_ref()
845            .map(|bf| bf.estimated_memory_size())
846            .unwrap_or_default();
847
848        let stats_size = self.min_value.as_ref().map(|v| v.len()).unwrap_or_default()
849            + self.max_value.as_ref().map(|v| v.len()).unwrap_or_default();
850
851        encoder_size + bloom_filter_size + stats_size
852    }
853
854    fn estimated_dict_page_size(&self) -> Option<usize> {
855        Some(self.dict_encoder.as_ref()?.estimated_dict_page_size())
856    }
857
858    /// Returns an estimate of the data page size in bytes
859    ///
860    /// This includes:
861    /// <already_written_encoded_byte_size> + <estimated_encoded_size_of_unflushed_bytes>
862    fn estimated_data_page_size(&self) -> usize {
863        match &self.dict_encoder {
864            Some(encoder) => encoder.estimated_data_page_size(),
865            None => self.fallback.estimated_data_page_size(),
866        }
867    }
868
869    fn flush_dict_page(&mut self) -> Result<Option<DictionaryPage>> {
870        match self.dict_encoder.take() {
871            Some(encoder) => {
872                if !encoder.indices.is_empty() {
873                    return Err(general_err!(
874                        "Must flush data pages before flushing dictionary"
875                    ));
876                }
877
878                if let Some(bloom_filter) = &mut self.bloom_filter {
879                    for value in encoder.interner.iter() {
880                        bloom_filter.insert(value);
881                    }
882                }
883
884                Ok(Some(encoder.flush_dict_page()))
885            }
886            _ => Ok(None),
887        }
888    }
889
890    fn flush_data_page(&mut self) -> Result<DataPageValues<ByteArray>> {
891        let min_value = self.min_value.take();
892        let max_value = self.max_value.take();
893
894        match &mut self.dict_encoder {
895            Some(encoder) => Ok(encoder.flush_data_page(min_value, max_value)),
896            _ => self.fallback.flush_data_page(min_value, max_value),
897        }
898    }
899
900    fn flush_geospatial_statistics(&mut self) -> Option<Box<GeospatialStatistics>> {
901        self.geo_stats_accumulator.as_mut().map(|a| a.finish())?
902    }
903}
904
905/// Encodes the provided `values` and `indices` to `encoder`
906///
907/// This is a free function so it can be used with `downcast_op!`
908fn encode<T, I>(values: T, indices: I, encoder: &mut ByteArrayEncoder) -> Result<()>
909where
910    T: ArrayAccessor + Copy,
911    T::Item: Copy + Ord + AsRef<[u8]>,
912    I: ExactSizeIterator<Item = usize> + Clone,
913{
914    if encoder.statistics_enabled != EnabledStatistics::None {
915        if let Some(accumulator) = encoder.geo_stats_accumulator.as_mut() {
916            update_geo_stats_accumulator(accumulator.as_mut(), values, indices.clone());
917        } else if let Some((min, max)) =
918            compute_min_max(values, indices.clone(), encoder.is_decimal)
919        {
920            // Compare before copying: `write_gather` runs once per
921            // mini-batch, and a byte-budgeted mini-batch of large values can
922            // hold a single value, so an unconditional copy here would
923            // duplicate every value once for `min` and once for `max`.
924            let min = min.as_ref();
925            if encoder
926                .min_value
927                .as_ref()
928                .is_none_or(|m| is_greater(encoder.is_decimal, m.data(), min))
929            {
930                encoder.min_value = Some(min.to_vec().into());
931            }
932
933            let max = max.as_ref();
934            if encoder
935                .max_value
936                .as_ref()
937                .is_none_or(|m| is_greater(encoder.is_decimal, max, m.data()))
938            {
939                encoder.max_value = Some(max.to_vec().into());
940            }
941        }
942    }
943
944    // While a dictionary is in use the filter is populated from its distinct values in
945    // `flush_dict_page`, so each value is hashed once rather than once per row.
946    match &mut encoder.dict_encoder {
947        Some(dict_encoder) => dict_encoder.encode(values, indices),
948        None => {
949            if let Some(bloom_filter) = &mut encoder.bloom_filter {
950                for idx in indices.clone() {
951                    bloom_filter.insert(values.value(idx).as_ref());
952                }
953            }
954            encoder.fallback.encode(values, indices);
955
956            Ok(())
957        }
958    }
959}
960
961/// Upper bound on any single value's byte length in a view array.
962fn max_view_value_len(buffers: &[Buffer]) -> usize {
963    /// Bytes that fit inline in a u128 view word (the rest is len + prefix).
964    const MAX_INLINE_VIEW_LEN: usize = 12;
965    // An out-of-line view's data is a contiguous slice of exactly one data
966    // buffer, so it cannot exceed the largest buffer; inline views hold at
967    // most `MAX_INLINE_VIEW_LEN`. Loose (a value is usually far smaller than
968    // a whole buffer) but O(number of buffers) and always sound.
969    buffers
970        .iter()
971        .map(|b| b.len())
972        .max()
973        .unwrap_or(0)
974        .max(MAX_INLINE_VIEW_LEN)
975}
976
977/// Number of leading `indices` whose cumulative plain-encoded size fits
978/// `byte_budget` (boundary value included), for view arrays (`Utf8View`,
979/// `BinaryView`).
980fn count_within_budget_views(
981    views: &[u128],
982    indices: &[usize],
983    byte_budget: usize,
984    max_value_len: usize,
985) -> usize {
986    // Each plain-encoded BYTE_ARRAY value carries a 4-byte length prefix, so
987    // the budget is compared against `value_len + size_of::<u32>()` — the
988    // bytes actually written to the page, not just the payload.
989    //
990    // Stage 1: O(1) conservative bound. View arrays have no prefix-sum
991    // offsets buffer, so the exact span subtraction used by
992    // `count_within_budget_offsets` is unavailable; instead bound every
993    // value by `max_value_len`. Skips the walk for the common small-value
994    // case (what view arrays are built for, and where there is nothing to
995    // bound).
996    let per_value = max_value_len + std::mem::size_of::<u32>();
997    if indices.len().saturating_mul(per_value) <= byte_budget {
998        return indices.len();
999    }
1000    // Stage 2: exact per-value scan, reading each length from the low 32
1001    // bits of its u128 view word (no data-buffer dereference).
1002    let mut cum: usize = 0;
1003    for (i, idx) in indices.iter().enumerate() {
1004        let len = (views[*idx] as u32) as usize;
1005        cum = cum.saturating_add(len + std::mem::size_of::<u32>());
1006        if cum > byte_budget {
1007            return i + 1;
1008        }
1009    }
1010    indices.len()
1011}
1012
1013/// Number of leading `indices` whose cumulative plain-encoded size fits
1014/// `byte_budget` (boundary value included), for offset-buffer byte arrays
1015/// (`Utf8`/`LargeUtf8`/`Binary`/`LargeBinary`).
1016///
1017/// `indices` are assumed sorted ascending — they always are here, since
1018/// they come from `non_null_indices`, which is built in array order.
1019fn count_within_budget_offsets<T: ByteArrayType>(
1020    values: &GenericByteArray<T>,
1021    indices: &[usize],
1022    byte_budget: usize,
1023) -> usize {
1024    if indices.is_empty() {
1025        return 0;
1026    }
1027    let n = indices.len();
1028    let first = indices[0];
1029    let last = indices[n - 1];
1030    let offsets = values.value_offsets();
1031    // Each plain-encoded value carries a 4-byte length prefix on the page.
1032    let prefix_overhead = std::mem::size_of::<u32>();
1033
1034    // Stage 1: O(1) span upper bound. The span `offsets[last+1] -
1035    // offsets[first]` covers every array position in `[first, last]`, a
1036    // superset of `indices` — and the skipped positions in a nullable
1037    // column are nulls with zero offset delta, so the span still equals the
1038    // exact payload. If it fits the budget, every value fits. Covers the
1039    // common small-value case for both non-null and (sparse) nullable
1040    // columns.
1041    if last >= first {
1042        let payload = (offsets[last + 1] - offsets[first]).as_usize();
1043        if payload + n * prefix_overhead <= byte_budget {
1044            return n;
1045        }
1046    }
1047
1048    // Stage 2: scan per-index lengths from the offsets buffer.
1049    let mut cum: usize = 0;
1050    for (i, idx) in indices.iter().enumerate() {
1051        let len = (offsets[idx + 1] - offsets[*idx]).as_usize() + prefix_overhead;
1052        cum = cum.saturating_add(len);
1053        if cum > byte_budget {
1054            return i + 1;
1055        }
1056    }
1057    n
1058}
1059
1060/// Returns `true` if `a > b`.
1061#[inline]
1062fn is_greater(is_decimal: bool, a: &[u8], b: &[u8]) -> bool {
1063    if is_decimal {
1064        compare_greater_byte_array_decimals(a, b)
1065    } else {
1066        a > b
1067    }
1068}
1069
1070/// Computes the min and max for the provided array and indices
1071///
1072/// This is a free function so it can be used with `downcast_op!`
1073fn compute_min_max<T>(
1074    array: T,
1075    mut valid: impl Iterator<Item = usize>,
1076    is_decimal: bool,
1077) -> Option<(T::Item, T::Item)>
1078where
1079    T: ArrayAccessor,
1080    T::Item: Copy + Ord + AsRef<[u8]>,
1081{
1082    let first_idx = valid.next()?;
1083
1084    let first_val = array.value(first_idx);
1085    let mut min = first_val;
1086    let mut max = first_val;
1087    for idx in valid {
1088        let val = array.value(idx);
1089        if is_greater(is_decimal, min.as_ref(), val.as_ref()) {
1090            min = val;
1091        }
1092        if is_greater(is_decimal, val.as_ref(), max.as_ref()) {
1093            max = val;
1094        }
1095    }
1096    Some((min, max))
1097}
1098
1099/// Updates geospatial statistics for the provided array and indices
1100fn update_geo_stats_accumulator<T>(
1101    bounder: &mut dyn GeoStatsAccumulator,
1102    array: T,
1103    valid: impl Iterator<Item = usize>,
1104) where
1105    T: ArrayAccessor,
1106    T::Item: Copy + Ord + AsRef<[u8]>,
1107{
1108    if bounder.is_valid() {
1109        for idx in valid {
1110            let val = array.value(idx);
1111            bounder.update_wkb(val.as_ref());
1112        }
1113    }
1114}