Skip to main content

parquet/column/writer/
encoder.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 bytes::Bytes;
19
20use crate::basic::{ConvertedType, Encoding, LogicalType, Type};
21use crate::bloom_filter::Sbbf;
22use crate::column::writer::{
23    compare_greater, fallback_encoding, has_dictionary_support, is_nan, update_max, update_min,
24};
25use crate::data_type::DataType;
26use crate::data_type::private::ParquetValueType;
27use crate::encodings::encoding::{DictEncoder, Encoder, get_encoder};
28use crate::errors::{ParquetError, Result};
29use crate::file::properties::{EnabledStatistics, ResolvedColumnProperties, WriterProperties};
30use crate::geospatial::accumulator::{GeoStatsAccumulator, try_new_geo_stats_accumulator};
31use crate::geospatial::statistics::GeospatialStatistics;
32use crate::schema::types::{BasicTypeInfo, ColumnDescPtr};
33
34/// A collection of [`ParquetValueType`] encoded by a [`ColumnValueEncoder`]
35pub trait ColumnValues {
36    /// The number of values in this collection
37    fn len(&self) -> usize;
38}
39
40#[cfg(feature = "arrow")]
41impl ColumnValues for dyn arrow_array::Array {
42    fn len(&self) -> usize {
43        arrow_array::Array::len(self)
44    }
45}
46
47impl<T: ParquetValueType> ColumnValues for [T] {
48    fn len(&self) -> usize {
49        self.len()
50    }
51}
52
53/// The encoded data for a dictionary page
54pub struct DictionaryPage {
55    pub buf: Bytes,
56    pub num_values: usize,
57    pub is_sorted: bool,
58}
59
60/// The encoded values for a data page, with optional statistics
61pub struct DataPageValues<T> {
62    pub buf: Bytes,
63    pub num_values: usize,
64    pub encoding: Encoding,
65    pub min_value: Option<T>,
66    pub max_value: Option<T>,
67    pub nan_count: Option<u64>,
68    pub variable_length_bytes: Option<i64>,
69}
70
71/// A generic encoder of [`ColumnValues`] to data and dictionary pages used by
72/// [`super::GenericColumnWriter`]
73pub trait ColumnValueEncoder {
74    /// The underlying value type of [`Self::Values`]
75    ///
76    /// Note: this avoids needing to fully qualify `<Self::Values as ColumnValues>::T`
77    type T: ParquetValueType;
78
79    /// The values encoded by this encoder
80    type Values: ColumnValues + ?Sized;
81
82    /// Create a new [`ColumnValueEncoder`]
83    ///
84    /// `column_props` holds the settings in `props` that apply specifically to
85    /// `descr`, already resolved by the caller.
86    #[expect(
87        private_interfaces,
88        reason = "this trait is not nameable outside the crate"
89    )]
90    fn try_new(
91        descr: &ColumnDescPtr,
92        props: &WriterProperties,
93        column_props: &ResolvedColumnProperties,
94    ) -> Result<Self>
95    where
96        Self: Sized;
97
98    /// Write the corresponding values to this [`ColumnValueEncoder`]
99    fn write(&mut self, values: &Self::Values, offset: usize, len: usize) -> Result<()>;
100
101    /// Write the values at the indexes in `indices` to this [`ColumnValueEncoder`]
102    fn write_gather(&mut self, values: &Self::Values, indices: &[usize]) -> Result<()>;
103
104    /// Returns the largest `k` such that the first `k` values in
105    /// `values[offset..offset + len]` encode to at most `byte_budget`
106    /// bytes — i.e. how many values fit in a single page byte budget.
107    ///
108    /// Returns `len` if every value fits. Returns at least 1 if a single
109    /// value alone exceeds the budget, matching parquet's "at least one
110    /// value per data page" rule.
111    ///
112    /// `None` means "no cheap estimate available"; the caller stays on
113    /// the batched fast path and lets the post-write
114    /// `should_add_data_page` check handle bounding.
115    ///
116    /// Implementations should short-circuit aggressively: the typical
117    /// case is "everything fits, return `len`", and the next-most-common
118    /// case is "one wide value, return 1." The variable-width walk only
119    /// needs to be precise when the chunk is genuinely near the budget.
120    fn count_values_within_byte_budget(
121        _values: &Self::Values,
122        _offset: usize,
123        _len: usize,
124        _byte_budget: usize,
125    ) -> Option<usize> {
126        None
127    }
128
129    /// As [`Self::count_values_within_byte_budget`] but using gather
130    /// `indices` rather than a contiguous range. Returns the number of
131    /// `indices` that fit, not the maximum index value.
132    fn count_values_within_byte_budget_gather(
133        _values: &Self::Values,
134        _indices: &[usize],
135        _byte_budget: usize,
136    ) -> Option<usize> {
137        None
138    }
139
140    /// Returns the number of buffered values
141    fn num_values(&self) -> usize;
142
143    /// Returns true if this encoder has a dictionary page
144    fn has_dictionary(&self) -> bool;
145
146    /// Returns true if the encoder compresses each value against the value
147    /// immediately before it, within the current page.
148    ///
149    /// For such encodings a page boundary is not free: flushing discards the
150    /// previous value, so the first value of the next page is stored in full.
151    /// [`GenericColumnWriter::should_add_data_page`] uses this to exempt a
152    /// page's mandatory first value from the data page byte limit.
153    ///
154    /// Per encoding:
155    /// * `DELTA_BYTE_ARRAY`: true. Each value is stored as the length of the
156    ///   prefix it shares with its predecessor plus the remaining suffix.
157    /// * Everything else: false, the default. `PLAIN` and
158    ///   `DELTA_LENGTH_BYTE_ARRAY` store a value at the same cost wherever it
159    ///   lands, and a dictionary outlives the pages that index into it, so no
160    ///   page boundary makes a value more expensive.
161    ///
162    /// [`GenericColumnWriter::should_add_data_page`]: crate::column::writer::GenericColumnWriter::should_add_data_page
163    fn compresses_against_previous_value(&self) -> bool {
164        false
165    }
166
167    /// Returns the estimated total memory usage of the encoder
168    ///
169    fn estimated_memory_size(&self) -> usize;
170
171    /// Returns an estimate of the encoded size of dictionary page size in bytes, or `None` if no dictionary
172    fn estimated_dict_page_size(&self) -> Option<usize>;
173
174    /// Returns an estimate of the encoded data page size in bytes
175    ///
176    /// This should include:
177    /// <already_written_encoded_byte_size> + <estimated_encoded_size_of_unflushed_bytes>
178    fn estimated_data_page_size(&self) -> usize;
179
180    /// Flush the dictionary page for this column chunk if any. Any subsequent calls to
181    /// [`Self::write`] will not be dictionary encoded
182    ///
183    /// Note: [`Self::flush_data_page`] must be called first, as this will error if there
184    /// are any pending page values
185    fn flush_dict_page(&mut self) -> Result<Option<DictionaryPage>>;
186
187    /// Flush the next data page for this column chunk
188    fn flush_data_page(&mut self) -> Result<DataPageValues<Self::T>>;
189
190    /// Flushes bloom filter if enabled and returns it, otherwise returns `None`. Subsequent writes
191    /// will *not* be tracked by the bloom filter as it is empty since. This should be called once
192    /// near the end of encoding.
193    fn flush_bloom_filter(&mut self) -> Option<Sbbf>;
194
195    /// Computes [`GeospatialStatistics`], if any, and resets internal state such that any internal
196    /// accumulator is prepared to accumulate statistics for the next column chunk.
197    fn flush_geospatial_statistics(&mut self) -> Option<Box<GeospatialStatistics>>;
198}
199
200pub struct ColumnValueEncoderImpl<T: DataType> {
201    encoder: Box<dyn Encoder<T>>,
202    dict_encoder: Option<DictEncoder<T>>,
203    descr: ColumnDescPtr,
204    num_values: usize,
205    statistics_enabled: EnabledStatistics,
206    min_value: Option<T::T>,
207    max_value: Option<T::T>,
208    nan_count: Option<u64>,
209    bloom_filter: Option<Sbbf>,
210    bloom_filter_target_fpp: f64,
211    variable_length_bytes: Option<i64>,
212    geo_stats_accumulator: Option<Box<dyn GeoStatsAccumulator>>,
213}
214
215impl<T: DataType> ColumnValueEncoderImpl<T> {
216    fn is_floating_point_column(&self) -> bool {
217        matches!(self.descr.physical_type(), Type::FLOAT | Type::DOUBLE)
218            || self.descr.logical_type_ref() == Some(&LogicalType::Float16)
219    }
220
221    fn write_slice(&mut self, slice: &[T::T]) -> Result<()> {
222        if self.statistics_enabled != EnabledStatistics::None
223            // INTERVAL, Geometry, and Geography have undefined sort order, so don't write min/max stats for them
224            && self.descr.converted_type() != ConvertedType::INTERVAL
225        {
226            if let Some(accumulator) = self.geo_stats_accumulator.as_deref_mut() {
227                update_geo_stats_accumulator(accumulator, slice.iter());
228            } else if let Some((min, max, nan_count)) =
229                get_min_max(self.descr.get_basic_info(), slice.iter())
230            {
231                update_min(&self.descr, &min, &mut self.min_value);
232                update_max(&self.descr, &max, &mut self.max_value);
233                if self.is_floating_point_column() {
234                    *self.nan_count.get_or_insert(0) += nan_count;
235                }
236            }
237
238            if let Some(var_bytes) = T::T::variable_length_bytes(slice) {
239                *self.variable_length_bytes.get_or_insert(0) += var_bytes;
240            }
241        }
242
243        // While a dictionary is in use the filter is populated from its distinct values
244        // in `flush_dict_page`, so each value is hashed once rather than once per row.
245        match &mut self.dict_encoder {
246            Some(encoder) => encoder.put(slice),
247            _ => {
248                if let Some(bloom_filter) = &mut self.bloom_filter {
249                    for value in slice {
250                        bloom_filter.insert(value);
251                    }
252                }
253                self.encoder.put(slice)
254            }
255        }
256    }
257}
258
259impl<T: DataType> ColumnValueEncoder for ColumnValueEncoderImpl<T> {
260    type T = T::T;
261
262    type Values = [T::T];
263
264    fn flush_bloom_filter(&mut self) -> Option<Sbbf> {
265        let mut sbbf = self.bloom_filter.take()?;
266        sbbf.fold_to_target_fpp(self.bloom_filter_target_fpp);
267        Some(sbbf)
268    }
269
270    #[expect(
271        private_interfaces,
272        reason = "this trait is not nameable outside the crate"
273    )]
274    fn try_new(
275        descr: &ColumnDescPtr,
276        props: &WriterProperties,
277        column_props: &ResolvedColumnProperties,
278    ) -> Result<Self> {
279        let dict_supported = column_props.dictionary_enabled
280            && has_dictionary_support(T::get_physical_type(), props);
281        let dict_encoder = dict_supported.then(|| DictEncoder::new(descr.clone()));
282
283        // Set either main encoder or fallback encoder.
284        let encoder = get_encoder(
285            column_props
286                .encoding
287                .unwrap_or_else(|| fallback_encoding(T::get_physical_type(), props)),
288            descr,
289        )?;
290
291        let statistics_enabled = column_props.statistics_enabled;
292
293        let (bloom_filter, bloom_filter_target_fpp) = create_bloom_filter(column_props)?;
294
295        let geo_stats_accumulator = try_new_geo_stats_accumulator(descr);
296
297        Ok(Self {
298            encoder,
299            dict_encoder,
300            descr: descr.clone(),
301            num_values: 0,
302            statistics_enabled,
303            bloom_filter,
304            bloom_filter_target_fpp,
305            min_value: None,
306            max_value: None,
307            nan_count: None,
308            variable_length_bytes: None,
309            geo_stats_accumulator,
310        })
311    }
312
313    fn write(&mut self, values: &[T::T], offset: usize, len: usize) -> Result<()> {
314        self.num_values += len;
315
316        let slice = values.get(offset..offset + len).ok_or_else(|| {
317            general_err!(
318                "Expected to write {} values, but have only {}",
319                len,
320                values.len() - offset
321            )
322        })?;
323
324        self.write_slice(slice)
325    }
326
327    fn write_gather(&mut self, values: &Self::Values, indices: &[usize]) -> Result<()> {
328        self.num_values += indices.len();
329        let slice: Vec<_> = indices.iter().map(|idx| values[*idx].clone()).collect();
330        self.write_slice(&slice)
331    }
332
333    fn count_values_within_byte_budget(
334        values: &[T::T],
335        offset: usize,
336        len: usize,
337        byte_budget: usize,
338    ) -> Option<usize> {
339        // Clamp so that a caller-supplied `len` that overruns the input
340        // (e.g. a level/value mismatch the encoder will reject later)
341        // returns an estimate instead of panicking here.
342        let end = (offset + len).min(values.len());
343        let start = offset.min(end);
344        count_within_budget::<T>(
345            end - start,
346            byte_budget,
347            values[start..end].iter().map(Some),
348        )
349    }
350
351    fn count_values_within_byte_budget_gather(
352        values: &[T::T],
353        indices: &[usize],
354        byte_budget: usize,
355    ) -> Option<usize> {
356        // `values.get` yields `None` for an out-of-range index (defensive
357        // against a level/value mismatch the encoder rejects later); such a
358        // position is counted but contributes no bytes.
359        count_within_budget::<T>(
360            indices.len(),
361            byte_budget,
362            indices.iter().map(|&i| values.get(i)),
363        )
364    }
365
366    fn num_values(&self) -> usize {
367        self.num_values
368    }
369
370    fn has_dictionary(&self) -> bool {
371        self.dict_encoder.is_some()
372    }
373
374    fn compresses_against_previous_value(&self) -> bool {
375        // While dictionary encoding is active `self.encoder` is unused: the
376        // data page holds RLE indices, which carry no cross-value state.
377        self.dict_encoder.is_none() && self.encoder.encoding() == Encoding::DELTA_BYTE_ARRAY
378    }
379
380    fn estimated_memory_size(&self) -> usize {
381        let encoder_size = self.encoder.estimated_memory_size();
382
383        let dict_encoder_size = self
384            .dict_encoder
385            .as_ref()
386            .map(|encoder| encoder.estimated_memory_size())
387            .unwrap_or_default();
388
389        let bloom_filter_size = self
390            .bloom_filter
391            .as_ref()
392            .map(|bf| bf.estimated_memory_size())
393            .unwrap_or_default();
394
395        encoder_size + dict_encoder_size + bloom_filter_size
396    }
397
398    fn estimated_dict_page_size(&self) -> Option<usize> {
399        Some(self.dict_encoder.as_ref()?.dict_encoded_size())
400    }
401
402    fn estimated_data_page_size(&self) -> usize {
403        match &self.dict_encoder {
404            Some(encoder) => encoder.estimated_data_encoded_size(),
405            _ => self.encoder.estimated_data_encoded_size(),
406        }
407    }
408
409    fn flush_dict_page(&mut self) -> Result<Option<DictionaryPage>> {
410        match self.dict_encoder.take() {
411            Some(encoder) => {
412                if self.num_values != 0 {
413                    return Err(general_err!(
414                        "Must flush data pages before flushing dictionary"
415                    ));
416                }
417
418                if let Some(bloom_filter) = &mut self.bloom_filter {
419                    for value in encoder.uniques() {
420                        bloom_filter.insert(value);
421                    }
422                }
423
424                let buf = encoder.write_dict()?;
425
426                Ok(Some(DictionaryPage {
427                    buf,
428                    num_values: encoder.num_entries(),
429                    is_sorted: encoder.is_sorted(),
430                }))
431            }
432            _ => Ok(None),
433        }
434    }
435
436    fn flush_data_page(&mut self) -> Result<DataPageValues<T::T>> {
437        let (buf, encoding) = match &mut self.dict_encoder {
438            Some(encoder) => (encoder.write_indices()?, Encoding::RLE_DICTIONARY),
439            _ => (self.encoder.flush_buffer()?, self.encoder.encoding()),
440        };
441
442        Ok(DataPageValues {
443            buf,
444            encoding,
445            num_values: std::mem::take(&mut self.num_values),
446            min_value: self.min_value.take(),
447            max_value: self.max_value.take(),
448            nan_count: self.nan_count.take(),
449            variable_length_bytes: self.variable_length_bytes.take(),
450        })
451    }
452
453    fn flush_geospatial_statistics(&mut self) -> Option<Box<GeospatialStatistics>> {
454        self.geo_stats_accumulator.as_mut().map(|a| a.finish())?
455    }
456}
457
458// Get min and max values for all values in `iter`.
459//
460// For floating point we need to compare NaN values until we encounter a non-NaN
461// value which then becomes the new min/max. After this, only non-NaN values are
462// evaluated. If all values are NaN, then the min/max NaNs as determined by
463// IEEE 754 total order are returned.
464fn get_min_max<'a, T, I>(basic_type_info: &BasicTypeInfo, mut iter: I) -> Option<(T, T, u64)>
465where
466    T: ParquetValueType + 'a,
467    I: Iterator<Item = &'a T>,
468{
469    let first = iter.next()?;
470    let mut min_max_nan = is_nan(basic_type_info, first);
471    let mut nan_count = min_max_nan as u64;
472
473    let mut min = first;
474    let mut max = first;
475    for val in iter {
476        match (min_max_nan, is_nan(basic_type_info, val)) {
477            // skip NaNs if we've encounter non-NaN
478            (false, true) => {
479                nan_count += 1;
480            }
481            // if min/max are NaN, check for non-NaN and reset
482            (true, false) => {
483                min = val;
484                max = val;
485                min_max_nan = false;
486            }
487            // both are NaN or non-NaN, so do the comparison
488            (_, val_is_nan) => {
489                nan_count += val_is_nan as u64;
490                // we've already initialized min and max, so a single value can't be both
491                // extremes
492                if compare_greater(basic_type_info, min, val) {
493                    min = val;
494                } else if compare_greater(basic_type_info, val, max) {
495                    max = val;
496                }
497            }
498        }
499    }
500
501    Some((min.clone(), max.clone(), nan_count))
502}
503
504/// Creates a bloom filter sized for the column's configured NDV, returning the filter
505/// and the target FPP for folding.
506pub(crate) fn create_bloom_filter(
507    column_props: &ResolvedColumnProperties,
508) -> Result<(Option<Sbbf>, f64)> {
509    match column_props.bloom_filter_properties.as_ref() {
510        Some(bf_props) => Ok((
511            Some(Sbbf::new_with_ndv_fpp(bf_props.ndv(), bf_props.fpp())?),
512            bf_props.fpp(),
513        )),
514        None => Ok((None, 0.0)),
515    }
516}
517
518fn update_geo_stats_accumulator<'a, T, I>(bounder: &mut dyn GeoStatsAccumulator, iter: I)
519where
520    T: ParquetValueType + 'a,
521    I: Iterator<Item = &'a T>,
522{
523    if bounder.is_valid() {
524        for val in iter {
525            bounder.update_wkb(val.as_bytes());
526        }
527    }
528}
529
530/// Plain-encoded byte cost of a single value of type `T::T`.
531///
532/// Derived from [`ParquetValueType::dict_encoding_size`] (which returns
533/// `(per-value overhead, value-bytes)`) so we don't add a parallel
534/// per-value-size hook to the trait. Mirrors the dispatch in
535/// `KeyStorage::push` (`encodings/encoding/dict_encoder.rs`).
536///
537/// Placed at the end of the module deliberately. Inserting it above the
538/// `ColumnValueEncoder` trait shifts the trait and `ColumnValueEncoderImpl`
539/// within the compiled module enough to perturb downstream code placement,
540/// which measurably regresses unrelated arrow-writer string benchmarks
541/// (~5-9% on `string` / `string_and_binary_view`). Defining it last keeps
542/// the hot encoder code at the offsets it has on `main`.
543#[inline]
544fn plain_encoded_byte_size<T: DataType>(value: &T::T) -> usize {
545    let (overhead, bytes) = value.dict_encoding_size();
546    match <T::T as ParquetValueType>::PHYSICAL_TYPE {
547        // Plain BYTE_ARRAY = 4-byte length prefix + payload.
548        Type::BYTE_ARRAY => overhead + bytes,
549        // Plain FLBA = raw bytes only; `dict_encoding_size`'s length prefix
550        // is irrelevant here, so the encoder passes `type_length` directly.
551        Type::FIXED_LEN_BYTE_ARRAY => bytes,
552        // Numeric/bool are short-circuited by the caller via
553        // `mem::size_of`, so this is unreachable in practice; fall back to
554        // `overhead` defensively.
555        _ => overhead,
556    }
557}
558
559/// How many leading values fit in `byte_budget` bytes, shared by the two
560/// `ColumnValueEncoder::count_values_within_byte_budget*` methods (one walks a
561/// contiguous slice, the other gathers by index).
562///
563/// `n` is the answer when everything fits; `vals` yields each candidate value,
564/// or `None` for a position that should still be counted but contributes no
565/// bytes (an out-of-range gather index). The boundary value that crosses the
566/// budget is included in the count so the caller's page-flush check trips on
567/// this mini-batch rather than leaving a sliver for the next page; this also
568/// catches a lone outlier wherever it lands among small values.
569///
570/// Defined at the end of the module alongside `plain_encoded_byte_size` for
571/// the same reason — see that function's note on code placement and the
572/// `string` / `string_and_binary_view` benchmarks.
573#[inline]
574fn count_within_budget<'a, T: DataType>(
575    n: usize,
576    byte_budget: usize,
577    vals: impl Iterator<Item = Option<&'a T::T>>,
578) -> Option<usize>
579where
580    T::T: 'a,
581{
582    // Fixed-size physical types have a constant per-value byte cost, so the
583    // answer is one division — no walk needed.
584    let phys = <T::T as ParquetValueType>::PHYSICAL_TYPE;
585    if phys != Type::BYTE_ARRAY && phys != Type::FIXED_LEN_BYTE_ARRAY {
586        let per = std::mem::size_of::<T::T>().max(1);
587        return Some((byte_budget / per).max(1).min(n));
588    }
589    // Variable-width: accumulate, exit at the first value past the budget.
590    let mut cum: usize = 0;
591    for (i, v) in vals.enumerate() {
592        if let Some(v) = v {
593            cum = cum.saturating_add(plain_encoded_byte_size::<T>(v));
594        }
595        if cum > byte_budget {
596            return Some(i + 1);
597        }
598    }
599    Some(n)
600}