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::sync::Arc;
19use std::time::{Duration, Instant};
20
21use api::helper::{ColumnDataTypeWrapper, to_grpc_value};
22use api::v1::bulk_wal_entry::Body;
23use api::v1::{ArrowIpc, BulkWalEntry, Mutation, OpType};
24use bytes::Bytes;
25use common_grpc::flight::{FlightDecoder, FlightEncoder, FlightMessage};
26use common_recordbatch::DfRecordBatch as RecordBatch;
27use common_time::Timestamp;
28use datafusion_common::Column;
29use datafusion_common::pruning::PruningStatistics;
30use datafusion_expr::utils::expr_to_columns;
31use datatypes::arrow;
32use datatypes::arrow::array::{
33    Array, ArrayRef, BinaryArray, BooleanArray, StringDictionaryBuilder, UInt8Array, UInt64Array,
34};
35use datatypes::arrow::compute::{SortColumn, SortOptions, concat_batches};
36use datatypes::arrow::datatypes::{
37    DataType as ArrowDataType, Field, Schema, SchemaRef, UInt32Type,
38};
39use datatypes::data_type::DataType;
40use datatypes::extension::json::is_structured_json_field;
41use datatypes::prelude::{MutableVector, Vector};
42use datatypes::value::ValueRef;
43use datatypes::vectors::Helper;
44use mito_codec::key_values::{KeyValue, KeyValues};
45use mito_codec::row_converter::{PrimaryKeyCodec, SortField, build_primary_key_codec_with_fields};
46use parquet::arrow::ArrowWriter;
47use parquet::basic::{Compression, ZstdLevel};
48use parquet::file::metadata::ParquetMetaData;
49use parquet::file::properties::WriterProperties;
50use smallvec::SmallVec;
51use snafu::{OptionExt, ResultExt};
52use store_api::codec::PrimaryKeyEncoding;
53use store_api::metadata::{RegionMetadata, RegionMetadataRef};
54use store_api::storage::consts::PRIMARY_KEY_COLUMN_NAME;
55use store_api::storage::{ColumnId, FileId, SequenceNumber, SequenceRange};
56
57use crate::error::{
58    self, ColumnNotFoundSnafu, ComputeArrowSnafu, CreateDefaultSnafu, DataTypeMismatchSnafu,
59    EncodeMemtableSnafu, EncodeSnafu, InvalidMetadataSnafu, InvalidRequestSnafu,
60    NewRecordBatchSnafu, Result,
61};
62use crate::memtable::bulk::context::{BulkIterContext, BulkIterContextRef};
63use crate::memtable::bulk::json_align::Json2Aligner;
64use crate::memtable::bulk::part_reader::EncodedBulkPartIter;
65use crate::memtable::time_series::{ValueBuilder, Values};
66use crate::memtable::{BoxedRecordBatchIterator, MemScanMetrics, MemtableStats};
67use crate::sst::SeriesEstimator;
68use crate::sst::index::IndexOutput;
69use crate::sst::parquet::flat_format::primary_key_column_index;
70use crate::sst::parquet::format::{PrimaryKeyArray, PrimaryKeyArrayBuilder};
71use crate::sst::parquet::{PARQUET_METADATA_KEY, SstInfo};
72
73const INIT_DICT_VALUE_CAPACITY: usize = 8;
74
75/// A raw bulk part in the memtable.
76#[derive(Clone)]
77pub struct BulkPart {
78    pub batch: RecordBatch,
79    pub max_timestamp: i64,
80    pub min_timestamp: i64,
81    pub sequence: u64,
82    pub timestamp_index: usize,
83    pub raw_data: Option<ArrowIpc>,
84}
85
86impl TryFrom<BulkWalEntry> for BulkPart {
87    type Error = error::Error;
88
89    fn try_from(value: BulkWalEntry) -> std::result::Result<Self, Self::Error> {
90        match value.body.expect("Entry payload should be present") {
91            Body::ArrowIpc(ipc) => {
92                let mut decoder = FlightDecoder::try_from_schema_bytes(&ipc.schema)
93                    .context(error::ConvertBulkWalEntrySnafu)?;
94                let batch = decoder
95                    .try_decode_record_batch(&ipc.data_header, &ipc.payload)
96                    .context(error::ConvertBulkWalEntrySnafu)?;
97                Ok(Self {
98                    batch,
99                    max_timestamp: value.max_ts,
100                    min_timestamp: value.min_ts,
101                    sequence: value.sequence,
102                    timestamp_index: value.timestamp_index as usize,
103                    raw_data: Some(ipc),
104                })
105            }
106        }
107    }
108}
109
110impl TryFrom<&BulkPart> for BulkWalEntry {
111    type Error = error::Error;
112
113    fn try_from(value: &BulkPart) -> Result<Self> {
114        if let Some(ipc) = &value.raw_data {
115            Ok(BulkWalEntry {
116                sequence: value.sequence,
117                max_ts: value.max_timestamp,
118                min_ts: value.min_timestamp,
119                timestamp_index: value.timestamp_index as u32,
120                body: Some(Body::ArrowIpc(ipc.clone())),
121            })
122        } else {
123            let mut encoder = FlightEncoder::default();
124            let schema_bytes = encoder
125                .encode_schema(value.batch.schema().as_ref())
126                .data_header;
127            let [rb_data] = encoder
128                .encode(FlightMessage::RecordBatch(value.batch.clone()))
129                .try_into()
130                .map_err(|_| {
131                    error::UnsupportedOperationSnafu {
132                        err_msg: "create BulkWalEntry from RecordBatch with dictionary arrays",
133                    }
134                    .build()
135                })?;
136            Ok(BulkWalEntry {
137                sequence: value.sequence,
138                max_ts: value.max_timestamp,
139                min_ts: value.min_timestamp,
140                timestamp_index: value.timestamp_index as u32,
141                body: Some(Body::ArrowIpc(ArrowIpc {
142                    schema: schema_bytes,
143                    data_header: rb_data.data_header,
144                    payload: rb_data.data_body,
145                })),
146            })
147        }
148    }
149}
150
151impl BulkPart {
152    pub(crate) fn schema(&self) -> SchemaRef {
153        self.batch.schema()
154    }
155
156    pub(crate) fn estimated_size(&self) -> usize {
157        record_batch_estimated_size(&self.batch)
158    }
159
160    /// Returns the estimated series count in this BulkPart.
161    /// This is calculated from the dictionary values count of the PrimaryKeyArray.
162    pub fn estimated_series_count(&self) -> usize {
163        let pk_column_idx = primary_key_column_index(self.batch.num_columns());
164        let pk_column = self.batch.column(pk_column_idx);
165        if let Some(dict_array) = pk_column.as_any().downcast_ref::<PrimaryKeyArray>() {
166            dict_array.values().len()
167        } else {
168            0
169        }
170    }
171
172    /// Creates MemtableStats from this BulkPart.
173    pub fn to_memtable_stats(&self, region_metadata: &RegionMetadataRef) -> MemtableStats {
174        let ts_type = region_metadata.time_index_type();
175        let min_ts = ts_type.create_timestamp(self.min_timestamp);
176        let max_ts = ts_type.create_timestamp(self.max_timestamp);
177
178        MemtableStats {
179            estimated_bytes: self.estimated_size(),
180            time_range: Some((min_ts, max_ts)),
181            num_rows: self.num_rows(),
182            num_ranges: 1,
183            max_sequence: self.sequence,
184            series_count: self.estimated_series_count(),
185        }
186    }
187
188    /// Fills missing columns in the BulkPart batch with default values.
189    ///
190    /// This function checks if the batch schema matches the region metadata schema,
191    /// and if there are missing columns, it fills them with default values (or null
192    /// for nullable columns).
193    ///
194    /// # Arguments
195    ///
196    /// * `region_metadata` - The region metadata containing the expected schema
197    pub fn fill_missing_columns(&mut self, region_metadata: &RegionMetadata) -> Result<()> {
198        // Builds a map of existing columns in the batch
199        let batch_schema = self.batch.schema();
200        let batch_columns: HashSet<_> = batch_schema
201            .fields()
202            .iter()
203            .map(|f| f.name().as_str())
204            .collect();
205
206        // Finds columns that need to be filled
207        let mut columns_to_fill = Vec::new();
208        for column_meta in &region_metadata.column_metadatas {
209            // TODO(yingwen): Returns error if it is impure default after we support filling
210            // bulk insert request in the frontend
211            if !batch_columns.contains(column_meta.column_schema.name.as_str()) {
212                columns_to_fill.push(column_meta);
213            }
214        }
215
216        if columns_to_fill.is_empty() {
217            return Ok(());
218        }
219
220        let num_rows = self.batch.num_rows();
221
222        let mut new_columns = Vec::new();
223        let mut new_fields = Vec::new();
224
225        // First, adds all existing columns
226        new_fields.extend(batch_schema.fields().iter().cloned());
227        new_columns.extend_from_slice(self.batch.columns());
228
229        let region_id = region_metadata.region_id;
230        // Then adds the missing columns with default values
231        for column_meta in columns_to_fill {
232            let default_vector = column_meta
233                .column_schema
234                .create_default_vector(num_rows)
235                .context(CreateDefaultSnafu {
236                    region_id,
237                    column: &column_meta.column_schema.name,
238                })?
239                .with_context(|| InvalidRequestSnafu {
240                    region_id,
241                    reason: format!(
242                        "column {} does not have default value",
243                        column_meta.column_schema.name
244                    ),
245                })?;
246            let arrow_array = default_vector.to_arrow_array();
247            column_meta.column_schema.data_type.as_arrow_type();
248
249            new_fields.push(Arc::new(Field::new(
250                column_meta.column_schema.name.clone(),
251                column_meta.column_schema.data_type.as_arrow_type(),
252                column_meta.column_schema.is_nullable(),
253            )));
254            new_columns.push(arrow_array);
255        }
256
257        // Create a new schema and batch with the filled columns
258        let new_schema = Arc::new(Schema::new(new_fields));
259        let new_batch =
260            RecordBatch::try_new(new_schema, new_columns).context(NewRecordBatchSnafu)?;
261
262        // Update the batch
263        self.batch = new_batch;
264
265        Ok(())
266    }
267
268    /// Converts [BulkPart] to [Mutation] for fallback `write_bulk` implementation.
269    pub(crate) fn to_mutation(&self, region_metadata: &RegionMetadataRef) -> Result<Mutation> {
270        let vectors = region_metadata
271            .schema
272            .column_schemas()
273            .iter()
274            .map(|col| match self.batch.column_by_name(&col.name) {
275                None => Ok(None),
276                Some(col) => Helper::try_into_vector(col).map(Some),
277            })
278            .collect::<datatypes::error::Result<Vec<_>>>()
279            .context(error::ComputeVectorSnafu)?;
280
281        let rows = (0..self.num_rows())
282            .map(|row_idx| {
283                let values = (0..self.batch.num_columns())
284                    .map(|col_idx| {
285                        if let Some(v) = &vectors[col_idx] {
286                            to_grpc_value(v.get(row_idx))
287                        } else {
288                            api::v1::Value { value_data: None }
289                        }
290                    })
291                    .collect::<Vec<_>>();
292                api::v1::Row { values }
293            })
294            .collect::<Vec<_>>();
295
296        let schema = region_metadata
297            .column_metadatas
298            .iter()
299            .map(|c| {
300                let data_type_wrapper =
301                    ColumnDataTypeWrapper::try_from(c.column_schema.data_type.clone())?;
302                Ok(api::v1::ColumnSchema {
303                    column_name: c.column_schema.name.clone(),
304                    datatype: data_type_wrapper.datatype() as i32,
305                    semantic_type: c.semantic_type as i32,
306                    ..Default::default()
307                })
308            })
309            .collect::<api::error::Result<Vec<_>>>()
310            .context(error::ConvertColumnDataTypeSnafu {
311                reason: "failed to convert region metadata to column schema",
312            })?;
313
314        let rows = api::v1::Rows { schema, rows };
315
316        Ok(Mutation {
317            op_type: OpType::Put as i32,
318            sequence: self.sequence,
319            rows: Some(rows),
320            write_hint: None,
321        })
322    }
323
324    pub fn timestamps(&self) -> &ArrayRef {
325        self.batch.column(self.timestamp_index)
326    }
327
328    pub fn num_rows(&self) -> usize {
329        self.batch.num_rows()
330    }
331}
332
333/// A collection of small unordered bulk parts.
334/// Used to batch small parts together before merging them into a sorted part.
335pub struct UnorderedPart {
336    /// Small bulk parts that haven't been sorted yet.
337    parts: Vec<BulkPart>,
338    /// Total number of rows across all parts.
339    total_rows: usize,
340    /// Total estimated uncompressed bytes across all parts.
341    total_bytes: usize,
342    /// Minimum timestamp across all parts.
343    min_timestamp: i64,
344    /// Maximum timestamp across all parts.
345    max_timestamp: i64,
346    /// Maximum sequence number across all parts.
347    max_sequence: u64,
348    /// Row count threshold for accepting parts (default: 1024).
349    threshold: usize,
350    /// Row count threshold for compacting (default: 4096).
351    compact_threshold: usize,
352}
353
354impl Default for UnorderedPart {
355    fn default() -> Self {
356        Self::new()
357    }
358}
359
360impl UnorderedPart {
361    /// Creates a new empty UnorderedPart.
362    pub fn new() -> Self {
363        Self {
364            parts: Vec::new(),
365            total_rows: 0,
366            total_bytes: 0,
367            min_timestamp: i64::MAX,
368            max_timestamp: i64::MIN,
369            max_sequence: 0,
370            threshold: 1024,
371            compact_threshold: 4096,
372        }
373    }
374
375    /// Sets the threshold for accepting parts into unordered_part.
376    pub fn set_threshold(&mut self, threshold: usize) {
377        self.threshold = threshold;
378    }
379
380    /// Sets the threshold for compacting unordered_part.
381    pub fn set_compact_threshold(&mut self, compact_threshold: usize) {
382        self.compact_threshold = compact_threshold;
383    }
384
385    /// Returns the threshold for accepting parts.
386    pub fn threshold(&self) -> usize {
387        self.threshold
388    }
389
390    /// Returns the compact threshold.
391    pub fn compact_threshold(&self) -> usize {
392        self.compact_threshold
393    }
394
395    /// Returns true if this part should accept the given row count.
396    pub fn should_accept(&self, num_rows: usize) -> bool {
397        num_rows < self.threshold
398    }
399
400    /// Returns true if this part should be compacted by row count.
401    pub fn should_compact(&self) -> bool {
402        self.total_rows >= self.compact_threshold
403    }
404
405    /// Returns the total estimated uncompressed bytes across all parts.
406    pub(super) fn estimated_bytes(&self) -> usize {
407        self.total_bytes
408    }
409
410    /// Adds a BulkPart to this unordered collection.
411    pub fn push(&mut self, part: BulkPart) {
412        self.total_rows += part.num_rows();
413        self.total_bytes = self.total_bytes.saturating_add(part.estimated_size());
414        self.min_timestamp = self.min_timestamp.min(part.min_timestamp);
415        self.max_timestamp = self.max_timestamp.max(part.max_timestamp);
416        self.max_sequence = self.max_sequence.max(part.sequence);
417        self.parts.push(part);
418    }
419
420    /// Returns the total number of rows across all parts.
421    pub fn num_rows(&self) -> usize {
422        self.total_rows
423    }
424
425    /// Returns true if there are no parts.
426    pub fn is_empty(&self) -> bool {
427        self.parts.is_empty()
428    }
429
430    /// Returns the number of parts in this collection.
431    pub fn num_parts(&self) -> usize {
432        self.parts.len()
433    }
434
435    /// Concatenates and sorts all parts into a single RecordBatch.
436    /// Returns None if the collection is empty.
437    pub fn concat_and_sort(&self) -> Result<Option<RecordBatch>> {
438        if self.parts.is_empty() {
439            return Ok(None);
440        }
441
442        if self.parts.len() == 1 {
443            // If there's only one part, return its batch directly
444            return Ok(Some(self.parts[0].batch.clone()));
445        }
446
447        // Get the schema from the first part
448        let schema = self.parts[0].batch.schema();
449        let concatenated = if schema.fields().iter().any(is_structured_json_field) {
450            let aligner = Json2Aligner::try_new(self.parts.iter().map(|part| part.batch.schema()))?;
451            let aligned_batches =
452                aligner.align_batches(self.parts.iter().map(|part| part.batch.clone()))?;
453            concat_batches(aligner.schema(), &aligned_batches).context(ComputeArrowSnafu)?
454        } else {
455            concat_batches(&schema, self.parts.iter().map(|x| &x.batch))
456                .context(ComputeArrowSnafu)?
457        };
458
459        // Sort the concatenated batch
460        let sorted_batch = sort_primary_key_record_batch(&concatenated)?;
461
462        Ok(Some(sorted_batch))
463    }
464
465    /// Converts all parts into a single sorted BulkPart.
466    /// Returns None if the collection is empty.
467    pub fn to_bulk_part(&self) -> Result<Option<BulkPart>> {
468        let Some(sorted_batch) = self.concat_and_sort()? else {
469            return Ok(None);
470        };
471
472        let timestamp_index = self.parts[0].timestamp_index;
473
474        Ok(Some(BulkPart {
475            batch: sorted_batch,
476            max_timestamp: self.max_timestamp,
477            min_timestamp: self.min_timestamp,
478            sequence: self.max_sequence,
479            timestamp_index,
480            raw_data: None,
481        }))
482    }
483
484    /// Clears all parts from this collection.
485    pub fn clear(&mut self) {
486        self.parts.clear();
487        self.total_rows = 0;
488        self.total_bytes = 0;
489        self.min_timestamp = i64::MAX;
490        self.max_timestamp = i64::MIN;
491        self.max_sequence = 0;
492    }
493}
494
495/// More accurate estimation of the size of a record batch.
496pub fn record_batch_estimated_size(batch: &RecordBatch) -> usize {
497    batch
498        .columns()
499        .iter()
500        // If can not get slice memory size, assume 0 here.
501        .map(|c| c.to_data().get_slice_memory_size().unwrap_or(0))
502        .sum()
503}
504
505/// Primary key column builder for handling strings specially.
506enum PrimaryKeyColumnBuilder {
507    /// String dictionary builder for string types.
508    StringDict(StringDictionaryBuilder<UInt32Type>),
509    /// Generic mutable vector for other types.
510    Vector(Box<dyn MutableVector>),
511}
512
513impl PrimaryKeyColumnBuilder {
514    /// Appends a value to the builder.
515    fn push_value_ref(&mut self, value: ValueRef) -> Result<()> {
516        match self {
517            PrimaryKeyColumnBuilder::StringDict(builder) => {
518                if let Some(s) = value.try_into_string().context(DataTypeMismatchSnafu)? {
519                    // We know the value is a string.
520                    builder.append_value(s);
521                } else {
522                    builder.append_null();
523                }
524            }
525            PrimaryKeyColumnBuilder::Vector(builder) => {
526                builder.push_value_ref(&value);
527            }
528        }
529        Ok(())
530    }
531
532    /// Converts the builder to an ArrayRef.
533    fn into_arrow_array(self) -> ArrayRef {
534        match self {
535            PrimaryKeyColumnBuilder::StringDict(mut builder) => Arc::new(builder.finish()),
536            PrimaryKeyColumnBuilder::Vector(mut builder) => builder.to_vector().to_arrow_array(),
537        }
538    }
539}
540
541/// Converter that converts structs into [BulkPart].
542pub struct BulkPartConverter {
543    /// Schema of the converted batch.
544    schema: SchemaRef,
545    /// Primary key codec for encoding keys
546    primary_key_codec: Arc<dyn PrimaryKeyCodec>,
547    /// Buffer for encoding primary key.
548    key_buf: Vec<u8>,
549    /// Primary key array builder.
550    key_array_builder: PrimaryKeyArrayBuilder,
551    /// Builders for non-primary key columns.
552    value_builder: ValueBuilder,
553    /// Builders for individual primary key columns.
554    /// The order of builders is the same as the order of primary key columns in the region metadata.
555    primary_key_column_builders: Vec<PrimaryKeyColumnBuilder>,
556
557    /// Max timestamp value.
558    max_ts: i64,
559    /// Min timestamp value.
560    min_ts: i64,
561    /// Max sequence number.
562    max_sequence: SequenceNumber,
563}
564
565impl BulkPartConverter {
566    /// Creates a new converter.
567    ///
568    /// If `store_primary_key_columns` is true and the encoding is not sparse encoding, it
569    /// stores primary key columns in arrays additionally.
570    pub fn new(
571        region_metadata: &RegionMetadataRef,
572        schema: SchemaRef,
573        capacity: usize,
574        primary_key_codec: Arc<dyn PrimaryKeyCodec>,
575        store_primary_key_columns: bool,
576    ) -> Self {
577        debug_assert_eq!(
578            region_metadata.primary_key_encoding,
579            primary_key_codec.encoding()
580        );
581
582        let primary_key_column_builders = if store_primary_key_columns
583            && region_metadata.primary_key_encoding != PrimaryKeyEncoding::Sparse
584        {
585            new_primary_key_column_builders(region_metadata, capacity)
586        } else {
587            Vec::new()
588        };
589
590        Self {
591            schema,
592            primary_key_codec,
593            key_buf: Vec::new(),
594            key_array_builder: PrimaryKeyArrayBuilder::new(),
595            value_builder: ValueBuilder::new(region_metadata, capacity),
596            primary_key_column_builders,
597            min_ts: i64::MAX,
598            max_ts: i64::MIN,
599            max_sequence: SequenceNumber::MIN,
600        }
601    }
602
603    /// Appends a [KeyValues] into the converter.
604    pub fn append_key_values(&mut self, key_values: &KeyValues) -> Result<()> {
605        for kv in key_values.iter() {
606            self.append_key_value(&kv)?;
607        }
608
609        Ok(())
610    }
611
612    /// Appends a [KeyValue] to builders.
613    ///
614    /// If the primary key uses sparse encoding, callers must encoded the primary key in the [KeyValue].
615    fn append_key_value(&mut self, kv: &KeyValue) -> Result<()> {
616        // Handles primary key based on encoding type
617        if self.primary_key_codec.encoding() == PrimaryKeyEncoding::Sparse {
618            // For sparse encoding, the primary key is already encoded in the KeyValue
619            // Gets the first (and only) primary key value which contains the encoded key
620            let mut primary_keys = kv.primary_keys();
621            if let Some(encoded) = primary_keys
622                .next()
623                .context(ColumnNotFoundSnafu {
624                    column: PRIMARY_KEY_COLUMN_NAME,
625                })?
626                .try_into_binary()
627                .context(DataTypeMismatchSnafu)?
628            {
629                self.key_array_builder
630                    .append(encoded)
631                    .context(ComputeArrowSnafu)?;
632            } else {
633                self.key_array_builder
634                    .append("")
635                    .context(ComputeArrowSnafu)?;
636            }
637        } else {
638            // For dense encoding, we need to encode the primary key columns
639            self.key_buf.clear();
640            self.primary_key_codec
641                .encode_key_value(kv, &mut self.key_buf)
642                .context(EncodeSnafu)?;
643            self.key_array_builder
644                .append(&self.key_buf)
645                .context(ComputeArrowSnafu)?;
646        };
647
648        // If storing primary key columns, append values to individual builders
649        if !self.primary_key_column_builders.is_empty() {
650            for (builder, pk_value) in self
651                .primary_key_column_builders
652                .iter_mut()
653                .zip(kv.primary_keys())
654            {
655                builder.push_value_ref(pk_value)?;
656            }
657        }
658
659        // Pushes other columns.
660        self.value_builder.push(
661            kv.timestamp(),
662            kv.sequence(),
663            kv.op_type() as u8,
664            kv.fields(),
665        );
666
667        // Updates statistics
668        // Safety: timestamp of kv must be both present and a valid timestamp value.
669        let ts = kv
670            .timestamp()
671            .try_into_timestamp()
672            .unwrap()
673            .unwrap()
674            .value();
675        self.min_ts = self.min_ts.min(ts);
676        self.max_ts = self.max_ts.max(ts);
677        self.max_sequence = self.max_sequence.max(kv.sequence());
678
679        Ok(())
680    }
681
682    /// Converts buffered content into a [BulkPart].
683    ///
684    /// It sorts the record batch by (primary key, timestamp, sequence desc).
685    pub fn convert(mut self) -> Result<BulkPart> {
686        let values = Values::from(self.value_builder);
687        let mut columns =
688            Vec::with_capacity(4 + values.fields.len() + self.primary_key_column_builders.len());
689
690        // Build primary key column arrays if enabled.
691        for builder in self.primary_key_column_builders {
692            columns.push(builder.into_arrow_array());
693        }
694        // Then fields columns.
695        columns.extend(values.fields.iter().map(|field| field.to_arrow_array()));
696        // Time index.
697        let timestamp_index = columns.len();
698        columns.push(values.timestamp.to_arrow_array());
699        // Primary key.
700        let pk_array = self.key_array_builder.finish();
701        columns.push(Arc::new(pk_array));
702        // Sequence and op type.
703        columns.push(values.sequence.to_arrow_array());
704        columns.push(values.op_type.to_arrow_array());
705
706        // The actual datatype of JSON array is data oriented, not to be derived from the Region
707        // metadata, which is static. So here we have to align the schema.
708        let schema = align_schema_with_json_array(self.schema, &columns);
709        let batch = RecordBatch::try_new(schema, columns).context(NewRecordBatchSnafu)?;
710        // Sorts the record batch.
711        let batch = sort_primary_key_record_batch(&batch)?;
712
713        Ok(BulkPart {
714            batch,
715            max_timestamp: self.max_ts,
716            min_timestamp: self.min_ts,
717            sequence: self.max_sequence,
718            timestamp_index,
719            raw_data: None,
720        })
721    }
722}
723
724fn align_schema_with_json_array(schema: SchemaRef, columns: &[ArrayRef]) -> SchemaRef {
725    if schema.fields().iter().all(|f| !is_structured_json_field(f)) {
726        return schema;
727    }
728
729    let mut fields = Vec::with_capacity(schema.fields().len());
730    for (field, array) in schema.fields().iter().zip(columns) {
731        if !is_structured_json_field(field) {
732            fields.push(field.clone());
733            continue;
734        }
735
736        let mut field = field.as_ref().clone();
737        field.set_data_type(array.data_type().clone());
738        fields.push(Arc::new(field));
739    }
740
741    Arc::new(Schema::new_with_metadata(fields, schema.metadata().clone()))
742}
743
744fn new_primary_key_column_builders(
745    metadata: &RegionMetadata,
746    capacity: usize,
747) -> Vec<PrimaryKeyColumnBuilder> {
748    metadata
749        .primary_key_columns()
750        .map(|col| {
751            if col.column_schema.data_type.is_string() {
752                PrimaryKeyColumnBuilder::StringDict(StringDictionaryBuilder::with_capacity(
753                    capacity,
754                    INIT_DICT_VALUE_CAPACITY,
755                    capacity,
756                ))
757            } else {
758                PrimaryKeyColumnBuilder::Vector(
759                    col.column_schema.data_type.create_mutable_vector(capacity),
760                )
761            }
762        })
763        .collect()
764}
765
766/// Sorts the record batch with primary key format.
767pub fn sort_primary_key_record_batch(batch: &RecordBatch) -> Result<RecordBatch> {
768    let total_columns = batch.num_columns();
769    let sort_columns = vec![
770        // Primary key column (ascending)
771        SortColumn {
772            values: batch.column(total_columns - 3).clone(),
773            options: Some(SortOptions {
774                descending: false,
775                nulls_first: true,
776            }),
777        },
778        // Time index column (ascending)
779        SortColumn {
780            values: batch.column(total_columns - 4).clone(),
781            options: Some(SortOptions {
782                descending: false,
783                nulls_first: true,
784            }),
785        },
786        // Sequence column (descending)
787        SortColumn {
788            values: batch.column(total_columns - 2).clone(),
789            options: Some(SortOptions {
790                descending: true,
791                nulls_first: true,
792            }),
793        },
794    ];
795
796    let indices = datatypes::arrow::compute::lexsort_to_indices(&sort_columns, None)
797        .context(ComputeArrowSnafu)?;
798
799    datatypes::arrow::compute::take_record_batch(batch, &indices).context(ComputeArrowSnafu)
800}
801
802/// Converts a `BulkPart` that is unordered and without encoded primary keys into a `BulkPart`
803/// with the same format as produced by [BulkPartConverter].
804///
805/// This function takes a `BulkPart` where:
806/// - For dense encoding: Primary key columns may be stored as individual columns
807/// - For sparse encoding: The `__primary_key` column should already be present with encoded keys
808/// - The batch may not be sorted
809///
810/// And produces a `BulkPart` where:
811/// - Primary key columns are optionally stored (depending on `store_primary_key_columns` and encoding)
812/// - An encoded `__primary_key` dictionary column is present
813/// - The batch is sorted by (primary_key, timestamp, sequence desc)
814///
815/// # Arguments
816///
817/// * `part` - The input `BulkPart` to convert
818/// * `region_metadata` - Region metadata containing schema information
819/// * `primary_key_codec` - Codec for encoding primary keys
820/// * `schema` - Target schema for the output batch
821/// * `store_primary_key_columns` - If true and encoding is not sparse, stores individual primary key columns
822///
823/// # Returns
824///
825/// Returns `None` if the input part has no rows, otherwise returns a new `BulkPart` with
826/// encoded primary keys and sorted data.
827pub fn convert_bulk_part(
828    part: BulkPart,
829    region_metadata: &RegionMetadataRef,
830    primary_key_codec: Arc<dyn PrimaryKeyCodec>,
831    schema: SchemaRef,
832    store_primary_key_columns: bool,
833) -> Result<Option<BulkPart>> {
834    if part.num_rows() == 0 {
835        return Ok(None);
836    }
837
838    let num_rows = part.num_rows();
839    let is_sparse = region_metadata.primary_key_encoding == PrimaryKeyEncoding::Sparse;
840
841    // Builds a column name-to-index map for efficient lookups
842    let input_schema = part.batch.schema();
843    let column_indices: HashMap<&str, usize> = input_schema
844        .fields()
845        .iter()
846        .enumerate()
847        .map(|(idx, field)| (field.name().as_str(), idx))
848        .collect();
849
850    // Determines the structure of the input batch by looking up columns by name
851    let mut output_columns = Vec::new();
852
853    // Extracts primary key columns if we need to encode them (dense encoding)
854    let pk_array = if is_sparse {
855        // For sparse encoding, the input should already have the __primary_key column
856        // We need to find it in the input batch
857        None
858    } else {
859        // For dense encoding, extract and encode primary key columns by name
860        let pk_vectors: Result<Vec<_>> = region_metadata
861            .primary_key_columns()
862            .map(|col_meta| {
863                let col_idx = column_indices
864                    .get(col_meta.column_schema.name.as_str())
865                    .context(ColumnNotFoundSnafu {
866                        column: &col_meta.column_schema.name,
867                    })?;
868                let col = part.batch.column(*col_idx);
869                Helper::try_into_vector(col).context(error::ComputeVectorSnafu)
870            })
871            .collect();
872        let pk_vectors = pk_vectors?;
873
874        let mut key_array_builder = PrimaryKeyArrayBuilder::new();
875        let mut encode_buf = Vec::new();
876
877        for row_idx in 0..num_rows {
878            encode_buf.clear();
879
880            // Collects primary key values with column IDs for this row
881            let pk_values_with_ids: Vec<_> = region_metadata
882                .primary_key
883                .iter()
884                .zip(pk_vectors.iter())
885                .map(|(col_id, vector)| (*col_id, vector.get_ref(row_idx)))
886                .collect();
887
888            // Encodes the primary key
889            primary_key_codec
890                .encode_value_refs(&pk_values_with_ids, &mut encode_buf)
891                .context(EncodeSnafu)?;
892
893            key_array_builder
894                .append(&encode_buf)
895                .context(ComputeArrowSnafu)?;
896        }
897
898        Some(key_array_builder.finish())
899    };
900
901    // Adds primary key columns if storing them (only for dense encoding)
902    if store_primary_key_columns && !is_sparse {
903        for col_meta in region_metadata.primary_key_columns() {
904            let col_idx = column_indices
905                .get(col_meta.column_schema.name.as_str())
906                .context(ColumnNotFoundSnafu {
907                    column: &col_meta.column_schema.name,
908                })?;
909            let col = part.batch.column(*col_idx);
910
911            // Converts to dictionary if needed for string types
912            let col = if col_meta.column_schema.data_type.is_string() {
913                let target_type = ArrowDataType::Dictionary(
914                    Box::new(ArrowDataType::UInt32),
915                    Box::new(ArrowDataType::Utf8),
916                );
917                arrow::compute::cast(col, &target_type).context(ComputeArrowSnafu)?
918            } else {
919                col.clone()
920            };
921            output_columns.push(col);
922        }
923    }
924
925    // Adds field columns
926    for col_meta in region_metadata.field_columns() {
927        let col_idx = column_indices
928            .get(col_meta.column_schema.name.as_str())
929            .context(ColumnNotFoundSnafu {
930                column: &col_meta.column_schema.name,
931            })?;
932        output_columns.push(part.batch.column(*col_idx).clone());
933    }
934
935    // Adds timestamp column
936    let new_timestamp_index = output_columns.len();
937    let ts_col_idx = column_indices
938        .get(
939            region_metadata
940                .time_index_column()
941                .column_schema
942                .name
943                .as_str(),
944        )
945        .context(ColumnNotFoundSnafu {
946            column: &region_metadata.time_index_column().column_schema.name,
947        })?;
948    output_columns.push(part.batch.column(*ts_col_idx).clone());
949
950    // Adds encoded primary key dictionary column
951    let pk_dictionary = if let Some(pk_dict_array) = pk_array {
952        Arc::new(pk_dict_array) as ArrayRef
953    } else {
954        let pk_col_idx =
955            column_indices
956                .get(PRIMARY_KEY_COLUMN_NAME)
957                .context(ColumnNotFoundSnafu {
958                    column: PRIMARY_KEY_COLUMN_NAME,
959                })?;
960        let col = part.batch.column(*pk_col_idx);
961
962        // Casts to dictionary type if needed
963        let target_type = ArrowDataType::Dictionary(
964            Box::new(ArrowDataType::UInt32),
965            Box::new(ArrowDataType::Binary),
966        );
967        arrow::compute::cast(col, &target_type).context(ComputeArrowSnafu)?
968    };
969    output_columns.push(pk_dictionary);
970
971    let sequence_array = UInt64Array::from(vec![part.sequence; num_rows]);
972    output_columns.push(Arc::new(sequence_array) as ArrayRef);
973
974    let op_type_array = UInt8Array::from(vec![OpType::Put as u8; num_rows]);
975    output_columns.push(Arc::new(op_type_array) as ArrayRef);
976
977    let batch = RecordBatch::try_new(schema, output_columns).context(NewRecordBatchSnafu)?;
978
979    // Sorts the batch by (primary_key, timestamp, sequence desc)
980    let sorted_batch = sort_primary_key_record_batch(&batch)?;
981
982    Ok(Some(BulkPart {
983        batch: sorted_batch,
984        max_timestamp: part.max_timestamp,
985        min_timestamp: part.min_timestamp,
986        sequence: part.sequence,
987        timestamp_index: new_timestamp_index,
988        raw_data: None,
989    }))
990}
991
992#[derive(Debug, Clone)]
993pub struct EncodedBulkPart {
994    data: Bytes,
995    metadata: BulkPartMeta,
996    /// Cached Arrow schema to avoid rebuilding it from parquet metadata.
997    schema: SchemaRef,
998}
999
1000impl EncodedBulkPart {
1001    pub fn new(data: Bytes, metadata: BulkPartMeta, schema: SchemaRef) -> Self {
1002        Self {
1003            data,
1004            metadata,
1005            schema,
1006        }
1007    }
1008
1009    pub fn metadata(&self) -> &BulkPartMeta {
1010        &self.metadata
1011    }
1012
1013    pub(crate) fn schema(&self) -> SchemaRef {
1014        self.schema.clone()
1015    }
1016
1017    /// Returns the size of the encoded data in bytes
1018    pub(crate) fn size_bytes(&self) -> usize {
1019        self.data.len()
1020    }
1021
1022    /// Returns the encoded data.
1023    pub fn data(&self) -> &Bytes {
1024        &self.data
1025    }
1026
1027    /// Creates MemtableStats from this EncodedBulkPart.
1028    pub fn to_memtable_stats(&self) -> MemtableStats {
1029        let meta = &self.metadata;
1030        let ts_type = meta.region_metadata.time_index_type();
1031        let min_ts = ts_type.create_timestamp(meta.min_timestamp);
1032        let max_ts = ts_type.create_timestamp(meta.max_timestamp);
1033
1034        MemtableStats {
1035            estimated_bytes: self.size_bytes(),
1036            time_range: Some((min_ts, max_ts)),
1037            num_rows: meta.num_rows,
1038            num_ranges: 1,
1039            max_sequence: meta.max_sequence,
1040            series_count: meta.num_series as usize,
1041        }
1042    }
1043
1044    /// Converts this `EncodedBulkPart` to `SstInfo`.
1045    ///
1046    /// # Arguments
1047    /// * `file_id` - The SST file ID to assign to this part
1048    ///
1049    /// # Returns
1050    /// Returns a `SstInfo` instance with information derived from this bulk part's metadata
1051    pub(crate) fn to_sst_info(&self, file_id: FileId) -> SstInfo {
1052        let unit = self.metadata.region_metadata.time_index_type().unit();
1053        let max_row_group_uncompressed_size: u64 = self
1054            .metadata
1055            .parquet_metadata
1056            .row_groups()
1057            .iter()
1058            .map(|rg| {
1059                rg.columns()
1060                    .iter()
1061                    .map(|c| c.uncompressed_size() as u64)
1062                    .sum::<u64>()
1063            })
1064            .max()
1065            .unwrap_or(0);
1066        SstInfo {
1067            file_id,
1068            time_range: (
1069                Timestamp::new(self.metadata.min_timestamp, unit),
1070                Timestamp::new(self.metadata.max_timestamp, unit),
1071            ),
1072            file_size: self.data.len() as u64,
1073            max_row_group_uncompressed_size,
1074            num_rows: self.metadata.num_rows,
1075            num_row_groups: self.metadata.parquet_metadata.num_row_groups() as u64,
1076            file_metadata: Some(self.metadata.parquet_metadata.clone()),
1077            index_metadata: IndexOutput::default(),
1078            num_series: self.metadata.num_series,
1079        }
1080    }
1081
1082    pub(crate) fn read(
1083        &self,
1084        context: BulkIterContextRef,
1085        sequence: Option<SequenceRange>,
1086        mem_scan_metrics: Option<MemScanMetrics>,
1087    ) -> Result<Option<BoxedRecordBatchIterator>> {
1088        // Compute skip_fields for row group pruning from the configured pre-filter mode.
1089        let skip_fields_for_pruning = context.pre_filter_mode().skip_fields();
1090
1091        // use predicate to find row groups to read.
1092        let row_groups_to_read =
1093            context.row_groups_to_read(&self.metadata.parquet_metadata, skip_fields_for_pruning);
1094
1095        if row_groups_to_read.is_empty() {
1096            // All row groups are filtered.
1097            return Ok(None);
1098        }
1099
1100        let iter = EncodedBulkPartIter::try_new(
1101            self,
1102            context,
1103            row_groups_to_read,
1104            sequence,
1105            mem_scan_metrics,
1106        )?;
1107        Ok(Some(Box::new(iter) as BoxedRecordBatchIterator))
1108    }
1109}
1110
1111// TODO(yingwen): max_sequence
1112#[derive(Debug, Clone)]
1113pub struct BulkPartMeta {
1114    /// Total rows in part.
1115    pub num_rows: usize,
1116    /// Max timestamp in part.
1117    pub max_timestamp: i64,
1118    /// Min timestamp in part.
1119    pub min_timestamp: i64,
1120    /// Part file metadata.
1121    pub parquet_metadata: Arc<ParquetMetaData>,
1122    /// Part region schema.
1123    pub region_metadata: RegionMetadataRef,
1124    /// Number of series.
1125    pub num_series: u64,
1126    /// Maximum sequence number in part.
1127    pub max_sequence: u64,
1128}
1129
1130/// Metrics for encoding a part.
1131#[derive(Default, Debug)]
1132pub struct BulkPartEncodeMetrics {
1133    /// Cost of iterating over the data.
1134    pub iter_cost: Duration,
1135    /// Cost of writing the data.
1136    pub write_cost: Duration,
1137    /// Size of data before encoding.
1138    pub raw_size: usize,
1139    /// Size of data after encoding.
1140    pub encoded_size: usize,
1141    /// Number of rows in part.
1142    pub num_rows: usize,
1143}
1144
1145pub struct BulkPartEncoder {
1146    metadata: RegionMetadataRef,
1147    writer_props: Option<WriterProperties>,
1148}
1149
1150impl BulkPartEncoder {
1151    pub fn new(metadata: RegionMetadataRef, row_group_size: usize) -> Result<BulkPartEncoder> {
1152        // TODO(yingwen): Skip arrow schema if needed.
1153        let json = metadata.to_json().context(InvalidMetadataSnafu)?;
1154        let key_value_meta =
1155            parquet::file::metadata::KeyValue::new(PARQUET_METADATA_KEY.to_string(), json);
1156
1157        // TODO(yingwen): Do we need compression?
1158        let writer_props = Some(
1159            WriterProperties::builder()
1160                .set_key_value_metadata(Some(vec![key_value_meta]))
1161                .set_write_batch_size(row_group_size)
1162                .set_max_row_group_row_count(Some(row_group_size))
1163                .set_compression(Compression::ZSTD(ZstdLevel::default()))
1164                .set_column_index_truncate_length(None)
1165                .set_statistics_truncate_length(None)
1166                .build(),
1167        );
1168
1169        Ok(Self {
1170            metadata,
1171            writer_props,
1172        })
1173    }
1174}
1175
1176impl BulkPartEncoder {
1177    /// Encodes [BoxedRecordBatchIterator] into [EncodedBulkPart] with min/max timestamps.
1178    pub fn encode_record_batch_iter(
1179        &self,
1180        iter: BoxedRecordBatchIterator,
1181        arrow_schema: SchemaRef,
1182        min_timestamp: i64,
1183        max_timestamp: i64,
1184        max_sequence: u64,
1185        metrics: &mut BulkPartEncodeMetrics,
1186    ) -> Result<Option<EncodedBulkPart>> {
1187        let mut buf = Vec::with_capacity(4096);
1188        let mut writer =
1189            ArrowWriter::try_new(&mut buf, arrow_schema.clone(), self.writer_props.clone())
1190                .context(EncodeMemtableSnafu)?;
1191        let mut total_rows = 0;
1192        let mut series_estimator = SeriesEstimator::default();
1193
1194        // Process each batch from the iterator
1195        let mut iter_start = Instant::now();
1196        for batch_result in iter {
1197            metrics.iter_cost += iter_start.elapsed();
1198            let batch = batch_result?;
1199            if batch.num_rows() == 0 {
1200                continue;
1201            }
1202
1203            series_estimator.update_flat(&batch);
1204            metrics.raw_size += record_batch_estimated_size(&batch);
1205            let write_start = Instant::now();
1206            writer.write(&batch).context(EncodeMemtableSnafu)?;
1207            metrics.write_cost += write_start.elapsed();
1208            total_rows += batch.num_rows();
1209            iter_start = Instant::now();
1210        }
1211        metrics.iter_cost += iter_start.elapsed();
1212
1213        if total_rows == 0 {
1214            return Ok(None);
1215        }
1216
1217        let close_start = Instant::now();
1218        let file_metadata = writer.close().context(EncodeMemtableSnafu)?;
1219        metrics.write_cost += close_start.elapsed();
1220        metrics.encoded_size += buf.len();
1221        metrics.num_rows += total_rows;
1222
1223        let buf = Bytes::from(buf);
1224        let parquet_metadata = Arc::new(file_metadata);
1225        let num_series = series_estimator.finish();
1226
1227        Ok(Some(EncodedBulkPart {
1228            data: buf,
1229            metadata: BulkPartMeta {
1230                num_rows: total_rows,
1231                max_timestamp,
1232                min_timestamp,
1233                parquet_metadata,
1234                region_metadata: self.metadata.clone(),
1235                num_series,
1236                max_sequence,
1237            },
1238            schema: arrow_schema,
1239        }))
1240    }
1241
1242    /// Encodes bulk part to a [EncodedBulkPart], returns the encoded data.
1243    pub fn encode_part(&self, part: &BulkPart) -> Result<Option<EncodedBulkPart>> {
1244        if part.batch.num_rows() == 0 {
1245            return Ok(None);
1246        }
1247
1248        let mut buf = Vec::with_capacity(4096);
1249        let arrow_schema = part.batch.schema();
1250
1251        let file_metadata = {
1252            let mut writer =
1253                ArrowWriter::try_new(&mut buf, arrow_schema.clone(), self.writer_props.clone())
1254                    .context(EncodeMemtableSnafu)?;
1255            writer.write(&part.batch).context(EncodeMemtableSnafu)?;
1256            writer.finish().context(EncodeMemtableSnafu)?
1257        };
1258
1259        let buf = Bytes::from(buf);
1260        let parquet_metadata = Arc::new(file_metadata);
1261
1262        Ok(Some(EncodedBulkPart {
1263            data: buf,
1264            metadata: BulkPartMeta {
1265                num_rows: part.batch.num_rows(),
1266                max_timestamp: part.max_timestamp,
1267                min_timestamp: part.min_timestamp,
1268                parquet_metadata,
1269                region_metadata: self.metadata.clone(),
1270                num_series: part.estimated_series_count() as u64,
1271                max_sequence: part.sequence,
1272            },
1273            schema: arrow_schema,
1274        }))
1275    }
1276}
1277
1278/// Per-batch min/max statistics for the first tag column in a `MultiBulkPart`.
1279///
1280/// Since batches are sorted by primary key, we can extract the min/max of the first tag
1281/// from the first/last row's encoded primary key in each batch. These statistics enable
1282/// batch-level pruning using predicates, analogous to row-group pruning in parquet.
1283#[derive(Debug, Clone)]
1284struct BatchStats {
1285    /// Number of batches.
1286    num_batches: usize,
1287    /// Column id of the first tag.
1288    first_tag_id: ColumnId,
1289    /// Min values of the first tag, one element per batch.
1290    min_values: ArrayRef,
1291    /// Max values of the first tag, one element per batch.
1292    max_values: ArrayRef,
1293}
1294
1295impl BatchStats {
1296    /// Computes batch statistics from a slice of record batches.
1297    ///
1298    /// Returns `None` if there is no primary key (no first tag to collect stats for)
1299    /// or if extracting statistics fails.
1300    fn compute(batches: &[RecordBatch], metadata: &RegionMetadata) -> Option<Self> {
1301        // `primary_key.first()` is correct for both dense and sparse encodings.
1302        // For dense, values follow the order of `metadata.primary_key`.
1303        // For sparse, `decode_leftmost` decodes the first value which also
1304        // corresponds to `primary_key.first()`. See `SparsePrimaryKeyCodec` for format details.
1305        let first_tag_id = *metadata.primary_key.first()?;
1306        let first_tag_column = metadata.column_by_id(first_tag_id)?;
1307        let data_type = &first_tag_column.column_schema.data_type;
1308
1309        let converter = build_primary_key_codec_with_fields(
1310            metadata.primary_key_encoding,
1311            [(first_tag_id, SortField::new(data_type.clone()))].into_iter(),
1312        );
1313        let pk_index = primary_key_column_index(batches.first()?.num_columns());
1314
1315        let mut min_builder = data_type.create_mutable_vector(batches.len());
1316        let mut max_builder = data_type.create_mutable_vector(batches.len());
1317
1318        for batch in batches {
1319            match Self::extract_first_tag_bounds(batch, pk_index, &*converter) {
1320                Some((min_val, max_val)) => {
1321                    min_builder.push_value_ref(&min_val.as_value_ref());
1322                    max_builder.push_value_ref(&max_val.as_value_ref());
1323                }
1324                None => {
1325                    min_builder.push_null();
1326                    max_builder.push_null();
1327                }
1328            }
1329        }
1330
1331        Some(Self {
1332            num_batches: batches.len(),
1333            first_tag_id,
1334            min_values: min_builder.to_vector().to_arrow_array(),
1335            max_values: max_builder.to_vector().to_arrow_array(),
1336        })
1337    }
1338
1339    /// Extracts the first tag value from the first and last rows of a batch.
1340    fn extract_first_tag_bounds(
1341        batch: &RecordBatch,
1342        pk_index: usize,
1343        converter: &dyn PrimaryKeyCodec,
1344    ) -> Option<(datatypes::value::Value, datatypes::value::Value)> {
1345        if batch.num_rows() == 0 {
1346            return None;
1347        }
1348
1349        let pk_dict = batch
1350            .column(pk_index)
1351            .as_any()
1352            .downcast_ref::<PrimaryKeyArray>()?;
1353        let pk_values = pk_dict.values().as_any().downcast_ref::<BinaryArray>()?;
1354
1355        let keys = pk_dict.keys();
1356        let min_key = keys.value(0);
1357        let max_key = keys.value(batch.num_rows() - 1);
1358        let min_bytes = pk_values.value(min_key as usize);
1359        let max_bytes = pk_values.value(max_key as usize);
1360
1361        Some((
1362            converter.decode_leftmost(min_bytes).ok()??,
1363            converter.decode_leftmost(max_bytes).ok()??,
1364        ))
1365    }
1366}
1367
1368/// Adapter implementing `PruningStatistics` for `BatchStats`.
1369///
1370/// Used with `Predicate::prune_with_stats()` to skip batches whose first-tag
1371/// min/max range does not match the query predicate.
1372struct BatchPruningStats<'a> {
1373    stats: &'a BatchStats,
1374    metadata: &'a RegionMetadataRef,
1375}
1376
1377impl PruningStatistics for BatchPruningStats<'_> {
1378    fn min_values(&self, column: &Column) -> Option<ArrayRef> {
1379        let col = self.metadata.column_by_name(&column.name)?;
1380        if col.column_id == self.stats.first_tag_id {
1381            Some(self.stats.min_values.clone())
1382        } else {
1383            None
1384        }
1385    }
1386
1387    fn max_values(&self, column: &Column) -> Option<ArrayRef> {
1388        let col = self.metadata.column_by_name(&column.name)?;
1389        if col.column_id == self.stats.first_tag_id {
1390            Some(self.stats.max_values.clone())
1391        } else {
1392            None
1393        }
1394    }
1395
1396    fn num_containers(&self) -> usize {
1397        self.stats.num_batches
1398    }
1399
1400    fn null_counts(&self, _column: &Column) -> Option<ArrayRef> {
1401        None
1402    }
1403
1404    fn row_counts(&self, _column: &Column) -> Option<ArrayRef> {
1405        None
1406    }
1407
1408    fn contained(
1409        &self,
1410        _column: &Column,
1411        _values: &std::collections::HashSet<datafusion_common::ScalarValue>,
1412    ) -> Option<BooleanArray> {
1413        None
1414    }
1415}
1416
1417/// Returns true if the predicate references the given column name.
1418fn predicate_references_column(predicate: &table::predicate::Predicate, column_name: &str) -> bool {
1419    let mut columns = HashSet::new();
1420    for expr in predicate.exprs() {
1421        let _ = expr_to_columns(expr, &mut columns);
1422    }
1423    columns.iter().any(|col| col.name == column_name)
1424}
1425
1426/// Returns true if the batch should be pruned (skipped) based on the first-tag min/max
1427/// statistics and the predicate in the context. Returns false if no pruning is possible
1428/// (no primary key, no predicate, or the batch matches the predicate).
1429pub(crate) fn should_prune_bulk_part(
1430    batch: &RecordBatch,
1431    context: &BulkIterContext,
1432    metadata: &RegionMetadata,
1433) -> bool {
1434    let predicate = match &context.predicate {
1435        Some(p) => p,
1436        None => return false,
1437    };
1438    // Check if the predicate references the first tag column to avoid computing
1439    // expensive batch statistics when they won't help with pruning.
1440    let first_tag_id = match metadata.primary_key.first() {
1441        Some(id) => *id,
1442        None => return false,
1443    };
1444    // Safety: `first_tag_id` comes from `metadata.primary_key` so the column always exists.
1445    let first_tag_name = &metadata
1446        .column_by_id(first_tag_id)
1447        .unwrap()
1448        .column_schema
1449        .name;
1450    if !predicate_references_column(predicate, first_tag_name) {
1451        return false;
1452    }
1453    let stats = match BatchStats::compute(std::slice::from_ref(batch), metadata) {
1454        Some(s) => s,
1455        None => return false,
1456    };
1457    let region_meta = context.read_format().metadata();
1458    let pruning_stats = BatchPruningStats {
1459        stats: &stats,
1460        metadata: region_meta,
1461    };
1462    let mask = predicate.prune_with_stats(&pruning_stats, region_meta.schema.arrow_schema());
1463    !mask.first().copied().unwrap_or(true)
1464}
1465
1466/// A collection of ordered RecordBatches representing a bulk part without parquet encoding.
1467///
1468/// Similar to `EncodedBulkPart` but stores raw RecordBatches instead of encoded parquet data.
1469/// The RecordBatches must be ordered by (primary key, timestamp, sequence desc).
1470/// Uses SmallVec to optimize for the common case of few batches while avoiding heap allocation.
1471#[derive(Debug, Clone)]
1472pub struct MultiBulkPart {
1473    /// Ordered record batches. SmallVec optimized for up to 4 batches inline.
1474    batches: SmallVec<[RecordBatch; 4]>,
1475    /// Total rows across all batches.
1476    total_rows: usize,
1477    /// Max timestamp in part.
1478    max_timestamp: i64,
1479    /// Min timestamp in part.
1480    min_timestamp: i64,
1481    /// Max sequence number in part.
1482    max_sequence: SequenceNumber,
1483    /// Number of series.
1484    series_count: usize,
1485    /// Pre-computed per-batch statistics for the first tag column.
1486    /// `None` if there is no primary key.
1487    batch_stats: Option<BatchStats>,
1488}
1489
1490impl MultiBulkPart {
1491    /// Creates a new MultiBulkPart from a single BulkPart.
1492    pub fn from_bulk_part(part: BulkPart, metadata: &RegionMetadata) -> Self {
1493        let num_rows = part.num_rows();
1494        let series_count = part.estimated_series_count();
1495        let batch_stats = BatchStats::compute(std::slice::from_ref(&part.batch), metadata);
1496        let mut batches = SmallVec::new();
1497        batches.push(part.batch);
1498
1499        Self {
1500            batches,
1501            total_rows: num_rows,
1502            max_timestamp: part.max_timestamp,
1503            min_timestamp: part.min_timestamp,
1504            max_sequence: part.sequence,
1505            series_count,
1506            batch_stats,
1507        }
1508    }
1509
1510    /// Creates a new MultiBulkPart from multiple ordered RecordBatches.
1511    ///
1512    /// # Arguments
1513    /// * `batches` - Ordered record batches
1514    /// * `min_timestamp` - Minimum timestamp across all batches
1515    /// * `max_timestamp` - Maximum timestamp across all batches
1516    /// * `max_sequence` - Maximum sequence number across all batches
1517    /// * `series_count` - Number of series in the batches
1518    /// * `metadata` - Region metadata for computing batch statistics
1519    ///
1520    /// # Panics
1521    /// Panics if batches is empty.
1522    pub fn new(
1523        batches: Vec<RecordBatch>,
1524        min_timestamp: i64,
1525        max_timestamp: i64,
1526        max_sequence: SequenceNumber,
1527        series_count: usize,
1528        metadata: &RegionMetadata,
1529    ) -> Self {
1530        assert!(!batches.is_empty(), "batches must not be empty");
1531
1532        let total_rows = batches.iter().map(|b| b.num_rows()).sum();
1533        let batch_stats = BatchStats::compute(&batches, metadata);
1534
1535        Self {
1536            batches: SmallVec::from_vec(batches),
1537            total_rows,
1538            max_timestamp,
1539            min_timestamp,
1540            max_sequence,
1541            series_count,
1542            batch_stats,
1543        }
1544    }
1545
1546    /// Returns the total number of rows across all batches.
1547    pub fn num_rows(&self) -> usize {
1548        self.total_rows
1549    }
1550
1551    pub(crate) fn schemas(&self) -> impl Iterator<Item = SchemaRef> + '_ {
1552        self.batches.iter().map(|batch| batch.schema())
1553    }
1554
1555    /// Returns the minimum timestamp.
1556    pub fn min_timestamp(&self) -> i64 {
1557        self.min_timestamp
1558    }
1559
1560    /// Returns the maximum timestamp.
1561    pub fn max_timestamp(&self) -> i64 {
1562        self.max_timestamp
1563    }
1564
1565    /// Returns the maximum sequence number.
1566    pub fn max_sequence(&self) -> SequenceNumber {
1567        self.max_sequence
1568    }
1569
1570    /// Returns the number of series.
1571    pub fn series_count(&self) -> usize {
1572        self.series_count
1573    }
1574
1575    /// Returns the number of record batches in this part.
1576    pub fn num_batches(&self) -> usize {
1577        self.batches.len()
1578    }
1579
1580    /// Returns the estimated memory size of all batches.
1581    pub(crate) fn estimated_size(&self) -> usize {
1582        self.batches.iter().map(record_batch_estimated_size).sum()
1583    }
1584
1585    /// Reads data from this part with the given context and filters.
1586    ///
1587    /// If batch-level statistics are available and a predicate is set, prunes
1588    /// batches whose first-tag min/max range doesn't match the predicate before
1589    /// creating the iterator.
1590    pub(crate) fn read(
1591        &self,
1592        context: BulkIterContextRef,
1593        sequence: Option<SequenceRange>,
1594        mem_scan_metrics: Option<MemScanMetrics>,
1595    ) -> Result<Option<BoxedRecordBatchIterator>> {
1596        if self.batches.is_empty() {
1597            return Ok(None);
1598        }
1599
1600        let batches_to_read = self.prune_batches(&context);
1601
1602        if batches_to_read.is_empty() {
1603            return Ok(None);
1604        }
1605
1606        let iter = crate::memtable::bulk::part_reader::BulkPartBatchIter::new(
1607            batches_to_read,
1608            context,
1609            sequence,
1610            self.series_count,
1611            mem_scan_metrics,
1612        );
1613        Ok(Some(Box::new(iter) as BoxedRecordBatchIterator))
1614    }
1615
1616    /// Prunes batches using the first-tag min/max statistics and the predicate.
1617    /// Returns all batches if no stats or no predicate is available.
1618    fn prune_batches(&self, context: &BulkIterContextRef) -> Vec<RecordBatch> {
1619        if let Some(stats) = &self.batch_stats
1620            && let Some(predicate) = &context.predicate
1621        {
1622            let region_meta = context.read_format().metadata();
1623            let pruning_stats = BatchPruningStats {
1624                stats,
1625                metadata: region_meta,
1626            };
1627            let mask =
1628                predicate.prune_with_stats(&pruning_stats, region_meta.schema.arrow_schema());
1629            self.batches
1630                .iter()
1631                .zip(mask.iter())
1632                .filter_map(
1633                    |(batch, &selected)| {
1634                        if selected { Some(batch.clone()) } else { None }
1635                    },
1636                )
1637                .collect()
1638        } else {
1639            self.batches.iter().cloned().collect()
1640        }
1641    }
1642
1643    /// Converts this `MultiBulkPart` to `MemtableStats`.
1644    pub fn to_memtable_stats(&self, region_metadata: &RegionMetadataRef) -> MemtableStats {
1645        let ts_type = region_metadata.time_index_type();
1646        let min_ts = ts_type.create_timestamp(self.min_timestamp);
1647        let max_ts = ts_type.create_timestamp(self.max_timestamp);
1648
1649        MemtableStats {
1650            estimated_bytes: self.estimated_size(),
1651            time_range: Some((min_ts, max_ts)),
1652            num_rows: self.num_rows(),
1653            num_ranges: 1,
1654            max_sequence: self.max_sequence,
1655            series_count: self.series_count,
1656        }
1657    }
1658}
1659
1660#[cfg(test)]
1661mod tests {
1662    use api::v1::{Row, SemanticType, WriteHint};
1663    use datafusion_common::ScalarValue;
1664    use datatypes::arrow::array::{
1665        BinaryArray, DictionaryArray, Float64Array, TimestampMillisecondArray,
1666    };
1667    use datatypes::arrow::datatypes::UInt32Type;
1668    use datatypes::prelude::{ConcreteDataType, Value};
1669    use datatypes::schema::ColumnSchema;
1670    use mito_codec::row_converter::build_primary_key_codec;
1671    use store_api::metadata::{ColumnMetadata, RegionMetadataBuilder};
1672    use store_api::storage::RegionId;
1673    use store_api::storage::consts::ReservedColumnId;
1674    use table::predicate::Predicate;
1675
1676    use super::*;
1677    use crate::memtable::bulk::context::BulkIterContext;
1678    use crate::sst::{FlatSchemaOptions, to_flat_sst_arrow_schema};
1679    use crate::test_util::memtable_util::{build_key_values_with_ts_seq_values, metadata_for_test};
1680
1681    struct MutationInput<'a> {
1682        k0: &'a str,
1683        k1: u32,
1684        timestamps: &'a [i64],
1685        v1: &'a [Option<f64>],
1686        sequence: u64,
1687    }
1688
1689    #[test]
1690    fn test_unordered_part_tracks_estimated_bytes() {
1691        let mut part = UnorderedPart::new();
1692        let bulk_part = BulkPart {
1693            batch: RecordBatch::new_empty(Arc::new(arrow::datatypes::Schema::empty())),
1694            max_timestamp: 0,
1695            min_timestamp: 0,
1696            sequence: 0,
1697            timestamp_index: 0,
1698            raw_data: None,
1699        };
1700        let estimated_size = bulk_part.estimated_size();
1701
1702        part.push(bulk_part);
1703        assert_eq!(estimated_size, part.estimated_bytes());
1704        part.clear();
1705        assert_eq!(0, part.estimated_bytes());
1706        assert!(part.is_empty());
1707    }
1708
1709    #[test]
1710    fn test_unordered_part_should_accept() {
1711        let mut part = UnorderedPart::new();
1712        part.set_threshold(10);
1713        assert!(part.should_accept(9));
1714        assert!(!part.should_accept(10));
1715    }
1716
1717    fn encode(input: &[MutationInput]) -> EncodedBulkPart {
1718        let metadata = metadata_for_test();
1719        let kvs = input
1720            .iter()
1721            .map(|m| {
1722                build_key_values_with_ts_seq_values(
1723                    &metadata,
1724                    m.k0.to_string(),
1725                    m.k1,
1726                    m.timestamps.iter().copied(),
1727                    m.v1.iter().copied(),
1728                    m.sequence,
1729                )
1730            })
1731            .collect::<Vec<_>>();
1732        let schema = to_flat_sst_arrow_schema(&metadata, &FlatSchemaOptions::default());
1733        let primary_key_codec = build_primary_key_codec(&metadata);
1734        let mut converter = BulkPartConverter::new(&metadata, schema, 64, primary_key_codec, true);
1735        for kv in kvs {
1736            converter.append_key_values(&kv).unwrap();
1737        }
1738        let part = converter.convert().unwrap();
1739        let encoder = BulkPartEncoder::new(metadata, 1024).unwrap();
1740        encoder.encode_part(&part).unwrap().unwrap()
1741    }
1742
1743    #[test]
1744    fn test_write_and_read_part_projection() {
1745        let part = encode(&[
1746            MutationInput {
1747                k0: "a",
1748                k1: 0,
1749                timestamps: &[1],
1750                v1: &[Some(0.1)],
1751                sequence: 0,
1752            },
1753            MutationInput {
1754                k0: "b",
1755                k1: 0,
1756                timestamps: &[1],
1757                v1: &[Some(0.0)],
1758                sequence: 0,
1759            },
1760            MutationInput {
1761                k0: "a",
1762                k1: 0,
1763                timestamps: &[2],
1764                v1: &[Some(0.2)],
1765                sequence: 1,
1766            },
1767        ]);
1768
1769        let projection = &[4u32];
1770        let reader = part
1771            .read(
1772                Arc::new(
1773                    BulkIterContext::new(
1774                        part.metadata.region_metadata.clone(),
1775                        Some(projection.as_slice()),
1776                        None,
1777                        false,
1778                        crate::sst::parquet::DEFAULT_READ_BATCH_SIZE,
1779                    )
1780                    .unwrap(),
1781                ),
1782                None,
1783                None,
1784            )
1785            .unwrap()
1786            .expect("expect at least one row group");
1787
1788        let mut total_rows_read = 0;
1789        let mut field: Vec<f64> = vec![];
1790        for res in reader {
1791            let batch = res.unwrap();
1792            assert_eq!(5, batch.num_columns());
1793            field.extend_from_slice(
1794                batch
1795                    .column(0)
1796                    .as_any()
1797                    .downcast_ref::<Float64Array>()
1798                    .unwrap()
1799                    .values(),
1800            );
1801            total_rows_read += batch.num_rows();
1802        }
1803        assert_eq!(3, total_rows_read);
1804        assert_eq!(vec![0.1, 0.2, 0.0], field);
1805    }
1806
1807    fn prepare(key_values: Vec<(&str, u32, (i64, i64), u64)>) -> EncodedBulkPart {
1808        let metadata = metadata_for_test();
1809        let kvs = key_values
1810            .into_iter()
1811            .map(|(k0, k1, (start, end), sequence)| {
1812                let ts = start..end;
1813                let v1 = (start..end).map(|_| None);
1814                build_key_values_with_ts_seq_values(&metadata, k0.to_string(), k1, ts, v1, sequence)
1815            })
1816            .collect::<Vec<_>>();
1817        let schema = to_flat_sst_arrow_schema(&metadata, &FlatSchemaOptions::default());
1818        let primary_key_codec = build_primary_key_codec(&metadata);
1819        let mut converter = BulkPartConverter::new(&metadata, schema, 64, primary_key_codec, true);
1820        for kv in kvs {
1821            converter.append_key_values(&kv).unwrap();
1822        }
1823        let part = converter.convert().unwrap();
1824        let encoder = BulkPartEncoder::new(metadata, 1024).unwrap();
1825        encoder.encode_part(&part).unwrap().unwrap()
1826    }
1827
1828    fn check_prune_row_group(
1829        part: &EncodedBulkPart,
1830        predicate: Option<Predicate>,
1831        expected_rows: usize,
1832    ) {
1833        let context = Arc::new(
1834            BulkIterContext::new(
1835                part.metadata.region_metadata.clone(),
1836                None,
1837                predicate,
1838                false,
1839                crate::sst::parquet::DEFAULT_READ_BATCH_SIZE,
1840            )
1841            .unwrap(),
1842        );
1843        let reader = part
1844            .read(context, None, None)
1845            .unwrap()
1846            .expect("expect at least one row group");
1847        let mut total_rows_read = 0;
1848        for res in reader {
1849            let batch = res.unwrap();
1850            total_rows_read += batch.num_rows();
1851        }
1852        // Should only read row group 1.
1853        assert_eq!(expected_rows, total_rows_read);
1854    }
1855
1856    #[test]
1857    fn test_prune_row_groups() {
1858        let part = prepare(vec![
1859            ("a", 0, (0, 40), 1),
1860            ("a", 1, (0, 60), 1),
1861            ("b", 0, (0, 100), 2),
1862            ("b", 1, (100, 180), 3),
1863            ("b", 1, (180, 210), 4),
1864        ]);
1865
1866        let context = Arc::new(
1867            BulkIterContext::new(
1868                part.metadata.region_metadata.clone(),
1869                None,
1870                Some(Predicate::new(vec![datafusion_expr::col("ts").eq(
1871                    datafusion_expr::lit(ScalarValue::TimestampMillisecond(Some(300), None)),
1872                )])),
1873                false,
1874                crate::sst::parquet::DEFAULT_READ_BATCH_SIZE,
1875            )
1876            .unwrap(),
1877        );
1878        assert!(part.read(context, None, None).unwrap().is_none());
1879
1880        check_prune_row_group(&part, None, 310);
1881
1882        check_prune_row_group(
1883            &part,
1884            Some(Predicate::new(vec![
1885                datafusion_expr::col("k0").eq(datafusion_expr::lit("a")),
1886                datafusion_expr::col("k1").eq(datafusion_expr::lit(0u32)),
1887            ])),
1888            40,
1889        );
1890
1891        check_prune_row_group(
1892            &part,
1893            Some(Predicate::new(vec![
1894                datafusion_expr::col("k0").eq(datafusion_expr::lit("a")),
1895                datafusion_expr::col("k1").eq(datafusion_expr::lit(1u32)),
1896            ])),
1897            60,
1898        );
1899
1900        check_prune_row_group(
1901            &part,
1902            Some(Predicate::new(vec![
1903                datafusion_expr::col("k0").eq(datafusion_expr::lit("a")),
1904            ])),
1905            100,
1906        );
1907
1908        check_prune_row_group(
1909            &part,
1910            Some(Predicate::new(vec![
1911                datafusion_expr::col("k0").eq(datafusion_expr::lit("b")),
1912                datafusion_expr::col("k1").eq(datafusion_expr::lit(0u32)),
1913            ])),
1914            100,
1915        );
1916
1917        // Predicates over field column can do precise filtering.
1918        check_prune_row_group(
1919            &part,
1920            Some(Predicate::new(vec![
1921                datafusion_expr::col("v0").eq(datafusion_expr::lit(150i64)),
1922            ])),
1923            1,
1924        );
1925    }
1926
1927    #[test]
1928    fn test_bulk_part_converter_append_and_convert() {
1929        let metadata = metadata_for_test();
1930        let capacity = 100;
1931        let primary_key_codec = build_primary_key_codec(&metadata);
1932        let schema = to_flat_sst_arrow_schema(
1933            &metadata,
1934            &FlatSchemaOptions::from_encoding(metadata.primary_key_encoding),
1935        );
1936
1937        let mut converter =
1938            BulkPartConverter::new(&metadata, schema, capacity, primary_key_codec, true);
1939
1940        let key_values1 = build_key_values_with_ts_seq_values(
1941            &metadata,
1942            "key1".to_string(),
1943            1u32,
1944            vec![1000, 2000].into_iter(),
1945            vec![Some(1.0), Some(2.0)].into_iter(),
1946            1,
1947        );
1948
1949        let key_values2 = build_key_values_with_ts_seq_values(
1950            &metadata,
1951            "key2".to_string(),
1952            2u32,
1953            vec![1500].into_iter(),
1954            vec![Some(3.0)].into_iter(),
1955            2,
1956        );
1957
1958        converter.append_key_values(&key_values1).unwrap();
1959        converter.append_key_values(&key_values2).unwrap();
1960
1961        let bulk_part = converter.convert().unwrap();
1962
1963        assert_eq!(bulk_part.num_rows(), 3);
1964        assert_eq!(bulk_part.min_timestamp, 1000);
1965        assert_eq!(bulk_part.max_timestamp, 2000);
1966        assert_eq!(bulk_part.sequence, 2);
1967        assert_eq!(bulk_part.timestamp_index, bulk_part.batch.num_columns() - 4);
1968
1969        // Validate primary key columns are stored
1970        // Schema should include primary key columns k0 and k1 at the beginning
1971        let schema = bulk_part.batch.schema();
1972        let field_names: Vec<&str> = schema.fields().iter().map(|f| f.name().as_str()).collect();
1973        assert_eq!(
1974            field_names,
1975            vec![
1976                "k0",
1977                "k1",
1978                "v0",
1979                "v1",
1980                "ts",
1981                "__primary_key",
1982                "__sequence",
1983                "__op_type"
1984            ]
1985        );
1986    }
1987
1988    #[test]
1989    fn test_bulk_part_converter_sorting() {
1990        let metadata = metadata_for_test();
1991        let capacity = 100;
1992        let primary_key_codec = build_primary_key_codec(&metadata);
1993        let schema = to_flat_sst_arrow_schema(
1994            &metadata,
1995            &FlatSchemaOptions::from_encoding(metadata.primary_key_encoding),
1996        );
1997
1998        let mut converter =
1999            BulkPartConverter::new(&metadata, schema, capacity, primary_key_codec, true);
2000
2001        let key_values1 = build_key_values_with_ts_seq_values(
2002            &metadata,
2003            "z_key".to_string(),
2004            3u32,
2005            vec![3000].into_iter(),
2006            vec![Some(3.0)].into_iter(),
2007            3,
2008        );
2009
2010        let key_values2 = build_key_values_with_ts_seq_values(
2011            &metadata,
2012            "a_key".to_string(),
2013            1u32,
2014            vec![1000].into_iter(),
2015            vec![Some(1.0)].into_iter(),
2016            1,
2017        );
2018
2019        let key_values3 = build_key_values_with_ts_seq_values(
2020            &metadata,
2021            "m_key".to_string(),
2022            2u32,
2023            vec![2000].into_iter(),
2024            vec![Some(2.0)].into_iter(),
2025            2,
2026        );
2027
2028        converter.append_key_values(&key_values1).unwrap();
2029        converter.append_key_values(&key_values2).unwrap();
2030        converter.append_key_values(&key_values3).unwrap();
2031
2032        let bulk_part = converter.convert().unwrap();
2033
2034        assert_eq!(bulk_part.num_rows(), 3);
2035
2036        let ts_column = bulk_part.batch.column(bulk_part.timestamp_index);
2037        let seq_column = bulk_part.batch.column(bulk_part.batch.num_columns() - 2);
2038
2039        let ts_array = ts_column
2040            .as_any()
2041            .downcast_ref::<TimestampMillisecondArray>()
2042            .unwrap();
2043        let seq_array = seq_column.as_any().downcast_ref::<UInt64Array>().unwrap();
2044
2045        assert_eq!(ts_array.values(), &[1000, 2000, 3000]);
2046        assert_eq!(seq_array.values(), &[1, 2, 3]);
2047
2048        // Validate primary key columns are stored
2049        let schema = bulk_part.batch.schema();
2050        let field_names: Vec<&str> = schema.fields().iter().map(|f| f.name().as_str()).collect();
2051        assert_eq!(
2052            field_names,
2053            vec![
2054                "k0",
2055                "k1",
2056                "v0",
2057                "v1",
2058                "ts",
2059                "__primary_key",
2060                "__sequence",
2061                "__op_type"
2062            ]
2063        );
2064    }
2065
2066    #[test]
2067    fn test_bulk_part_converter_empty() {
2068        let metadata = metadata_for_test();
2069        let capacity = 10;
2070        let primary_key_codec = build_primary_key_codec(&metadata);
2071        let schema = to_flat_sst_arrow_schema(
2072            &metadata,
2073            &FlatSchemaOptions::from_encoding(metadata.primary_key_encoding),
2074        );
2075
2076        let converter =
2077            BulkPartConverter::new(&metadata, schema, capacity, primary_key_codec, true);
2078
2079        let bulk_part = converter.convert().unwrap();
2080
2081        assert_eq!(bulk_part.num_rows(), 0);
2082        assert_eq!(bulk_part.min_timestamp, i64::MAX);
2083        assert_eq!(bulk_part.max_timestamp, i64::MIN);
2084        assert_eq!(bulk_part.sequence, SequenceNumber::MIN);
2085
2086        // Validate primary key columns are present in schema even for empty batch
2087        let schema = bulk_part.batch.schema();
2088        let field_names: Vec<&str> = schema.fields().iter().map(|f| f.name().as_str()).collect();
2089        assert_eq!(
2090            field_names,
2091            vec![
2092                "k0",
2093                "k1",
2094                "v0",
2095                "v1",
2096                "ts",
2097                "__primary_key",
2098                "__sequence",
2099                "__op_type"
2100            ]
2101        );
2102    }
2103
2104    #[test]
2105    fn test_bulk_part_converter_without_primary_key_columns() {
2106        let metadata = metadata_for_test();
2107        let primary_key_codec = build_primary_key_codec(&metadata);
2108        let schema = to_flat_sst_arrow_schema(
2109            &metadata,
2110            &FlatSchemaOptions {
2111                raw_pk_columns: false,
2112                string_pk_use_dict: true,
2113                ..Default::default()
2114            },
2115        );
2116
2117        let capacity = 100;
2118        let mut converter =
2119            BulkPartConverter::new(&metadata, schema, capacity, primary_key_codec, false);
2120
2121        let key_values1 = build_key_values_with_ts_seq_values(
2122            &metadata,
2123            "key1".to_string(),
2124            1u32,
2125            vec![1000, 2000].into_iter(),
2126            vec![Some(1.0), Some(2.0)].into_iter(),
2127            1,
2128        );
2129
2130        let key_values2 = build_key_values_with_ts_seq_values(
2131            &metadata,
2132            "key2".to_string(),
2133            2u32,
2134            vec![1500].into_iter(),
2135            vec![Some(3.0)].into_iter(),
2136            2,
2137        );
2138
2139        converter.append_key_values(&key_values1).unwrap();
2140        converter.append_key_values(&key_values2).unwrap();
2141
2142        let bulk_part = converter.convert().unwrap();
2143
2144        assert_eq!(bulk_part.num_rows(), 3);
2145        assert_eq!(bulk_part.min_timestamp, 1000);
2146        assert_eq!(bulk_part.max_timestamp, 2000);
2147        assert_eq!(bulk_part.sequence, 2);
2148        assert_eq!(bulk_part.timestamp_index, bulk_part.batch.num_columns() - 4);
2149
2150        // Validate primary key columns are NOT stored individually
2151        let schema = bulk_part.batch.schema();
2152        let field_names: Vec<&str> = schema.fields().iter().map(|f| f.name().as_str()).collect();
2153        assert_eq!(
2154            field_names,
2155            vec!["v0", "v1", "ts", "__primary_key", "__sequence", "__op_type"]
2156        );
2157    }
2158
2159    #[allow(clippy::too_many_arguments)]
2160    fn build_key_values_with_sparse_encoding(
2161        metadata: &RegionMetadataRef,
2162        primary_key_codec: &Arc<dyn PrimaryKeyCodec>,
2163        table_id: u32,
2164        tsid: u64,
2165        k0: String,
2166        k1: String,
2167        timestamps: impl Iterator<Item = i64>,
2168        values: impl Iterator<Item = Option<f64>>,
2169        sequence: SequenceNumber,
2170    ) -> KeyValues {
2171        // Encode the primary key (__table_id, __tsid, k0, k1) into binary format using the sparse codec
2172        let pk_values = vec![
2173            (ReservedColumnId::table_id(), Value::UInt32(table_id)),
2174            (ReservedColumnId::tsid(), Value::UInt64(tsid)),
2175            (0, Value::String(k0.clone().into())),
2176            (1, Value::String(k1.clone().into())),
2177        ];
2178        let mut encoded_key = Vec::new();
2179        primary_key_codec
2180            .encode_values(&pk_values, &mut encoded_key)
2181            .unwrap();
2182        assert!(!encoded_key.is_empty());
2183
2184        // Create schema for sparse encoding: __primary_key, ts, v0, v1
2185        let column_schema = vec![
2186            api::v1::ColumnSchema {
2187                column_name: PRIMARY_KEY_COLUMN_NAME.to_string(),
2188                datatype: api::helper::ColumnDataTypeWrapper::try_from(
2189                    ConcreteDataType::binary_datatype(),
2190                )
2191                .unwrap()
2192                .datatype() as i32,
2193                semantic_type: api::v1::SemanticType::Tag as i32,
2194                ..Default::default()
2195            },
2196            api::v1::ColumnSchema {
2197                column_name: "ts".to_string(),
2198                datatype: api::helper::ColumnDataTypeWrapper::try_from(
2199                    ConcreteDataType::timestamp_millisecond_datatype(),
2200                )
2201                .unwrap()
2202                .datatype() as i32,
2203                semantic_type: api::v1::SemanticType::Timestamp as i32,
2204                ..Default::default()
2205            },
2206            api::v1::ColumnSchema {
2207                column_name: "v0".to_string(),
2208                datatype: api::helper::ColumnDataTypeWrapper::try_from(
2209                    ConcreteDataType::int64_datatype(),
2210                )
2211                .unwrap()
2212                .datatype() as i32,
2213                semantic_type: api::v1::SemanticType::Field as i32,
2214                ..Default::default()
2215            },
2216            api::v1::ColumnSchema {
2217                column_name: "v1".to_string(),
2218                datatype: api::helper::ColumnDataTypeWrapper::try_from(
2219                    ConcreteDataType::float64_datatype(),
2220                )
2221                .unwrap()
2222                .datatype() as i32,
2223                semantic_type: api::v1::SemanticType::Field as i32,
2224                ..Default::default()
2225            },
2226        ];
2227
2228        let rows = timestamps
2229            .zip(values)
2230            .map(|(ts, v)| Row {
2231                values: vec![
2232                    api::v1::Value {
2233                        value_data: Some(api::v1::value::ValueData::BinaryValue(
2234                            encoded_key.clone(),
2235                        )),
2236                    },
2237                    api::v1::Value {
2238                        value_data: Some(api::v1::value::ValueData::TimestampMillisecondValue(ts)),
2239                    },
2240                    api::v1::Value {
2241                        value_data: Some(api::v1::value::ValueData::I64Value(ts)),
2242                    },
2243                    api::v1::Value {
2244                        value_data: v.map(api::v1::value::ValueData::F64Value),
2245                    },
2246                ],
2247            })
2248            .collect();
2249
2250        let mutation = api::v1::Mutation {
2251            op_type: 1,
2252            sequence,
2253            rows: Some(api::v1::Rows {
2254                schema: column_schema,
2255                rows,
2256            }),
2257            write_hint: Some(WriteHint {
2258                primary_key_encoding: api::v1::PrimaryKeyEncoding::Sparse.into(),
2259            }),
2260        };
2261        KeyValues::new(metadata.as_ref(), mutation).unwrap()
2262    }
2263
2264    #[test]
2265    fn test_bulk_part_converter_sparse_primary_key_encoding() {
2266        use api::v1::SemanticType;
2267        use datatypes::schema::ColumnSchema;
2268        use store_api::metadata::{ColumnMetadata, RegionMetadataBuilder};
2269        use store_api::storage::RegionId;
2270
2271        let mut builder = RegionMetadataBuilder::new(RegionId::new(123, 456));
2272        builder
2273            .push_column_metadata(ColumnMetadata {
2274                column_schema: ColumnSchema::new("k0", ConcreteDataType::string_datatype(), false),
2275                semantic_type: SemanticType::Tag,
2276                column_id: 0,
2277            })
2278            .push_column_metadata(ColumnMetadata {
2279                column_schema: ColumnSchema::new("k1", ConcreteDataType::string_datatype(), false),
2280                semantic_type: SemanticType::Tag,
2281                column_id: 1,
2282            })
2283            .push_column_metadata(ColumnMetadata {
2284                column_schema: ColumnSchema::new(
2285                    "ts",
2286                    ConcreteDataType::timestamp_millisecond_datatype(),
2287                    false,
2288                ),
2289                semantic_type: SemanticType::Timestamp,
2290                column_id: 2,
2291            })
2292            .push_column_metadata(ColumnMetadata {
2293                column_schema: ColumnSchema::new("v0", ConcreteDataType::int64_datatype(), true),
2294                semantic_type: SemanticType::Field,
2295                column_id: 3,
2296            })
2297            .push_column_metadata(ColumnMetadata {
2298                column_schema: ColumnSchema::new("v1", ConcreteDataType::float64_datatype(), true),
2299                semantic_type: SemanticType::Field,
2300                column_id: 4,
2301            })
2302            .primary_key(vec![0, 1])
2303            .primary_key_encoding(PrimaryKeyEncoding::Sparse);
2304        let metadata = Arc::new(builder.build().unwrap());
2305
2306        let primary_key_codec = build_primary_key_codec(&metadata);
2307        let schema = to_flat_sst_arrow_schema(
2308            &metadata,
2309            &FlatSchemaOptions::from_encoding(metadata.primary_key_encoding),
2310        );
2311
2312        assert_eq!(metadata.primary_key_encoding, PrimaryKeyEncoding::Sparse);
2313        assert_eq!(primary_key_codec.encoding(), PrimaryKeyEncoding::Sparse);
2314
2315        let capacity = 100;
2316        let mut converter =
2317            BulkPartConverter::new(&metadata, schema, capacity, primary_key_codec.clone(), true);
2318
2319        let key_values1 = build_key_values_with_sparse_encoding(
2320            &metadata,
2321            &primary_key_codec,
2322            2048u32, // table_id
2323            100u64,  // tsid
2324            "key11".to_string(),
2325            "key21".to_string(),
2326            vec![1000, 2000].into_iter(),
2327            vec![Some(1.0), Some(2.0)].into_iter(),
2328            1,
2329        );
2330
2331        let key_values2 = build_key_values_with_sparse_encoding(
2332            &metadata,
2333            &primary_key_codec,
2334            4096u32, // table_id
2335            200u64,  // tsid
2336            "key12".to_string(),
2337            "key22".to_string(),
2338            vec![1500].into_iter(),
2339            vec![Some(3.0)].into_iter(),
2340            2,
2341        );
2342
2343        converter.append_key_values(&key_values1).unwrap();
2344        converter.append_key_values(&key_values2).unwrap();
2345
2346        let bulk_part = converter.convert().unwrap();
2347
2348        assert_eq!(bulk_part.num_rows(), 3);
2349        assert_eq!(bulk_part.min_timestamp, 1000);
2350        assert_eq!(bulk_part.max_timestamp, 2000);
2351        assert_eq!(bulk_part.sequence, 2);
2352        assert_eq!(bulk_part.timestamp_index, bulk_part.batch.num_columns() - 4);
2353
2354        // For sparse encoding, primary key columns should NOT be stored individually
2355        // even when store_primary_key_columns is true, because sparse encoding
2356        // stores the encoded primary key in the __primary_key column
2357        let schema = bulk_part.batch.schema();
2358        let field_names: Vec<&str> = schema.fields().iter().map(|f| f.name().as_str()).collect();
2359        assert_eq!(
2360            field_names,
2361            vec!["v0", "v1", "ts", "__primary_key", "__sequence", "__op_type"]
2362        );
2363
2364        // Verify the __primary_key column contains encoded sparse keys
2365        let primary_key_column = bulk_part.batch.column_by_name("__primary_key").unwrap();
2366        let dict_array = primary_key_column
2367            .as_any()
2368            .downcast_ref::<DictionaryArray<UInt32Type>>()
2369            .unwrap();
2370
2371        // Should have non-zero entries indicating encoded primary keys
2372        assert!(!dict_array.is_empty());
2373        assert_eq!(dict_array.len(), 3); // 3 rows total
2374
2375        // Verify values are properly encoded binary data (not empty)
2376        let values = dict_array
2377            .values()
2378            .as_any()
2379            .downcast_ref::<BinaryArray>()
2380            .unwrap();
2381        for i in 0..values.len() {
2382            assert!(
2383                !values.value(i).is_empty(),
2384                "Encoded primary key should not be empty"
2385            );
2386        }
2387    }
2388
2389    #[test]
2390    fn test_convert_bulk_part_empty() {
2391        let metadata = metadata_for_test();
2392        let schema = to_flat_sst_arrow_schema(
2393            &metadata,
2394            &FlatSchemaOptions::from_encoding(metadata.primary_key_encoding),
2395        );
2396        let primary_key_codec = build_primary_key_codec(&metadata);
2397
2398        // Create empty batch
2399        let empty_batch = RecordBatch::new_empty(schema.clone());
2400        let empty_part = BulkPart {
2401            batch: empty_batch,
2402            max_timestamp: 0,
2403            min_timestamp: 0,
2404            sequence: 0,
2405            timestamp_index: 0,
2406            raw_data: None,
2407        };
2408
2409        let result =
2410            convert_bulk_part(empty_part, &metadata, primary_key_codec, schema, true).unwrap();
2411        assert!(result.is_none());
2412    }
2413
2414    #[test]
2415    fn test_convert_bulk_part_dense_with_pk_columns() {
2416        let metadata = metadata_for_test();
2417        let primary_key_codec = build_primary_key_codec(&metadata);
2418
2419        let k0_array = Arc::new(arrow::array::StringArray::from(vec![
2420            "key1", "key2", "key1",
2421        ]));
2422        let k1_array = Arc::new(arrow::array::UInt32Array::from(vec![1, 2, 1]));
2423        let v0_array = Arc::new(arrow::array::Int64Array::from(vec![100, 200, 300]));
2424        let v1_array = Arc::new(arrow::array::Float64Array::from(vec![1.0, 2.0, 3.0]));
2425        let ts_array = Arc::new(TimestampMillisecondArray::from(vec![1000, 2000, 1500]));
2426
2427        let input_schema = Arc::new(Schema::new(vec![
2428            Field::new("k0", ArrowDataType::Utf8, false),
2429            Field::new("k1", ArrowDataType::UInt32, false),
2430            Field::new("v0", ArrowDataType::Int64, true),
2431            Field::new("v1", ArrowDataType::Float64, true),
2432            Field::new(
2433                "ts",
2434                ArrowDataType::Timestamp(arrow::datatypes::TimeUnit::Millisecond, None),
2435                false,
2436            ),
2437        ]));
2438
2439        let input_batch = RecordBatch::try_new(
2440            input_schema,
2441            vec![k0_array, k1_array, v0_array, v1_array, ts_array],
2442        )
2443        .unwrap();
2444
2445        let part = BulkPart {
2446            batch: input_batch,
2447            max_timestamp: 2000,
2448            min_timestamp: 1000,
2449            sequence: 5,
2450            timestamp_index: 4,
2451            raw_data: None,
2452        };
2453
2454        let output_schema = to_flat_sst_arrow_schema(
2455            &metadata,
2456            &FlatSchemaOptions::from_encoding(metadata.primary_key_encoding),
2457        );
2458
2459        let result = convert_bulk_part(
2460            part,
2461            &metadata,
2462            primary_key_codec,
2463            output_schema,
2464            true, // store primary key columns
2465        )
2466        .unwrap();
2467
2468        let converted = result.unwrap();
2469
2470        assert_eq!(converted.num_rows(), 3);
2471        assert_eq!(converted.max_timestamp, 2000);
2472        assert_eq!(converted.min_timestamp, 1000);
2473        assert_eq!(converted.sequence, 5);
2474
2475        let schema = converted.batch.schema();
2476        let field_names: Vec<&str> = schema.fields().iter().map(|f| f.name().as_str()).collect();
2477        assert_eq!(
2478            field_names,
2479            vec![
2480                "k0",
2481                "k1",
2482                "v0",
2483                "v1",
2484                "ts",
2485                "__primary_key",
2486                "__sequence",
2487                "__op_type"
2488            ]
2489        );
2490
2491        let k0_col = converted.batch.column_by_name("k0").unwrap();
2492        assert!(matches!(
2493            k0_col.data_type(),
2494            ArrowDataType::Dictionary(_, _)
2495        ));
2496
2497        let pk_col = converted.batch.column_by_name("__primary_key").unwrap();
2498        let dict_array = pk_col
2499            .as_any()
2500            .downcast_ref::<DictionaryArray<UInt32Type>>()
2501            .unwrap();
2502        let keys = dict_array.keys();
2503
2504        assert_eq!(keys.len(), 3);
2505    }
2506
2507    #[test]
2508    fn test_convert_bulk_part_dense_without_pk_columns() {
2509        let metadata = metadata_for_test();
2510        let primary_key_codec = build_primary_key_codec(&metadata);
2511
2512        // Create input batch with primary key columns (k0, k1)
2513        let k0_array = Arc::new(arrow::array::StringArray::from(vec!["key1", "key2"]));
2514        let k1_array = Arc::new(arrow::array::UInt32Array::from(vec![1, 2]));
2515        let v0_array = Arc::new(arrow::array::Int64Array::from(vec![100, 200]));
2516        let v1_array = Arc::new(arrow::array::Float64Array::from(vec![1.0, 2.0]));
2517        let ts_array = Arc::new(TimestampMillisecondArray::from(vec![1000, 2000]));
2518
2519        let input_schema = Arc::new(Schema::new(vec![
2520            Field::new("k0", ArrowDataType::Utf8, false),
2521            Field::new("k1", ArrowDataType::UInt32, false),
2522            Field::new("v0", ArrowDataType::Int64, true),
2523            Field::new("v1", ArrowDataType::Float64, true),
2524            Field::new(
2525                "ts",
2526                ArrowDataType::Timestamp(arrow::datatypes::TimeUnit::Millisecond, None),
2527                false,
2528            ),
2529        ]));
2530
2531        let input_batch = RecordBatch::try_new(
2532            input_schema,
2533            vec![k0_array, k1_array, v0_array, v1_array, ts_array],
2534        )
2535        .unwrap();
2536
2537        let part = BulkPart {
2538            batch: input_batch,
2539            max_timestamp: 2000,
2540            min_timestamp: 1000,
2541            sequence: 3,
2542            timestamp_index: 4,
2543            raw_data: None,
2544        };
2545
2546        let output_schema = to_flat_sst_arrow_schema(
2547            &metadata,
2548            &FlatSchemaOptions {
2549                raw_pk_columns: false,
2550                string_pk_use_dict: true,
2551                ..Default::default()
2552            },
2553        );
2554
2555        let result = convert_bulk_part(
2556            part,
2557            &metadata,
2558            primary_key_codec,
2559            output_schema,
2560            false, // don't store primary key columns
2561        )
2562        .unwrap();
2563
2564        let converted = result.unwrap();
2565
2566        assert_eq!(converted.num_rows(), 2);
2567        assert_eq!(converted.max_timestamp, 2000);
2568        assert_eq!(converted.min_timestamp, 1000);
2569        assert_eq!(converted.sequence, 3);
2570
2571        // Verify schema does NOT include individual primary key columns
2572        let schema = converted.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        // Verify __primary_key column is present and is a dictionary
2580        let pk_col = converted.batch.column_by_name("__primary_key").unwrap();
2581        assert!(matches!(
2582            pk_col.data_type(),
2583            ArrowDataType::Dictionary(_, _)
2584        ));
2585    }
2586
2587    #[test]
2588    fn test_convert_bulk_part_sparse_encoding() {
2589        let mut builder = RegionMetadataBuilder::new(RegionId::new(123, 456));
2590        builder
2591            .push_column_metadata(ColumnMetadata {
2592                column_schema: ColumnSchema::new("k0", ConcreteDataType::string_datatype(), false),
2593                semantic_type: SemanticType::Tag,
2594                column_id: 0,
2595            })
2596            .push_column_metadata(ColumnMetadata {
2597                column_schema: ColumnSchema::new("k1", ConcreteDataType::string_datatype(), false),
2598                semantic_type: SemanticType::Tag,
2599                column_id: 1,
2600            })
2601            .push_column_metadata(ColumnMetadata {
2602                column_schema: ColumnSchema::new(
2603                    "ts",
2604                    ConcreteDataType::timestamp_millisecond_datatype(),
2605                    false,
2606                ),
2607                semantic_type: SemanticType::Timestamp,
2608                column_id: 2,
2609            })
2610            .push_column_metadata(ColumnMetadata {
2611                column_schema: ColumnSchema::new("v0", ConcreteDataType::int64_datatype(), true),
2612                semantic_type: SemanticType::Field,
2613                column_id: 3,
2614            })
2615            .push_column_metadata(ColumnMetadata {
2616                column_schema: ColumnSchema::new("v1", ConcreteDataType::float64_datatype(), true),
2617                semantic_type: SemanticType::Field,
2618                column_id: 4,
2619            })
2620            .primary_key(vec![0, 1])
2621            .primary_key_encoding(PrimaryKeyEncoding::Sparse);
2622        let metadata = Arc::new(builder.build().unwrap());
2623
2624        let primary_key_codec = build_primary_key_codec(&metadata);
2625
2626        // Create input batch with __primary_key column (sparse encoding)
2627        let pk_array = Arc::new(arrow::array::BinaryArray::from(vec![
2628            b"encoded_key_1".as_slice(),
2629            b"encoded_key_2".as_slice(),
2630        ]));
2631        let v0_array = Arc::new(arrow::array::Int64Array::from(vec![100, 200]));
2632        let v1_array = Arc::new(arrow::array::Float64Array::from(vec![1.0, 2.0]));
2633        let ts_array = Arc::new(TimestampMillisecondArray::from(vec![1000, 2000]));
2634
2635        let input_schema = Arc::new(Schema::new(vec![
2636            Field::new("__primary_key", ArrowDataType::Binary, false),
2637            Field::new("v0", ArrowDataType::Int64, true),
2638            Field::new("v1", ArrowDataType::Float64, true),
2639            Field::new(
2640                "ts",
2641                ArrowDataType::Timestamp(arrow::datatypes::TimeUnit::Millisecond, None),
2642                false,
2643            ),
2644        ]));
2645
2646        let input_batch =
2647            RecordBatch::try_new(input_schema, vec![pk_array, v0_array, v1_array, ts_array])
2648                .unwrap();
2649
2650        let part = BulkPart {
2651            batch: input_batch,
2652            max_timestamp: 2000,
2653            min_timestamp: 1000,
2654            sequence: 7,
2655            timestamp_index: 3,
2656            raw_data: None,
2657        };
2658
2659        let output_schema = to_flat_sst_arrow_schema(
2660            &metadata,
2661            &FlatSchemaOptions::from_encoding(metadata.primary_key_encoding),
2662        );
2663
2664        let result = convert_bulk_part(
2665            part,
2666            &metadata,
2667            primary_key_codec,
2668            output_schema,
2669            true, // store_primary_key_columns (ignored for sparse)
2670        )
2671        .unwrap();
2672
2673        let converted = result.unwrap();
2674
2675        assert_eq!(converted.num_rows(), 2);
2676        assert_eq!(converted.max_timestamp, 2000);
2677        assert_eq!(converted.min_timestamp, 1000);
2678        assert_eq!(converted.sequence, 7);
2679
2680        // Verify schema does NOT include individual primary key columns (sparse encoding)
2681        let schema = converted.batch.schema();
2682        let field_names: Vec<&str> = schema.fields().iter().map(|f| f.name().as_str()).collect();
2683        assert_eq!(
2684            field_names,
2685            vec!["v0", "v1", "ts", "__primary_key", "__sequence", "__op_type"]
2686        );
2687
2688        // Verify __primary_key is dictionary encoded
2689        let pk_col = converted.batch.column_by_name("__primary_key").unwrap();
2690        assert!(matches!(
2691            pk_col.data_type(),
2692            ArrowDataType::Dictionary(_, _)
2693        ));
2694    }
2695
2696    #[test]
2697    fn test_convert_bulk_part_sorting_with_multiple_series() {
2698        let metadata = metadata_for_test();
2699        let primary_key_codec = build_primary_key_codec(&metadata);
2700
2701        // Create unsorted batch with multiple series and timestamps
2702        let k0_array = Arc::new(arrow::array::StringArray::from(vec![
2703            "series_b", "series_a", "series_b", "series_a",
2704        ]));
2705        let k1_array = Arc::new(arrow::array::UInt32Array::from(vec![2, 1, 2, 1]));
2706        let v0_array = Arc::new(arrow::array::Int64Array::from(vec![200, 100, 400, 300]));
2707        let v1_array = Arc::new(arrow::array::Float64Array::from(vec![2.0, 1.0, 4.0, 3.0]));
2708        let ts_array = Arc::new(TimestampMillisecondArray::from(vec![
2709            2000, 1000, 4000, 3000,
2710        ]));
2711
2712        let input_schema = Arc::new(Schema::new(vec![
2713            Field::new("k0", ArrowDataType::Utf8, false),
2714            Field::new("k1", ArrowDataType::UInt32, false),
2715            Field::new("v0", ArrowDataType::Int64, true),
2716            Field::new("v1", ArrowDataType::Float64, true),
2717            Field::new(
2718                "ts",
2719                ArrowDataType::Timestamp(arrow::datatypes::TimeUnit::Millisecond, None),
2720                false,
2721            ),
2722        ]));
2723
2724        let input_batch = RecordBatch::try_new(
2725            input_schema,
2726            vec![k0_array, k1_array, v0_array, v1_array, ts_array],
2727        )
2728        .unwrap();
2729
2730        let part = BulkPart {
2731            batch: input_batch,
2732            max_timestamp: 4000,
2733            min_timestamp: 1000,
2734            sequence: 10,
2735            timestamp_index: 4,
2736            raw_data: None,
2737        };
2738
2739        let output_schema = to_flat_sst_arrow_schema(
2740            &metadata,
2741            &FlatSchemaOptions::from_encoding(metadata.primary_key_encoding),
2742        );
2743
2744        let result =
2745            convert_bulk_part(part, &metadata, primary_key_codec, output_schema, true).unwrap();
2746
2747        let converted = result.unwrap();
2748
2749        assert_eq!(converted.num_rows(), 4);
2750
2751        // Verify data is sorted by (primary_key, timestamp, sequence desc)
2752        let ts_col = converted.batch.column(converted.timestamp_index);
2753        let ts_array = ts_col
2754            .as_any()
2755            .downcast_ref::<TimestampMillisecondArray>()
2756            .unwrap();
2757
2758        // After sorting by (pk, ts), we should have:
2759        // series_a,1: ts=1000, 3000
2760        // series_b,2: ts=2000, 4000
2761        let timestamps: Vec<i64> = ts_array.values().to_vec();
2762        assert_eq!(timestamps, vec![1000, 3000, 2000, 4000]);
2763    }
2764
2765    /// Helper to create a converted BulkPart (with __primary_key column) from MutationInputs.
2766    fn build_converted_bulk_part(inputs: &[MutationInput]) -> BulkPart {
2767        let metadata = metadata_for_test();
2768        let kvs = inputs
2769            .iter()
2770            .map(|m| {
2771                build_key_values_with_ts_seq_values(
2772                    &metadata,
2773                    m.k0.to_string(),
2774                    m.k1,
2775                    m.timestamps.iter().copied(),
2776                    m.v1.iter().copied(),
2777                    m.sequence,
2778                )
2779            })
2780            .collect::<Vec<_>>();
2781        let schema = to_flat_sst_arrow_schema(&metadata, &FlatSchemaOptions::default());
2782        let primary_key_codec = build_primary_key_codec(&metadata);
2783        let mut converter = BulkPartConverter::new(&metadata, schema, 64, primary_key_codec, true);
2784        for kv in kvs {
2785            converter.append_key_values(&kv).unwrap();
2786        }
2787        converter.convert().unwrap()
2788    }
2789
2790    /// Helper to create a MultiBulkPart where each group becomes a separate batch.
2791    fn build_multi_bulk_part(groups: &[&[MutationInput]]) -> (MultiBulkPart, RegionMetadataRef) {
2792        let metadata = metadata_for_test();
2793        let mut all_batches = Vec::new();
2794        let mut min_ts = i64::MAX;
2795        let mut max_ts = i64::MIN;
2796        let mut max_seq = 0u64;
2797
2798        for inputs in groups {
2799            let part = build_converted_bulk_part(inputs);
2800            min_ts = min_ts.min(part.min_timestamp);
2801            max_ts = max_ts.max(part.max_timestamp);
2802            max_seq = max_seq.max(part.sequence);
2803            all_batches.push(part.batch);
2804        }
2805
2806        let multi = MultiBulkPart::new(
2807            all_batches,
2808            min_ts,
2809            max_ts,
2810            max_seq,
2811            groups.len(),
2812            &metadata,
2813        );
2814        (multi, metadata)
2815    }
2816
2817    #[test]
2818    fn test_multi_bulk_part_prune_batches() {
2819        // Three batches with distinct k0 ranges: ["a"], ["m"], ["z"].
2820        let (multi, metadata) = build_multi_bulk_part(&[
2821            &[MutationInput {
2822                k0: "a",
2823                k1: 0,
2824                timestamps: &[1, 2],
2825                v1: &[Some(1.0), Some(2.0)],
2826                sequence: 0,
2827            }],
2828            &[MutationInput {
2829                k0: "m",
2830                k1: 0,
2831                timestamps: &[3, 4],
2832                v1: &[Some(3.0), Some(4.0)],
2833                sequence: 1,
2834            }],
2835            &[MutationInput {
2836                k0: "z",
2837                k1: 0,
2838                timestamps: &[5, 6],
2839                v1: &[Some(5.0), Some(6.0)],
2840                sequence: 2,
2841            }],
2842        ]);
2843        assert_eq!(multi.num_rows(), 6);
2844        assert_eq!(multi.num_batches(), 3);
2845
2846        // k0 = "m" => only middle batch (2 rows).
2847        let context = Arc::new(
2848            BulkIterContext::new(
2849                metadata.clone(),
2850                None,
2851                Some(Predicate::new(vec![
2852                    datafusion_expr::col("k0").eq(datafusion_expr::lit("m")),
2853                ])),
2854                false,
2855                crate::sst::parquet::DEFAULT_READ_BATCH_SIZE,
2856            )
2857            .unwrap(),
2858        );
2859        let reader = multi
2860            .read(context, None, None)
2861            .unwrap()
2862            .expect("should have results");
2863        let total_rows: usize = reader.map(|r| r.unwrap().num_rows()).sum();
2864        assert_eq!(total_rows, 2);
2865
2866        // k0 = "nonexistent" => all pruned, returns None.
2867        let context = Arc::new(
2868            BulkIterContext::new(
2869                metadata.clone(),
2870                None,
2871                Some(Predicate::new(vec![
2872                    datafusion_expr::col("k0").eq(datafusion_expr::lit("nonexistent")),
2873                ])),
2874                false,
2875                crate::sst::parquet::DEFAULT_READ_BATCH_SIZE,
2876            )
2877            .unwrap(),
2878        );
2879        assert!(multi.read(context, None, None).unwrap().is_none());
2880
2881        // No predicate => all 6 rows.
2882        let context = Arc::new(
2883            BulkIterContext::new(
2884                metadata.clone(),
2885                None,
2886                None,
2887                false,
2888                crate::sst::parquet::DEFAULT_READ_BATCH_SIZE,
2889            )
2890            .unwrap(),
2891        );
2892        let reader = multi
2893            .read(context, None, None)
2894            .unwrap()
2895            .expect("should have results");
2896        let total_rows: usize = reader.map(|r| r.unwrap().num_rows()).sum();
2897        assert_eq!(total_rows, 6);
2898    }
2899}