Skip to main content

mito2/
sst.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//! Sorted strings tables.
16
17use std::collections::HashMap;
18use std::sync::Arc;
19
20use api::v1::SemanticType;
21use arrow_schema::DataType;
22use arrow_schema::extension::{EXTENSION_TYPE_NAME_KEY, ExtensionType};
23use common_base::readable_size::ReadableSize;
24use common_query::native_histogram::{
25    is_native_histogram_value_type, native_histogram_list_element_id, native_histogram_subfield_id,
26};
27use datatypes::arrow::datatypes::{
28    DataType as ArrowDataType, Field, FieldRef, Fields, Schema, SchemaRef,
29};
30use datatypes::arrow::record_batch::RecordBatch;
31use datatypes::extension::histogram::HistogramExtensionType;
32use datatypes::prelude::ConcreteDataType;
33use datatypes::timestamp::timestamp_array_to_primitive;
34use serde::{Deserialize, Serialize};
35use store_api::codec::PrimaryKeyEncoding;
36use store_api::metadata::RegionMetadata;
37use store_api::storage::consts::{
38    OP_TYPE_COLUMN_NAME, PRIMARY_KEY_COLUMN_NAME, SEQUENCE_COLUMN_NAME,
39};
40
41use crate::error::{InvalidNativeHistogramFieldIdSnafu, InvalidNativeHistogramSubfieldSnafu};
42use crate::sst::parquet::flat_format::time_index_column_index;
43
44pub mod file;
45pub mod file_purger;
46pub mod file_ref;
47pub mod index;
48pub mod location;
49pub mod parquet;
50pub mod range_index;
51pub(crate) mod version;
52
53/// Default write buffer size, it should be greater than the default minimum upload part of S3 (5mb).
54pub const DEFAULT_WRITE_BUFFER_SIZE: ReadableSize = ReadableSize::mb(8);
55
56/// Default number of concurrent write, it only works on object store backend(e.g., S3).
57pub const DEFAULT_WRITE_CONCURRENCY: usize = 8;
58
59/// Format type of the SST file.
60#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, strum::EnumString)]
61#[serde(rename_all = "snake_case")]
62#[strum(serialize_all = "snake_case")]
63pub enum FormatType {
64    /// Parquet with primary key encoded.
65    #[default]
66    PrimaryKey,
67    /// Flat Parquet format.
68    Flat,
69}
70
71/// Iceberg-compatible column field ID key stored in Parquet column metadata.
72pub const PARQUET_FIELD_ID_KEY: &str = "PARQUET:field_id";
73
74/// Adds `PARQUET:field_id` metadata to a top-level Arrow field.
75///
76/// Native-histogram sub-field ids are stamped separately at parquet-write
77/// time by [`stamp_native_histogram_subfield_ids`], not here.
78pub fn with_field_id(mut field: Field, column_id: u32) -> Field {
79    field
80        .metadata_mut()
81        .insert(PARQUET_FIELD_ID_KEY.to_string(), column_id.to_string());
82    field
83}
84
85/// Stamps the `greptime.histogram` extension and reserved `PARQUET:field_id`s
86/// onto a native-histogram struct field (and its sub-fields / list element
87/// fields), so external readers can identify it by extension and resolve
88/// nested fields by id.
89///
90/// Detection is by the exact native-histogram struct type
91/// (`is_native_histogram_value_type`); other struct columns are left untouched.
92/// mito2 reads SST columns by schema position, never by field metadata, so this
93/// only affects external readers.
94///
95/// Returns an error if the parent column's `PARQUET:field_id` is missing,
96/// malformed, or exceeds `i32::MAX`, or if a sub-field id cannot be derived
97/// — because the sub-field name is not a known native-histogram field, or
98/// the derived id overflows a positive `i32` (an absurdly large parent
99/// `column_id`); see [`native_histogram_subfield_id`].
100fn stamp_native_histogram_subfield_ids(field: &mut Field) -> crate::error::Result<()> {
101    if !is_native_histogram_value_type(&ConcreteDataType::from_arrow_type(field.data_type())) {
102        return Ok(());
103    }
104    // Namespace sub-field ids by the parent column's field id (its
105    // `PARQUET:field_id`, stamped earlier by `with_field_id`) so several
106    // histogram columns in one table get disjoint ids. Fail loudly if the id
107    // is absent, malformed, or too large to fit a positive `i32`.
108    let column_id = field
109        .metadata()
110        .get(PARQUET_FIELD_ID_KEY)
111        .and_then(|s| s.parse::<i32>().ok())
112        .ok_or_else(|| {
113            InvalidNativeHistogramFieldIdSnafu {
114                field_name: field.name().clone(),
115            }
116            .build()
117        })?;
118    // Tag the field with the greptime.histogram extension.
119    field.metadata_mut().insert(
120        EXTENSION_TYPE_NAME_KEY.to_string(),
121        HistogramExtensionType::NAME.to_string(),
122    );
123    let ArrowDataType::Struct(children) = field.data_type() else {
124        return Ok(());
125    };
126    let new_children: crate::error::Result<Fields> = children
127        .iter()
128        .map(|child| {
129            let mut c = (**child).clone();
130            // `None` here means either the sub-field name is not a known
131            // native-histogram field, or the derived id overflowed i32.
132            // Surface it as an error rather than silently leaving the field
133            // without an id.
134            let id = native_histogram_subfield_id(column_id, c.name()).ok_or_else(|| {
135                InvalidNativeHistogramSubfieldSnafu {
136                    column_id,
137                    field_name: c.name().clone(),
138                }
139                .build()
140            })?;
141            // Stamp the sub-field's own id.
142            c.metadata_mut()
143                .insert(PARQUET_FIELD_ID_KEY.to_string(), id.to_string());
144            // If the sub-field is a list, stamp its element field's id.
145            if let ArrowDataType::List(elem) = c.data_type() {
146                let elem_id =
147                    native_histogram_list_element_id(column_id, c.name()).ok_or_else(|| {
148                        InvalidNativeHistogramSubfieldSnafu {
149                            column_id,
150                            field_name: c.name().clone(),
151                        }
152                        .build()
153                    })?;
154                let mut new_elem = (**elem).clone();
155                new_elem
156                    .metadata_mut()
157                    .insert(PARQUET_FIELD_ID_KEY.to_string(), elem_id.to_string());
158                c.set_data_type(ArrowDataType::List(Arc::new(new_elem)));
159            }
160            Ok(Arc::new(c))
161        })
162        .collect();
163    field.set_data_type(ArrowDataType::Struct(new_children?));
164    Ok(())
165}
166
167/// Returns a copy of `schema` with native-histogram sub-field ids stamped,
168/// for the parquet writer.
169///
170/// This is called on the schema handed to `AsyncArrowWriter`, not in
171/// [`with_field_id`], because the SST arrow schema is also the memtable's
172/// in-memory schema, whose `Struct` equality (`PartialEq`) is
173/// metadata-sensitive — stamping there would break writes. The parquet writer
174/// compares types with `DataType::equals_datatype`, which ignores field
175/// metadata, so a stamped schema accepts an unstamped batch.
176pub fn maybe_wrap_schema(schema: &SchemaRef) -> crate::error::Result<SchemaRef> {
177    // Fast path: only a struct column can be a native histogram; if there are
178    // none, skip the rebuild.
179    if !schema
180        .fields()
181        .iter()
182        .any(|f| matches!(f.data_type(), ArrowDataType::Struct(_)))
183    {
184        return Ok(schema.clone());
185    }
186    let new_fields: crate::error::Result<Vec<FieldRef>> = schema
187        .fields()
188        .iter()
189        .map(|f| {
190            let mut field = (**f).clone();
191            stamp_native_histogram_subfield_ids(&mut field)?;
192            Ok(Arc::new(field))
193        })
194        .collect();
195    Ok(Arc::new(Schema::new_with_metadata(
196        Fields::from(new_fields?),
197        schema.metadata().clone(),
198    )))
199}
200
201/// Parquet field ID base for internal columns (__primary_key, __sequence, __op_type).
202/// Uses bit 30 to distinguish from user column IDs and fit in positive i32 range.
203pub(crate) const INTERNAL_PARQUET_FIELD_ID_BASE: u32 = 1 << 30;
204
205/// Parquet field ID for the __primary_key column.
206pub(crate) const PRIMARY_KEY_PARQUET_FIELD_ID: u32 = INTERNAL_PARQUET_FIELD_ID_BASE;
207/// Parquet field ID for the __sequence column.
208pub(crate) const SEQUENCE_PARQUET_FIELD_ID: u32 = INTERNAL_PARQUET_FIELD_ID_BASE + 1;
209/// Parquet field ID for the __op_type column.
210pub(crate) const OP_TYPE_PARQUET_FIELD_ID: u32 = INTERNAL_PARQUET_FIELD_ID_BASE + 2;
211
212/// Gets the arrow schema to store in parquet.
213pub fn to_sst_arrow_schema(metadata: &RegionMetadata) -> SchemaRef {
214    let fields = Fields::from_iter(
215        metadata
216            .schema
217            .arrow_schema()
218            .fields()
219            .iter()
220            .zip(&metadata.column_metadatas)
221            .filter_map(|(field, column_meta)| {
222                if column_meta.semantic_type == SemanticType::Field {
223                    Some(Arc::new(with_field_id(
224                        (**field).clone(),
225                        column_meta.column_id,
226                    )))
227                } else {
228                    // We have fixed positions for tags (primary key) and time index.
229                    None
230                }
231            })
232            .chain([Arc::new(with_field_id(
233                (*metadata.time_index_field()).clone(),
234                metadata.time_index_column().column_id,
235            ))])
236            .chain(internal_fields()),
237    );
238
239    Arc::new(Schema::new(fields))
240}
241
242/// Options of flat schema.
243pub struct FlatSchemaOptions {
244    /// Whether to store primary key columns additionally instead of an encoded column.
245    pub raw_pk_columns: bool,
246    /// Whether to use dictionary encoding for string primary key columns
247    /// when storing primary key columns.
248    /// Only takes effect when `raw_pk_columns` is true.
249    pub string_pk_use_dict: bool,
250    /// The column's concretized JSON types, to be set into Arrow schema.
251    /// Otherwise it's empty struct in the Arrow schema.
252    pub concretized_json_types: HashMap<String, DataType>,
253}
254
255impl Default for FlatSchemaOptions {
256    fn default() -> Self {
257        Self {
258            raw_pk_columns: true,
259            string_pk_use_dict: true,
260            concretized_json_types: HashMap::new(),
261        }
262    }
263}
264
265impl FlatSchemaOptions {
266    /// Creates a options according to the primary key encoding.
267    pub fn from_encoding(encoding: PrimaryKeyEncoding) -> Self {
268        if encoding == PrimaryKeyEncoding::Dense {
269            Self::default()
270        } else {
271            Self {
272                raw_pk_columns: false,
273                string_pk_use_dict: false,
274                concretized_json_types: HashMap::new(),
275            }
276        }
277    }
278}
279
280/// Gets the arrow schema to store in parquet.
281///
282/// The schema is:
283/// ```text
284/// primary key columns, field columns, time index, __primary_key, __sequence, __op_type
285/// ```
286///
287/// # Panics
288/// Panics if the metadata is invalid.
289pub fn to_flat_sst_arrow_schema(
290    metadata: &RegionMetadata,
291    options: &FlatSchemaOptions,
292) -> SchemaRef {
293    let num_fields = flat_sst_arrow_schema_column_num(metadata, options);
294    let mut fields = Vec::with_capacity(num_fields);
295    let schema = metadata.schema.arrow_schema();
296    if options.raw_pk_columns {
297        for pk_id in &metadata.primary_key {
298            let pk_index = metadata.column_index_by_id(*pk_id).unwrap();
299            let column_id = metadata.column_metadatas[pk_index].column_id;
300            if options.string_pk_use_dict {
301                let old_field = &schema.fields[pk_index];
302                let new_field = tag_maybe_to_dictionary_field(
303                    &metadata.column_metadatas[pk_index].column_schema.data_type,
304                    old_field,
305                );
306                let new_field = concretize_json_type(new_field, options);
307                fields.push(Arc::new(with_field_id((*new_field).clone(), column_id)));
308            }
309        }
310    }
311    let remaining_fields = schema
312        .fields()
313        .iter()
314        .zip(&metadata.column_metadatas)
315        .filter_map(|(field, column_meta)| {
316            if column_meta.semantic_type == SemanticType::Field {
317                let field = concretize_json_type(field.clone(), options);
318                Some(Arc::new(with_field_id(
319                    Arc::unwrap_or_clone(field),
320                    column_meta.column_id,
321                )))
322            } else {
323                None
324            }
325        })
326        .chain([Arc::new(with_field_id(
327            (*metadata.time_index_field()).clone(),
328            metadata.time_index_column().column_id,
329        ))])
330        .chain(internal_fields());
331    for field in remaining_fields {
332        fields.push(field);
333    }
334
335    Arc::new(Schema::new(fields))
336}
337
338fn concretize_json_type(field: Arc<Field>, options: &FlatSchemaOptions) -> Arc<Field> {
339    if let Some(data_type) = options.concretized_json_types.get(field.name()) {
340        let mut field = Arc::unwrap_or_clone(field);
341        field.set_data_type(data_type.clone());
342        Arc::new(field)
343    } else {
344        field
345    }
346}
347
348/// Returns the number of columns in the flat format.
349pub fn flat_sst_arrow_schema_column_num(
350    metadata: &RegionMetadata,
351    options: &FlatSchemaOptions,
352) -> usize {
353    if options.raw_pk_columns {
354        metadata.column_metadatas.len() + 3
355    } else {
356        metadata.column_metadatas.len() + 3 - metadata.primary_key.len()
357    }
358}
359
360/// Helper function to create a dictionary field from a field.
361fn to_dictionary_field(field: &Field) -> Field {
362    let mut new_field = Field::new_dictionary(
363        field.name(),
364        datatypes::arrow::datatypes::DataType::UInt32,
365        field.data_type().clone(),
366        field.is_nullable(),
367    );
368
369    // retain field_id metadata
370    if let Some(field_id) = field.metadata().get(PARQUET_FIELD_ID_KEY) {
371        new_field
372            .metadata_mut()
373            .insert(PARQUET_FIELD_ID_KEY.to_string(), field_id.clone());
374    }
375
376    new_field
377}
378
379/// Helper function to create a dictionary field from a field if it is a string column.
380pub(crate) fn tag_maybe_to_dictionary_field(
381    data_type: &ConcreteDataType,
382    field: &Arc<Field>,
383) -> Arc<Field> {
384    if data_type.is_string() {
385        Arc::new(to_dictionary_field(field))
386    } else {
387        field.clone()
388    }
389}
390
391/// Fields for internal columns.
392pub(crate) fn internal_fields() -> [FieldRef; 3] {
393    // Internal columns are always not null.
394    [
395        Arc::new(with_field_id(
396            Field::new_dictionary(
397                PRIMARY_KEY_COLUMN_NAME,
398                ArrowDataType::UInt32,
399                ArrowDataType::Binary,
400                false,
401            ),
402            PRIMARY_KEY_PARQUET_FIELD_ID,
403        )),
404        Arc::new(with_field_id(
405            Field::new(SEQUENCE_COLUMN_NAME, ArrowDataType::UInt64, false),
406            SEQUENCE_PARQUET_FIELD_ID,
407        )),
408        Arc::new(with_field_id(
409            Field::new(OP_TYPE_COLUMN_NAME, ArrowDataType::UInt8, false),
410            OP_TYPE_PARQUET_FIELD_ID,
411        )),
412    ]
413}
414
415/// Returns a copy of `schema` with the `__primary_key` field replaced by a plain `Binary` field.
416pub(crate) fn override_pk_field_to_binary(schema: &SchemaRef) -> SchemaRef {
417    let new_fields = schema
418        .fields()
419        .iter()
420        .map(|field| {
421            if field.name() == PRIMARY_KEY_COLUMN_NAME {
422                let mut new_field = Field::new(
423                    PRIMARY_KEY_COLUMN_NAME,
424                    ArrowDataType::Binary,
425                    field.is_nullable(),
426                );
427                // Preserve the field_id metadata so parquet readers that require
428                // all columns to carry a field_id don't fail.
429                if let Some(field_id) = field.metadata().get(PARQUET_FIELD_ID_KEY) {
430                    new_field
431                        .metadata_mut()
432                        .insert(PARQUET_FIELD_ID_KEY.to_string(), field_id.clone());
433                }
434                Arc::new(new_field)
435            } else {
436                field.clone()
437            }
438        })
439        .collect::<Vec<_>>();
440    Arc::new(Schema::new(new_fields))
441}
442
443/// Gets the estimated number of series from record batches.
444///
445/// This struct tracks the last timestamp value to detect series boundaries
446/// by observing when timestamps decrease (indicating a new series).
447#[derive(Default)]
448pub(crate) struct SeriesEstimator {
449    /// The last timestamp value seen
450    last_timestamp: Option<i64>,
451    /// The estimated number of series
452    series_count: u64,
453}
454
455impl SeriesEstimator {
456    /// Updates the estimator with a new record batch in flat format.
457    ///
458    /// This method examines the time index column to detect series boundaries.
459    pub(crate) fn update_flat(&mut self, record_batch: &RecordBatch) {
460        let batch_rows = record_batch.num_rows();
461        if batch_rows == 0 {
462            return;
463        }
464
465        let time_index_pos = time_index_column_index(record_batch.num_columns());
466        let timestamps = record_batch.column(time_index_pos);
467        let Some((ts_values, _unit)) = timestamp_array_to_primitive(timestamps) else {
468            return;
469        };
470        let values = ts_values.values();
471
472        // Checks if there's a boundary between the last batch and this batch
473        if let Some(last_ts) = self.last_timestamp {
474            if values[0] <= last_ts {
475                self.series_count += 1;
476            }
477        } else {
478            // First batch, counts as first series
479            self.series_count = 1;
480        }
481
482        // Counts series boundaries within this batch.
483        for i in 0..batch_rows - 1 {
484            // We assumes the same timestamp as a new series, which is different from
485            // how we split batches.
486            if values[i] >= values[i + 1] {
487                self.series_count += 1;
488            }
489        }
490
491        // Updates the last timestamp
492        self.last_timestamp = Some(values[batch_rows - 1]);
493    }
494
495    /// Returns the estimated number of series.
496    pub(crate) fn finish(&mut self) -> u64 {
497        self.last_timestamp = None;
498        let count = self.series_count;
499        self.series_count = 0;
500
501        count
502    }
503}
504
505#[cfg(test)]
506mod tests {
507    use std::sync::Arc;
508
509    use ::parquet::arrow::AsyncArrowWriter;
510    use ::parquet::arrow::arrow_reader::ParquetRecordBatchReaderBuilder;
511    use ::parquet::basic::LogicalType;
512    use ::parquet::variant::{VariantArray, VariantType, json_to_variant};
513    use common_query::prelude::greptime_native_histogram;
514    use datatypes::arrow::array::{
515        ArrayRef, BinaryArray, DictionaryArray, Int64Array, StringArray, StructArray,
516        TimestampMillisecondArray, UInt8Array, UInt32Array, UInt64Array,
517    };
518    use datatypes::arrow::datatypes::{DataType as ArrowDataType, Field, Schema, TimeUnit};
519    use datatypes::arrow::record_batch::RecordBatch;
520    use datatypes::extension::json::{Json2ExtensionType, Json2PhysicalLayout};
521    use datatypes::vectors::json::array::JsonArray;
522    use serde_json::json;
523
524    use super::*;
525
526    fn new_flat_record_batch(timestamps: &[i64]) -> RecordBatch {
527        // Flat format has: [fields..., time_index, __primary_key, __sequence, __op_type]
528        let num_cols = 4; // time_index + 3 internal columns
529        let time_index_pos = time_index_column_index(num_cols);
530        assert_eq!(time_index_pos, 0); // For 4 columns, time index should be at position 0
531
532        let time_array = Arc::new(TimestampMillisecondArray::from(timestamps.to_vec()));
533        let pk_array = Arc::new(DictionaryArray::new(
534            UInt32Array::from(vec![0; timestamps.len()]),
535            Arc::new(BinaryArray::from(vec![b"test".as_slice()])),
536        ));
537        let seq_array = Arc::new(UInt64Array::from(vec![1; timestamps.len()]));
538        let op_array = Arc::new(UInt8Array::from(vec![1; timestamps.len()]));
539
540        let schema = Arc::new(Schema::new(vec![
541            Field::new(
542                "time",
543                ArrowDataType::Timestamp(TimeUnit::Millisecond, None),
544                false,
545            ),
546            Field::new_dictionary(
547                "__primary_key",
548                ArrowDataType::UInt32,
549                ArrowDataType::Binary,
550                false,
551            ),
552            Field::new("__sequence", ArrowDataType::UInt64, false),
553            Field::new("__op_type", ArrowDataType::UInt8, false),
554        ]));
555
556        RecordBatch::try_new(schema, vec![time_array, pk_array, seq_array, op_array]).unwrap()
557    }
558
559    #[test]
560    fn test_series_estimator_flat_empty_batch() {
561        let mut estimator = SeriesEstimator::default();
562        let record_batch = new_flat_record_batch(&[]);
563        estimator.update_flat(&record_batch);
564        assert_eq!(0, estimator.finish());
565    }
566
567    #[test]
568    fn test_series_estimator_flat_single_batch() {
569        let mut estimator = SeriesEstimator::default();
570        let record_batch = new_flat_record_batch(&[1, 2, 3]);
571        estimator.update_flat(&record_batch);
572        assert_eq!(1, estimator.finish());
573    }
574
575    #[test]
576    fn test_series_estimator_flat_series_boundary_within_batch() {
577        let mut estimator = SeriesEstimator::default();
578        // Timestamps decrease from 3 to 2, indicating a series boundary
579        let record_batch = new_flat_record_batch(&[1, 2, 3, 2, 4, 5]);
580        estimator.update_flat(&record_batch);
581        // Should detect boundary at position 3 (3 >= 2)
582        assert_eq!(2, estimator.finish());
583    }
584
585    #[test]
586    fn test_series_estimator_flat_multiple_boundaries_within_batch() {
587        let mut estimator = SeriesEstimator::default();
588        // Multiple series boundaries: 5>=4, 6>=3
589        let record_batch = new_flat_record_batch(&[1, 2, 5, 4, 6, 3, 7]);
590        estimator.update_flat(&record_batch);
591        assert_eq!(3, estimator.finish());
592    }
593
594    #[test]
595    fn test_series_estimator_flat_equal_timestamps() {
596        let mut estimator = SeriesEstimator::default();
597        // Equal timestamps are considered as new series
598        let record_batch = new_flat_record_batch(&[1, 2, 2, 3, 3, 3, 4]);
599        estimator.update_flat(&record_batch);
600        // Boundaries at: 2>=2, 3>=3, 3>=3
601        assert_eq!(4, estimator.finish());
602    }
603
604    #[test]
605    fn test_series_estimator_flat_multiple_batches_continuation() {
606        let mut estimator = SeriesEstimator::default();
607
608        // First batch: timestamps 1, 2, 3
609        let batch1 = new_flat_record_batch(&[1, 2, 3]);
610        estimator.update_flat(&batch1);
611
612        // Second batch: timestamps 4, 5, 6 (continuation)
613        let batch2 = new_flat_record_batch(&[4, 5, 6]);
614        estimator.update_flat(&batch2);
615
616        assert_eq!(1, estimator.finish());
617    }
618
619    #[test]
620    fn test_series_estimator_flat_multiple_batches_new_series() {
621        let mut estimator = SeriesEstimator::default();
622
623        // First batch: timestamps 1, 2, 3
624        let batch1 = new_flat_record_batch(&[1, 2, 3]);
625        estimator.update_flat(&batch1);
626
627        // Second batch: timestamps 2, 3, 4 (goes back to 2, new series)
628        let batch2 = new_flat_record_batch(&[2, 3, 4]);
629        estimator.update_flat(&batch2);
630
631        assert_eq!(2, estimator.finish());
632    }
633
634    #[test]
635    fn test_series_estimator_flat_boundary_at_batch_edge_equal() {
636        let mut estimator = SeriesEstimator::default();
637
638        // First batch ending at 5
639        let batch1 = new_flat_record_batch(&[1, 2, 5]);
640        estimator.update_flat(&batch1);
641
642        // Second batch starting at 5 (equal timestamp, new series)
643        let batch2 = new_flat_record_batch(&[5, 6, 7]);
644        estimator.update_flat(&batch2);
645
646        assert_eq!(2, estimator.finish());
647    }
648
649    #[test]
650    fn test_series_estimator_flat_mixed_batches() {
651        let mut estimator = SeriesEstimator::default();
652
653        // Batch 1: single series [10, 20, 30]
654        let batch1 = new_flat_record_batch(&[10, 20, 30]);
655        estimator.update_flat(&batch1);
656
657        // Batch 2: starts new series [5, 15], boundary within batch [15, 10, 25]
658        let batch2 = new_flat_record_batch(&[5, 15, 10, 25]);
659        estimator.update_flat(&batch2);
660
661        // Batch 3: continues from 25 to [30, 35]
662        let batch3 = new_flat_record_batch(&[30, 35]);
663        estimator.update_flat(&batch3);
664
665        // Expected: 1 (batch1) + 1 (batch2 start) + 1 (within batch2) = 3
666        assert_eq!(3, estimator.finish());
667    }
668
669    #[test]
670    fn test_series_estimator_flat_descending_timestamps() {
671        let mut estimator = SeriesEstimator::default();
672        // Strictly descending timestamps - each pair creates a boundary
673        let record_batch = new_flat_record_batch(&[10, 9, 8, 7, 6]);
674        estimator.update_flat(&record_batch);
675        // Boundaries: 10>=9, 9>=8, 8>=7, 7>=6 = 4 boundaries + 1 initial = 5 series
676        assert_eq!(5, estimator.finish());
677    }
678
679    #[test]
680    fn test_series_estimator_flat_finish_resets_state() {
681        let mut estimator = SeriesEstimator::default();
682
683        let batch1 = new_flat_record_batch(&[1, 2, 3]);
684        estimator.update_flat(&batch1);
685
686        assert_eq!(1, estimator.finish());
687
688        // After finish, state should be reset
689        let batch2 = new_flat_record_batch(&[4, 5, 6]);
690        estimator.update_flat(&batch2);
691
692        assert_eq!(1, estimator.finish());
693    }
694
695    /// Build a native-histogram struct field whose top-level `PARQUET:field_id`
696    /// is `column_id` (as `with_field_id` does on the real write path).
697    fn histogram_field(name: &str, column_id: u32) -> Field {
698        use common_query::native_histogram::native_histogram_value_type;
699        use datatypes::data_type::DataType;
700        with_field_id(
701            Field::new(name, native_histogram_value_type().as_arrow_type(), true),
702            column_id,
703        )
704    }
705
706    /// Asserts `field` is a stamped native-histogram struct: it carries the
707    /// `greptime.histogram` extension and every sub-field (and list element)
708    /// carries its reserved `PARQUET:field_id` namespaced by `column_id`.
709    fn assert_histogram_stamped(field: &Field, column_id: i32) {
710        use arrow_schema::extension::ExtensionType;
711        use common_query::native_histogram::{
712            native_histogram_list_element_id, native_histogram_subfield_id,
713        };
714        use datatypes::extension::histogram::HistogramExtensionType;
715
716        assert_eq!(
717            field
718                .metadata()
719                .get(arrow_schema::extension::EXTENSION_TYPE_NAME_KEY)
720                .map(|s| s.as_str()),
721            Some(HistogramExtensionType::NAME),
722            "histogram field must carry the greptime.histogram extension"
723        );
724        let ArrowDataType::Struct(children) = field.data_type() else {
725            panic!("expected a struct, got {:?}", field.data_type());
726        };
727        for child in children {
728            let expected = native_histogram_subfield_id(column_id, child.name())
729                .unwrap_or_else(|| panic!("no id for sub-field {}", child.name()));
730            let got: i32 = child
731                .metadata()
732                .get(PARQUET_FIELD_ID_KEY)
733                .unwrap_or_else(|| panic!("sub-field {} missing field id", child.name()))
734                .parse()
735                .unwrap();
736            assert_eq!(got, expected, "sub-field {} id", child.name());
737            if let ArrowDataType::List(elem) = child.data_type() {
738                let elem_expected =
739                    native_histogram_list_element_id(column_id, child.name()).unwrap();
740                let elem_got: i32 = elem
741                    .metadata()
742                    .get(PARQUET_FIELD_ID_KEY)
743                    .unwrap_or_else(|| panic!("list element of {} missing id", child.name()))
744                    .parse()
745                    .unwrap();
746                assert_eq!(
747                    elem_got,
748                    elem_expected,
749                    "list element id of {}",
750                    child.name()
751                );
752            }
753        }
754    }
755
756    #[test]
757    fn test_maybe_wrap_schema_native_histogram() {
758        let schema = Arc::new(Schema::new(vec![
759            Field::new(
760                "greptime_timestamp",
761                ArrowDataType::Timestamp(TimeUnit::Millisecond, None),
762                false,
763            ),
764            histogram_field(greptime_native_histogram(), 1),
765        ]));
766
767        let wrapped = maybe_wrap_schema(&schema).unwrap();
768        let hist = wrapped
769            .field_with_name(greptime_native_histogram())
770            .expect("histogram field present");
771        // The struct has 18 sub-fields.
772        let ArrowDataType::Struct(children) = hist.data_type() else {
773            unreachable!()
774        };
775        assert_eq!(children.len(), 18);
776        assert_histogram_stamped(hist, 1);
777    }
778
779    #[test]
780    fn test_maybe_wrap_schema_multiple_histograms_disjoint_ids() {
781        // Two histogram columns with distinct parent column ids get disjoint
782        // sub-field ids (defensive: the metric engine yields at most one
783        // histogram column, but the scheme must stay correct if more appear).
784        use common_query::native_histogram::native_histogram_subfield_id;
785
786        let schema = Arc::new(Schema::new(vec![
787            histogram_field(greptime_native_histogram(), 1),
788            histogram_field(greptime_native_histogram(), 7),
789        ]));
790        let wrapped = maybe_wrap_schema(&schema).unwrap();
791        let h1 = &wrapped.fields()[0];
792        let h2 = &wrapped.fields()[1];
793        assert_histogram_stamped(h1, 1);
794        assert_histogram_stamped(h2, 7);
795        // The same sub-field name resolves to different ids across columns.
796        assert_ne!(
797            native_histogram_subfield_id(1, "sum"),
798            native_histogram_subfield_id(7, "sum")
799        );
800    }
801
802    #[test]
803    fn test_maybe_wrap_schema_recognizes_histogram_by_type() {
804        let schema = Arc::new(Schema::new(vec![histogram_field("custom_histogram", 5)]));
805
806        let wrapped = maybe_wrap_schema(&schema).unwrap();
807        let hist = wrapped.field_with_name("custom_histogram").unwrap();
808        assert_histogram_stamped(hist, 5);
809    }
810
811    #[test]
812    fn test_maybe_wrap_schema_plain_struct_not_stamped() {
813        use arrow_schema::extension::EXTENSION_TYPE_NAME_KEY;
814
815        let plain = ArrowDataType::Struct(
816            vec![
817                Arc::new(Field::new("a", ArrowDataType::Int32, true)),
818                Arc::new(Field::new("b", ArrowDataType::Utf8, true)),
819            ]
820            .into(),
821        );
822        let schema = Arc::new(Schema::new(vec![
823            Field::new(
824                "ts",
825                ArrowDataType::Timestamp(TimeUnit::Millisecond, None),
826                false,
827            ),
828            Field::new("data", plain, true),
829        ]));
830
831        let wrapped = maybe_wrap_schema(&schema).unwrap();
832        let data = wrapped.field_with_name("data").unwrap();
833        assert!(
834            data.metadata().get(EXTENSION_TYPE_NAME_KEY).is_none(),
835            "non-histogram struct must not get the extension"
836        );
837        if let ArrowDataType::Struct(children) = data.data_type() {
838            for child in children {
839                assert!(
840                    child.metadata().get(PARQUET_FIELD_ID_KEY).is_none(),
841                    "non-histogram sub-field {} must not get a field id",
842                    child.name()
843                );
844            }
845        }
846    }
847
848    #[test]
849    fn test_maybe_wrap_schema_no_struct_unchanged() {
850        let schema: Arc<Schema> = Arc::new(Schema::new(vec![
851            Field::new(
852                "ts",
853                ArrowDataType::Timestamp(TimeUnit::Millisecond, None),
854                false,
855            ),
856            Field::new("v", ArrowDataType::Float64, true),
857        ]));
858        let wrapped = maybe_wrap_schema(&schema).unwrap();
859        assert!(
860            Arc::ptr_eq(&wrapped, &schema),
861            "a schema without any struct column must be returned unchanged"
862        );
863    }
864
865    /// Writes `schema` through `maybe_wrap_schema` and a real parquet
866    /// [`ArrowWriter`], then returns the arrow schema read back from the file
867    /// footer. This proves the `greptime.histogram` extension and the nested
868    /// `PARQUET:field_id`s actually land on disk, not just in memory.
869    ///
870    /// `maybe_wrap_schema` is exactly what the SST parquet writer hands to
871    /// `AsyncArrowWriter` (see `writer.rs`); the sync [`ArrowWriter`] shares
872    /// the same arrow-to-parquet schema conversion, so the footer it emits is
873    /// the on-disk contract this change introduces. An empty batch suffices
874    /// because the parquet footer always carries the schema.
875    fn parquet_footer_arrow_schema(schema: &SchemaRef) -> SchemaRef {
876        use ::parquet::arrow::ArrowWriter;
877        use ::parquet::arrow::arrow_reader::ParquetRecordBatchReaderBuilder;
878        use ::parquet::file::properties::WriterProperties;
879        use bytes::Bytes;
880
881        let wrapped = maybe_wrap_schema(schema).unwrap();
882        let mut bytes = Vec::new();
883        let props = WriterProperties::builder().build();
884        let mut writer = ArrowWriter::try_new(&mut bytes, wrapped.clone(), Some(props)).unwrap();
885        writer
886            .write(&RecordBatch::new_empty(wrapped.clone()))
887            .unwrap();
888        writer.close().unwrap();
889
890        ParquetRecordBatchReaderBuilder::try_new(Bytes::from(bytes))
891            .unwrap()
892            .schema()
893            .clone()
894    }
895
896    #[test]
897    fn test_maybe_wrap_schema_survives_parquet_roundtrip() {
898        // On-disk contract: after writing through the parquet writer path, the
899        // footer still carries the greptime.histogram extension and every
900        // nested (sub-field + list-element) PARQUET:field_id.
901        let schema = Arc::new(Schema::new(vec![
902            Field::new(
903                "greptime_timestamp",
904                ArrowDataType::Timestamp(TimeUnit::Millisecond, None),
905                false,
906            ),
907            histogram_field(greptime_native_histogram(), 3),
908        ]));
909
910        let on_disk = parquet_footer_arrow_schema(&schema);
911        let hist = on_disk
912            .field_with_name(greptime_native_histogram())
913            .expect("histogram field present");
914        assert_histogram_stamped(hist, 3);
915    }
916
917    #[test]
918    fn test_parquet_roundtrip_recognizes_histogram_by_type() {
919        // The persisted type, rather than a process-local configured name,
920        // identifies native histograms across upgrades and prefix changes.
921        let schema = Arc::new(Schema::new(vec![
922            Field::new(
923                "ts",
924                ArrowDataType::Timestamp(TimeUnit::Millisecond, None),
925                false,
926            ),
927            histogram_field("custom_histogram", 9),
928        ]));
929
930        let on_disk = parquet_footer_arrow_schema(&schema);
931        let hist = on_disk.field_with_name("custom_histogram").unwrap();
932        assert_histogram_stamped(hist, 9);
933    }
934
935    #[test]
936    fn test_maybe_wrap_schema_overflows_return_error() {
937        // A column id of 12_582_912 makes the derived sub-field id overflow
938        // i32 (BASE + column_id*64 == i32::MAX + 1). The write path must
939        // surface this as an error rather than silently dropping the field
940        // id, wrapping, or panicking.
941        let schema = Arc::new(Schema::new(vec![
942            Field::new(
943                "greptime_timestamp",
944                ArrowDataType::Timestamp(TimeUnit::Millisecond, None),
945                false,
946            ),
947            histogram_field(greptime_native_histogram(), 12_582_912),
948        ]));
949        let err = maybe_wrap_schema(&schema).unwrap_err();
950        assert!(
951            matches!(
952                err,
953                crate::error::Error::InvalidNativeHistogramSubfield { .. }
954            ),
955            "expected InvalidNativeHistogramSubfield, got {:?}",
956            err
957        );
958    }
959
960    #[test]
961    fn test_maybe_wrap_schema_missing_field_id_returns_error() {
962        // The parent column's PARQUET:field_id namespaces every sub-field id.
963        // If it is absent (e.g. a histogram struct handed to the writer
964        // without the write path's stamping), the writer must fail loudly
965        // rather than silently namespace under column 0, which would collide
966        // with that column's nested ids.
967        use common_query::native_histogram::native_histogram_value_type;
968        use datatypes::data_type::DataType;
969
970        let field = Field::new(
971            greptime_native_histogram(),
972            native_histogram_value_type().as_arrow_type(),
973            true,
974        );
975        assert!(
976            field.metadata().get(PARQUET_FIELD_ID_KEY).is_none(),
977            "fixture must not carry a field id"
978        );
979        let schema = Arc::new(Schema::new(vec![field]));
980        let err = maybe_wrap_schema(&schema).unwrap_err();
981        assert!(
982            matches!(
983                err,
984                crate::error::Error::InvalidNativeHistogramFieldId { .. }
985            ),
986            "expected InvalidNativeHistogramFieldId, got {:?}",
987            err
988        );
989    }
990
991    #[test]
992    fn test_maybe_wrap_schema_field_id_above_i32_max_returns_error() {
993        // `with_field_id` serializes the column id from a u32, so a valid id
994        // above i32::MAX (e.g. u32::MAX) must not be silently parsed as a
995        // failed i32 and collapsed onto column 0's nested ids. It must
996        // surface a checked-conversion error instead.
997        let schema = Arc::new(Schema::new(vec![
998            Field::new(
999                "greptime_timestamp",
1000                ArrowDataType::Timestamp(TimeUnit::Millisecond, None),
1001                false,
1002            ),
1003            histogram_field(greptime_native_histogram(), u32::MAX),
1004        ]));
1005        let err = maybe_wrap_schema(&schema).unwrap_err();
1006        assert!(
1007            matches!(
1008                err,
1009                crate::error::Error::InvalidNativeHistogramFieldId { .. }
1010            ),
1011            "expected InvalidNativeHistogramFieldId, got {:?}",
1012            err
1013        );
1014    }
1015
1016    fn json2_v2_test_type() -> ArrowDataType {
1017        ArrowDataType::Struct(
1018            vec![
1019                Arc::new(Field::new("active", ArrowDataType::Boolean, true)),
1020                Arc::new(Field::new("hot", ArrowDataType::Int64, true)),
1021                Arc::new(Field::new("name", ArrowDataType::Utf8, true)),
1022            ]
1023            .into(),
1024        )
1025    }
1026
1027    /// Validates the persisted-format foundation for the JSON2 v2 remainder.
1028    ///
1029    /// JSON2 will store `!__remainder__!` as a nested Variant child of its root
1030    /// Struct. Before enabling that layout in production, this test ensures the
1031    /// SST schema wrapper and Arrow writer preserve the Variant extension,
1032    /// encode the Parquet Variant logical type, and round-trip the values
1033    /// without changing the surrounding Struct.
1034    #[tokio::test]
1035    async fn test_nested_variant_survives_sst_writer_schema_roundtrip()
1036    -> Result<(), Box<dyn std::error::Error>> {
1037        let json: ArrayRef = Arc::new(StringArray::from(vec![
1038            Some(r#"{}"#),
1039            Some(r#"{"name":"Alice","active":true}"#),
1040            Some(r#"{"nested":{"count":42},"items":[1,"two",null]}"#),
1041            Some(r#"{"\u5b57\u6bb5":"\u503c"}"#),
1042            None,
1043        ]));
1044        let remainder = json_to_variant(&json)?;
1045        let remainder_field = remainder.field("!__remainder__!");
1046        let remainder_array = ArrayRef::from(remainder);
1047        let hot_field = Field::new("hot", ArrowDataType::Int64, true);
1048        let data_array = Arc::new(StructArray::new(
1049            vec![remainder_field.clone(), hot_field.clone()].into(),
1050            vec![
1051                remainder_array,
1052                Arc::new(Int64Array::from(vec![
1053                    Some(1),
1054                    Some(2),
1055                    Some(3),
1056                    Some(4),
1057                    None,
1058                ])),
1059            ],
1060            None,
1061        ));
1062        let data_field = Field::new(
1063            "data",
1064            ArrowDataType::Struct(vec![remainder_field, hot_field].into()),
1065            true,
1066        )
1067        .with_extension_type(Json2ExtensionType::default());
1068        let schema = Arc::new(Schema::new(vec![data_field]));
1069        let source = RecordBatch::try_new(schema.clone(), vec![data_array])?;
1070
1071        let wrapped = maybe_wrap_schema(&schema)?;
1072        let mut buffer = Vec::new();
1073        let mut writer = AsyncArrowWriter::try_new(&mut buffer, wrapped, None)?;
1074        writer.write(&source).await?;
1075        writer.close().await?;
1076
1077        let builder = ParquetRecordBatchReaderBuilder::try_new(bytes::Bytes::from(buffer))?;
1078        let parquet_remainder =
1079            &builder.parquet_schema().root_schema().get_fields()[0].get_fields()[0];
1080        assert_eq!(
1081            parquet_remainder.get_basic_info().logical_type_ref(),
1082            Some(&LogicalType::Variant {
1083                specification_version: None,
1084            })
1085        );
1086
1087        let ArrowDataType::Struct(children) = builder.schema().field_with_name("data")?.data_type()
1088        else {
1089            unreachable!();
1090        };
1091        assert!(children[0].has_valid_extension_type::<VariantType>());
1092
1093        let mut reader = builder.build()?;
1094        let result = reader.next().unwrap()?;
1095        assert_eq!(source, result);
1096        let result_field = result.schema().field(0).clone();
1097        let result = result
1098            .column(0)
1099            .as_any()
1100            .downcast_ref::<StructArray>()
1101            .unwrap();
1102        VariantArray::try_new(result.column(0))?;
1103        let result: ArrayRef = Arc::new(result.clone());
1104        let result =
1105            JsonArray::from(&result).project_to_v2(&result_field, &json2_v2_test_type())?;
1106        assert_eq!(
1107            json!({"active": true, "hot": 2, "name": "Alice"}),
1108            JsonArray::from(&result).try_get_value(1)?
1109        );
1110        Ok(())
1111    }
1112
1113    /// Ensures future readers retain compatibility with the first JSON2 v2 layout.
1114    #[test]
1115    fn test_read_json2_v2_fixture() -> Result<(), Box<dyn std::error::Error>> {
1116        let bytes = bytes::Bytes::from_static(include_bytes!("../test-data/json2-v2.parquet"));
1117        let builder = ParquetRecordBatchReaderBuilder::try_new(bytes)?;
1118        let field = builder.schema().field(0).clone();
1119        assert!(Json2PhysicalLayout::try_from_root(&field)?.is_version_2());
1120
1121        let batch = builder.build()?.next().unwrap()?;
1122        let data = batch
1123            .column(0)
1124            .as_any()
1125            .downcast_ref::<StructArray>()
1126            .unwrap();
1127        VariantArray::try_new(data.column(0))?;
1128        let data: ArrayRef = Arc::new(data.clone());
1129        let data = JsonArray::from(&data).project_to_v2(&field, &json2_v2_test_type())?;
1130        assert_eq!(
1131            json!({"active": true, "hot": 2, "name": "Alice"}),
1132            JsonArray::from(&data).try_get_value(1)?
1133        );
1134        Ok(())
1135    }
1136}