Skip to main content

mito2/memtable/bulk/
part.rs

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