Skip to main content

mito2/sst/parquet/
flat_format.rs

1// Copyright 2023 Greptime Team
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15//! Format to store in parquet.
16//!
17//! It can store both encoded primary key and raw key columns.
18//!
19//! We store two additional internal columns at last:
20//! - `__primary_key`, the encoded primary key of the row (tags). Type: dictionary(uint32, binary)
21//! - `__sequence`, the sequence number of a row. Type: uint64
22//! - `__op_type`, the op type of the row. Type: uint8
23//!
24//! The format is
25//! ```text
26//! primary key columns, field columns, time index, encoded primary key, __sequence, __op_type.
27//!
28//! It stores field columns in the same order as [RegionMetadata::field_columns()](store_api::metadata::RegionMetadata::field_columns())
29//! and stores primary key columns in the same order as [RegionMetadata::primary_key].
30
31use std::borrow::Borrow;
32use std::collections::HashMap;
33use std::sync::Arc;
34
35use api::v1::SemanticType;
36use datatypes::arrow::array::{
37    Array, ArrayRef, BinaryArray, DictionaryArray, UInt32Array, UInt64Array,
38};
39use datatypes::arrow::compute::kernels::take::take;
40use datatypes::arrow::datatypes::{Schema, SchemaRef};
41use datatypes::arrow::record_batch::RecordBatch;
42use datatypes::prelude::{ConcreteDataType, DataType};
43use mito_codec::row_converter::{CompositeValues, PrimaryKeyCodec, build_primary_key_codec};
44use parquet::file::metadata::RowGroupMetaData;
45use snafu::{OptionExt, ResultExt, ensure};
46use store_api::codec::PrimaryKeyEncoding;
47use store_api::metadata::{RegionMetadata, RegionMetadataRef};
48use store_api::storage::{ColumnId, SequenceNumber};
49
50use crate::error::{
51    ComputeArrowSnafu, DecodeSnafu, InvalidParquetSnafu, InvalidRecordBatchSnafu,
52    NewRecordBatchSnafu, Result,
53};
54use crate::read::read_columns::ReadColumns;
55use crate::sst::parquet::format::{
56    FIXED_POS_COLUMN_NUM, FormatProjection, INTERNAL_COLUMN_NUM, PrimaryKeyArray,
57    PrimaryKeyReadFormat, StatValues, column_null_counts, column_values,
58};
59use crate::sst::parquet::json_align::align_schema_by_nested_paths;
60use crate::sst::parquet::read_columns::ParquetReadColumns;
61use crate::sst::{
62    FlatSchemaOptions, flat_sst_arrow_schema_column_num, tag_maybe_to_dictionary_field,
63    to_flat_sst_arrow_schema, with_field_id,
64};
65
66/// Helper for writing the SST format.
67pub(crate) struct FlatWriteFormat {
68    /// SST file schema.
69    arrow_schema: SchemaRef,
70    override_sequence: Option<SequenceNumber>,
71}
72
73impl FlatWriteFormat {
74    /// Creates a new helper.
75    pub(crate) fn new(metadata: RegionMetadataRef, options: &FlatSchemaOptions) -> FlatWriteFormat {
76        let arrow_schema = to_flat_sst_arrow_schema(&metadata, options);
77        FlatWriteFormat {
78            arrow_schema,
79            override_sequence: None,
80        }
81    }
82
83    /// Set override sequence.
84    pub(crate) fn with_override_sequence(
85        mut self,
86        override_sequence: Option<SequenceNumber>,
87    ) -> Self {
88        self.override_sequence = override_sequence;
89        self
90    }
91
92    /// Gets the arrow schema to store in parquet.
93    #[cfg(test)]
94    pub(crate) fn arrow_schema(&self) -> &SchemaRef {
95        &self.arrow_schema
96    }
97
98    /// Convert `batch` to a arrow record batch to store in parquet.
99    pub(crate) fn convert_batch(&self, batch: &RecordBatch) -> Result<RecordBatch> {
100        debug_assert_eq!(batch.num_columns(), self.arrow_schema.fields().len());
101
102        let Some(override_sequence) = self.override_sequence else {
103            return Ok(batch.clone());
104        };
105
106        let mut columns = batch.columns().to_vec();
107        let sequence_array = Arc::new(UInt64Array::from(vec![override_sequence; batch.num_rows()]));
108        columns[sequence_column_index(batch.num_columns())] = sequence_array;
109
110        RecordBatch::try_new(batch.schema(), columns).context(NewRecordBatchSnafu)
111    }
112}
113
114/// Returns the position of the sequence column.
115pub(crate) fn sequence_column_index(num_columns: usize) -> usize {
116    num_columns - 2
117}
118
119/// Returns the position of the time index column.
120pub(crate) fn time_index_column_index(num_columns: usize) -> usize {
121    num_columns - 4
122}
123
124/// Returns the position of the primary key column.
125pub(crate) fn primary_key_column_index(num_columns: usize) -> usize {
126    num_columns - 3
127}
128
129/// Wraps the `__primary_key` `BinaryArray` back into a `DictionaryArray<UInt32, Binary>` with identity keys.
130pub(crate) fn wrap_pk_binary_to_dict(
131    record_batch: RecordBatch,
132    dict_schema: &SchemaRef,
133) -> Result<RecordBatch> {
134    let pk_idx = primary_key_column_index(record_batch.num_columns());
135    let pk_column = record_batch.column(pk_idx);
136    let binary_array = pk_column
137        .as_any()
138        .downcast_ref::<BinaryArray>()
139        .with_context(|| InvalidRecordBatchSnafu {
140            reason: format!(
141                "expected BinaryArray for __primary_key, got {:?}",
142                pk_column.data_type()
143            ),
144        })?;
145    let n = binary_array.len();
146    let keys = UInt32Array::from_iter_values(0..n as u32);
147    let dict_array: ArrayRef = Arc::new(DictionaryArray::new(keys, pk_column.clone()));
148
149    let mut columns = record_batch.columns().to_vec();
150    columns[pk_idx] = dict_array;
151
152    RecordBatch::try_new(dict_schema.clone(), columns).context(NewRecordBatchSnafu)
153}
154
155/// Returns the position of the op type key column.
156pub(crate) fn op_type_column_index(num_columns: usize) -> usize {
157    num_columns - 1
158}
159
160/// Returns the start index of field columns in a flat batch.
161///
162/// `num_columns` is the total number of columns in the flat batch schema,
163/// including tag columns (if present), field columns, and fixed position columns
164/// (time index, primary key, sequence, op type).
165///
166/// For Dense encoding (raw PK columns included): field_column_start = primary_key.len()
167/// For Sparse encoding (no raw PK columns): field_column_start = 0
168pub(crate) fn field_column_start(metadata: &RegionMetadata, num_columns: usize) -> usize {
169    // Calculates field column start: total columns - fixed columns - field columns
170    // Field column count = total metadata columns - time index column - primary key columns
171    let field_column_count = metadata.column_metadatas.len() - 1 - metadata.primary_key.len();
172    num_columns - FIXED_POS_COLUMN_NUM - field_column_count
173}
174
175// TODO(yingwen): Add an option to skip reading internal columns if the region is
176// append only and doesn't use sparse encoding (We need to check the table id under
177// sparse encoding).
178/// Helper for reading the flat SST format with projection.
179///
180/// It only supports flat format that stores primary keys additionally.
181pub struct FlatReadFormat {
182    /// Sequence number to override the sequence read from the SST.
183    override_sequence: Option<SequenceNumber>,
184    /// Parquet format adapter.
185    parquet_adapter: ParquetAdapter,
186    /// Output schema to wrap binary `__primary_key` back to a dictionary; `None` disables wrapping.
187    pk_dict_wrap_schema: Option<SchemaRef>,
188}
189
190impl FlatReadFormat {
191    /// Creates a helper with existing `metadata` and `column_ids` to read.
192    ///
193    /// If `skip_auto_convert` is true, skips auto conversion of format when the encoding is sparse encoding.
194    pub fn new(
195        metadata: RegionMetadataRef,
196        read_cols: ReadColumns,
197        file_schema: Option<SchemaRef>,
198        file_path: &str,
199        skip_auto_convert: bool,
200    ) -> Result<FlatReadFormat> {
201        let num_columns = file_schema.as_ref().map(|x| x.fields().len());
202        let is_legacy = match num_columns {
203            Some(num) => Self::is_legacy_format(&metadata, num, file_path)?,
204            None => metadata.primary_key_encoding == PrimaryKeyEncoding::Sparse,
205        };
206
207        let parquet_adapter = if is_legacy {
208            // Safety: is_legacy_format() ensures primary_key is not empty.
209            if metadata.primary_key_encoding == PrimaryKeyEncoding::Sparse {
210                // Only skip auto convert when the primary key encoding is sparse.
211                ParquetAdapter::PrimaryKeyToFlat(ParquetPrimaryKeyToFlat::new(
212                    metadata,
213                    read_cols,
214                    skip_auto_convert,
215                ))
216            } else {
217                ParquetAdapter::PrimaryKeyToFlat(ParquetPrimaryKeyToFlat::new(
218                    metadata, read_cols, false,
219                ))
220            }
221        } else {
222            let file_schema = file_schema
223                .unwrap_or_else(|| to_flat_sst_arrow_schema(&metadata, &Default::default()));
224            ParquetAdapter::Flat(ParquetFlat::new(metadata, read_cols, file_schema))
225        };
226
227        Ok(FlatReadFormat {
228            override_sequence: None,
229            parquet_adapter,
230            pk_dict_wrap_schema: None,
231        })
232    }
233
234    /// Sets the sequence number to override.
235    pub(crate) fn set_override_sequence(&mut self, sequence: Option<SequenceNumber>) {
236        self.override_sequence = sequence;
237    }
238
239    /// Enables wrapping binary `__primary_key` batches back to a dictionary in [`Self::convert_batch`].
240    pub(crate) fn set_pk_as_binary(&mut self) -> Result<()> {
241        self.pk_dict_wrap_schema = Some(self.output_arrow_schema()?);
242        Ok(())
243    }
244
245    /// Index of a column in the projected batch by its column id.
246    pub fn projected_index_by_id(&self, column_id: ColumnId) -> Option<usize> {
247        self.format_projection()
248            .column_id_to_projected_index
249            .get(&column_id)
250            .copied()
251    }
252
253    /// Returns min values of specific column in row groups.
254    pub fn min_values(
255        &self,
256        row_groups: &[impl Borrow<RowGroupMetaData>],
257        column_id: ColumnId,
258    ) -> StatValues {
259        match &self.parquet_adapter {
260            ParquetAdapter::Flat(p) => p.min_values(row_groups, column_id),
261            ParquetAdapter::PrimaryKeyToFlat(p) => p.format.min_values(row_groups, column_id),
262        }
263    }
264
265    /// Returns max values of specific column in row groups.
266    pub fn max_values(
267        &self,
268        row_groups: &[impl Borrow<RowGroupMetaData>],
269        column_id: ColumnId,
270    ) -> StatValues {
271        match &self.parquet_adapter {
272            ParquetAdapter::Flat(p) => p.max_values(row_groups, column_id),
273            ParquetAdapter::PrimaryKeyToFlat(p) => p.format.max_values(row_groups, column_id),
274        }
275    }
276
277    /// Returns null counts of specific column in row groups.
278    pub fn null_counts(
279        &self,
280        row_groups: &[impl Borrow<RowGroupMetaData>],
281        column_id: ColumnId,
282    ) -> StatValues {
283        match &self.parquet_adapter {
284            ParquetAdapter::Flat(p) => p.null_counts(row_groups, column_id),
285            ParquetAdapter::PrimaryKeyToFlat(p) => p.format.null_counts(row_groups, column_id),
286        }
287    }
288
289    /// Gets the arrow schema of the SST file.
290    pub(crate) fn arrow_schema(&self) -> &SchemaRef {
291        match &self.parquet_adapter {
292            ParquetAdapter::Flat(p) => &p.arrow_schema,
293            ParquetAdapter::PrimaryKeyToFlat(p) => p.format.arrow_schema(),
294        }
295    }
296
297    /// Gets the projected output schema produced by parquet reading.
298    pub(crate) fn output_arrow_schema(&self) -> Result<SchemaRef> {
299        let read_columns = self.parquet_read_columns();
300        let projection = read_columns.root_indices();
301        let mut schema = self
302            .arrow_schema()
303            .project(projection)
304            .context(ComputeArrowSnafu)?;
305        if read_columns.has_nested() {
306            debug_assert_eq!(schema.fields().len(), read_columns.columns().len());
307            let nested_paths = read_columns.columns().iter().map(|x| x.nested_paths());
308            align_schema_by_nested_paths(&mut schema, nested_paths);
309        }
310        Ok(Arc::new(schema))
311    }
312
313    /// Gets the metadata of the SST.
314    pub(crate) fn metadata(&self) -> &RegionMetadataRef {
315        match &self.parquet_adapter {
316            ParquetAdapter::Flat(p) => &p.metadata,
317            ParquetAdapter::PrimaryKeyToFlat(p) => p.format.metadata(),
318        }
319    }
320
321    /// Get the sorted read columns to read from the sst file.
322    pub(crate) fn parquet_read_columns(&self) -> &ParquetReadColumns {
323        match &self.parquet_adapter {
324            ParquetAdapter::Flat(p) => &p.format_projection.parquet_read_cols,
325            ParquetAdapter::PrimaryKeyToFlat(p) => p.format.parquet_read_columns(),
326        }
327    }
328
329    /// Gets the projection in the flat format.
330    ///
331    /// When `skip_auto_convert` is enabled (primary-key format read), this returns the
332    /// primary-key format projection so filter/prune can resolve projected indices.
333    pub(crate) fn format_projection(&self) -> &FormatProjection {
334        match &self.parquet_adapter {
335            ParquetAdapter::Flat(p) => &p.format_projection,
336            ParquetAdapter::PrimaryKeyToFlat(p) => &p.format_projection,
337        }
338    }
339
340    /// Returns `true` if raw batches from parquet use the flat layout and
341    /// stores primary key columns as raw columns.
342    /// Returns `false` for the legacy primary-key-to-flat conversion path.
343    pub(crate) fn batch_has_raw_pk_columns(&self) -> bool {
344        matches!(&self.parquet_adapter, ParquetAdapter::Flat(_))
345    }
346
347    /// Creates a sequence array to override.
348    pub(crate) fn new_override_sequence_array(&self, length: usize) -> Option<ArrayRef> {
349        self.override_sequence
350            .map(|seq| Arc::new(UInt64Array::from_value(seq, length)) as ArrayRef)
351    }
352
353    /// Convert a record batch to apply flat format conversion and override sequence array.
354    ///
355    /// Returns a new RecordBatch with flat format conversion applied first (if enabled),
356    /// then the sequence column replaced by the override sequence array.
357    pub(crate) fn convert_batch(
358        &self,
359        record_batch: RecordBatch,
360        override_sequence_array: Option<&ArrayRef>,
361    ) -> Result<RecordBatch> {
362        let record_batch = if let Some(dict_schema) = &self.pk_dict_wrap_schema {
363            wrap_pk_binary_to_dict(record_batch, dict_schema)?
364        } else {
365            record_batch
366        };
367
368        // First, apply flat format conversion.
369        let batch = match &self.parquet_adapter {
370            ParquetAdapter::Flat(_) => record_batch,
371            ParquetAdapter::PrimaryKeyToFlat(p) => p.convert_batch(record_batch)?,
372        };
373
374        // Then apply sequence override if provided
375        let Some(override_array) = override_sequence_array else {
376            return Ok(batch);
377        };
378
379        let mut columns = batch.columns().to_vec();
380        let sequence_column_idx = sequence_column_index(batch.num_columns());
381
382        // Use the provided override sequence array, slicing if necessary to match batch length
383        let sequence_array = if override_array.len() > batch.num_rows() {
384            override_array.slice(0, batch.num_rows())
385        } else {
386            override_array.clone()
387        };
388
389        columns[sequence_column_idx] = sequence_array;
390
391        RecordBatch::try_new(batch.schema(), columns).context(NewRecordBatchSnafu)
392    }
393
394    /// Checks whether the batch from the parquet file needs to be converted to match the flat format.
395    ///
396    /// * `metadata` is the region metadata (always assumes flat format).
397    /// * `num_columns` is the number of columns in the parquet file.
398    /// * `file_path` is the path to the parquet file, for error message.
399    pub(crate) fn is_legacy_format(
400        metadata: &RegionMetadata,
401        num_columns: usize,
402        file_path: &str,
403    ) -> Result<bool> {
404        if metadata.primary_key.is_empty() {
405            return Ok(false);
406        }
407
408        // For flat format, compute expected column number:
409        // all columns + internal columns (pk, sequence, op_type)
410        let expected_columns = metadata.column_metadatas.len() + INTERNAL_COLUMN_NUM;
411
412        if expected_columns == num_columns {
413            // Same number of columns, no conversion needed
414            Ok(false)
415        } else {
416            ensure!(
417                expected_columns >= num_columns,
418                InvalidParquetSnafu {
419                    file: file_path,
420                    reason: format!(
421                        "Expected columns {} should be >= actual columns {}",
422                        expected_columns, num_columns
423                    )
424                }
425            );
426
427            // Different number of columns, check if the difference matches primary key count
428            let column_diff = expected_columns - num_columns;
429
430            ensure!(
431                column_diff == metadata.primary_key.len(),
432                InvalidParquetSnafu {
433                    file: file_path,
434                    reason: format!(
435                        "Column number difference {} does not match primary key count {}",
436                        column_diff,
437                        metadata.primary_key.len()
438                    )
439                }
440            );
441
442            Ok(true)
443        }
444    }
445}
446
447/// Wraps the parquet helper for different formats.
448enum ParquetAdapter {
449    Flat(ParquetFlat),
450    PrimaryKeyToFlat(ParquetPrimaryKeyToFlat),
451}
452
453/// Helper to reads the parquet from primary key format into the flat format.
454struct ParquetPrimaryKeyToFlat {
455    /// The primary key format to read the parquet.
456    format: PrimaryKeyReadFormat,
457    /// Format converter for handling flat format conversion.
458    convert_format: Option<FlatConvertFormat>,
459    /// Projection computed for the flat format.
460    format_projection: FormatProjection,
461}
462
463impl ParquetPrimaryKeyToFlat {
464    /// Creates a helper with existing `metadata` and `column_ids` to read.
465    fn new(
466        metadata: RegionMetadataRef,
467        read_cols: ReadColumns,
468        skip_auto_convert: bool,
469    ) -> ParquetPrimaryKeyToFlat {
470        assert!(if skip_auto_convert {
471            metadata.primary_key_encoding == PrimaryKeyEncoding::Sparse
472        } else {
473            true
474        });
475
476        // Creates a map to lookup index based on the new format.
477        let id_to_index = sst_column_id_indices(&metadata);
478        let sst_column_num =
479            flat_sst_arrow_schema_column_num(&metadata, &FlatSchemaOptions::default());
480
481        let codec = build_primary_key_codec(&metadata);
482        let format = PrimaryKeyReadFormat::new(metadata.clone(), read_cols.clone());
483        let (convert_format, format_projection) = if skip_auto_convert {
484            (
485                None,
486                FormatProjection {
487                    parquet_read_cols: format.parquet_read_columns().clone(),
488                    column_id_to_projected_index: format.field_id_to_projected_index().clone(),
489                },
490            )
491        } else {
492            // Computes the format projection for the new format.
493            let format_projection = FormatProjection::compute_format_projection(
494                &id_to_index,
495                sst_column_num,
496                read_cols.clone(),
497            );
498            (
499                FlatConvertFormat::new(Arc::clone(&metadata), &format_projection, codec),
500                format_projection,
501            )
502        };
503
504        Self {
505            format,
506            convert_format,
507            format_projection,
508        }
509    }
510
511    fn convert_batch(&self, record_batch: RecordBatch) -> Result<RecordBatch> {
512        if let Some(convert_format) = &self.convert_format {
513            convert_format.convert(record_batch)
514        } else {
515            Ok(record_batch)
516        }
517    }
518}
519
520/// Helper to reads the parquet in flat format directly.
521struct ParquetFlat {
522    /// The metadata stored in the SST.
523    metadata: RegionMetadataRef,
524    /// SST file schema.
525    arrow_schema: SchemaRef,
526    /// Projection computed for the flat format.
527    format_projection: FormatProjection,
528    /// Column id to index in SST.
529    column_id_to_sst_index: HashMap<ColumnId, usize>,
530}
531
532impl ParquetFlat {
533    /// Creates a helper with existing `metadata` and `column_ids` to read.
534    fn new(
535        metadata: RegionMetadataRef,
536        read_cols: ReadColumns,
537        arrow_schema: SchemaRef,
538    ) -> ParquetFlat {
539        // Creates a map to lookup index.
540        let id_to_index = sst_column_id_indices(&metadata);
541        let sst_column_num =
542            flat_sst_arrow_schema_column_num(&metadata, &FlatSchemaOptions::default());
543        let format_projection =
544            FormatProjection::compute_format_projection(&id_to_index, sst_column_num, read_cols);
545
546        Self {
547            metadata,
548            arrow_schema,
549            format_projection,
550            column_id_to_sst_index: id_to_index,
551        }
552    }
553
554    /// Returns min values of specific column in row groups.
555    fn min_values(
556        &self,
557        row_groups: &[impl Borrow<RowGroupMetaData>],
558        column_id: ColumnId,
559    ) -> StatValues {
560        self.get_stat_values(row_groups, column_id, true)
561    }
562
563    /// Returns max values of specific column in row groups.
564    fn max_values(
565        &self,
566        row_groups: &[impl Borrow<RowGroupMetaData>],
567        column_id: ColumnId,
568    ) -> StatValues {
569        self.get_stat_values(row_groups, column_id, false)
570    }
571
572    /// Returns null counts of specific column in row groups.
573    fn null_counts(
574        &self,
575        row_groups: &[impl Borrow<RowGroupMetaData>],
576        column_id: ColumnId,
577    ) -> StatValues {
578        let Some(index) = self.column_id_to_sst_index.get(&column_id) else {
579            // No such column in the SST.
580            return StatValues::NoColumn;
581        };
582
583        let stats = column_null_counts(row_groups, *index);
584        StatValues::from_stats_opt(stats)
585    }
586
587    fn get_stat_values(
588        &self,
589        row_groups: &[impl Borrow<RowGroupMetaData>],
590        column_id: ColumnId,
591        is_min: bool,
592    ) -> StatValues {
593        let Some(column) = self.metadata.column_by_id(column_id) else {
594            // No such column in the SST.
595            return StatValues::NoColumn;
596        };
597        // Safety: `column_id_to_sst_index` is built from `metadata`.
598        let index = self.column_id_to_sst_index.get(&column_id).unwrap();
599
600        let stats = column_values(row_groups, column, *index, is_min);
601        StatValues::from_stats_opt(stats)
602    }
603}
604
605/// Returns a map that the key is the column id and the value is the column position
606/// in the SST.
607/// It only supports SSTs with raw primary key columns.
608pub(crate) fn sst_column_id_indices(metadata: &RegionMetadata) -> HashMap<ColumnId, usize> {
609    let mut id_to_index = HashMap::with_capacity(metadata.column_metadatas.len());
610    let mut column_index = 0;
611    // keys
612    for pk_id in &metadata.primary_key {
613        id_to_index.insert(*pk_id, column_index);
614        column_index += 1;
615    }
616    // fields
617    for column in &metadata.column_metadatas {
618        if column.semantic_type == SemanticType::Field {
619            id_to_index.insert(column.column_id, column_index);
620            column_index += 1;
621        }
622    }
623    // time index
624    id_to_index.insert(metadata.time_index_column().column_id, column_index);
625
626    id_to_index
627}
628
629/// Decodes primary keys from a batch and returns decoded primary key information.
630///
631/// The batch must contain a primary key column at the expected index.
632pub(crate) fn decode_primary_keys(
633    codec: &dyn PrimaryKeyCodec,
634    batch: &RecordBatch,
635) -> Result<DecodedPrimaryKeys> {
636    let primary_key_index = primary_key_column_index(batch.num_columns());
637    let pk_dict_array = batch
638        .column(primary_key_index)
639        .as_any()
640        .downcast_ref::<PrimaryKeyArray>()
641        .with_context(|| InvalidRecordBatchSnafu {
642            reason: "Primary key column is not a dictionary array".to_string(),
643        })?;
644    let pk_values_array = pk_dict_array
645        .values()
646        .as_any()
647        .downcast_ref::<BinaryArray>()
648        .with_context(|| InvalidRecordBatchSnafu {
649            reason: "Primary key values are not binary array".to_string(),
650        })?;
651
652    let keys = pk_dict_array.keys();
653
654    // Decodes primary key values by iterating through keys, reusing decoded values for duplicate keys.
655    // Maps original key index -> new decoded value index
656    let mut key_to_decoded_index = Vec::with_capacity(keys.len());
657    let mut decoded_pk_values = Vec::new();
658    let mut prev_key: Option<u32> = None;
659
660    // The parquet reader may read the whole dictionary page into the dictionary values, so
661    // we may decode many primary keys not in this batch if we decode the values array directly.
662    let pk_indices = keys.values();
663    for &current_key in pk_indices.iter().take(keys.len()) {
664        // Check if current key is the same as previous key
665        if let Some(prev) = prev_key
666            && prev == current_key
667        {
668            // Reuse the last decoded index
669            key_to_decoded_index.push((decoded_pk_values.len() - 1) as u32);
670            continue;
671        }
672
673        // New key, decodes the value
674        let pk_bytes = pk_values_array.value(current_key as usize);
675        let decoded_value = codec.decode(pk_bytes).context(DecodeSnafu)?;
676
677        decoded_pk_values.push(decoded_value);
678        key_to_decoded_index.push((decoded_pk_values.len() - 1) as u32);
679        prev_key = Some(current_key);
680    }
681
682    // Create the keys array from key_to_decoded_index
683    let keys_array = UInt32Array::from(key_to_decoded_index);
684
685    Ok(DecodedPrimaryKeys {
686        decoded_pk_values,
687        keys_array,
688    })
689}
690
691/// Holds decoded primary key values and their indices.
692pub(crate) struct DecodedPrimaryKeys {
693    /// Decoded primary key values for unique keys in the dictionary.
694    decoded_pk_values: Vec<CompositeValues>,
695    /// Prebuilt keys array for creating dictionary arrays.
696    keys_array: UInt32Array,
697}
698
699impl DecodedPrimaryKeys {
700    /// Gets a tag column array by column id and data type.
701    ///
702    /// For sparse encoding, uses column_id to lookup values.
703    /// For dense encoding, uses pk_index to get values.
704    pub(crate) fn get_tag_column(
705        &self,
706        column_id: ColumnId,
707        pk_index: Option<usize>,
708        column_type: &ConcreteDataType,
709    ) -> Result<ArrayRef> {
710        // Gets values from the primary key.
711        let mut builder = column_type.create_mutable_vector(self.decoded_pk_values.len());
712        for decoded in &self.decoded_pk_values {
713            match decoded {
714                CompositeValues::Dense(dense) => {
715                    let pk_idx = pk_index.expect("pk_index required for dense encoding");
716                    if pk_idx < dense.len() {
717                        builder.push_value_ref(&dense[pk_idx].1.as_value_ref());
718                    } else {
719                        builder.push_null();
720                    }
721                }
722                CompositeValues::Sparse(sparse) => {
723                    let value = sparse.get_or_null(column_id);
724                    builder.push_value_ref(&value.as_value_ref());
725                }
726            };
727        }
728
729        let values_vector = builder.to_vector();
730        let values_array = values_vector.to_arrow_array();
731
732        // Only creates dictionary array for string types, otherwise take values by keys
733        if column_type.is_string() {
734            // Creates dictionary array using the same keys for string types
735            // Note that the dictionary values may have nulls.
736            let dict_array = DictionaryArray::new(self.keys_array.clone(), values_array);
737            Ok(Arc::new(dict_array))
738        } else {
739            // For non-string types, takes values by keys indices to create a regular array
740            let taken_array =
741                take(&values_array, &self.keys_array, None).context(ComputeArrowSnafu)?;
742            Ok(taken_array)
743        }
744    }
745}
746
747/// Converts a batch that doesn't have decoded primary key columns into a batch that has decoded
748/// primary key columns in flat format.
749pub(crate) struct FlatConvertFormat {
750    /// Metadata of the region.
751    metadata: RegionMetadataRef,
752    /// Primary key codec to decode primary keys.
753    codec: Arc<dyn PrimaryKeyCodec>,
754    /// Projected primary key column information: (column_id, pk_index, column_index in metadata).
755    projected_primary_keys: Vec<(ColumnId, usize, usize)>,
756}
757
758impl FlatConvertFormat {
759    /// Creates a new `FlatConvertFormat`.
760    ///
761    /// The `format_projection` is the projection computed in the [FlatReadFormat] with the `metadata`.
762    /// The `codec` is the primary key codec of the `metadata`.
763    ///
764    /// Returns `None` if there is no primary key.
765    pub(crate) fn new(
766        metadata: RegionMetadataRef,
767        format_projection: &FormatProjection,
768        codec: Arc<dyn PrimaryKeyCodec>,
769    ) -> Option<Self> {
770        if metadata.primary_key.is_empty() {
771            return None;
772        }
773
774        // Builds projected primary keys list maintaining the order of RegionMetadata::primary_key
775        let mut projected_primary_keys = Vec::new();
776        for (pk_index, &column_id) in metadata.primary_key.iter().enumerate() {
777            if format_projection
778                .column_id_to_projected_index
779                .contains_key(&column_id)
780            {
781                // We expect the format_projection is built from the metadata.
782                let column_index = metadata.column_index_by_id(column_id).unwrap();
783                projected_primary_keys.push((column_id, pk_index, column_index));
784            }
785        }
786
787        Some(Self {
788            metadata,
789            codec,
790            projected_primary_keys,
791        })
792    }
793
794    /// Converts a batch to have decoded primary key columns in flat format.
795    ///
796    /// The primary key array in the batch is a dictionary array.
797    pub(crate) fn convert(&self, batch: RecordBatch) -> Result<RecordBatch> {
798        if self.projected_primary_keys.is_empty() {
799            return Ok(batch);
800        }
801
802        let decoded_pks = decode_primary_keys(self.codec.as_ref(), &batch)?;
803
804        // Builds decoded tag column arrays.
805        let mut decoded_columns = Vec::new();
806        for (column_id, pk_index, column_index) in &self.projected_primary_keys {
807            let column_metadata = &self.metadata.column_metadatas[*column_index];
808            let tag_column = decoded_pks.get_tag_column(
809                *column_id,
810                Some(*pk_index),
811                &column_metadata.column_schema.data_type,
812            )?;
813            decoded_columns.push(tag_column);
814        }
815
816        // Builds new columns: decoded tag columns first, then original columns
817        let mut new_columns = Vec::with_capacity(batch.num_columns() + decoded_columns.len());
818        new_columns.extend(decoded_columns);
819        new_columns.extend_from_slice(batch.columns());
820
821        // Builds new schema
822        let mut new_fields =
823            Vec::with_capacity(batch.schema().fields().len() + self.projected_primary_keys.len());
824        for (column_id, _, column_index) in &self.projected_primary_keys {
825            let column_metadata = &self.metadata.column_metadatas[*column_index];
826            let old_field = &self.metadata.schema.arrow_schema().fields()[*column_index];
827            let field =
828                tag_maybe_to_dictionary_field(&column_metadata.column_schema.data_type, old_field);
829            new_fields.push(Arc::new(with_field_id((*field).clone(), *column_id)));
830        }
831        new_fields.extend(batch.schema().fields().iter().cloned());
832
833        let new_schema = Arc::new(Schema::new(new_fields));
834        RecordBatch::try_new(new_schema, new_columns).context(NewRecordBatchSnafu)
835    }
836}
837
838#[cfg(test)]
839impl FlatReadFormat {
840    /// Creates a helper with existing `metadata` and all columns.
841    pub fn new_with_all_columns(metadata: RegionMetadataRef) -> FlatReadFormat {
842        Self::new(
843            Arc::clone(&metadata),
844            ReadColumns::from_deduped_column_ids(
845                metadata.column_metadatas.iter().map(|c| c.column_id),
846            ),
847            None,
848            "test",
849            false,
850        )
851        .unwrap()
852    }
853}
854
855#[cfg(test)]
856mod tests {
857    use std::sync::Arc;
858
859    use api::v1::SemanticType;
860    use datatypes::arrow::array::{
861        ArrayRef, BinaryArray, TimestampMillisecondArray, UInt8Array, UInt32Array, UInt64Array,
862    };
863    use datatypes::arrow::datatypes::DataType as ArrowDataType;
864    use datatypes::arrow::record_batch::RecordBatch;
865    use datatypes::prelude::ConcreteDataType;
866    use datatypes::schema::ColumnSchema;
867    use store_api::codec::PrimaryKeyEncoding;
868    use store_api::metadata::{ColumnMetadata, RegionMetadata, RegionMetadataBuilder};
869    use store_api::storage::RegionId;
870    use store_api::storage::consts::PRIMARY_KEY_COLUMN_NAME;
871
872    use super::*;
873    use crate::read::read_columns::ReadColumns;
874    use crate::sst::{
875        FlatSchemaOptions, PARQUET_FIELD_ID_KEY, PRIMARY_KEY_PARQUET_FIELD_ID,
876        flat_sst_arrow_schema_column_num, override_pk_field_to_binary, to_flat_sst_arrow_schema,
877    };
878
879    /// Builds a `RegionMetadata` with the given number of tags and fields.
880    fn build_metadata(
881        num_tags: usize,
882        num_fields: usize,
883        encoding: PrimaryKeyEncoding,
884    ) -> RegionMetadata {
885        let mut builder = RegionMetadataBuilder::new(RegionId::new(0, 0));
886        let mut col_id = 0u32;
887
888        for i in 0..num_tags {
889            builder.push_column_metadata(ColumnMetadata {
890                column_schema: ColumnSchema::new(
891                    format!("tag_{i}"),
892                    ConcreteDataType::string_datatype(),
893                    true,
894                ),
895                semantic_type: SemanticType::Tag,
896                column_id: col_id,
897            });
898            col_id += 1;
899        }
900
901        for i in 0..num_fields {
902            builder.push_column_metadata(ColumnMetadata {
903                column_schema: ColumnSchema::new(
904                    format!("field_{i}"),
905                    ConcreteDataType::uint64_datatype(),
906                    true,
907                ),
908                semantic_type: SemanticType::Field,
909                column_id: col_id,
910            });
911            col_id += 1;
912        }
913
914        builder.push_column_metadata(ColumnMetadata {
915            column_schema: ColumnSchema::new(
916                "ts".to_string(),
917                ConcreteDataType::timestamp_millisecond_datatype(),
918                false,
919            ),
920            semantic_type: SemanticType::Timestamp,
921            column_id: col_id,
922        });
923
924        let primary_key: Vec<u32> = (0..num_tags as u32).collect();
925        builder.primary_key(primary_key);
926        builder.primary_key_encoding(encoding);
927        builder.build().unwrap()
928    }
929
930    #[test]
931    fn test_field_column_start() {
932        // (num_tags, num_fields, encoding, expected)
933        let cases = [
934            (1, 1, PrimaryKeyEncoding::Dense, 1),
935            (2, 2, PrimaryKeyEncoding::Dense, 2),
936            (0, 2, PrimaryKeyEncoding::Dense, 0),
937            (2, 2, PrimaryKeyEncoding::Sparse, 0),
938        ];
939
940        for (num_tags, num_fields, encoding, expected) in cases {
941            let metadata = build_metadata(num_tags, num_fields, encoding);
942            let options = FlatSchemaOptions::from_encoding(encoding);
943            let num_columns = flat_sst_arrow_schema_column_num(&metadata, &options);
944            let result = field_column_start(&metadata, num_columns);
945            assert_eq!(
946                result, expected,
947                "num_tags={num_tags}, num_fields={num_fields}, encoding={encoding:?}"
948            );
949        }
950    }
951
952    #[test]
953    fn test_convert_batch_wraps_binary_pk_to_dict() {
954        use datatypes::arrow::array::{Array, DictionaryArray, StringArray};
955        use datatypes::arrow::datatypes::UInt32Type;
956
957        // build_metadata(1, 1, Dense) projects to:
958        // [tag_0: Dict<UInt32, Utf8>, field_0: UInt64, ts: Timestamp(ms),
959        //  __primary_key: Dict<UInt32, Binary>, __sequence: UInt64, __op_type: UInt8]
960        let metadata = Arc::new(build_metadata(1, 1, PrimaryKeyEncoding::Dense));
961        let column_ids: Vec<u32> = metadata
962            .column_metadatas
963            .iter()
964            .map(|c| c.column_id)
965            .collect();
966        let mut read_format = FlatReadFormat::new(
967            metadata.clone(),
968            ReadColumns::from_deduped_column_ids(column_ids),
969            None,
970            "test",
971            false,
972        )
973        .unwrap();
974        read_format.set_pk_as_binary().unwrap();
975
976        let output_schema = read_format.output_arrow_schema().unwrap();
977        let binary_schema = override_pk_field_to_binary(&output_schema);
978
979        // The __primary_key field must preserve its field_id metadata after
980        // being converted from dictionary to plain binary.
981        let pk_field = binary_schema
982            .field_with_name(PRIMARY_KEY_COLUMN_NAME)
983            .unwrap();
984        assert_eq!(
985            pk_field.metadata().get(PARQUET_FIELD_ID_KEY),
986            Some(&PRIMARY_KEY_PARQUET_FIELD_ID.to_string()),
987            "__primary_key field must retain its PARQUET:field_id after override_pk_field_to_binary"
988        );
989
990        // Repeat the second pk to verify identity keys (no dedup).
991        let tag_keys = UInt32Array::from(vec![0u32, 1, 1]);
992        let tag_values = Arc::new(StringArray::from(vec!["t0", "t1"]));
993        let tag_array: ArrayRef =
994            Arc::new(DictionaryArray::<UInt32Type>::new(tag_keys, tag_values));
995        let field_array: ArrayRef = Arc::new(UInt64Array::from(vec![10u64, 11, 12]));
996        let ts_array: ArrayRef = Arc::new(TimestampMillisecondArray::from(vec![1i64, 2, 3]));
997        let pk_array: ArrayRef = Arc::new(BinaryArray::from_iter_values(
998            [b"alpha".as_ref(), b"beta", b"beta"].iter().copied(),
999        ));
1000        let seq_array: ArrayRef = Arc::new(UInt64Array::from(vec![100u64, 101, 102]));
1001        let op_array: ArrayRef = Arc::new(UInt8Array::from(vec![1u8, 1, 1]));
1002
1003        let batch = RecordBatch::try_new(
1004            binary_schema,
1005            vec![
1006                tag_array,
1007                field_array,
1008                ts_array,
1009                pk_array,
1010                seq_array,
1011                op_array,
1012            ],
1013        )
1014        .unwrap();
1015
1016        let wrapped = read_format.convert_batch(batch, None).unwrap();
1017        assert_eq!(wrapped.schema(), output_schema);
1018
1019        let pk_idx = primary_key_column_index(wrapped.num_columns());
1020        let pk_col = wrapped.column(pk_idx);
1021        assert_eq!(
1022            pk_col.data_type(),
1023            &ArrowDataType::Dictionary(
1024                Box::new(ArrowDataType::UInt32),
1025                Box::new(ArrowDataType::Binary)
1026            )
1027        );
1028        let dict = pk_col
1029            .as_any()
1030            .downcast_ref::<DictionaryArray<UInt32Type>>()
1031            .unwrap();
1032        assert_eq!(dict.keys().values(), &[0, 1, 2]);
1033        let values = dict
1034            .values()
1035            .as_any()
1036            .downcast_ref::<BinaryArray>()
1037            .unwrap();
1038        assert_eq!(values.value(0), b"alpha");
1039        assert_eq!(values.value(1), b"beta");
1040        assert_eq!(values.value(2), b"beta");
1041    }
1042
1043    #[test]
1044    fn test_output_arrow_schema_uses_projection() {
1045        let metadata = Arc::new(build_metadata(1, 2, PrimaryKeyEncoding::Dense));
1046        let read_format = FlatReadFormat::new(
1047            metadata.clone(),
1048            ReadColumns::from_deduped_column_ids([0_u32, 2_u32]),
1049            None,
1050            "test",
1051            false,
1052        )
1053        .unwrap();
1054
1055        let output_schema = read_format.output_arrow_schema().unwrap();
1056        let projection = read_format.parquet_read_columns().root_indices();
1057        let expected = Arc::new(
1058            to_flat_sst_arrow_schema(&metadata, &FlatSchemaOptions::default())
1059                .project(projection)
1060                .unwrap(),
1061        );
1062
1063        assert_eq!(expected, output_schema);
1064    }
1065}