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