Skip to main content

mito2/memtable/bulk/
part.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//! Bulk part encoder/decoder.
16
17use std::collections::{HashMap, HashSet};
18use std::ops::RangeInclusive;
19use std::sync::Arc;
20use std::time::{Duration, Instant};
21
22use api::helper::{ColumnDataTypeWrapper, to_grpc_value};
23use api::v1::bulk_wal_entry::Body;
24use api::v1::{ArrowIpc, BulkWalEntry, Mutation, OpType, SemanticType};
25use bytes::Bytes;
26use common_grpc::flight::{FlightDecoder, FlightEncoder, FlightMessage};
27use common_recordbatch::DfRecordBatch as RecordBatch;
28use common_time::Timestamp;
29use datafusion_common::Column;
30use datafusion_common::pruning::PruningStatistics;
31use datafusion_expr::utils::expr_to_columns;
32use datatypes::arrow;
33use datatypes::arrow::array::{
34    Array, ArrayRef, BinaryArray, BooleanArray, DictionaryArray, StringDictionaryBuilder,
35    TimestampMicrosecondArray, TimestampMillisecondArray, TimestampNanosecondArray,
36    TimestampSecondArray, UInt8Array, UInt32Array, UInt64Array,
37};
38use datatypes::arrow::compute::{SortColumn, SortOptions, concat_batches};
39use datatypes::arrow::datatypes::{
40    DataType as ArrowDataType, Field, Schema, SchemaRef, TimeUnit, UInt32Type,
41};
42use datatypes::data_type::DataType;
43use datatypes::extension::json::align_schema_with_json_array;
44use datatypes::prelude::{MutableVector, Vector};
45use datatypes::value::ValueRef;
46use datatypes::vectors::Helper;
47use mito_codec::key_values::{KeyValue, KeyValues};
48use mito_codec::row_converter::sparse::SparsePrimaryKeyValidator;
49use mito_codec::row_converter::{PrimaryKeyCodec, SortField, build_primary_key_codec_with_fields};
50use parquet::arrow::ArrowWriter;
51use parquet::basic::{Compression, ZstdLevel};
52use parquet::file::metadata::ParquetMetaData;
53use parquet::file::properties::WriterProperties;
54use smallvec::SmallVec;
55use snafu::{OptionExt, ResultExt, ensure};
56use store_api::codec::PrimaryKeyEncoding;
57use store_api::metadata::{RegionMetadata, RegionMetadataRef};
58use store_api::mito_engine_options::FloatFieldEncoding;
59use store_api::storage::consts::PRIMARY_KEY_COLUMN_NAME;
60use store_api::storage::{ColumnId, FileId, SequenceNumber, SequenceRange};
61
62use crate::compaction::{collect_json2_rewrite_plans, rewrite_json2_batch, rewrite_json2_schema};
63use crate::error::{
64    self, ColumnNotFoundSnafu, ComputeArrowSnafu, CreateDefaultSnafu, DataTypeMismatchSnafu,
65    EncodeMemtableSnafu, EncodeSnafu, InvalidMetadataSnafu, InvalidRequestSnafu,
66    NewRecordBatchSnafu, Result,
67};
68use crate::memtable::bulk::context::{BulkIterContext, BulkIterContextRef};
69use crate::memtable::bulk::part_reader::EncodedBulkPartIter;
70use crate::memtable::time_series::{ValueBuilder, Values};
71use crate::memtable::{BoxedRecordBatchIterator, MemScanMetrics, MemtableStats};
72use crate::sst::SeriesEstimator;
73use crate::sst::index::IndexOutput;
74use crate::sst::parquet::flat_format::primary_key_column_index;
75use crate::sst::parquet::format::{PrimaryKeyArray, PrimaryKeyArrayBuilder};
76use crate::sst::parquet::{
77    COLUMN_INDEX_TRUNCATE_LENGTH, PARQUET_METADATA_KEY, SstInfo, apply_column_compression,
78    apply_float_field_encoding, apply_internal_column_encoding,
79};
80
81const INIT_DICT_VALUE_CAPACITY: usize = 8;
82
83/// A raw bulk part in the memtable.
84#[derive(Clone)]
85pub struct BulkPart {
86    pub batch: RecordBatch,
87    pub max_timestamp: i64,
88    pub min_timestamp: i64,
89    /// Uniform sequence for external bulk writes, or the maximum row sequence
90    /// for parts produced by [`BulkPartConverter`]. Rows need not share it.
91    pub sequence: u64,
92    /// Lower bound of row sequences. Slices may inherit a lower bound from
93    /// their parent part rather than recomputing an exact minimum.
94    pub min_sequence: SequenceNumber,
95    pub timestamp_index: usize,
96    pub raw_data: Option<ArrowIpc>,
97}
98
99impl TryFrom<BulkWalEntry> for BulkPart {
100    type Error = error::Error;
101
102    fn try_from(value: BulkWalEntry) -> std::result::Result<Self, Self::Error> {
103        match value.body.expect("Entry payload should be present") {
104            Body::ArrowIpc(ipc) => {
105                let mut decoder = FlightDecoder::try_from_schema_bytes(&ipc.schema)
106                    .context(error::ConvertBulkWalEntrySnafu)?;
107                let batch = decoder
108                    .try_decode_record_batch(&ipc.data_header, &ipc.payload)
109                    .context(error::ConvertBulkWalEntrySnafu)?;
110                Ok(Self {
111                    batch,
112                    max_timestamp: value.max_ts,
113                    min_timestamp: value.min_ts,
114                    sequence: value.sequence,
115                    // Bulk WAL entries represent uniform-sequence external writes.
116                    // Row mutations reconstruct their bounds through BulkPartConverter.
117                    min_sequence: value.sequence,
118                    timestamp_index: value.timestamp_index as usize,
119                    raw_data: Some(ipc),
120                })
121            }
122        }
123    }
124}
125
126impl TryFrom<&BulkPart> for BulkWalEntry {
127    type Error = error::Error;
128
129    fn try_from(value: &BulkPart) -> Result<Self> {
130        if let Some(ipc) = &value.raw_data {
131            Ok(BulkWalEntry {
132                sequence: value.sequence,
133                max_ts: value.max_timestamp,
134                min_ts: value.min_timestamp,
135                timestamp_index: value.timestamp_index as u32,
136                body: Some(Body::ArrowIpc(ipc.clone())),
137            })
138        } else {
139            let mut encoder = FlightEncoder::default();
140            let schema_bytes = encoder
141                .encode_schema(value.batch.schema().as_ref())
142                .data_header;
143            let [rb_data] = encoder
144                .encode(FlightMessage::RecordBatch(value.batch.clone()))
145                .try_into()
146                .map_err(|_| {
147                    error::UnsupportedOperationSnafu {
148                        err_msg: "create BulkWalEntry from RecordBatch with dictionary arrays",
149                    }
150                    .build()
151                })?;
152            Ok(BulkWalEntry {
153                sequence: value.sequence,
154                max_ts: value.max_timestamp,
155                min_ts: value.min_timestamp,
156                timestamp_index: value.timestamp_index as u32,
157                body: Some(Body::ArrowIpc(ArrowIpc {
158                    schema: schema_bytes,
159                    data_header: rb_data.data_header,
160                    payload: rb_data.data_body,
161                })),
162            })
163        }
164    }
165}
166
167impl BulkPart {
168    pub(crate) fn schema(&self) -> SchemaRef {
169        self.batch.schema()
170    }
171
172    pub(crate) fn estimated_size(&self) -> usize {
173        record_batch_estimated_size(&self.batch)
174    }
175
176    /// Returns the estimated series count in this BulkPart.
177    /// This is calculated from the dictionary values count of the PrimaryKeyArray.
178    pub fn estimated_series_count(&self) -> usize {
179        let pk_column_idx = primary_key_column_index(self.batch.num_columns());
180        let pk_column = self.batch.column(pk_column_idx);
181        if let Some(dict_array) = pk_column.as_any().downcast_ref::<PrimaryKeyArray>() {
182            dict_array.values().len()
183        } else {
184            0
185        }
186    }
187
188    /// Creates MemtableStats from this BulkPart.
189    pub fn to_memtable_stats(&self, region_metadata: &RegionMetadataRef) -> MemtableStats {
190        let ts_type = region_metadata.time_index_type();
191        let min_ts = ts_type.create_timestamp(self.min_timestamp);
192        let max_ts = ts_type.create_timestamp(self.max_timestamp);
193
194        MemtableStats {
195            estimated_bytes: self.estimated_size(),
196            time_range: Some((min_ts, max_ts)),
197            num_rows: self.num_rows(),
198            num_ranges: 1,
199            max_sequence: self.sequence,
200            min_sequence: self.min_sequence,
201            series_count: self.estimated_series_count(),
202        }
203    }
204
205    /// Fills missing columns in the BulkPart batch with default values.
206    ///
207    /// This function checks if the batch schema matches the region metadata schema,
208    /// and if there are missing columns, it fills them with default values (or null
209    /// for nullable columns).
210    ///
211    /// # Arguments
212    ///
213    /// * `region_metadata` - The region metadata containing the expected schema
214    pub fn fill_missing_columns(&mut self, region_metadata: &RegionMetadata) -> Result<()> {
215        // Builds a map of existing columns in the batch
216        let batch_schema = self.batch.schema();
217        let batch_columns: HashSet<_> = batch_schema
218            .fields()
219            .iter()
220            .map(|f| f.name().as_str())
221            .collect();
222
223        // Finds columns that need to be filled
224        let mut columns_to_fill = Vec::new();
225        for column_meta in &region_metadata.column_metadatas {
226            // Sparse bulk batches already carry tags in the encoded primary key.
227            if batch_columns.contains(PRIMARY_KEY_COLUMN_NAME)
228                && column_meta.semantic_type == SemanticType::Tag
229            {
230                continue;
231            }
232            // TODO(yingwen): Returns error if it is impure default after we support filling
233            // bulk insert request in the frontend
234            if !batch_columns.contains(column_meta.column_schema.name.as_str()) {
235                columns_to_fill.push(column_meta);
236            }
237        }
238
239        if columns_to_fill.is_empty() {
240            return Ok(());
241        }
242
243        let num_rows = self.batch.num_rows();
244
245        let mut new_columns = Vec::new();
246        let mut new_fields = Vec::new();
247
248        // First, adds all existing columns
249        new_fields.extend(batch_schema.fields().iter().cloned());
250        new_columns.extend_from_slice(self.batch.columns());
251
252        let region_id = region_metadata.region_id;
253        // Then adds the missing columns with default values
254        for column_meta in columns_to_fill {
255            let default_vector = column_meta
256                .column_schema
257                .create_default_vector(num_rows)
258                .context(CreateDefaultSnafu {
259                    region_id,
260                    column: &column_meta.column_schema.name,
261                })?
262                .with_context(|| InvalidRequestSnafu {
263                    region_id,
264                    reason: format!(
265                        "column {} does not have default value",
266                        column_meta.column_schema.name
267                    ),
268                })?;
269            let arrow_array = default_vector.to_arrow_array();
270            new_fields.push(Arc::new(
271                Field::new(
272                    column_meta.column_schema.name.clone(),
273                    arrow_array.data_type().clone(),
274                    column_meta.column_schema.is_nullable(),
275                )
276                .with_metadata(column_meta.column_schema.metadata().clone()),
277            ));
278            new_columns.push(arrow_array);
279        }
280
281        // Create a new schema and batch with the filled columns
282        let new_schema = Arc::new(Schema::new(new_fields));
283        let new_batch =
284            RecordBatch::try_new(new_schema, new_columns).context(NewRecordBatchSnafu)?;
285
286        // Update the batch
287        self.batch = new_batch;
288
289        // The raw data no longer matches the batch after filling missing
290        // columns. Clears it so the WAL entry is re-encoded from the filled
291        // batch, otherwise replaying the entry restores a batch without the
292        // filled columns and the memtable rejects it.
293        self.raw_data = None;
294
295        Ok(())
296    }
297
298    /// Converts [BulkPart] to [Mutation] for fallback `write_bulk` implementation.
299    pub(crate) fn to_mutation(&self, region_metadata: &RegionMetadataRef) -> Result<Mutation> {
300        let vectors = region_metadata
301            .schema
302            .column_schemas()
303            .iter()
304            .map(|col| match self.batch.column_by_name(&col.name) {
305                None => Ok(None),
306                Some(col) => Helper::try_into_vector(col).map(Some),
307            })
308            .collect::<datatypes::error::Result<Vec<_>>>()
309            .context(error::ComputeVectorSnafu)?;
310
311        let rows = (0..self.num_rows())
312            .map(|row_idx| {
313                let values = (0..self.batch.num_columns())
314                    .map(|col_idx| {
315                        if let Some(v) = &vectors[col_idx] {
316                            to_grpc_value(v.get(row_idx))
317                        } else {
318                            api::v1::Value { value_data: None }
319                        }
320                    })
321                    .collect::<Vec<_>>();
322                api::v1::Row { values }
323            })
324            .collect::<Vec<_>>();
325
326        let schema = region_metadata
327            .column_metadatas
328            .iter()
329            .map(|c| {
330                let data_type_wrapper =
331                    ColumnDataTypeWrapper::try_from(c.column_schema.data_type.clone())?;
332                Ok(api::v1::ColumnSchema {
333                    column_name: c.column_schema.name.clone(),
334                    datatype: data_type_wrapper.datatype() as i32,
335                    semantic_type: c.semantic_type as i32,
336                    ..Default::default()
337                })
338            })
339            .collect::<api::error::Result<Vec<_>>>()
340            .context(error::ConvertColumnDataTypeSnafu {
341                reason: "failed to convert region metadata to column schema",
342            })?;
343
344        let rows = api::v1::Rows { schema, rows };
345
346        Ok(Mutation {
347            op_type: OpType::Put as i32,
348            sequence: self.sequence,
349            rows: Some(rows),
350            write_hint: None,
351        })
352    }
353
354    pub fn timestamps(&self) -> &ArrayRef {
355        self.batch.column(self.timestamp_index)
356    }
357
358    pub fn num_rows(&self) -> usize {
359        self.batch.num_rows()
360    }
361}
362
363/// A collection of small unordered bulk parts.
364/// Used to batch small parts together before merging them into a sorted part.
365pub struct UnorderedPart {
366    /// Small bulk parts that haven't been sorted yet.
367    parts: Vec<BulkPart>,
368    /// Total number of rows across all parts.
369    total_rows: usize,
370    /// Total estimated uncompressed bytes across all parts.
371    total_bytes: usize,
372    /// Minimum timestamp across all parts.
373    min_timestamp: i64,
374    /// Maximum timestamp across all parts.
375    max_timestamp: i64,
376    /// Maximum sequence number across all parts.
377    max_sequence: u64,
378    /// Minimum sequence lower bound across all parts.
379    min_sequence: SequenceNumber,
380    /// Row count threshold for accepting parts (default: 1024).
381    threshold: usize,
382    /// Row count threshold for compacting (default: 4096).
383    compact_threshold: usize,
384}
385
386impl Default for UnorderedPart {
387    fn default() -> Self {
388        Self::new()
389    }
390}
391
392impl UnorderedPart {
393    /// Creates a new empty UnorderedPart.
394    pub fn new() -> Self {
395        Self {
396            parts: Vec::new(),
397            total_rows: 0,
398            total_bytes: 0,
399            min_timestamp: i64::MAX,
400            max_timestamp: i64::MIN,
401            max_sequence: 0,
402            min_sequence: SequenceNumber::MAX,
403            threshold: 1024,
404            compact_threshold: 4096,
405        }
406    }
407
408    /// Sets the threshold for accepting parts into unordered_part.
409    pub fn set_threshold(&mut self, threshold: usize) {
410        self.threshold = threshold;
411    }
412
413    /// Sets the threshold for compacting unordered_part.
414    pub fn set_compact_threshold(&mut self, compact_threshold: usize) {
415        self.compact_threshold = compact_threshold;
416    }
417
418    /// Returns the threshold for accepting parts.
419    pub fn threshold(&self) -> usize {
420        self.threshold
421    }
422
423    /// Returns the compact threshold.
424    pub fn compact_threshold(&self) -> usize {
425        self.compact_threshold
426    }
427
428    /// Returns true if this part should accept the given row count.
429    pub fn should_accept(&self, num_rows: usize) -> bool {
430        num_rows < self.threshold
431    }
432
433    /// Returns true if this part should be compacted by row count.
434    pub fn should_compact(&self) -> bool {
435        self.total_rows >= self.compact_threshold
436    }
437
438    /// Returns the total estimated uncompressed bytes across all parts.
439    pub(super) fn estimated_bytes(&self) -> usize {
440        self.total_bytes
441    }
442
443    /// Returns the sequence interval occupied by pending small writes.
444    pub(super) fn sequence_bounds(&self) -> Option<(SequenceNumber, SequenceNumber)> {
445        (!self.is_empty()).then_some((self.min_sequence, self.max_sequence))
446    }
447
448    /// Adds a BulkPart to this unordered collection.
449    pub fn push(&mut self, part: BulkPart) {
450        self.total_rows += part.num_rows();
451        self.total_bytes = self.total_bytes.saturating_add(part.estimated_size());
452        self.min_timestamp = self.min_timestamp.min(part.min_timestamp);
453        self.max_timestamp = self.max_timestamp.max(part.max_timestamp);
454        self.max_sequence = self.max_sequence.max(part.sequence);
455        self.min_sequence = self.min_sequence.min(part.min_sequence);
456        self.parts.push(part);
457    }
458
459    /// Returns the total number of rows across all parts.
460    pub fn num_rows(&self) -> usize {
461        self.total_rows
462    }
463
464    /// Returns true if there are no parts.
465    pub fn is_empty(&self) -> bool {
466        self.parts.is_empty()
467    }
468
469    /// Returns the number of parts in this collection.
470    pub fn num_parts(&self) -> usize {
471        self.parts.len()
472    }
473
474    /// Concatenates and sorts all parts into a single RecordBatch.
475    /// Returns None if the collection is empty.
476    pub fn concat_and_sort(&self, metadata: &RegionMetadataRef) -> Result<Option<RecordBatch>> {
477        if self.parts.is_empty() {
478            return Ok(None);
479        }
480
481        if self.parts.len() == 1 {
482            // If there's only one part, return its batch directly
483            return Ok(Some(self.parts[0].batch.clone()));
484        }
485
486        let schemas = self
487            .parts
488            .iter()
489            .map(|x| (x.batch.schema(), x.num_rows() as u64))
490            .collect::<Vec<_>>();
491        let plans = collect_json2_rewrite_plans(metadata, &schemas)?;
492
493        debug_assert!(self.parts.windows(2).all(|w| rewrite_json2_schema(
494            &w[0].batch.schema(),
495            &plans
496        ) == rewrite_json2_schema(
497            &w[1].batch.schema(),
498            &plans
499        )));
500        let schema = rewrite_json2_schema(&self.parts[0].batch.schema(), &plans);
501
502        let batches = self
503            .parts
504            .iter()
505            .map(|x| rewrite_json2_batch(x.batch.clone(), &plans))
506            .collect::<Result<Vec<_>>>()?;
507        let concatenated = concat_batches(&schema, &batches).context(ComputeArrowSnafu)?;
508
509        // Sort the concatenated batch
510        let sorted_batch = sort_primary_key_record_batch(&concatenated)?;
511
512        Ok(Some(sorted_batch))
513    }
514
515    /// Converts all parts into a single sorted BulkPart.
516    /// Returns None if the collection is empty.
517    pub fn to_bulk_part(&self, metadata: &RegionMetadataRef) -> Result<Option<BulkPart>> {
518        let Some(sorted_batch) = self.concat_and_sort(metadata)? else {
519            return Ok(None);
520        };
521
522        let timestamp_index = self.parts[0].timestamp_index;
523
524        Ok(Some(BulkPart {
525            batch: sorted_batch,
526            max_timestamp: self.max_timestamp,
527            min_timestamp: self.min_timestamp,
528            sequence: self.max_sequence,
529            min_sequence: self.min_sequence,
530            timestamp_index,
531            raw_data: None,
532        }))
533    }
534
535    /// Clears all parts from this collection.
536    pub fn clear(&mut self) {
537        self.parts.clear();
538        self.total_rows = 0;
539        self.total_bytes = 0;
540        self.min_timestamp = i64::MAX;
541        self.max_timestamp = i64::MIN;
542        self.max_sequence = 0;
543        self.min_sequence = SequenceNumber::MAX;
544    }
545}
546
547/// More accurate estimation of the size of a record batch.
548pub fn record_batch_estimated_size(batch: &RecordBatch) -> usize {
549    batch
550        .columns()
551        .iter()
552        // If can not get slice memory size, assume 0 here.
553        .map(|c| c.to_data().get_slice_memory_size().unwrap_or(0))
554        .sum()
555}
556
557/// Primary key column builder for handling strings specially.
558enum PrimaryKeyColumnBuilder {
559    /// String dictionary builder for string types.
560    StringDict(StringDictionaryBuilder<UInt32Type>),
561    /// Generic mutable vector for other types.
562    Vector(Box<dyn MutableVector>),
563}
564
565impl PrimaryKeyColumnBuilder {
566    /// Appends a value to the builder.
567    fn push_value_ref(&mut self, value: ValueRef) -> Result<()> {
568        match self {
569            PrimaryKeyColumnBuilder::StringDict(builder) => {
570                if let Some(s) = value.try_into_string().context(DataTypeMismatchSnafu)? {
571                    // We know the value is a string.
572                    builder.append_value(s);
573                } else {
574                    builder.append_null();
575                }
576            }
577            PrimaryKeyColumnBuilder::Vector(builder) => {
578                builder.push_value_ref(&value);
579            }
580        }
581        Ok(())
582    }
583
584    /// Converts the builder to an ArrayRef.
585    fn into_arrow_array(self) -> ArrayRef {
586        match self {
587            PrimaryKeyColumnBuilder::StringDict(mut builder) => Arc::new(builder.finish()),
588            PrimaryKeyColumnBuilder::Vector(mut builder) => builder.to_vector().to_arrow_array(),
589        }
590    }
591}
592
593/// Converter that converts structs into [BulkPart].
594pub struct BulkPartConverter {
595    /// Schema of the converted batch.
596    schema: SchemaRef,
597    /// Primary key codec for encoding keys
598    primary_key_codec: Arc<dyn PrimaryKeyCodec>,
599    /// Buffer for encoding primary key.
600    key_buf: Vec<u8>,
601    /// Primary key array builder.
602    key_array_builder: PrimaryKeyArrayBuilder,
603    /// Whether any input key was supplied pre-encoded and still needs validation.
604    has_pre_encoded_keys: bool,
605    /// Builders for non-primary key columns.
606    value_builder: ValueBuilder,
607    /// Builders for individual primary key columns.
608    /// The order of builders is the same as the order of primary key columns in the region metadata.
609    primary_key_column_builders: Vec<PrimaryKeyColumnBuilder>,
610
611    /// Max timestamp value.
612    max_ts: i64,
613    /// Min timestamp value.
614    min_ts: i64,
615    /// Max sequence number.
616    max_sequence: SequenceNumber,
617    /// Min sequence number.
618    min_sequence: SequenceNumber,
619}
620
621impl BulkPartConverter {
622    /// Creates a new converter.
623    ///
624    /// If `store_primary_key_columns` is true and the encoding is not sparse encoding, it
625    /// stores primary key columns in arrays additionally.
626    pub fn new(
627        region_metadata: &RegionMetadataRef,
628        schema: SchemaRef,
629        capacity: usize,
630        primary_key_codec: Arc<dyn PrimaryKeyCodec>,
631        store_primary_key_columns: bool,
632    ) -> Self {
633        debug_assert_eq!(
634            region_metadata.primary_key_encoding,
635            primary_key_codec.encoding()
636        );
637
638        let primary_key_column_builders = if store_primary_key_columns
639            && region_metadata.primary_key_encoding != PrimaryKeyEncoding::Sparse
640        {
641            new_primary_key_column_builders(region_metadata, capacity)
642        } else {
643            Vec::new()
644        };
645
646        Self {
647            schema,
648            primary_key_codec,
649            key_buf: Vec::new(),
650            key_array_builder: PrimaryKeyArrayBuilder::new(),
651            has_pre_encoded_keys: false,
652            value_builder: ValueBuilder::new(region_metadata, capacity),
653            primary_key_column_builders,
654            min_ts: i64::MAX,
655            max_ts: i64::MIN,
656            max_sequence: SequenceNumber::MIN,
657            min_sequence: SequenceNumber::MAX,
658        }
659    }
660
661    /// Appends a [KeyValues] into the converter.
662    pub fn append_key_values(&mut self, key_values: &KeyValues) -> Result<()> {
663        for kv in key_values.iter() {
664            self.append_key_value(&kv)?;
665        }
666
667        Ok(())
668    }
669
670    /// Appends a [KeyValue] to builders.
671    ///
672    /// Metric-sparse hints carry encoded keys; raw tags use the destination codec.
673    fn append_key_value(&mut self, kv: &KeyValue) -> Result<()> {
674        // Handles primary key based on encoding type
675        if kv.primary_key_encoding() == PrimaryKeyEncoding::Sparse {
676            ensure!(
677                self.primary_key_codec.encoding() == PrimaryKeyEncoding::Sparse,
678                error::InvalidRecordBatchSnafu {
679                    reason: "pre-encoded primary keys require a sparse destination layout",
680                }
681            );
682            // For sparse encoding, the primary key is already encoded in the KeyValue
683            // Gets the first (and only) primary key value which contains the encoded key
684            let mut primary_keys = kv.primary_keys();
685            if let Some(encoded) = primary_keys
686                .next()
687                .context(ColumnNotFoundSnafu {
688                    column: PRIMARY_KEY_COLUMN_NAME,
689                })?
690                .try_into_binary()
691                .context(DataTypeMismatchSnafu)?
692            {
693                self.key_array_builder
694                    .append(encoded)
695                    .context(ComputeArrowSnafu)?;
696                self.has_pre_encoded_keys = true;
697            } else {
698                return error::InvalidRecordBatchSnafu {
699                    reason: "encoded metric primary key must not be null",
700                }
701                .fail();
702            }
703        } else {
704            // Encode raw primary key columns with the destination codec.
705            self.key_buf.clear();
706            self.primary_key_codec
707                .encode_key_value(kv, &mut self.key_buf)
708                .context(EncodeSnafu)?;
709            self.key_array_builder
710                .append(&self.key_buf)
711                .context(ComputeArrowSnafu)?;
712        };
713
714        // If storing primary key columns, append values to individual builders
715        if !self.primary_key_column_builders.is_empty() {
716            for (builder, pk_value) in self
717                .primary_key_column_builders
718                .iter_mut()
719                .zip(kv.primary_keys())
720            {
721                builder.push_value_ref(pk_value)?;
722            }
723        }
724
725        // Pushes other columns.
726        self.value_builder.push(
727            kv.timestamp(),
728            kv.sequence(),
729            kv.op_type() as u8,
730            kv.fields(),
731        );
732
733        // Updates statistics
734        // Safety: timestamp of kv must be both present and a valid timestamp value.
735        let ts = kv
736            .timestamp()
737            .try_into_timestamp()
738            .unwrap()
739            .unwrap()
740            .value();
741        self.min_ts = self.min_ts.min(ts);
742        self.max_ts = self.max_ts.max(ts);
743        self.max_sequence = self.max_sequence.max(kv.sequence());
744        self.min_sequence = self.min_sequence.min(kv.sequence());
745
746        Ok(())
747    }
748
749    /// Converts buffered content into a [BulkPart].
750    ///
751    /// It sorts the record batch by (primary key, timestamp, sequence desc).
752    pub fn convert(mut self) -> Result<BulkPart> {
753        let values = Values::from(self.value_builder);
754        let mut columns =
755            Vec::with_capacity(4 + values.fields.len() + self.primary_key_column_builders.len());
756
757        // Build primary key column arrays if enabled.
758        for builder in self.primary_key_column_builders {
759            columns.push(builder.into_arrow_array());
760        }
761        // Then fields columns.
762        columns.extend(values.fields.iter().map(|field| field.to_arrow_array()));
763        // Time index.
764        let timestamp_index = columns.len();
765        columns.push(values.timestamp.to_arrow_array());
766        // Primary key.
767        let pk_array = self.key_array_builder.finish();
768        if self.has_pre_encoded_keys {
769            validate_sparse_dictionary(&pk_array)?;
770        }
771        columns.push(Arc::new(pk_array));
772        // Sequence and op type.
773        columns.push(values.sequence.to_arrow_array());
774        columns.push(values.op_type.to_arrow_array());
775
776        // The actual datatype of JSON array is data oriented, not to be derived from the Region
777        // metadata, which is static. So here we have to align the schema.
778        let schema = align_schema_with_json_array(self.schema, &columns);
779        let batch = RecordBatch::try_new(schema, columns).context(NewRecordBatchSnafu)?;
780        // Sorts the record batch.
781        let batch = sort_primary_key_record_batch(&batch)?;
782
783        Ok(BulkPart {
784            batch,
785            max_timestamp: self.max_ts,
786            min_timestamp: self.min_ts,
787            sequence: self.max_sequence,
788            min_sequence: self.min_sequence,
789            timestamp_index,
790            raw_data: None,
791        })
792    }
793}
794
795/// Validates each referenced key once; sliced dictionaries may retain unused invalid values.
796fn validate_sparse_dictionary(keys: &PrimaryKeyArray) -> Result<()> {
797    let values = keys
798        .values()
799        .as_any()
800        .downcast_ref::<BinaryArray>()
801        .context(error::InvalidRecordBatchSnafu {
802            reason: "encoded primary keys must be binary",
803        })?;
804    ensure!(
805        keys.logical_null_count() == 0,
806        error::InvalidRecordBatchSnafu {
807            reason: "encoded primary key must not be null",
808        }
809    );
810    let mut validated = vec![false; values.len()];
811    let mut validator = SparsePrimaryKeyValidator::default();
812    for &index in keys.keys().values() {
813        let index = index as usize;
814        if !validated[index] {
815            validator
816                .validate(values.value(index))
817                .context(EncodeSnafu)?;
818            validated[index] = true;
819        }
820    }
821    Ok(())
822}
823
824fn new_primary_key_column_builders(
825    metadata: &RegionMetadata,
826    capacity: usize,
827) -> Vec<PrimaryKeyColumnBuilder> {
828    metadata
829        .primary_key_columns()
830        .map(|col| {
831            if col.column_schema.data_type.is_string() {
832                PrimaryKeyColumnBuilder::StringDict(StringDictionaryBuilder::with_capacity(
833                    capacity,
834                    INIT_DICT_VALUE_CAPACITY,
835                    capacity,
836                ))
837            } else {
838                PrimaryKeyColumnBuilder::Vector(
839                    col.column_schema.data_type.create_mutable_vector(capacity),
840                )
841            }
842        })
843        .collect()
844}
845
846/// Sorts the record batch with primary key format.
847pub fn sort_primary_key_record_batch(batch: &RecordBatch) -> Result<RecordBatch> {
848    let indices = if let Some(indices) = try_sort_primary_key_indices(batch)? {
849        indices
850    } else {
851        lexsort_primary_key_indices(batch)?
852    };
853
854    datatypes::arrow::compute::take_record_batch(batch, &indices).context(ComputeArrowSnafu)
855}
856
857/// Sorts the known, non-null primary-key layout without comparing encoded keys for every row.
858///
859/// Returns `None` if the batch does not have the exact layout supported by this fast path.
860fn try_sort_primary_key_indices(batch: &RecordBatch) -> Result<Option<UInt32Array>> {
861    let total_columns = batch.num_columns();
862    let timestamp = batch.column(total_columns - 4);
863    let primary_key = batch.column(total_columns - 3);
864    let sequence = batch.column(total_columns - 2);
865
866    let Some(primary_key) = primary_key
867        .as_any()
868        .downcast_ref::<DictionaryArray<UInt32Type>>()
869    else {
870        return Ok(None);
871    };
872    let Some(sequence) = sequence.as_any().downcast_ref::<UInt64Array>() else {
873        return Ok(None);
874    };
875
876    if batch.num_rows() > u32::MAX as usize
877        || primary_key.null_count() != 0
878        || primary_key.values().data_type() != &ArrowDataType::Binary
879        || primary_key.values().null_count() != 0
880        || primary_key.values().len() > batch.num_rows()
881        || timestamp.null_count() != 0
882        || sequence.null_count() != 0
883    {
884        return Ok(None);
885    }
886
887    let indices = match timestamp.data_type() {
888        ArrowDataType::Timestamp(TimeUnit::Second, _) => timestamp
889            .as_any()
890            .downcast_ref::<TimestampSecondArray>()
891            .map(|timestamp| sort_primary_key_indices(primary_key, timestamp.values(), sequence)),
892        ArrowDataType::Timestamp(TimeUnit::Millisecond, _) => timestamp
893            .as_any()
894            .downcast_ref::<TimestampMillisecondArray>()
895            .map(|timestamp| sort_primary_key_indices(primary_key, timestamp.values(), sequence)),
896        ArrowDataType::Timestamp(TimeUnit::Microsecond, _) => timestamp
897            .as_any()
898            .downcast_ref::<TimestampMicrosecondArray>()
899            .map(|timestamp| sort_primary_key_indices(primary_key, timestamp.values(), sequence)),
900        ArrowDataType::Timestamp(TimeUnit::Nanosecond, _) => timestamp
901            .as_any()
902            .downcast_ref::<TimestampNanosecondArray>()
903            .map(|timestamp| sort_primary_key_indices(primary_key, timestamp.values(), sequence)),
904        _ => None,
905    };
906
907    indices.transpose()
908}
909
910/// Computes sorted row indices by ranking dictionary values, scattering rows into primary-key
911/// buckets, and sorting only buckets that are not already ordered by time and sequence.
912fn sort_primary_key_indices(
913    primary_key: &PrimaryKeyArray,
914    timestamps: &[i64],
915    sequences: &UInt64Array,
916) -> Result<UInt32Array> {
917    debug_assert_eq!(primary_key.len(), timestamps.len());
918    debug_assert_eq!(primary_key.len(), sequences.len());
919    debug_assert_eq!(primary_key.null_count(), 0);
920    debug_assert_eq!(primary_key.values().null_count(), 0);
921    debug_assert_eq!(sequences.null_count(), 0);
922
923    // Ranks compare in the same order as the dictionary values. Equal dictionary values receive
924    // the same rank, which is required because Arrow dictionary concatenation only deduplicates
925    // values on a best-effort basis.
926    let value_ranks = datatypes::arrow::compute::rank(
927        primary_key.values().as_ref(),
928        Some(SortOptions {
929            descending: false,
930            nulls_first: true,
931        }),
932    )
933    .context(ComputeArrowSnafu)?;
934
935    // A rank is at most the number of dictionary values. It is not necessarily dense when the
936    // dictionary contains duplicate values, so keep one extra slot and allow empty buckets.
937    let mut counts = vec![0usize; value_ranks.len() + 1];
938    for &key in primary_key.keys().values() {
939        counts[value_ranks[key as usize] as usize] += 1;
940    }
941
942    let mut bucket_offsets = vec![0usize; counts.len()];
943    let mut offset = 0;
944    for (bucket_offset, &count) in bucket_offsets.iter_mut().zip(&counts) {
945        *bucket_offset = offset;
946        offset += count;
947    }
948
949    // Scatter in input order. This preserves already sorted runs within each primary key.
950    let mut next_offsets = bucket_offsets.clone();
951    let mut indices = vec![0u32; primary_key.len()];
952    for (row, &key) in primary_key.keys().values().iter().enumerate() {
953        let rank = value_ranks[key as usize] as usize;
954        indices[next_offsets[rank]] = row as u32;
955        next_offsets[rank] += 1;
956    }
957
958    let sequences = sequences.values();
959    for (&start, count) in bucket_offsets.iter().zip(counts) {
960        let end = start + count;
961        let bucket = &mut indices[start..end];
962        if !bucket.is_sorted_by(|left, right| {
963            compare_time_sequence(timestamps, sequences, *left, *right).is_le()
964        }) {
965            bucket.sort_unstable_by(|left, right| {
966                compare_time_sequence(timestamps, sequences, *left, *right)
967            });
968        }
969    }
970
971    Ok(UInt32Array::from(indices))
972}
973
974#[inline]
975fn compare_time_sequence(
976    timestamps: &[i64],
977    sequences: &[u64],
978    left: u32,
979    right: u32,
980) -> std::cmp::Ordering {
981    let left = left as usize;
982    let right = right as usize;
983    timestamps[left]
984        .cmp(&timestamps[right])
985        .then_with(|| sequences[right].cmp(&sequences[left]))
986}
987
988fn lexsort_primary_key_indices(batch: &RecordBatch) -> Result<UInt32Array> {
989    let total_columns = batch.num_columns();
990    let sort_columns = vec![
991        // Primary key column (ascending)
992        SortColumn {
993            values: batch.column(total_columns - 3).clone(),
994            options: Some(SortOptions {
995                descending: false,
996                nulls_first: true,
997            }),
998        },
999        // Time index column (ascending)
1000        SortColumn {
1001            values: batch.column(total_columns - 4).clone(),
1002            options: Some(SortOptions {
1003                descending: false,
1004                nulls_first: true,
1005            }),
1006        },
1007        // Sequence column (descending)
1008        SortColumn {
1009            values: batch.column(total_columns - 2).clone(),
1010            options: Some(SortOptions {
1011                descending: true,
1012                nulls_first: true,
1013            }),
1014        },
1015    ];
1016
1017    datatypes::arrow::compute::lexsort_to_indices(&sort_columns, None).context(ComputeArrowSnafu)
1018}
1019
1020/// Converts an unordered `BulkPart` with raw tags or encoded primary keys into a `BulkPart`
1021/// with the same format as produced by [BulkPartConverter].
1022///
1023/// This function takes a `BulkPart` where:
1024/// - Raw primary key columns are stored individually and encoded with the destination codec
1025/// - Sparse destinations also accept encoded keys in a `__primary_key` column
1026/// - The batch may not be sorted
1027///
1028/// And produces a `BulkPart` where:
1029/// - Primary key columns are optionally stored (depending on `store_primary_key_columns` and encoding)
1030/// - An encoded `__primary_key` dictionary column is present
1031/// - The batch is sorted by (primary_key, timestamp, sequence desc)
1032///
1033/// # Arguments
1034///
1035/// * `part` - The input `BulkPart` to convert
1036/// * `region_metadata` - Region metadata containing schema information
1037/// * `primary_key_codec` - Codec for encoding primary keys
1038/// * `schema` - Target schema for the output batch
1039/// * `store_primary_key_columns` - If true and encoding is not sparse, stores individual primary key columns
1040///
1041/// # Returns
1042///
1043/// Returns `None` if the input part has no rows, otherwise returns a new `BulkPart` with
1044/// encoded primary keys and sorted data.
1045pub fn convert_bulk_part(
1046    part: BulkPart,
1047    region_metadata: &RegionMetadataRef,
1048    primary_key_codec: Arc<dyn PrimaryKeyCodec>,
1049    schema: SchemaRef,
1050    store_primary_key_columns: bool,
1051) -> Result<Option<BulkPart>> {
1052    if part.num_rows() == 0 {
1053        return Ok(None);
1054    }
1055
1056    let num_rows = part.num_rows();
1057    let is_sparse = region_metadata.primary_key_encoding == PrimaryKeyEncoding::Sparse;
1058
1059    // Builds a column name-to-index map for efficient lookups
1060    let input_schema = part.batch.schema();
1061    let column_indices: HashMap<&str, usize> = input_schema
1062        .fields()
1063        .iter()
1064        .enumerate()
1065        .map(|(idx, field)| (field.name().as_str(), idx))
1066        .collect();
1067
1068    // Determines the structure of the input batch by looking up columns by name
1069    let mut output_columns = Vec::new();
1070
1071    // Encoded input is explicit; otherwise encode raw tags with the destination codec.
1072    let pk_dictionary: ArrayRef = if column_indices.contains_key(PRIMARY_KEY_COLUMN_NAME) {
1073        ensure!(
1074            is_sparse,
1075            error::InvalidRecordBatchSnafu {
1076                reason: "pre-encoded bulk keys require a sparse destination layout"
1077            }
1078        );
1079        let column = part.batch.column(column_indices[PRIMARY_KEY_COLUMN_NAME]);
1080        ensure!(
1081            column.data_type() == &ArrowDataType::Binary
1082                || matches!(column.data_type(), ArrowDataType::Dictionary(_, value) if value.as_ref() == &ArrowDataType::Binary),
1083            error::InvalidRecordBatchSnafu {
1084                reason: "encoded primary keys must be binary"
1085            }
1086        );
1087        let target_type = ArrowDataType::Dictionary(
1088            Box::new(ArrowDataType::UInt32),
1089            Box::new(ArrowDataType::Binary),
1090        );
1091        let column = arrow::compute::cast(column, &target_type).context(ComputeArrowSnafu)?;
1092        let keys = column.as_any().downcast_ref::<PrimaryKeyArray>().context(
1093            error::InvalidRecordBatchSnafu {
1094                reason: "encoded primary keys must be a UInt32 binary dictionary",
1095            },
1096        )?;
1097        validate_sparse_dictionary(keys)?;
1098        column
1099    } else {
1100        // Extract and encode primary key columns by name.
1101        let pk_vectors: Result<Vec<_>> = region_metadata
1102            .primary_key_columns()
1103            .map(|col_meta| {
1104                let col_idx = column_indices
1105                    .get(col_meta.column_schema.name.as_str())
1106                    .context(ColumnNotFoundSnafu {
1107                        column: &col_meta.column_schema.name,
1108                    })?;
1109                let col = part.batch.column(*col_idx);
1110                Helper::try_into_vector(col).context(error::ComputeVectorSnafu)
1111            })
1112            .collect();
1113        let pk_vectors = pk_vectors?;
1114
1115        let mut key_array_builder = PrimaryKeyArrayBuilder::new();
1116        let mut encode_buf = Vec::new();
1117
1118        for row_idx in 0..num_rows {
1119            encode_buf.clear();
1120
1121            // Collects primary key values with column IDs for this row
1122            let pk_values_with_ids: Vec<_> = region_metadata
1123                .primary_key
1124                .iter()
1125                .zip(pk_vectors.iter())
1126                .map(|(col_id, vector)| (*col_id, vector.get_ref(row_idx)))
1127                .collect();
1128
1129            // Encodes the primary key
1130            primary_key_codec
1131                .encode_value_refs(&pk_values_with_ids, &mut encode_buf)
1132                .context(EncodeSnafu)?;
1133
1134            key_array_builder
1135                .append(&encode_buf)
1136                .context(ComputeArrowSnafu)?;
1137        }
1138
1139        Arc::new(key_array_builder.finish())
1140    };
1141
1142    // Adds primary key columns if storing them (only for dense encoding)
1143    if store_primary_key_columns && !is_sparse {
1144        for col_meta in region_metadata.primary_key_columns() {
1145            let col_idx = column_indices
1146                .get(col_meta.column_schema.name.as_str())
1147                .context(ColumnNotFoundSnafu {
1148                    column: &col_meta.column_schema.name,
1149                })?;
1150            let col = part.batch.column(*col_idx);
1151
1152            // Converts to dictionary if needed for string types
1153            let col = if col_meta.column_schema.data_type.is_string() {
1154                let target_type = ArrowDataType::Dictionary(
1155                    Box::new(ArrowDataType::UInt32),
1156                    Box::new(ArrowDataType::Utf8),
1157                );
1158                arrow::compute::cast(col, &target_type).context(ComputeArrowSnafu)?
1159            } else {
1160                col.clone()
1161            };
1162            output_columns.push(col);
1163        }
1164    }
1165
1166    // Adds field columns
1167    for col_meta in region_metadata.field_columns() {
1168        let col_idx = column_indices
1169            .get(col_meta.column_schema.name.as_str())
1170            .context(ColumnNotFoundSnafu {
1171                column: &col_meta.column_schema.name,
1172            })?;
1173        output_columns.push(part.batch.column(*col_idx).clone());
1174    }
1175
1176    // Adds timestamp column
1177    let new_timestamp_index = output_columns.len();
1178    let ts_col_idx = column_indices
1179        .get(
1180            region_metadata
1181                .time_index_column()
1182                .column_schema
1183                .name
1184                .as_str(),
1185        )
1186        .context(ColumnNotFoundSnafu {
1187            column: &region_metadata.time_index_column().column_schema.name,
1188        })?;
1189    output_columns.push(part.batch.column(*ts_col_idx).clone());
1190
1191    // Adds encoded primary key dictionary column
1192    output_columns.push(pk_dictionary);
1193
1194    let sequence_array = UInt64Array::from(vec![part.sequence; num_rows]);
1195    output_columns.push(Arc::new(sequence_array) as ArrayRef);
1196
1197    let op_type_array = UInt8Array::from(vec![OpType::Put as u8; num_rows]);
1198    output_columns.push(Arc::new(op_type_array) as ArrayRef);
1199
1200    // JSON2 arrays carry a data-dependent shape and their own physical layout version.
1201    // Keep that layout metadata while retaining the destination's column IDs.
1202    let schema = if region_metadata
1203        .field_columns()
1204        .any(|c| c.column_schema.data_type.is_json2())
1205    {
1206        let fields = schema
1207            .fields()
1208            .iter()
1209            .map(|field| {
1210                if !datatypes::extension::json::is_json2_extension_type(field) {
1211                    return field.clone();
1212                }
1213                let source = input_schema.field(column_indices[field.name().as_str()]);
1214                let mut metadata = field.metadata().clone();
1215                if datatypes::extension::json::is_json2_extension_type(source) {
1216                    for key in [
1217                        arrow_schema::extension::EXTENSION_TYPE_NAME_KEY,
1218                        arrow_schema::extension::EXTENSION_TYPE_METADATA_KEY,
1219                    ] {
1220                        if let Some(value) = source.metadata().get(key) {
1221                            metadata.insert(key.to_string(), value.clone());
1222                        } else {
1223                            metadata.remove(key);
1224                        }
1225                    }
1226                }
1227                Arc::new(
1228                    field
1229                        .as_ref()
1230                        .clone()
1231                        .with_data_type(source.data_type().clone())
1232                        .with_metadata(metadata),
1233                )
1234            })
1235            .collect::<Vec<_>>();
1236        Arc::new(Schema::new_with_metadata(fields, schema.metadata().clone()))
1237    } else {
1238        schema
1239    };
1240    let batch = RecordBatch::try_new(schema, output_columns).context(NewRecordBatchSnafu)?;
1241
1242    // Sorts the batch by (primary_key, timestamp, sequence desc)
1243    let sorted_batch = sort_primary_key_record_batch(&batch)?;
1244
1245    Ok(Some(BulkPart {
1246        batch: sorted_batch,
1247        max_timestamp: part.max_timestamp,
1248        min_timestamp: part.min_timestamp,
1249        sequence: part.sequence,
1250        min_sequence: part.sequence,
1251        timestamp_index: new_timestamp_index,
1252        raw_data: None,
1253    }))
1254}
1255
1256#[derive(Debug, Clone)]
1257pub struct EncodedBulkPart {
1258    data: Bytes,
1259    metadata: BulkPartMeta,
1260    /// Cached Arrow schema to avoid rebuilding it from parquet metadata.
1261    schema: SchemaRef,
1262}
1263
1264impl EncodedBulkPart {
1265    pub fn new(data: Bytes, metadata: BulkPartMeta, schema: SchemaRef) -> Self {
1266        Self {
1267            data,
1268            metadata,
1269            schema,
1270        }
1271    }
1272
1273    pub fn metadata(&self) -> &BulkPartMeta {
1274        &self.metadata
1275    }
1276
1277    pub(crate) fn schema(&self) -> SchemaRef {
1278        self.schema.clone()
1279    }
1280
1281    /// Returns the size of the encoded data in bytes
1282    pub(crate) fn size_bytes(&self) -> usize {
1283        self.data.len()
1284    }
1285
1286    /// Returns the encoded data.
1287    pub fn data(&self) -> &Bytes {
1288        &self.data
1289    }
1290
1291    /// Creates MemtableStats from this EncodedBulkPart.
1292    pub fn to_memtable_stats(&self) -> MemtableStats {
1293        let meta = &self.metadata;
1294        let ts_type = meta.region_metadata.time_index_type();
1295        let min_ts = ts_type.create_timestamp(meta.min_timestamp);
1296        let max_ts = ts_type.create_timestamp(meta.max_timestamp);
1297
1298        MemtableStats {
1299            estimated_bytes: self.size_bytes(),
1300            time_range: Some((min_ts, max_ts)),
1301            num_rows: meta.num_rows,
1302            num_ranges: 1,
1303            max_sequence: meta.max_sequence,
1304            min_sequence: meta.min_sequence,
1305            series_count: meta.num_series as usize,
1306        }
1307    }
1308
1309    /// Converts this `EncodedBulkPart` to `SstInfo`.
1310    ///
1311    /// # Arguments
1312    /// * `file_id` - The SST file ID to assign to this part
1313    ///
1314    /// # Returns
1315    /// Returns a `SstInfo` instance with information derived from this bulk part's metadata
1316    pub(crate) fn to_sst_info(&self, file_id: FileId) -> SstInfo {
1317        let unit = self.metadata.region_metadata.time_index_type().unit();
1318        let max_row_group_uncompressed_size: u64 = self
1319            .metadata
1320            .parquet_metadata
1321            .row_groups()
1322            .iter()
1323            .map(|rg| {
1324                rg.columns()
1325                    .iter()
1326                    .map(|c| c.uncompressed_size() as u64)
1327                    .sum::<u64>()
1328            })
1329            .max()
1330            .unwrap_or(0);
1331        SstInfo {
1332            file_id,
1333            time_range: (
1334                Timestamp::new(self.metadata.min_timestamp, unit),
1335                Timestamp::new(self.metadata.max_timestamp, unit),
1336            ),
1337            file_size: self.data.len() as u64,
1338            max_row_group_uncompressed_size,
1339            num_rows: self.metadata.num_rows,
1340            num_row_groups: self.metadata.parquet_metadata.num_row_groups() as u64,
1341            file_metadata: Some(self.metadata.parquet_metadata.clone()),
1342            index_metadata: IndexOutput::default(),
1343            num_series: self.metadata.num_series,
1344        }
1345    }
1346
1347    pub(crate) fn read(
1348        &self,
1349        context: BulkIterContextRef,
1350        sequence: Option<SequenceRange>,
1351        mem_scan_metrics: Option<MemScanMetrics>,
1352    ) -> Result<Option<BoxedRecordBatchIterator>> {
1353        // Compute skip_fields for row group pruning from the configured pre-filter mode.
1354        let skip_fields_for_pruning = context.pre_filter_mode().skip_fields();
1355
1356        // use predicate to find row groups to read.
1357        let row_groups_to_read =
1358            context.row_groups_to_read(&self.metadata.parquet_metadata, skip_fields_for_pruning);
1359
1360        if row_groups_to_read.is_empty() {
1361            // All row groups are filtered.
1362            return Ok(None);
1363        }
1364
1365        let iter = EncodedBulkPartIter::try_new(
1366            self,
1367            context,
1368            row_groups_to_read,
1369            sequence,
1370            mem_scan_metrics,
1371        )?;
1372        Ok(Some(Box::new(iter) as BoxedRecordBatchIterator))
1373    }
1374}
1375
1376// TODO(yingwen): max_sequence
1377#[derive(Debug, Clone)]
1378pub struct BulkPartMeta {
1379    /// Total rows in part.
1380    pub num_rows: usize,
1381    /// Max timestamp in part.
1382    pub max_timestamp: i64,
1383    /// Min timestamp in part.
1384    pub min_timestamp: i64,
1385    /// Part file metadata.
1386    pub parquet_metadata: Arc<ParquetMetaData>,
1387    /// Part region schema.
1388    pub region_metadata: RegionMetadataRef,
1389    /// Number of series.
1390    pub num_series: u64,
1391    /// Lower bound of input sequences, including rows removed by deduplication.
1392    pub min_sequence: SequenceNumber,
1393    /// Maximum sequence number in part.
1394    pub max_sequence: u64,
1395}
1396
1397/// Metrics for encoding a part.
1398#[derive(Default, Debug)]
1399pub struct BulkPartEncodeMetrics {
1400    /// Cost of iterating over the data.
1401    pub iter_cost: Duration,
1402    /// Cost of writing the data.
1403    pub write_cost: Duration,
1404    /// Size of data before encoding.
1405    pub raw_size: usize,
1406    /// Size of data after encoding.
1407    pub encoded_size: usize,
1408    /// Number of rows in part.
1409    pub num_rows: usize,
1410}
1411
1412pub struct BulkPartEncoder {
1413    metadata: RegionMetadataRef,
1414    writer_props: Option<WriterProperties>,
1415}
1416
1417impl BulkPartEncoder {
1418    pub fn new(metadata: RegionMetadataRef, row_group_size: usize) -> Result<BulkPartEncoder> {
1419        Self::new_with_float_field_encoding(metadata, row_group_size, FloatFieldEncoding::default())
1420    }
1421
1422    pub(super) fn new_with_float_field_encoding(
1423        metadata: RegionMetadataRef,
1424        row_group_size: usize,
1425        float_field_encoding: FloatFieldEncoding,
1426    ) -> Result<BulkPartEncoder> {
1427        // TODO(yingwen): Skip arrow schema if needed.
1428        let json = metadata.to_json().context(InvalidMetadataSnafu)?;
1429        let key_value_meta =
1430            parquet::file::metadata::KeyValue::new(PARQUET_METADATA_KEY.to_string(), json);
1431
1432        let mut props = WriterProperties::builder()
1433            .set_key_value_metadata(Some(vec![key_value_meta]))
1434            .set_write_batch_size(row_group_size)
1435            .set_max_row_group_row_count(Some(row_group_size))
1436            .set_compression(Compression::ZSTD(ZstdLevel::default()))
1437            .set_column_index_truncate_length(COLUMN_INDEX_TRUNCATE_LENGTH)
1438            .set_statistics_truncate_length(None);
1439        props = apply_internal_column_encoding(props, &metadata);
1440        props = apply_float_field_encoding(props, &metadata, float_field_encoding);
1441        props = apply_column_compression(props, &metadata)?;
1442        let writer_props = Some(props.build());
1443
1444        Ok(Self {
1445            metadata,
1446            writer_props,
1447        })
1448    }
1449}
1450
1451impl BulkPartEncoder {
1452    /// Encodes [BoxedRecordBatchIterator] into [EncodedBulkPart] with min/max timestamps.
1453    pub fn encode_record_batch_iter(
1454        &self,
1455        iter: BoxedRecordBatchIterator,
1456        arrow_schema: SchemaRef,
1457        min_timestamp: i64,
1458        max_timestamp: i64,
1459        sequence_bounds: RangeInclusive<SequenceNumber>,
1460        metrics: &mut BulkPartEncodeMetrics,
1461    ) -> Result<Option<EncodedBulkPart>> {
1462        let mut buf = Vec::with_capacity(4096);
1463        let mut writer =
1464            ArrowWriter::try_new(&mut buf, arrow_schema.clone(), self.writer_props.clone())
1465                .context(EncodeMemtableSnafu)?;
1466        let mut total_rows = 0;
1467        let mut series_estimator = SeriesEstimator::default();
1468
1469        // Process each batch from the iterator
1470        let mut iter_start = Instant::now();
1471        for batch_result in iter {
1472            metrics.iter_cost += iter_start.elapsed();
1473            let batch = batch_result?;
1474            if batch.num_rows() == 0 {
1475                continue;
1476            }
1477
1478            series_estimator.update_flat(&batch);
1479            metrics.raw_size += record_batch_estimated_size(&batch);
1480            let write_start = Instant::now();
1481            writer.write(&batch).context(EncodeMemtableSnafu)?;
1482            metrics.write_cost += write_start.elapsed();
1483            total_rows += batch.num_rows();
1484            iter_start = Instant::now();
1485        }
1486        metrics.iter_cost += iter_start.elapsed();
1487
1488        if total_rows == 0 {
1489            return Ok(None);
1490        }
1491
1492        let close_start = Instant::now();
1493        let file_metadata = writer.close().context(EncodeMemtableSnafu)?;
1494        metrics.write_cost += close_start.elapsed();
1495        metrics.encoded_size += buf.len();
1496        metrics.num_rows += total_rows;
1497
1498        let buf = Bytes::from(buf);
1499        let parquet_metadata = Arc::new(file_metadata);
1500        let num_series = series_estimator.finish();
1501
1502        Ok(Some(EncodedBulkPart {
1503            data: buf,
1504            metadata: BulkPartMeta {
1505                num_rows: total_rows,
1506                max_timestamp,
1507                min_timestamp,
1508                parquet_metadata,
1509                region_metadata: self.metadata.clone(),
1510                num_series,
1511                min_sequence: *sequence_bounds.start(),
1512                max_sequence: *sequence_bounds.end(),
1513            },
1514            schema: arrow_schema,
1515        }))
1516    }
1517
1518    /// Encodes bulk part to a [EncodedBulkPart], returns the encoded data.
1519    pub fn encode_part(&self, part: &BulkPart) -> Result<Option<EncodedBulkPart>> {
1520        if part.batch.num_rows() == 0 {
1521            return Ok(None);
1522        }
1523
1524        let mut buf = Vec::with_capacity(4096);
1525        let arrow_schema = part.batch.schema();
1526
1527        let file_metadata = {
1528            let mut writer =
1529                ArrowWriter::try_new(&mut buf, arrow_schema.clone(), self.writer_props.clone())
1530                    .context(EncodeMemtableSnafu)?;
1531            writer.write(&part.batch).context(EncodeMemtableSnafu)?;
1532            writer.finish().context(EncodeMemtableSnafu)?
1533        };
1534
1535        let buf = Bytes::from(buf);
1536        let parquet_metadata = Arc::new(file_metadata);
1537
1538        Ok(Some(EncodedBulkPart {
1539            data: buf,
1540            metadata: BulkPartMeta {
1541                num_rows: part.batch.num_rows(),
1542                max_timestamp: part.max_timestamp,
1543                min_timestamp: part.min_timestamp,
1544                parquet_metadata,
1545                region_metadata: self.metadata.clone(),
1546                num_series: part.estimated_series_count() as u64,
1547                max_sequence: part.sequence,
1548                min_sequence: part.min_sequence,
1549            },
1550            schema: arrow_schema,
1551        }))
1552    }
1553}
1554
1555/// Per-batch min/max statistics for the first tag column in a `MultiBulkPart`.
1556///
1557/// Since batches are sorted by primary key, we can extract the min/max of the first tag
1558/// from the first/last row's encoded primary key in each batch. These statistics enable
1559/// batch-level pruning using predicates, analogous to row-group pruning in parquet.
1560#[derive(Debug, Clone)]
1561struct BatchStats {
1562    /// Number of batches.
1563    num_batches: usize,
1564    /// Column id of the first tag.
1565    first_tag_id: ColumnId,
1566    /// Min values of the first tag, one element per batch.
1567    min_values: ArrayRef,
1568    /// Max values of the first tag, one element per batch.
1569    max_values: ArrayRef,
1570}
1571
1572impl BatchStats {
1573    /// Computes batch statistics from a slice of record batches.
1574    ///
1575    /// Returns `None` if there is no primary key (no first tag to collect stats for)
1576    /// or if extracting statistics fails.
1577    fn compute(batches: &[RecordBatch], metadata: &RegionMetadata) -> Option<Self> {
1578        // `primary_key.first()` is correct for both dense and sparse encodings.
1579        // For dense, values follow the order of `metadata.primary_key`.
1580        // For sparse, `decode_leftmost` decodes the first value which also
1581        // corresponds to `primary_key.first()`. See `SparsePrimaryKeyCodec` for format details.
1582        let first_tag_id = *metadata.primary_key.first()?;
1583        let first_tag_column = metadata.column_by_id(first_tag_id)?;
1584        let data_type = &first_tag_column.column_schema.data_type;
1585
1586        let converter = build_primary_key_codec_with_fields(
1587            metadata.primary_key_encoding,
1588            [(first_tag_id, SortField::new(data_type.clone()))].into_iter(),
1589        );
1590        let pk_index = primary_key_column_index(batches.first()?.num_columns());
1591
1592        let mut min_builder = data_type.create_mutable_vector(batches.len());
1593        let mut max_builder = data_type.create_mutable_vector(batches.len());
1594
1595        for batch in batches {
1596            match Self::extract_first_tag_bounds(batch, pk_index, &*converter) {
1597                Some((min_val, max_val)) => {
1598                    min_builder.push_value_ref(&min_val.as_value_ref());
1599                    max_builder.push_value_ref(&max_val.as_value_ref());
1600                }
1601                None => {
1602                    min_builder.push_null();
1603                    max_builder.push_null();
1604                }
1605            }
1606        }
1607
1608        Some(Self {
1609            num_batches: batches.len(),
1610            first_tag_id,
1611            min_values: min_builder.to_vector().to_arrow_array(),
1612            max_values: max_builder.to_vector().to_arrow_array(),
1613        })
1614    }
1615
1616    /// Extracts the first tag value from the first and last rows of a batch.
1617    fn extract_first_tag_bounds(
1618        batch: &RecordBatch,
1619        pk_index: usize,
1620        converter: &dyn PrimaryKeyCodec,
1621    ) -> Option<(datatypes::value::Value, datatypes::value::Value)> {
1622        if batch.num_rows() == 0 {
1623            return None;
1624        }
1625
1626        let pk_dict = batch
1627            .column(pk_index)
1628            .as_any()
1629            .downcast_ref::<PrimaryKeyArray>()?;
1630        let pk_values = pk_dict.values().as_any().downcast_ref::<BinaryArray>()?;
1631
1632        let keys = pk_dict.keys();
1633        let min_key = keys.value(0);
1634        let max_key = keys.value(batch.num_rows() - 1);
1635        let min_bytes = pk_values.value(min_key as usize);
1636        let max_bytes = pk_values.value(max_key as usize);
1637
1638        Some((
1639            converter.decode_leftmost(min_bytes).ok()??,
1640            converter.decode_leftmost(max_bytes).ok()??,
1641        ))
1642    }
1643}
1644
1645/// Adapter implementing `PruningStatistics` for `BatchStats`.
1646///
1647/// Used with `Predicate::prune_with_stats()` to skip batches whose first-tag
1648/// min/max range does not match the query predicate.
1649struct BatchPruningStats<'a> {
1650    stats: &'a BatchStats,
1651    metadata: &'a RegionMetadataRef,
1652}
1653
1654impl PruningStatistics for BatchPruningStats<'_> {
1655    fn min_values(&self, column: &Column) -> Option<ArrayRef> {
1656        let col = self.metadata.column_by_name(&column.name)?;
1657        if col.column_id == self.stats.first_tag_id {
1658            Some(self.stats.min_values.clone())
1659        } else {
1660            None
1661        }
1662    }
1663
1664    fn max_values(&self, column: &Column) -> Option<ArrayRef> {
1665        let col = self.metadata.column_by_name(&column.name)?;
1666        if col.column_id == self.stats.first_tag_id {
1667            Some(self.stats.max_values.clone())
1668        } else {
1669            None
1670        }
1671    }
1672
1673    fn num_containers(&self) -> usize {
1674        self.stats.num_batches
1675    }
1676
1677    fn null_counts(&self, _column: &Column) -> Option<ArrayRef> {
1678        None
1679    }
1680
1681    fn row_counts(&self) -> Option<ArrayRef> {
1682        None
1683    }
1684
1685    fn contained(
1686        &self,
1687        _column: &Column,
1688        _values: &std::collections::HashSet<datafusion_common::ScalarValue>,
1689    ) -> Option<BooleanArray> {
1690        None
1691    }
1692}
1693
1694/// Returns true if the predicate references the given column name.
1695fn predicate_references_column(predicate: &table::predicate::Predicate, column_name: &str) -> bool {
1696    let mut columns = HashSet::new();
1697    for expr in predicate.exprs() {
1698        let _ = expr_to_columns(expr, &mut columns);
1699    }
1700    columns.iter().any(|col| col.name == column_name)
1701}
1702
1703/// Returns true if the batch should be pruned (skipped) based on the first-tag min/max
1704/// statistics and the predicate in the context. Returns false if no pruning is possible
1705/// (no primary key, no predicate, or the batch matches the predicate).
1706pub(crate) fn should_prune_bulk_part(
1707    batch: &RecordBatch,
1708    context: &BulkIterContext,
1709    metadata: &RegionMetadata,
1710) -> bool {
1711    let predicate = match &context.predicate {
1712        Some(p) => p,
1713        None => return false,
1714    };
1715    // Check if the predicate references the first tag column to avoid computing
1716    // expensive batch statistics when they won't help with pruning.
1717    let first_tag_id = match metadata.primary_key.first() {
1718        Some(id) => *id,
1719        None => return false,
1720    };
1721    // Safety: `first_tag_id` comes from `metadata.primary_key` so the column always exists.
1722    let first_tag_name = &metadata
1723        .column_by_id(first_tag_id)
1724        .unwrap()
1725        .column_schema
1726        .name;
1727    if !predicate_references_column(predicate, first_tag_name) {
1728        return false;
1729    }
1730    let stats = match BatchStats::compute(std::slice::from_ref(batch), metadata) {
1731        Some(s) => s,
1732        None => return false,
1733    };
1734    let region_meta = context.read_format().metadata();
1735    let pruning_stats = BatchPruningStats {
1736        stats: &stats,
1737        metadata: region_meta,
1738    };
1739    let mask = predicate.prune_with_stats(&pruning_stats, region_meta.schema.arrow_schema());
1740    !mask.first().copied().unwrap_or(true)
1741}
1742
1743/// A collection of ordered RecordBatches representing a bulk part without parquet encoding.
1744///
1745/// Similar to `EncodedBulkPart` but stores raw RecordBatches instead of encoded parquet data.
1746/// The RecordBatches must be ordered by (primary key, timestamp, sequence desc).
1747/// Uses SmallVec to optimize for the common case of few batches while avoiding heap allocation.
1748#[derive(Debug, Clone)]
1749pub struct MultiBulkPart {
1750    /// Ordered record batches. SmallVec optimized for up to 4 batches inline.
1751    batches: SmallVec<[RecordBatch; 4]>,
1752    /// Total rows across all batches.
1753    total_rows: usize,
1754    /// Max timestamp in part.
1755    max_timestamp: i64,
1756    /// Min timestamp in part.
1757    min_timestamp: i64,
1758    /// Lower bound of input sequences, including rows removed by deduplication.
1759    min_sequence: SequenceNumber,
1760    /// Max sequence number in part.
1761    max_sequence: SequenceNumber,
1762    /// Number of series.
1763    series_count: usize,
1764    /// Pre-computed per-batch statistics for the first tag column.
1765    /// `None` if there is no primary key.
1766    batch_stats: Option<BatchStats>,
1767}
1768
1769impl MultiBulkPart {
1770    /// Creates a new MultiBulkPart from a single BulkPart.
1771    pub fn from_bulk_part(part: BulkPart, metadata: &RegionMetadata) -> Self {
1772        let num_rows = part.num_rows();
1773        let series_count = part.estimated_series_count();
1774        let batch_stats = BatchStats::compute(std::slice::from_ref(&part.batch), metadata);
1775        let mut batches = SmallVec::new();
1776        batches.push(part.batch);
1777
1778        Self {
1779            batches,
1780            total_rows: num_rows,
1781            max_timestamp: part.max_timestamp,
1782            min_timestamp: part.min_timestamp,
1783            min_sequence: part.min_sequence,
1784            max_sequence: part.sequence,
1785            series_count,
1786            batch_stats,
1787        }
1788    }
1789
1790    /// Creates a new MultiBulkPart from multiple ordered RecordBatches.
1791    ///
1792    /// # Arguments
1793    /// * `batches` - Ordered record batches
1794    /// * `min_timestamp` - Minimum timestamp across all batches
1795    /// * `max_timestamp` - Maximum timestamp across all batches
1796    /// * `min_sequence` - Lower bound of input sequences
1797    /// * `max_sequence` - Maximum sequence number across all batches
1798    /// * `series_count` - Number of series in the batches
1799    /// * `metadata` - Region metadata for computing batch statistics
1800    ///
1801    /// # Panics
1802    /// Panics if batches is empty.
1803    pub fn new(
1804        batches: Vec<RecordBatch>,
1805        min_timestamp: i64,
1806        max_timestamp: i64,
1807        min_sequence: SequenceNumber,
1808        max_sequence: SequenceNumber,
1809        series_count: usize,
1810        metadata: &RegionMetadata,
1811    ) -> Self {
1812        assert!(!batches.is_empty(), "batches must not be empty");
1813
1814        let total_rows = batches.iter().map(|b| b.num_rows()).sum();
1815        let batch_stats = BatchStats::compute(&batches, metadata);
1816
1817        Self {
1818            batches: SmallVec::from_vec(batches),
1819            total_rows,
1820            max_timestamp,
1821            min_timestamp,
1822            min_sequence,
1823            max_sequence,
1824            series_count,
1825            batch_stats,
1826        }
1827    }
1828
1829    /// Returns the total number of rows across all batches.
1830    pub fn num_rows(&self) -> usize {
1831        self.total_rows
1832    }
1833
1834    pub(crate) fn schemas(&self) -> impl Iterator<Item = SchemaRef> + '_ {
1835        self.batches.iter().map(|batch| batch.schema())
1836    }
1837
1838    /// Returns the minimum timestamp.
1839    pub fn min_timestamp(&self) -> i64 {
1840        self.min_timestamp
1841    }
1842
1843    /// Returns the maximum timestamp.
1844    pub fn max_timestamp(&self) -> i64 {
1845        self.max_timestamp
1846    }
1847
1848    /// Returns the lower bound of input sequences.
1849    pub fn min_sequence(&self) -> SequenceNumber {
1850        self.min_sequence
1851    }
1852
1853    /// Returns the maximum sequence number.
1854    pub fn max_sequence(&self) -> SequenceNumber {
1855        self.max_sequence
1856    }
1857
1858    /// Returns the number of series.
1859    pub fn series_count(&self) -> usize {
1860        self.series_count
1861    }
1862
1863    /// Returns the number of record batches in this part.
1864    pub fn num_batches(&self) -> usize {
1865        self.batches.len()
1866    }
1867
1868    /// Returns the estimated memory size of all batches.
1869    pub(crate) fn estimated_size(&self) -> usize {
1870        self.batches.iter().map(record_batch_estimated_size).sum()
1871    }
1872
1873    /// Reads data from this part with the given context and filters.
1874    ///
1875    /// If batch-level statistics are available and a predicate is set, prunes
1876    /// batches whose first-tag min/max range doesn't match the predicate before
1877    /// creating the iterator.
1878    pub(crate) fn read(
1879        &self,
1880        context: BulkIterContextRef,
1881        sequence: Option<SequenceRange>,
1882        mem_scan_metrics: Option<MemScanMetrics>,
1883    ) -> Result<Option<BoxedRecordBatchIterator>> {
1884        if self.batches.is_empty() {
1885            return Ok(None);
1886        }
1887
1888        let batches_to_read = self.prune_batches(&context);
1889
1890        if batches_to_read.is_empty() {
1891            return Ok(None);
1892        }
1893
1894        let iter = crate::memtable::bulk::part_reader::BulkPartBatchIter::new(
1895            batches_to_read,
1896            context,
1897            sequence,
1898            self.series_count,
1899            mem_scan_metrics,
1900        );
1901        Ok(Some(Box::new(iter) as BoxedRecordBatchIterator))
1902    }
1903
1904    /// Prunes batches using the first-tag min/max statistics and the predicate.
1905    /// Returns all batches if no stats or no predicate is available.
1906    fn prune_batches(&self, context: &BulkIterContextRef) -> Vec<RecordBatch> {
1907        if let Some(stats) = &self.batch_stats
1908            && let Some(predicate) = &context.predicate
1909        {
1910            let region_meta = context.read_format().metadata();
1911            let pruning_stats = BatchPruningStats {
1912                stats,
1913                metadata: region_meta,
1914            };
1915            let mask =
1916                predicate.prune_with_stats(&pruning_stats, region_meta.schema.arrow_schema());
1917            self.batches
1918                .iter()
1919                .zip(mask.iter())
1920                .filter_map(
1921                    |(batch, &selected)| {
1922                        if selected { Some(batch.clone()) } else { None }
1923                    },
1924                )
1925                .collect()
1926        } else {
1927            self.batches.iter().cloned().collect()
1928        }
1929    }
1930
1931    /// Converts this `MultiBulkPart` to `MemtableStats`.
1932    pub fn to_memtable_stats(&self, region_metadata: &RegionMetadataRef) -> MemtableStats {
1933        let ts_type = region_metadata.time_index_type();
1934        let min_ts = ts_type.create_timestamp(self.min_timestamp);
1935        let max_ts = ts_type.create_timestamp(self.max_timestamp);
1936
1937        MemtableStats {
1938            estimated_bytes: self.estimated_size(),
1939            time_range: Some((min_ts, max_ts)),
1940            num_rows: self.num_rows(),
1941            num_ranges: 1,
1942            max_sequence: self.max_sequence,
1943            min_sequence: self.min_sequence,
1944            series_count: self.series_count,
1945        }
1946    }
1947}
1948
1949#[cfg(test)]
1950mod tests {
1951    use api::v1::{Row, WriteHint};
1952    use datafusion_common::ScalarValue;
1953    use datatypes::arrow::array::{
1954        BinaryArray, DictionaryArray, Float64Array, TimestampMillisecondArray,
1955    };
1956    use datatypes::arrow::datatypes::UInt32Type;
1957    use datatypes::prelude::{ConcreteDataType, Value};
1958    use mito_codec::row_converter::build_primary_key_codec;
1959    use rand::rngs::StdRng;
1960    use rand::{Rng, SeedableRng};
1961    use store_api::metadata::RegionMetadataBuilder;
1962    use store_api::storage::consts::ReservedColumnId;
1963    use table::predicate::Predicate;
1964
1965    use super::*;
1966    use crate::memtable::bulk::context::BulkIterContext;
1967    use crate::sst::{FlatSchemaOptions, to_flat_sst_arrow_schema};
1968    use crate::test_util::memtable_util::{build_key_values_with_ts_seq_values, metadata_for_test};
1969
1970    struct MutationInput<'a> {
1971        k0: &'a str,
1972        k1: u32,
1973        timestamps: &'a [i64],
1974        v1: &'a [Option<f64>],
1975        sequence: u64,
1976    }
1977
1978    #[test]
1979    fn test_sort_primary_key_record_batch_with_duplicate_dictionary_values() {
1980        // Dictionary keys 1 and 2 both represent "series_a". This can happen after Arrow
1981        // concatenates dictionaries because dictionary deduplication is best-effort.
1982        let primary_key = DictionaryArray::try_new(
1983            UInt32Array::from(vec![0, 1, 0, 2, 2]),
1984            Arc::new(BinaryArray::from_vec(vec![
1985                b"series_b".as_slice(),
1986                b"series_a".as_slice(),
1987                b"series_a".as_slice(),
1988            ])),
1989        )
1990        .unwrap();
1991        let timestamps = TimestampMillisecondArray::from(vec![20, 30, 10, 30, 10]);
1992        let sequences = UInt64Array::from(vec![5, 6, 4, 8, 9]);
1993        let row_ids = UInt32Array::from_iter_values(0..5);
1994        let op_types = UInt8Array::from_value(OpType::Put as u8, 5);
1995        let schema = Arc::new(Schema::new(vec![
1996            Field::new("row_id", ArrowDataType::UInt32, false),
1997            Field::new(
1998                "ts",
1999                ArrowDataType::Timestamp(TimeUnit::Millisecond, None),
2000                false,
2001            ),
2002            Field::new(
2003                PRIMARY_KEY_COLUMN_NAME,
2004                ArrowDataType::Dictionary(
2005                    Box::new(ArrowDataType::UInt32),
2006                    Box::new(ArrowDataType::Binary),
2007                ),
2008                false,
2009            ),
2010            Field::new("__sequence", ArrowDataType::UInt64, false),
2011            Field::new("__op_type", ArrowDataType::UInt8, false),
2012        ]));
2013        let batch = RecordBatch::try_new(
2014            schema,
2015            vec![
2016                Arc::new(row_ids),
2017                Arc::new(timestamps),
2018                Arc::new(primary_key),
2019                Arc::new(sequences),
2020                Arc::new(op_types),
2021            ],
2022        )
2023        .unwrap();
2024
2025        let expected_indices = lexsort_primary_key_indices(&batch).unwrap();
2026        let expected =
2027            datatypes::arrow::compute::take_record_batch(&batch, &expected_indices).unwrap();
2028        let actual = sort_primary_key_record_batch(&batch).unwrap();
2029
2030        let expected_row_ids = expected
2031            .column(0)
2032            .as_any()
2033            .downcast_ref::<UInt32Array>()
2034            .unwrap();
2035        let actual_row_ids = actual
2036            .column(0)
2037            .as_any()
2038            .downcast_ref::<UInt32Array>()
2039            .unwrap();
2040        assert_eq!(expected_row_ids, actual_row_ids);
2041        assert_eq!(actual_row_ids.values(), &[4, 3, 1, 2, 0]);
2042    }
2043
2044    #[test]
2045    fn test_sort_primary_key_record_batch_against_lexsort() {
2046        let mut rng = StdRng::seed_from_u64(0x5eed);
2047        for _ in 0..100 {
2048            let num_rows = rng.random_range(0..128);
2049            let keys = UInt32Array::from_iter_values((0..num_rows).map(|_| rng.random_range(0..8)));
2050            let primary_key = DictionaryArray::try_new(
2051                keys,
2052                Arc::new(BinaryArray::from_vec(vec![
2053                    b"d".as_slice(),
2054                    b"a".as_slice(),
2055                    b"c".as_slice(),
2056                    b"b".as_slice(),
2057                    b"a".as_slice(),
2058                    b"d".as_slice(),
2059                    b"b".as_slice(),
2060                    b"c".as_slice(),
2061                ])),
2062            )
2063            .unwrap();
2064            let timestamps = TimestampMillisecondArray::from_iter_values(
2065                (0..num_rows).map(|_| rng.random_range(-1000..1000)),
2066            );
2067            // Unique sequences ensure that the oracle and fast path have no fully equal sort keys.
2068            let sequences = UInt64Array::from_iter_values(0..num_rows as u64);
2069            let row_ids = UInt32Array::from_iter_values(0..num_rows as u32);
2070            let op_types = UInt8Array::from_value(OpType::Put as u8, num_rows);
2071            let schema = Arc::new(Schema::new(vec![
2072                Field::new("row_id", ArrowDataType::UInt32, false),
2073                Field::new(
2074                    "ts",
2075                    ArrowDataType::Timestamp(TimeUnit::Millisecond, None),
2076                    false,
2077                ),
2078                Field::new(
2079                    PRIMARY_KEY_COLUMN_NAME,
2080                    ArrowDataType::Dictionary(
2081                        Box::new(ArrowDataType::UInt32),
2082                        Box::new(ArrowDataType::Binary),
2083                    ),
2084                    false,
2085                ),
2086                Field::new("__sequence", ArrowDataType::UInt64, false),
2087                Field::new("__op_type", ArrowDataType::UInt8, false),
2088            ]));
2089            let batch = RecordBatch::try_new(
2090                schema,
2091                vec![
2092                    Arc::new(row_ids),
2093                    Arc::new(timestamps),
2094                    Arc::new(primary_key),
2095                    Arc::new(sequences),
2096                    Arc::new(op_types),
2097                ],
2098            )
2099            .unwrap();
2100
2101            let expected_indices = lexsort_primary_key_indices(&batch).unwrap();
2102            let expected =
2103                datatypes::arrow::compute::take_record_batch(&batch, &expected_indices).unwrap();
2104            let actual = sort_primary_key_record_batch(&batch).unwrap();
2105            assert_eq!(expected.column(0), actual.column(0));
2106        }
2107    }
2108
2109    #[test]
2110    fn test_unordered_part_tracks_estimated_bytes() {
2111        let mut part = UnorderedPart::new();
2112        let bulk_part = BulkPart {
2113            batch: RecordBatch::new_empty(Arc::new(arrow::datatypes::Schema::empty())),
2114            max_timestamp: 0,
2115            min_timestamp: 0,
2116            sequence: 0,
2117            min_sequence: 0,
2118            timestamp_index: 0,
2119            raw_data: None,
2120        };
2121        let estimated_size = bulk_part.estimated_size();
2122
2123        part.push(bulk_part);
2124        assert_eq!(estimated_size, part.estimated_bytes());
2125        part.clear();
2126        assert_eq!(0, part.estimated_bytes());
2127        assert!(part.is_empty());
2128    }
2129
2130    #[test]
2131    fn test_unordered_part_should_accept() {
2132        let mut part = UnorderedPart::new();
2133        part.set_threshold(10);
2134        assert!(part.should_accept(9));
2135        assert!(!part.should_accept(10));
2136    }
2137
2138    fn encode(input: &[MutationInput]) -> EncodedBulkPart {
2139        let metadata = metadata_for_test();
2140        let kvs = input
2141            .iter()
2142            .map(|m| {
2143                build_key_values_with_ts_seq_values(
2144                    &metadata,
2145                    m.k0.to_string(),
2146                    m.k1,
2147                    m.timestamps.iter().copied(),
2148                    m.v1.iter().copied(),
2149                    m.sequence,
2150                )
2151            })
2152            .collect::<Vec<_>>();
2153        let schema = to_flat_sst_arrow_schema(&metadata, &FlatSchemaOptions::default());
2154        let primary_key_codec = build_primary_key_codec(&metadata);
2155        let mut converter = BulkPartConverter::new(&metadata, schema, 64, primary_key_codec, true);
2156        for kv in kvs {
2157            converter.append_key_values(&kv).unwrap();
2158        }
2159        let part = converter.convert().unwrap();
2160        let encoder = BulkPartEncoder::new(metadata, 1024).unwrap();
2161        encoder.encode_part(&part).unwrap().unwrap()
2162    }
2163
2164    #[test]
2165    fn test_write_and_read_part_projection() {
2166        let part = encode(&[
2167            MutationInput {
2168                k0: "a",
2169                k1: 0,
2170                timestamps: &[1],
2171                v1: &[Some(0.1)],
2172                sequence: 0,
2173            },
2174            MutationInput {
2175                k0: "b",
2176                k1: 0,
2177                timestamps: &[1],
2178                v1: &[Some(0.0)],
2179                sequence: 0,
2180            },
2181            MutationInput {
2182                k0: "a",
2183                k1: 0,
2184                timestamps: &[2],
2185                v1: &[Some(0.2)],
2186                sequence: 1,
2187            },
2188        ]);
2189
2190        let projection = &[4u32];
2191        let reader = part
2192            .read(
2193                Arc::new(
2194                    BulkIterContext::new(
2195                        part.metadata.region_metadata.clone(),
2196                        Some(projection.as_slice()),
2197                        None,
2198                        false,
2199                        crate::sst::parquet::DEFAULT_READ_BATCH_SIZE,
2200                    )
2201                    .unwrap(),
2202                ),
2203                None,
2204                None,
2205            )
2206            .unwrap()
2207            .expect("expect at least one row group");
2208
2209        let mut total_rows_read = 0;
2210        let mut field: Vec<f64> = vec![];
2211        for res in reader {
2212            let batch = res.unwrap();
2213            assert_eq!(5, batch.num_columns());
2214            field.extend_from_slice(
2215                batch
2216                    .column(0)
2217                    .as_any()
2218                    .downcast_ref::<Float64Array>()
2219                    .unwrap()
2220                    .values(),
2221            );
2222            total_rows_read += batch.num_rows();
2223        }
2224        assert_eq!(3, total_rows_read);
2225        assert_eq!(vec![0.1, 0.2, 0.0], field);
2226    }
2227
2228    fn prepare(key_values: Vec<(&str, u32, (i64, i64), u64)>) -> EncodedBulkPart {
2229        let metadata = metadata_for_test();
2230        let kvs = key_values
2231            .into_iter()
2232            .map(|(k0, k1, (start, end), sequence)| {
2233                let ts = start..end;
2234                let v1 = (start..end).map(|_| None);
2235                build_key_values_with_ts_seq_values(&metadata, k0.to_string(), k1, ts, v1, sequence)
2236            })
2237            .collect::<Vec<_>>();
2238        let schema = to_flat_sst_arrow_schema(&metadata, &FlatSchemaOptions::default());
2239        let primary_key_codec = build_primary_key_codec(&metadata);
2240        let mut converter = BulkPartConverter::new(&metadata, schema, 64, primary_key_codec, true);
2241        for kv in kvs {
2242            converter.append_key_values(&kv).unwrap();
2243        }
2244        let part = converter.convert().unwrap();
2245        let encoder = BulkPartEncoder::new(metadata, 1024).unwrap();
2246        encoder.encode_part(&part).unwrap().unwrap()
2247    }
2248
2249    fn check_prune_row_group(
2250        part: &EncodedBulkPart,
2251        predicate: Option<Predicate>,
2252        expected_rows: usize,
2253    ) {
2254        let context = Arc::new(
2255            BulkIterContext::new(
2256                part.metadata.region_metadata.clone(),
2257                None,
2258                predicate,
2259                false,
2260                crate::sst::parquet::DEFAULT_READ_BATCH_SIZE,
2261            )
2262            .unwrap(),
2263        );
2264        let reader = part
2265            .read(context, None, None)
2266            .unwrap()
2267            .expect("expect at least one row group");
2268        let mut total_rows_read = 0;
2269        for res in reader {
2270            let batch = res.unwrap();
2271            total_rows_read += batch.num_rows();
2272        }
2273        // Should only read row group 1.
2274        assert_eq!(expected_rows, total_rows_read);
2275    }
2276
2277    #[test]
2278    fn test_prune_row_groups() {
2279        let part = prepare(vec![
2280            ("a", 0, (0, 40), 1),
2281            ("a", 1, (0, 60), 1),
2282            ("b", 0, (0, 100), 2),
2283            ("b", 1, (100, 180), 3),
2284            ("b", 1, (180, 210), 4),
2285        ]);
2286
2287        let context = Arc::new(
2288            BulkIterContext::new(
2289                part.metadata.region_metadata.clone(),
2290                None,
2291                Some(Predicate::new(vec![datafusion_expr::col("ts").eq(
2292                    datafusion_expr::lit(ScalarValue::TimestampMillisecond(Some(300), None)),
2293                )])),
2294                false,
2295                crate::sst::parquet::DEFAULT_READ_BATCH_SIZE,
2296            )
2297            .unwrap(),
2298        );
2299        assert!(part.read(context, None, None).unwrap().is_none());
2300
2301        check_prune_row_group(&part, None, 310);
2302
2303        check_prune_row_group(
2304            &part,
2305            Some(Predicate::new(vec![
2306                datafusion_expr::col("k0").eq(datafusion_expr::lit("a")),
2307                datafusion_expr::col("k1").eq(datafusion_expr::lit(0u32)),
2308            ])),
2309            40,
2310        );
2311
2312        check_prune_row_group(
2313            &part,
2314            Some(Predicate::new(vec![
2315                datafusion_expr::col("k0").eq(datafusion_expr::lit("a")),
2316                datafusion_expr::col("k1").eq(datafusion_expr::lit(1u32)),
2317            ])),
2318            60,
2319        );
2320
2321        check_prune_row_group(
2322            &part,
2323            Some(Predicate::new(vec![
2324                datafusion_expr::col("k0").eq(datafusion_expr::lit("a")),
2325            ])),
2326            100,
2327        );
2328
2329        check_prune_row_group(
2330            &part,
2331            Some(Predicate::new(vec![
2332                datafusion_expr::col("k0").eq(datafusion_expr::lit("b")),
2333                datafusion_expr::col("k1").eq(datafusion_expr::lit(0u32)),
2334            ])),
2335            100,
2336        );
2337
2338        // Predicates over field column can do precise filtering.
2339        check_prune_row_group(
2340            &part,
2341            Some(Predicate::new(vec![
2342                datafusion_expr::col("v0").eq(datafusion_expr::lit(150i64)),
2343            ])),
2344            1,
2345        );
2346    }
2347
2348    #[test]
2349    fn test_bulk_part_converter_append_and_convert() {
2350        let metadata = metadata_for_test();
2351        let capacity = 100;
2352        let primary_key_codec = build_primary_key_codec(&metadata);
2353        let schema = to_flat_sst_arrow_schema(
2354            &metadata,
2355            &FlatSchemaOptions::from_encoding(metadata.primary_key_encoding),
2356        );
2357
2358        let mut converter =
2359            BulkPartConverter::new(&metadata, schema, capacity, primary_key_codec, true);
2360
2361        let key_values1 = build_key_values_with_ts_seq_values(
2362            &metadata,
2363            "key1".to_string(),
2364            1u32,
2365            vec![1000, 2000].into_iter(),
2366            vec![Some(1.0), Some(2.0)].into_iter(),
2367            1,
2368        );
2369
2370        let key_values2 = build_key_values_with_ts_seq_values(
2371            &metadata,
2372            "key2".to_string(),
2373            2u32,
2374            vec![1500].into_iter(),
2375            vec![Some(3.0)].into_iter(),
2376            2,
2377        );
2378
2379        converter.append_key_values(&key_values1).unwrap();
2380        converter.append_key_values(&key_values2).unwrap();
2381
2382        let bulk_part = converter.convert().unwrap();
2383
2384        assert_eq!(bulk_part.num_rows(), 3);
2385        assert_eq!(bulk_part.min_timestamp, 1000);
2386        assert_eq!(bulk_part.max_timestamp, 2000);
2387        assert_eq!(bulk_part.sequence, 2);
2388        assert_eq!(bulk_part.timestamp_index, bulk_part.batch.num_columns() - 4);
2389
2390        // Validate primary key columns are stored
2391        // Schema should include primary key columns k0 and k1 at the beginning
2392        let schema = bulk_part.batch.schema();
2393        let field_names: Vec<&str> = schema.fields().iter().map(|f| f.name().as_str()).collect();
2394        assert_eq!(
2395            field_names,
2396            vec![
2397                "k0",
2398                "k1",
2399                "v0",
2400                "v1",
2401                "ts",
2402                "__primary_key",
2403                "__sequence",
2404                "__op_type"
2405            ]
2406        );
2407    }
2408
2409    #[test]
2410    fn test_bulk_part_converter_sorting() {
2411        let metadata = metadata_for_test();
2412        let capacity = 100;
2413        let primary_key_codec = build_primary_key_codec(&metadata);
2414        let schema = to_flat_sst_arrow_schema(
2415            &metadata,
2416            &FlatSchemaOptions::from_encoding(metadata.primary_key_encoding),
2417        );
2418
2419        let mut converter =
2420            BulkPartConverter::new(&metadata, schema, capacity, primary_key_codec, true);
2421
2422        let key_values1 = build_key_values_with_ts_seq_values(
2423            &metadata,
2424            "z_key".to_string(),
2425            3u32,
2426            vec![3000].into_iter(),
2427            vec![Some(3.0)].into_iter(),
2428            3,
2429        );
2430
2431        let key_values2 = build_key_values_with_ts_seq_values(
2432            &metadata,
2433            "a_key".to_string(),
2434            1u32,
2435            vec![1000].into_iter(),
2436            vec![Some(1.0)].into_iter(),
2437            1,
2438        );
2439
2440        let key_values3 = build_key_values_with_ts_seq_values(
2441            &metadata,
2442            "m_key".to_string(),
2443            2u32,
2444            vec![2000].into_iter(),
2445            vec![Some(2.0)].into_iter(),
2446            2,
2447        );
2448
2449        converter.append_key_values(&key_values1).unwrap();
2450        converter.append_key_values(&key_values2).unwrap();
2451        converter.append_key_values(&key_values3).unwrap();
2452
2453        let bulk_part = converter.convert().unwrap();
2454
2455        assert_eq!(bulk_part.num_rows(), 3);
2456
2457        let ts_column = bulk_part.batch.column(bulk_part.timestamp_index);
2458        let seq_column = bulk_part.batch.column(bulk_part.batch.num_columns() - 2);
2459
2460        let ts_array = ts_column
2461            .as_any()
2462            .downcast_ref::<TimestampMillisecondArray>()
2463            .unwrap();
2464        let seq_array = seq_column.as_any().downcast_ref::<UInt64Array>().unwrap();
2465
2466        assert_eq!(ts_array.values(), &[1000, 2000, 3000]);
2467        assert_eq!(seq_array.values(), &[1, 2, 3]);
2468
2469        // Validate primary key columns are stored
2470        let schema = bulk_part.batch.schema();
2471        let field_names: Vec<&str> = schema.fields().iter().map(|f| f.name().as_str()).collect();
2472        assert_eq!(
2473            field_names,
2474            vec![
2475                "k0",
2476                "k1",
2477                "v0",
2478                "v1",
2479                "ts",
2480                "__primary_key",
2481                "__sequence",
2482                "__op_type"
2483            ]
2484        );
2485    }
2486
2487    #[test]
2488    fn test_bulk_part_converter_empty() {
2489        let metadata = metadata_for_test();
2490        let capacity = 10;
2491        let primary_key_codec = build_primary_key_codec(&metadata);
2492        let schema = to_flat_sst_arrow_schema(
2493            &metadata,
2494            &FlatSchemaOptions::from_encoding(metadata.primary_key_encoding),
2495        );
2496
2497        let converter =
2498            BulkPartConverter::new(&metadata, schema, capacity, primary_key_codec, true);
2499
2500        let bulk_part = converter.convert().unwrap();
2501
2502        assert_eq!(bulk_part.num_rows(), 0);
2503        assert_eq!(bulk_part.min_timestamp, i64::MAX);
2504        assert_eq!(bulk_part.max_timestamp, i64::MIN);
2505        assert_eq!(bulk_part.sequence, SequenceNumber::MIN);
2506
2507        // Validate primary key columns are present in schema even for empty batch
2508        let schema = bulk_part.batch.schema();
2509        let field_names: Vec<&str> = schema.fields().iter().map(|f| f.name().as_str()).collect();
2510        assert_eq!(
2511            field_names,
2512            vec![
2513                "k0",
2514                "k1",
2515                "v0",
2516                "v1",
2517                "ts",
2518                "__primary_key",
2519                "__sequence",
2520                "__op_type"
2521            ]
2522        );
2523    }
2524
2525    #[test]
2526    fn test_bulk_part_converter_without_primary_key_columns() {
2527        let metadata = metadata_for_test();
2528        let primary_key_codec = build_primary_key_codec(&metadata);
2529        let schema = to_flat_sst_arrow_schema(
2530            &metadata,
2531            &FlatSchemaOptions {
2532                raw_pk_columns: false,
2533                string_pk_use_dict: true,
2534                ..Default::default()
2535            },
2536        );
2537
2538        let capacity = 100;
2539        let mut converter =
2540            BulkPartConverter::new(&metadata, schema, capacity, primary_key_codec, false);
2541
2542        let key_values1 = build_key_values_with_ts_seq_values(
2543            &metadata,
2544            "key1".to_string(),
2545            1u32,
2546            vec![1000, 2000].into_iter(),
2547            vec![Some(1.0), Some(2.0)].into_iter(),
2548            1,
2549        );
2550
2551        let key_values2 = build_key_values_with_ts_seq_values(
2552            &metadata,
2553            "key2".to_string(),
2554            2u32,
2555            vec![1500].into_iter(),
2556            vec![Some(3.0)].into_iter(),
2557            2,
2558        );
2559
2560        converter.append_key_values(&key_values1).unwrap();
2561        converter.append_key_values(&key_values2).unwrap();
2562
2563        let bulk_part = converter.convert().unwrap();
2564
2565        assert_eq!(bulk_part.num_rows(), 3);
2566        assert_eq!(bulk_part.min_timestamp, 1000);
2567        assert_eq!(bulk_part.max_timestamp, 2000);
2568        assert_eq!(bulk_part.sequence, 2);
2569        assert_eq!(bulk_part.timestamp_index, bulk_part.batch.num_columns() - 4);
2570
2571        // Validate primary key columns are NOT stored individually
2572        let schema = bulk_part.batch.schema();
2573        let field_names: Vec<&str> = schema.fields().iter().map(|f| f.name().as_str()).collect();
2574        assert_eq!(
2575            field_names,
2576            vec!["v0", "v1", "ts", "__primary_key", "__sequence", "__op_type"]
2577        );
2578    }
2579
2580    #[allow(clippy::too_many_arguments)]
2581    fn build_key_values_with_sparse_encoding(
2582        metadata: &RegionMetadataRef,
2583        table_id: u32,
2584        tsid: u64,
2585        k0: String,
2586        k1: String,
2587        timestamps: impl Iterator<Item = i64>,
2588        values: impl Iterator<Item = Option<f64>>,
2589        sequence: SequenceNumber,
2590    ) -> KeyValues {
2591        // Encode the primary key (__table_id, __tsid, k0, k1) into binary format using the sparse codec
2592        let pk_values = vec![
2593            (ReservedColumnId::table_id(), Value::UInt32(table_id)),
2594            (ReservedColumnId::tsid(), Value::UInt64(tsid)),
2595            (0, Value::String(k0.clone().into())),
2596            (1, Value::String(k1.clone().into())),
2597        ];
2598        let mut encoded_key = Vec::new();
2599        mito_codec::row_converter::SparsePrimaryKeyCodec::schemaless()
2600            .encode_values(&pk_values, &mut encoded_key)
2601            .unwrap();
2602        assert!(!encoded_key.is_empty());
2603
2604        // Create schema for sparse encoding: __primary_key, ts, v0, v1
2605        let column_schema = vec![
2606            api::v1::ColumnSchema {
2607                column_name: PRIMARY_KEY_COLUMN_NAME.to_string(),
2608                datatype: api::helper::ColumnDataTypeWrapper::try_from(
2609                    ConcreteDataType::binary_datatype(),
2610                )
2611                .unwrap()
2612                .datatype() as i32,
2613                semantic_type: api::v1::SemanticType::Tag as i32,
2614                ..Default::default()
2615            },
2616            api::v1::ColumnSchema {
2617                column_name: "ts".to_string(),
2618                datatype: api::helper::ColumnDataTypeWrapper::try_from(
2619                    ConcreteDataType::timestamp_millisecond_datatype(),
2620                )
2621                .unwrap()
2622                .datatype() as i32,
2623                semantic_type: api::v1::SemanticType::Timestamp as i32,
2624                ..Default::default()
2625            },
2626            api::v1::ColumnSchema {
2627                column_name: "v0".to_string(),
2628                datatype: api::helper::ColumnDataTypeWrapper::try_from(
2629                    ConcreteDataType::int64_datatype(),
2630                )
2631                .unwrap()
2632                .datatype() as i32,
2633                semantic_type: api::v1::SemanticType::Field as i32,
2634                ..Default::default()
2635            },
2636            api::v1::ColumnSchema {
2637                column_name: "v1".to_string(),
2638                datatype: api::helper::ColumnDataTypeWrapper::try_from(
2639                    ConcreteDataType::float64_datatype(),
2640                )
2641                .unwrap()
2642                .datatype() as i32,
2643                semantic_type: api::v1::SemanticType::Field as i32,
2644                ..Default::default()
2645            },
2646        ];
2647
2648        let rows = timestamps
2649            .zip(values)
2650            .map(|(ts, v)| Row {
2651                values: vec![
2652                    api::v1::Value {
2653                        value_data: Some(api::v1::value::ValueData::BinaryValue(
2654                            encoded_key.clone(),
2655                        )),
2656                    },
2657                    api::v1::Value {
2658                        value_data: Some(api::v1::value::ValueData::TimestampMillisecondValue(ts)),
2659                    },
2660                    api::v1::Value {
2661                        value_data: Some(api::v1::value::ValueData::I64Value(ts)),
2662                    },
2663                    api::v1::Value {
2664                        value_data: v.map(api::v1::value::ValueData::F64Value),
2665                    },
2666                ],
2667            })
2668            .collect();
2669
2670        let mutation = api::v1::Mutation {
2671            op_type: 1,
2672            sequence,
2673            rows: Some(api::v1::Rows {
2674                schema: column_schema,
2675                rows,
2676            }),
2677            write_hint: Some(WriteHint {
2678                primary_key_encoding: api::v1::PrimaryKeyEncoding::Sparse.into(),
2679            }),
2680        };
2681        KeyValues::new(metadata.as_ref(), mutation).unwrap()
2682    }
2683
2684    fn sparse_metadata_for_test() -> RegionMetadataRef {
2685        let mut metadata = metadata_for_test().as_ref().clone();
2686        metadata.column_metadatas[1].column_schema.data_type = ConcreteDataType::string_datatype();
2687        let mut builder = RegionMetadataBuilder::from_existing(metadata);
2688        let reserved = crate::test_util::sst_util::sst_region_metadata_with_encoding(
2689            PrimaryKeyEncoding::Sparse,
2690        );
2691        for column in reserved.column_metadatas.into_iter().take(2) {
2692            builder.push_column_metadata(column);
2693        }
2694        builder
2695            .primary_key(vec![
2696                ReservedColumnId::table_id(),
2697                ReservedColumnId::tsid(),
2698                0,
2699                1,
2700            ])
2701            .primary_key_encoding(PrimaryKeyEncoding::Sparse);
2702        Arc::new(builder.build().unwrap())
2703    }
2704
2705    #[test]
2706    fn test_bulk_part_converter_sparse_primary_key_encoding() {
2707        let metadata = sparse_metadata_for_test();
2708
2709        let primary_key_codec = build_primary_key_codec(&metadata);
2710        let schema = to_flat_sst_arrow_schema(
2711            &metadata,
2712            &FlatSchemaOptions::from_encoding(metadata.primary_key_encoding),
2713        );
2714
2715        assert_eq!(metadata.primary_key_encoding, PrimaryKeyEncoding::Sparse);
2716        assert_eq!(primary_key_codec.encoding(), PrimaryKeyEncoding::Sparse);
2717
2718        let capacity = 100;
2719        let mut converter = BulkPartConverter::new(
2720            &metadata,
2721            schema.clone(),
2722            capacity,
2723            primary_key_codec.clone(),
2724            true,
2725        );
2726
2727        let key_values1 = build_key_values_with_sparse_encoding(
2728            &metadata,
2729            2048u32, // table_id
2730            100u64,  // tsid
2731            "key11".to_string(),
2732            "key21".to_string(),
2733            vec![1000, 2000].into_iter(),
2734            vec![Some(1.0), Some(2.0)].into_iter(),
2735            1,
2736        );
2737
2738        let mut key_values2 = build_key_values_with_sparse_encoding(
2739            &metadata,
2740            4096u32, // table_id
2741            200u64,  // tsid
2742            "key12".to_string(),
2743            "key22".to_string(),
2744            vec![1500].into_iter(),
2745            vec![Some(3.0)].into_iter(),
2746            2,
2747        );
2748        key_values2.mutation.op_type = OpType::Delete as i32;
2749
2750        let mut raw_mutation = key_values1.mutation.clone();
2751        raw_mutation.write_hint = None;
2752        let rows = raw_mutation.rows.as_mut().unwrap();
2753        rows.schema.splice(
2754            0..1,
2755            ["k0", "k1"].map(|name| api::v1::ColumnSchema {
2756                column_name: name.to_string(),
2757                datatype: api::v1::ColumnDataType::String as i32,
2758                semantic_type: SemanticType::Tag as i32,
2759                ..Default::default()
2760            }),
2761        );
2762        for row in &mut rows.rows {
2763            row.values.splice(
2764                0..1,
2765                ["raw🦀", ""]
2766                    .map(|value| api::v1::value::ValueData::StringValue(value.to_string()).into()),
2767            );
2768        }
2769        rows.schema.extend(
2770            [
2771                ("__table_id", api::v1::ColumnDataType::Uint32),
2772                ("__tsid", api::v1::ColumnDataType::Uint64),
2773            ]
2774            .map(|(name, datatype)| api::v1::ColumnSchema {
2775                column_name: name.into(),
2776                datatype: datatype as i32,
2777                semantic_type: SemanticType::Tag as i32,
2778                ..Default::default()
2779            }),
2780        );
2781        for row in &mut rows.rows {
2782            row.values.extend([
2783                api::v1::value::ValueData::U32Value(2048).into(),
2784                api::v1::value::ValueData::U64Value(100).into(),
2785            ]);
2786        }
2787        let raw = KeyValues::new(&metadata, raw_mutation).unwrap();
2788        let mut raw_converter = BulkPartConverter::new(
2789            &metadata,
2790            schema.clone(),
2791            capacity,
2792            primary_key_codec.clone(),
2793            true,
2794        );
2795        raw_converter.append_key_values(&raw).unwrap();
2796        let raw_part = raw_converter.convert().unwrap();
2797        assert_eq!(2, raw_part.num_rows());
2798        let keys = arrow::compute::cast(
2799            raw_part
2800                .batch
2801                .column_by_name(PRIMARY_KEY_COLUMN_NAME)
2802                .unwrap(),
2803            &ArrowDataType::Binary,
2804        )
2805        .unwrap();
2806        let keys = keys.as_any().downcast_ref::<BinaryArray>().unwrap();
2807        for key in keys.iter().flatten() {
2808            let decoded = primary_key_codec.decode(key).unwrap().into_sparse();
2809            assert_eq!(
2810                Some(&Value::UInt32(2048)),
2811                decoded.get(&ReservedColumnId::table_id())
2812            );
2813            assert_eq!(
2814                Some(&Value::UInt64(100)),
2815                decoded.get(&ReservedColumnId::tsid())
2816            );
2817            assert_eq!(Some(&Value::from("raw🦀")), decoded.get(&0));
2818            assert_eq!(Some(&Value::from("")), decoded.get(&1));
2819        }
2820
2821        for op_type in [OpType::Put, OpType::Delete] {
2822            let mut mutation = key_values1.mutation.clone();
2823            mutation.op_type = op_type as i32;
2824            mutation.rows.as_mut().unwrap().rows[1].values[0].value_data = Some(
2825                api::v1::value::ValueData::BinaryValue(b"truncated".to_vec()),
2826            );
2827            let invalid = KeyValues::new(&metadata, mutation).unwrap();
2828            // Raw rows before or after encoded input must not disable its validation.
2829            for batches in [vec![&invalid], vec![&raw, &invalid], vec![&invalid, &raw]] {
2830                let mut invalid_converter = BulkPartConverter::new(
2831                    &metadata,
2832                    schema.clone(),
2833                    capacity,
2834                    primary_key_codec.clone(),
2835                    true,
2836                );
2837                for batch in batches {
2838                    invalid_converter.append_key_values(batch).unwrap();
2839                }
2840                assert!(invalid_converter.convert().is_err());
2841            }
2842        }
2843
2844        converter.append_key_values(&key_values1).unwrap();
2845        converter.append_key_values(&key_values2).unwrap();
2846
2847        let bulk_part = converter.convert().unwrap();
2848
2849        assert_eq!(bulk_part.num_rows(), 3);
2850        assert_eq!(bulk_part.min_timestamp, 1000);
2851        assert_eq!(bulk_part.max_timestamp, 2000);
2852        assert_eq!(bulk_part.sequence, 2);
2853        assert_eq!(bulk_part.timestamp_index, bulk_part.batch.num_columns() - 4);
2854        assert_eq!(
2855            bulk_part
2856                .batch
2857                .column_by_name("__sequence")
2858                .unwrap()
2859                .as_ref(),
2860            &UInt64Array::from(vec![1, 2, 2]) as &dyn Array,
2861        );
2862        assert_eq!(
2863            bulk_part
2864                .batch
2865                .column_by_name("__op_type")
2866                .unwrap()
2867                .as_ref(),
2868            &UInt8Array::from(vec![
2869                OpType::Put as u8,
2870                OpType::Put as u8,
2871                OpType::Delete as u8
2872            ]) as &dyn Array,
2873        );
2874
2875        // For sparse encoding, primary key columns should NOT be stored individually
2876        // even when store_primary_key_columns is true, because sparse encoding
2877        // stores the encoded primary key in the __primary_key column
2878        let schema = bulk_part.batch.schema();
2879        let field_names: Vec<&str> = schema.fields().iter().map(|f| f.name().as_str()).collect();
2880        assert_eq!(
2881            field_names,
2882            vec!["v0", "v1", "ts", "__primary_key", "__sequence", "__op_type"]
2883        );
2884
2885        // Verify the __primary_key column contains encoded sparse keys
2886        let primary_key_column = bulk_part.batch.column_by_name("__primary_key").unwrap();
2887        let dict_array = primary_key_column
2888            .as_any()
2889            .downcast_ref::<DictionaryArray<UInt32Type>>()
2890            .unwrap();
2891
2892        // Should have non-zero entries indicating encoded primary keys
2893        assert!(!dict_array.is_empty());
2894        assert_eq!(dict_array.len(), 3); // 3 rows total
2895
2896        // Verify values are properly encoded binary data (not empty)
2897        let values = dict_array
2898            .values()
2899            .as_any()
2900            .downcast_ref::<BinaryArray>()
2901            .unwrap();
2902        for i in 0..values.len() {
2903            assert!(
2904                !values.value(i).is_empty(),
2905                "Encoded primary key should not be empty"
2906            );
2907        }
2908    }
2909
2910    #[test]
2911    fn test_convert_bulk_part_empty() {
2912        let metadata = metadata_for_test();
2913        let schema = to_flat_sst_arrow_schema(
2914            &metadata,
2915            &FlatSchemaOptions::from_encoding(metadata.primary_key_encoding),
2916        );
2917        let primary_key_codec = build_primary_key_codec(&metadata);
2918
2919        // Create empty batch
2920        let empty_batch = RecordBatch::new_empty(schema.clone());
2921        let empty_part = BulkPart {
2922            batch: empty_batch,
2923            max_timestamp: 0,
2924            min_timestamp: 0,
2925            sequence: 0,
2926            min_sequence: 0,
2927            timestamp_index: 0,
2928            raw_data: None,
2929        };
2930
2931        let result =
2932            convert_bulk_part(empty_part, &metadata, primary_key_codec, schema, true).unwrap();
2933        assert!(result.is_none());
2934    }
2935
2936    #[test]
2937    fn test_convert_bulk_part_dense_with_pk_columns() {
2938        let metadata = metadata_for_test();
2939        let primary_key_codec = build_primary_key_codec(&metadata);
2940
2941        let k0_array = Arc::new(arrow::array::StringArray::from(vec![
2942            "key1", "key2", "key1",
2943        ]));
2944        let k1_array = Arc::new(arrow::array::UInt32Array::from(vec![1, 2, 1]));
2945        let v0_array = Arc::new(arrow::array::Int64Array::from(vec![100, 200, 300]));
2946        let v1_array = Arc::new(arrow::array::Float64Array::from(vec![1.0, 2.0, 3.0]));
2947        let ts_array = Arc::new(TimestampMillisecondArray::from(vec![1000, 2000, 1500]));
2948
2949        let input_schema = Arc::new(Schema::new(vec![
2950            Field::new("k0", ArrowDataType::Utf8, false),
2951            Field::new("k1", ArrowDataType::UInt32, false),
2952            Field::new("v0", ArrowDataType::Int64, true),
2953            Field::new("v1", ArrowDataType::Float64, true),
2954            Field::new(
2955                "ts",
2956                ArrowDataType::Timestamp(arrow::datatypes::TimeUnit::Millisecond, None),
2957                false,
2958            ),
2959        ]));
2960
2961        let input_batch = RecordBatch::try_new(
2962            input_schema,
2963            vec![k0_array, k1_array, v0_array, v1_array, ts_array],
2964        )
2965        .unwrap();
2966
2967        let part = BulkPart {
2968            batch: input_batch,
2969            max_timestamp: 2000,
2970            min_timestamp: 1000,
2971            sequence: 5,
2972            min_sequence: 5,
2973            timestamp_index: 4,
2974            raw_data: None,
2975        };
2976
2977        let output_schema = to_flat_sst_arrow_schema(
2978            &metadata,
2979            &FlatSchemaOptions::from_encoding(metadata.primary_key_encoding),
2980        );
2981
2982        let result = convert_bulk_part(
2983            part,
2984            &metadata,
2985            primary_key_codec,
2986            output_schema,
2987            true, // store primary key columns
2988        )
2989        .unwrap();
2990
2991        let converted = result.unwrap();
2992
2993        assert_eq!(converted.num_rows(), 3);
2994        assert_eq!(converted.max_timestamp, 2000);
2995        assert_eq!(converted.min_timestamp, 1000);
2996        assert_eq!(converted.sequence, 5);
2997        assert_eq!(converted.min_sequence, 5);
2998
2999        let schema = converted.batch.schema();
3000        let field_names: Vec<&str> = schema.fields().iter().map(|f| f.name().as_str()).collect();
3001        assert_eq!(
3002            field_names,
3003            vec![
3004                "k0",
3005                "k1",
3006                "v0",
3007                "v1",
3008                "ts",
3009                "__primary_key",
3010                "__sequence",
3011                "__op_type"
3012            ]
3013        );
3014
3015        let k0_col = converted.batch.column_by_name("k0").unwrap();
3016        assert!(matches!(
3017            k0_col.data_type(),
3018            ArrowDataType::Dictionary(_, _)
3019        ));
3020
3021        let pk_col = converted.batch.column_by_name("__primary_key").unwrap();
3022        let dict_array = pk_col
3023            .as_any()
3024            .downcast_ref::<DictionaryArray<UInt32Type>>()
3025            .unwrap();
3026        let keys = dict_array.keys();
3027
3028        assert_eq!(keys.len(), 3);
3029    }
3030
3031    #[test]
3032    fn test_convert_bulk_part_dense_without_pk_columns() {
3033        let metadata = metadata_for_test();
3034        let primary_key_codec = build_primary_key_codec(&metadata);
3035
3036        // Create input batch with primary key columns (k0, k1)
3037        let k0_array = Arc::new(arrow::array::StringArray::from(vec!["key1", "key2"]));
3038        let k1_array = Arc::new(arrow::array::UInt32Array::from(vec![1, 2]));
3039        let v0_array = Arc::new(arrow::array::Int64Array::from(vec![100, 200]));
3040        let v1_array = Arc::new(arrow::array::Float64Array::from(vec![1.0, 2.0]));
3041        let ts_array = Arc::new(TimestampMillisecondArray::from(vec![1000, 2000]));
3042
3043        let input_schema = Arc::new(Schema::new(vec![
3044            Field::new("k0", ArrowDataType::Utf8, false),
3045            Field::new("k1", ArrowDataType::UInt32, false),
3046            Field::new("v0", ArrowDataType::Int64, true),
3047            Field::new("v1", ArrowDataType::Float64, true),
3048            Field::new(
3049                "ts",
3050                ArrowDataType::Timestamp(arrow::datatypes::TimeUnit::Millisecond, None),
3051                false,
3052            ),
3053        ]));
3054
3055        let input_batch = RecordBatch::try_new(
3056            input_schema,
3057            vec![k0_array, k1_array, v0_array, v1_array, ts_array],
3058        )
3059        .unwrap();
3060
3061        let part = BulkPart {
3062            batch: input_batch,
3063            max_timestamp: 2000,
3064            min_timestamp: 1000,
3065            sequence: 3,
3066            min_sequence: 3,
3067            timestamp_index: 4,
3068            raw_data: None,
3069        };
3070
3071        let output_schema = to_flat_sst_arrow_schema(
3072            &metadata,
3073            &FlatSchemaOptions {
3074                raw_pk_columns: false,
3075                string_pk_use_dict: true,
3076                ..Default::default()
3077            },
3078        );
3079
3080        let result = convert_bulk_part(
3081            part,
3082            &metadata,
3083            primary_key_codec,
3084            output_schema,
3085            false, // don't store primary key columns
3086        )
3087        .unwrap();
3088
3089        let converted = result.unwrap();
3090
3091        assert_eq!(converted.num_rows(), 2);
3092        assert_eq!(converted.max_timestamp, 2000);
3093        assert_eq!(converted.min_timestamp, 1000);
3094        assert_eq!(converted.sequence, 3);
3095
3096        // Verify schema does NOT include individual primary key columns
3097        let schema = converted.batch.schema();
3098        let field_names: Vec<&str> = schema.fields().iter().map(|f| f.name().as_str()).collect();
3099        assert_eq!(
3100            field_names,
3101            vec!["v0", "v1", "ts", "__primary_key", "__sequence", "__op_type"]
3102        );
3103
3104        // Verify __primary_key column is present and is a dictionary
3105        let pk_col = converted.batch.column_by_name("__primary_key").unwrap();
3106        assert!(matches!(
3107            pk_col.data_type(),
3108            ArrowDataType::Dictionary(_, _)
3109        ));
3110    }
3111
3112    #[test]
3113    fn test_convert_bulk_part_sparse_encoding() {
3114        let metadata = sparse_metadata_for_test();
3115
3116        let primary_key_codec = build_primary_key_codec(&metadata);
3117
3118        // Create input batch with __primary_key column (sparse encoding)
3119        let keys = (1..=2)
3120            .map(|tsid| {
3121                let mut key = Vec::new();
3122                mito_codec::row_converter::SparsePrimaryKeyCodec::schemaless()
3123                    .encode_internal(123, tsid, &mut key)
3124                    .unwrap();
3125                key
3126            })
3127            .collect::<Vec<_>>();
3128        let pk_array = Arc::new(BinaryArray::from_iter_values(&keys));
3129        let v0_array = Arc::new(arrow::array::Int64Array::from(vec![100, 200]));
3130        let v1_array = Arc::new(arrow::array::Float64Array::from(vec![1.0, 2.0]));
3131        let ts_array = Arc::new(TimestampMillisecondArray::from(vec![1000, 2000]));
3132
3133        let input_schema = Arc::new(Schema::new(vec![
3134            Field::new("__primary_key", ArrowDataType::Binary, false),
3135            Field::new("v0", ArrowDataType::Int64, true),
3136            Field::new("v1", ArrowDataType::Float64, true),
3137            Field::new(
3138                "ts",
3139                ArrowDataType::Timestamp(arrow::datatypes::TimeUnit::Millisecond, None),
3140                false,
3141            ),
3142        ]));
3143
3144        let input_batch =
3145            RecordBatch::try_new(input_schema, vec![pk_array, v0_array, v1_array, ts_array])
3146                .unwrap();
3147
3148        let part = BulkPart {
3149            batch: input_batch,
3150            max_timestamp: 2000,
3151            min_timestamp: 1000,
3152            sequence: 7,
3153            min_sequence: 7,
3154            timestamp_index: 3,
3155            raw_data: None,
3156        };
3157
3158        let output_schema = to_flat_sst_arrow_schema(
3159            &metadata,
3160            &FlatSchemaOptions::from_encoding(metadata.primary_key_encoding),
3161        );
3162
3163        let dictionary_values = Arc::new(BinaryArray::from(vec![
3164            Some(keys[0].as_slice()),
3165            Some(keys[1].as_slice()),
3166            Some(b"truncated".as_slice()),
3167            None,
3168            Some(keys[0].as_slice()),
3169        ]));
3170        let with_keys = |keys: ArrayRef| {
3171            let mut input = part.clone();
3172            let mut fields = input.batch.schema().fields().to_vec();
3173            fields[0] = Arc::new(Field::new(
3174                PRIMARY_KEY_COLUMN_NAME,
3175                keys.data_type().clone(),
3176                true,
3177            ));
3178            let mut columns = input.batch.columns().to_vec();
3179            columns[0] = keys;
3180            input.batch = RecordBatch::try_new(Arc::new(Schema::new(fields)), columns).unwrap();
3181            input
3182        };
3183        // Slicing retains malformed/null dictionary values outside the selected rows.
3184        let sliced = DictionaryArray::try_new(
3185            UInt32Array::from(vec![2, 0, 1, 3]),
3186            dictionary_values.clone(),
3187        )
3188        .unwrap()
3189        .slice(1, 2);
3190        for key_type in [
3191            ArrowDataType::Int8,
3192            ArrowDataType::Int16,
3193            ArrowDataType::Int32,
3194            ArrowDataType::Int64,
3195            ArrowDataType::UInt8,
3196            ArrowDataType::UInt16,
3197            ArrowDataType::UInt32,
3198            ArrowDataType::UInt64,
3199        ] {
3200            let keys = arrow::compute::cast(
3201                &sliced,
3202                &ArrowDataType::Dictionary(Box::new(key_type), Box::new(ArrowDataType::Binary)),
3203            )
3204            .unwrap();
3205            let converted = convert_bulk_part(
3206                with_keys(keys),
3207                &metadata,
3208                primary_key_codec.clone(),
3209                output_schema.clone(),
3210                true,
3211            )
3212            .unwrap()
3213            .unwrap();
3214            let keys = arrow::compute::cast(
3215                converted
3216                    .batch
3217                    .column_by_name(PRIMARY_KEY_COLUMN_NAME)
3218                    .unwrap(),
3219                &ArrowDataType::Binary,
3220            )
3221            .unwrap();
3222            assert_eq!(keys.as_ref(), part.batch.column(0).as_ref());
3223            for (name, index) in [("v0", 1), ("v1", 2), ("ts", 3)] {
3224                assert_eq!(
3225                    converted.batch.column_by_name(name).unwrap().as_ref(),
3226                    part.batch.column(index).as_ref()
3227                );
3228            }
3229            assert_eq!(
3230                converted
3231                    .batch
3232                    .column_by_name("__sequence")
3233                    .unwrap()
3234                    .as_ref(),
3235                &UInt64Array::from(vec![7, 7]) as &dyn Array
3236            );
3237            assert_eq!(
3238                converted
3239                    .batch
3240                    .column_by_name("__op_type")
3241                    .unwrap()
3242                    .as_ref(),
3243                &UInt8Array::from(vec![OpType::Put as u8; 2]) as &dyn Array
3244            );
3245        }
3246        // Equal bytes under distinct dictionary slots remain separate observations.
3247        let repeated =
3248            DictionaryArray::try_new(UInt32Array::from(vec![0, 4]), dictionary_values.clone())
3249                .unwrap();
3250        assert_eq!(
3251            convert_bulk_part(
3252                with_keys(Arc::new(repeated)),
3253                &metadata,
3254                primary_key_codec.clone(),
3255                output_schema.clone(),
3256                true
3257            )
3258            .unwrap()
3259            .unwrap()
3260            .num_rows(),
3261            2
3262        );
3263        for indices in [
3264            vec![Some(0), Some(2)],
3265            vec![Some(0), Some(3)],
3266            vec![Some(0), None],
3267        ] {
3268            let dictionary =
3269                DictionaryArray::try_new(UInt32Array::from(indices), dictionary_values.clone())
3270                    .unwrap();
3271            assert!(
3272                convert_bulk_part(
3273                    with_keys(Arc::new(dictionary)),
3274                    &metadata,
3275                    primary_key_codec.clone(),
3276                    output_schema.clone(),
3277                    true
3278                )
3279                .is_err()
3280            );
3281        }
3282
3283        for invalid_keys in [
3284            Arc::new(arrow::array::StringArray::from(vec!["invalid"; 2])) as ArrayRef,
3285            Arc::new(BinaryArray::from(vec![None::<&[u8]>; 2])),
3286            Arc::new(BinaryArray::from_iter_values([b"truncated"; 2])),
3287        ] {
3288            let mut invalid = part.clone();
3289            let mut fields = invalid.batch.schema().fields().to_vec();
3290            fields[0] = Arc::new(Field::new(
3291                PRIMARY_KEY_COLUMN_NAME,
3292                invalid_keys.data_type().clone(),
3293                true,
3294            ));
3295            let mut columns = invalid.batch.columns().to_vec();
3296            columns[0] = invalid_keys;
3297            invalid.batch = RecordBatch::try_new(Arc::new(Schema::new(fields)), columns).unwrap();
3298            assert!(
3299                convert_bulk_part(
3300                    invalid,
3301                    &metadata,
3302                    primary_key_codec.clone(),
3303                    output_schema.clone(),
3304                    true,
3305                )
3306                .is_err()
3307            );
3308        }
3309
3310        let result = convert_bulk_part(
3311            part,
3312            &metadata,
3313            primary_key_codec,
3314            output_schema,
3315            true, // store_primary_key_columns (ignored for sparse)
3316        )
3317        .unwrap();
3318
3319        let converted = result.unwrap();
3320
3321        assert_eq!(converted.num_rows(), 2);
3322        assert_eq!(converted.max_timestamp, 2000);
3323        assert_eq!(converted.min_timestamp, 1000);
3324        assert_eq!(converted.sequence, 7);
3325
3326        // Verify schema does NOT include individual primary key columns (sparse encoding)
3327        let schema = converted.batch.schema();
3328        let field_names: Vec<&str> = schema.fields().iter().map(|f| f.name().as_str()).collect();
3329        assert_eq!(
3330            field_names,
3331            vec!["v0", "v1", "ts", "__primary_key", "__sequence", "__op_type"]
3332        );
3333
3334        // Verify __primary_key is dictionary encoded
3335        let pk_col = converted.batch.column_by_name("__primary_key").unwrap();
3336        assert!(matches!(
3337            pk_col.data_type(),
3338            ArrowDataType::Dictionary(_, _)
3339        ));
3340    }
3341
3342    #[test]
3343    fn test_convert_bulk_part_sorting_with_multiple_series() {
3344        let metadata = metadata_for_test();
3345        let primary_key_codec = build_primary_key_codec(&metadata);
3346
3347        // Create unsorted batch with multiple series and timestamps
3348        let k0_array = Arc::new(arrow::array::StringArray::from(vec![
3349            "series_b", "series_a", "series_b", "series_a",
3350        ]));
3351        let k1_array = Arc::new(arrow::array::UInt32Array::from(vec![2, 1, 2, 1]));
3352        let v0_array = Arc::new(arrow::array::Int64Array::from(vec![200, 100, 400, 300]));
3353        let v1_array = Arc::new(arrow::array::Float64Array::from(vec![2.0, 1.0, 4.0, 3.0]));
3354        let ts_array = Arc::new(TimestampMillisecondArray::from(vec![
3355            2000, 1000, 4000, 3000,
3356        ]));
3357
3358        let input_schema = Arc::new(Schema::new(vec![
3359            Field::new("k0", ArrowDataType::Utf8, false),
3360            Field::new("k1", ArrowDataType::UInt32, false),
3361            Field::new("v0", ArrowDataType::Int64, true),
3362            Field::new("v1", ArrowDataType::Float64, true),
3363            Field::new(
3364                "ts",
3365                ArrowDataType::Timestamp(arrow::datatypes::TimeUnit::Millisecond, None),
3366                false,
3367            ),
3368        ]));
3369
3370        let input_batch = RecordBatch::try_new(
3371            input_schema,
3372            vec![k0_array, k1_array, v0_array, v1_array, ts_array],
3373        )
3374        .unwrap();
3375
3376        let part = BulkPart {
3377            batch: input_batch,
3378            max_timestamp: 4000,
3379            min_timestamp: 1000,
3380            sequence: 10,
3381            min_sequence: 10,
3382            timestamp_index: 4,
3383            raw_data: None,
3384        };
3385
3386        let output_schema = to_flat_sst_arrow_schema(
3387            &metadata,
3388            &FlatSchemaOptions::from_encoding(metadata.primary_key_encoding),
3389        );
3390
3391        let result =
3392            convert_bulk_part(part, &metadata, primary_key_codec, output_schema, true).unwrap();
3393
3394        let converted = result.unwrap();
3395
3396        assert_eq!(converted.num_rows(), 4);
3397
3398        // Verify data is sorted by (primary_key, timestamp, sequence desc)
3399        let ts_col = converted.batch.column(converted.timestamp_index);
3400        let ts_array = ts_col
3401            .as_any()
3402            .downcast_ref::<TimestampMillisecondArray>()
3403            .unwrap();
3404
3405        // After sorting by (pk, ts), we should have:
3406        // series_a,1: ts=1000, 3000
3407        // series_b,2: ts=2000, 4000
3408        let timestamps: Vec<i64> = ts_array.values().to_vec();
3409        assert_eq!(timestamps, vec![1000, 3000, 2000, 4000]);
3410    }
3411
3412    /// Helper to create a converted BulkPart (with __primary_key column) from MutationInputs.
3413    fn build_converted_bulk_part(inputs: &[MutationInput]) -> BulkPart {
3414        let metadata = metadata_for_test();
3415        let kvs = inputs
3416            .iter()
3417            .map(|m| {
3418                build_key_values_with_ts_seq_values(
3419                    &metadata,
3420                    m.k0.to_string(),
3421                    m.k1,
3422                    m.timestamps.iter().copied(),
3423                    m.v1.iter().copied(),
3424                    m.sequence,
3425                )
3426            })
3427            .collect::<Vec<_>>();
3428        let schema = to_flat_sst_arrow_schema(&metadata, &FlatSchemaOptions::default());
3429        let primary_key_codec = build_primary_key_codec(&metadata);
3430        let mut converter = BulkPartConverter::new(&metadata, schema, 64, primary_key_codec, true);
3431        for kv in kvs {
3432            converter.append_key_values(&kv).unwrap();
3433        }
3434        converter.convert().unwrap()
3435    }
3436
3437    #[test]
3438    fn test_min_sequence_survives_slicing_and_unordered_part_reuse() {
3439        let metadata = metadata_for_test();
3440        let make_part = |sequence| {
3441            build_converted_bulk_part(&[MutationInput {
3442                k0: "a",
3443                k1: 0,
3444                timestamps: &[2000, 1000],
3445                v1: &[Some(1.0), Some(2.0)],
3446                sequence,
3447            }])
3448        };
3449        let original = make_part(100);
3450        let sliced = crate::memtable::time_partition::filter_record_batch(&original, 1000, 1001)
3451            .unwrap()
3452            .unwrap();
3453        // The remaining row has sequence 101. Inheriting 100 is conservative
3454        // and avoids rescanning the sequence column after each partition split.
3455        assert_eq!(1, sliced.num_rows());
3456        assert_eq!(100, sliced.min_sequence);
3457        assert_eq!(101, sliced.sequence);
3458
3459        let mut unordered = UnorderedPart::new();
3460        unordered.push(sliced.clone());
3461        unordered.push(make_part(10));
3462        let merged = unordered.to_bulk_part(&metadata).unwrap().unwrap();
3463        assert_eq!(3, merged.num_rows());
3464        assert_eq!(10, merged.min_sequence);
3465        assert_eq!(101, merged.sequence);
3466        assert_eq!(10, merged.to_memtable_stats(&metadata).min_sequence);
3467
3468        unordered.clear();
3469        unordered.push(sliced);
3470        let reused = unordered.to_bulk_part(&metadata).unwrap().unwrap();
3471        assert_eq!(1, reused.num_rows());
3472        assert_eq!(100, reused.min_sequence);
3473    }
3474
3475    /// Helper to create a MultiBulkPart where each group becomes a separate batch.
3476    fn build_multi_bulk_part(groups: &[&[MutationInput]]) -> (MultiBulkPart, RegionMetadataRef) {
3477        let metadata = metadata_for_test();
3478        let mut all_batches = Vec::new();
3479        let mut min_ts = i64::MAX;
3480        let mut max_ts = i64::MIN;
3481        let mut min_seq = u64::MAX;
3482        let mut max_seq = 0u64;
3483
3484        for inputs in groups {
3485            let part = build_converted_bulk_part(inputs);
3486            min_ts = min_ts.min(part.min_timestamp);
3487            max_ts = max_ts.max(part.max_timestamp);
3488            min_seq = min_seq.min(part.min_sequence);
3489            max_seq = max_seq.max(part.sequence);
3490            all_batches.push(part.batch);
3491        }
3492
3493        let multi = MultiBulkPart::new(
3494            all_batches,
3495            min_ts,
3496            max_ts,
3497            min_seq,
3498            max_seq,
3499            groups.len(),
3500            &metadata,
3501        );
3502        (multi, metadata)
3503    }
3504
3505    #[test]
3506    fn test_multi_bulk_part_prune_batches() {
3507        // Three batches with distinct k0 ranges: ["a"], ["m"], ["z"].
3508        let (multi, metadata) = build_multi_bulk_part(&[
3509            &[MutationInput {
3510                k0: "a",
3511                k1: 0,
3512                timestamps: &[1, 2],
3513                v1: &[Some(1.0), Some(2.0)],
3514                sequence: 0,
3515            }],
3516            &[MutationInput {
3517                k0: "m",
3518                k1: 0,
3519                timestamps: &[3, 4],
3520                v1: &[Some(3.0), Some(4.0)],
3521                sequence: 1,
3522            }],
3523            &[MutationInput {
3524                k0: "z",
3525                k1: 0,
3526                timestamps: &[5, 6],
3527                v1: &[Some(5.0), Some(6.0)],
3528                sequence: 2,
3529            }],
3530        ]);
3531        assert_eq!(multi.num_rows(), 6);
3532        assert_eq!(multi.num_batches(), 3);
3533
3534        // k0 = "m" => only middle batch (2 rows).
3535        let context = Arc::new(
3536            BulkIterContext::new(
3537                metadata.clone(),
3538                None,
3539                Some(Predicate::new(vec![
3540                    datafusion_expr::col("k0").eq(datafusion_expr::lit("m")),
3541                ])),
3542                false,
3543                crate::sst::parquet::DEFAULT_READ_BATCH_SIZE,
3544            )
3545            .unwrap(),
3546        );
3547        let reader = multi
3548            .read(context, None, None)
3549            .unwrap()
3550            .expect("should have results");
3551        let total_rows: usize = reader.map(|r| r.unwrap().num_rows()).sum();
3552        assert_eq!(total_rows, 2);
3553
3554        // k0 = "nonexistent" => all pruned, returns None.
3555        let context = Arc::new(
3556            BulkIterContext::new(
3557                metadata.clone(),
3558                None,
3559                Some(Predicate::new(vec![
3560                    datafusion_expr::col("k0").eq(datafusion_expr::lit("nonexistent")),
3561                ])),
3562                false,
3563                crate::sst::parquet::DEFAULT_READ_BATCH_SIZE,
3564            )
3565            .unwrap(),
3566        );
3567        assert!(multi.read(context, None, None).unwrap().is_none());
3568
3569        // No predicate => all 6 rows.
3570        let context = Arc::new(
3571            BulkIterContext::new(
3572                metadata.clone(),
3573                None,
3574                None,
3575                false,
3576                crate::sst::parquet::DEFAULT_READ_BATCH_SIZE,
3577            )
3578            .unwrap(),
3579        );
3580        let reader = multi
3581            .read(context, None, None)
3582            .unwrap()
3583            .expect("should have results");
3584        let total_rows: usize = reader.map(|r| r.unwrap().num_rows()).sum();
3585        assert_eq!(total_rows, 6);
3586    }
3587
3588    #[test]
3589    fn test_fill_missing_columns_resets_raw_data() {
3590        let metadata = metadata_for_test();
3591
3592        // A batch missing the `v1` field column, e.g. built against an older
3593        // schema before `v1` was added.
3594        let k0_array = Arc::new(arrow::array::StringArray::from(vec!["key1", "key2"]));
3595        let k1_array = Arc::new(arrow::array::UInt32Array::from(vec![1u32, 2]));
3596        let v0_array = Arc::new(arrow::array::Int64Array::from(vec![100i64, 200]));
3597        let ts_array = Arc::new(TimestampMillisecondArray::from(vec![1000i64, 2000]));
3598        let input_schema = Arc::new(Schema::new(vec![
3599            Field::new("k0", ArrowDataType::Utf8, false),
3600            Field::new("k1", ArrowDataType::UInt32, false),
3601            Field::new("v0", ArrowDataType::Int64, true),
3602            Field::new(
3603                "ts",
3604                ArrowDataType::Timestamp(arrow::datatypes::TimeUnit::Millisecond, None),
3605                false,
3606            ),
3607        ]));
3608        let batch =
3609            RecordBatch::try_new(input_schema, vec![k0_array, k1_array, v0_array, ts_array])
3610                .unwrap();
3611
3612        // Encodes the batch as raw data like the bulk insert request does.
3613        let mut encoder = FlightEncoder::default();
3614        let schema_bytes = encoder.encode_schema(batch.schema().as_ref()).data_header;
3615        let [flight_data] = encoder
3616            .encode(FlightMessage::RecordBatch(batch.clone()))
3617            .try_into()
3618            .unwrap();
3619        let mut part = BulkPart {
3620            batch,
3621            max_timestamp: 2000,
3622            min_timestamp: 1000,
3623            sequence: 5,
3624            min_sequence: 5,
3625            timestamp_index: 3,
3626            raw_data: Some(ArrowIpc {
3627                schema: schema_bytes,
3628                data_header: flight_data.data_header,
3629                payload: flight_data.data_body,
3630            }),
3631        };
3632
3633        part.fill_missing_columns(&metadata).unwrap();
3634        assert!(part.batch.column_by_name("v1").is_some());
3635        // The raw data no longer matches the batch after filling missing
3636        // columns and must be cleared, otherwise the WAL entry built from
3637        // this part loses the filled columns.
3638        assert!(part.raw_data.is_none());
3639
3640        // The WAL entry round trip keeps the filled column.
3641        let entry = BulkWalEntry::try_from(&part).unwrap();
3642        let replayed = BulkPart::try_from(entry).unwrap();
3643        assert_eq!(part.sequence, replayed.min_sequence);
3644        assert_eq!(2, replayed.num_rows());
3645        assert!(replayed.batch.column_by_name("v1").is_some());
3646    }
3647}