Skip to main content

mito2/read/
flat_projection.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//! Utilities for projection on flat format.
16
17use std::sync::Arc;
18
19use api::v1::SemanticType;
20use common_error::ext::BoxedError;
21use common_recordbatch::error::{
22    ArrowComputeSnafu, DataTypesSnafu, ExternalSnafu, NewDfRecordBatchSnafu,
23};
24use common_recordbatch::{DfRecordBatch, RecordBatch};
25use datatypes::arrow::array::{Array, AsArray};
26use datatypes::arrow::datatypes::{DataType as ArrowDataType, Field};
27use datatypes::extension::json::is_json2_extension_type;
28use datatypes::prelude::{ConcreteDataType, DataType};
29use datatypes::schema::{Schema, SchemaRef};
30use datatypes::value::Value;
31use datatypes::vectors::Helper;
32use datatypes::vectors::json::array::JsonArray;
33use datatypes::vectors::json::json2_physical_data_type;
34use snafu::{OptionExt, ResultExt};
35use store_api::metadata::{RegionMetadata, RegionMetadataRef};
36use store_api::storage::ColumnId;
37
38use crate::cache::CacheStrategy;
39use crate::error::{InvalidRequestSnafu, RecordBatchSnafu, Result};
40use crate::read::projection::{read_column_ids_from_projection, repeated_vector_with_cache};
41use crate::read::read_columns::{JsonTargetTypes, ReadColumns};
42use crate::sst::parquet::Json2RewriteTargets;
43use crate::sst::parquet::flat_format::sst_column_id_indices;
44use crate::sst::parquet::format::FormatProjection;
45use crate::sst::{
46    FlatSchemaOptions, internal_fields, tag_maybe_to_dictionary_field, to_flat_sst_arrow_schema,
47    with_field_id,
48};
49
50/// Handles projection and converts batches in flat format with correct schema.
51///
52/// This mapper support duplicate and unsorted projection indices.
53/// The output schema is determined by the projection indices.
54pub struct FlatProjectionMapper {
55    /// Metadata of the region.
56    metadata: RegionMetadataRef,
57    /// Schema for converted [RecordBatch] to return.
58    output_schema: SchemaRef,
59    /// The columns to read from memtables and SSTs.
60    /// The mapper won't deduplicate the column ids.
61    ///
62    /// Note that this doesn't contain the `__table_id` and `__tsid`.
63    read_cols: ReadColumns,
64    /// Ids and DataTypes of columns of the expected batch.
65    /// We can use this to check if the batch is compatible with the expected schema.
66    ///
67    /// It doesn't contain internal columns but always contains the time index column.
68    batch_schema: Vec<(ColumnId, ConcreteDataType)>,
69    /// `true` If the original projection is empty.
70    is_empty_projection: bool,
71    /// The index in flat format [RecordBatch] for each column in the output [RecordBatch].
72    batch_indices: Vec<usize>,
73    /// Precomputed Arrow schema for input batches.
74    input_arrow_schema: datatypes::arrow::datatypes::SchemaRef,
75}
76
77impl FlatProjectionMapper {
78    /// Returns a new mapper with projection.
79    /// If `projection` is empty, it outputs [RecordBatch] without any column but only a row count.
80    /// `SELECT COUNT(*) FROM table` is an example that uses an empty projection. DataFusion accepts
81    /// empty `RecordBatch` and only use its row count in this query.
82    pub fn new(
83        metadata: &RegionMetadataRef,
84        projection: impl IntoIterator<Item = usize>,
85    ) -> Result<Self> {
86        let projection: Vec<_> = projection.into_iter().collect();
87        let read_column_ids = read_column_ids_from_projection(metadata, &projection)?;
88        let read_cols = ReadColumns::new(read_column_ids);
89        Self::new_with_read_columns(metadata, projection, read_cols)
90    }
91
92    /// Returns a new mapper with output projection and explicit read columns.
93    pub fn new_with_read_columns(
94        metadata: &RegionMetadataRef,
95        projection: Vec<usize>,
96        read_cols: ReadColumns,
97    ) -> Result<Self> {
98        Self::new_with_json2_rewrite_targets(
99            metadata,
100            projection,
101            read_cols,
102            &Json2RewriteTargets::default(),
103        )
104    }
105
106    /// Returns a mapper for a compaction read with fixed JSON2 output layouts.
107    pub(crate) fn new_with_json2_rewrite_targets(
108        metadata: &RegionMetadataRef,
109        projection: Vec<usize>,
110        read_cols: ReadColumns,
111        json2_rewrite_targets: &Json2RewriteTargets,
112    ) -> Result<Self> {
113        // If the original projection is empty.
114        let is_empty_projection = projection.is_empty();
115
116        // Output column schemas for the projection.
117        let mut col_schemas = Vec::with_capacity(projection.len());
118        // Column ids of the output projection without deduplication.
119        let mut output_col_ids = Vec::with_capacity(projection.len());
120        for idx in &projection {
121            let col = metadata
122                .column_metadatas
123                .get(*idx)
124                .with_context(|| InvalidRequestSnafu {
125                    region_id: metadata.region_id,
126                    reason: format!("projection index {} is out of bound", idx),
127                })?;
128            output_col_ids.push(col.column_id);
129
130            let mut schema = col.column_schema.clone();
131            if let Some(data_type) = read_cols.json_target_type(col.column_id) {
132                schema.data_type = ConcreteDataType::json2(data_type.clone());
133            }
134            col_schemas.push(schema);
135        }
136
137        // Creates a map to lookup index.
138        let id_to_index = sst_column_id_indices(metadata);
139
140        // TODO(yingwen): Support different flat schema options.
141        let format_projection = FormatProjection::compute_format_projection(
142            metadata,
143            &id_to_index,
144            // All columns with internal columns.
145            metadata.column_metadatas.len() + 3,
146            read_cols.clone(),
147        );
148
149        let batch_schema =
150            flat_projected_columns(metadata, &format_projection, read_cols.json_target_types());
151
152        let input_arrow_schema =
153            compute_input_arrow_schema(metadata, &batch_schema, &read_cols, json2_rewrite_targets);
154
155        // If projection is empty, we don't output any column.
156        let output_schema = if is_empty_projection {
157            Arc::new(Schema::new(vec![]))
158        } else {
159            // Safety: Columns come from existing schema.
160            Arc::new(Schema::new(col_schemas))
161        };
162
163        let batch_indices = if is_empty_projection {
164            vec![]
165        } else {
166            output_col_ids
167                .iter()
168                .map(|id| {
169                    // Safety: The map is computed from the read projection.
170                    format_projection
171                        .column_id_to_projected_index
172                        .get(id)
173                        .copied()
174                        .with_context(|| {
175                            let name = metadata
176                                .column_by_id(*id)
177                                .map(|column| column.column_schema.name.clone())
178                                .unwrap_or_else(|| id.to_string());
179                            InvalidRequestSnafu {
180                                region_id: metadata.region_id,
181                                reason: format!(
182                                    "output column {} is missing in read projection",
183                                    name
184                                ),
185                            }
186                        })
187                })
188                .collect::<Result<Vec<_>>>()?
189        };
190
191        Ok(FlatProjectionMapper {
192            metadata: metadata.clone(),
193            output_schema,
194            read_cols,
195            batch_schema,
196            is_empty_projection,
197            batch_indices,
198            input_arrow_schema,
199        })
200    }
201
202    /// Returns a new mapper without projection.
203    pub fn all(metadata: &RegionMetadataRef) -> Result<Self> {
204        FlatProjectionMapper::new(metadata, 0..metadata.column_metadatas.len())
205    }
206
207    /// Keeps projected string primary-key columns in their flat-format dictionary encoding.
208    pub(crate) fn with_pk_dictionary_encoding(mut self) -> Self {
209        let mut changed = false;
210        let columns = self
211            .output_schema
212            .column_schemas()
213            .iter()
214            .map(|column| {
215                let mut column = column.clone();
216                if column.data_type == ConcreteDataType::string_datatype()
217                    && self
218                        .metadata
219                        .column_by_name(&column.name)
220                        .is_some_and(|metadata| metadata.semantic_type == SemanticType::Tag)
221                {
222                    changed = true;
223                    column.data_type = ConcreteDataType::dictionary_datatype(
224                        ConcreteDataType::uint32_datatype(),
225                        column.data_type.clone(),
226                    );
227                }
228                column
229            })
230            .collect();
231        if changed {
232            self.output_schema = Arc::new(Schema::new_with_version(
233                columns,
234                self.output_schema.version(),
235            ));
236        }
237        self
238    }
239
240    /// Returns the metadata that created the mapper.
241    pub(crate) fn metadata(&self) -> &RegionMetadataRef {
242        &self.metadata
243    }
244    /// Returns projected columns that we need to read from memtables and SSTs.
245    pub(crate) fn read_columns(&self) -> &ReadColumns {
246        &self.read_cols
247    }
248
249    /// Returns the field column start index in output batch.
250    pub(crate) fn field_column_start(&self) -> usize {
251        for (idx, column_id) in self
252            .batch_schema
253            .iter()
254            .map(|(column_id, _)| column_id)
255            .enumerate()
256        {
257            // Safety: We get the column id from the metadata in new().
258            if self
259                .metadata
260                .column_by_id(*column_id)
261                .unwrap()
262                .semantic_type
263                == SemanticType::Field
264            {
265                return idx;
266            }
267        }
268
269        self.batch_schema.len()
270    }
271
272    /// Returns ids of columns of the batch that the mapper expects to convert.
273    pub(crate) fn batch_schema(&self) -> &[(ColumnId, ConcreteDataType)] {
274        &self.batch_schema
275    }
276
277    /// Returns the input arrow schema from sources.
278    ///
279    /// The merge reader can use this schema.
280    pub(crate) fn input_arrow_schema(
281        &self,
282        compaction: bool,
283    ) -> datatypes::arrow::datatypes::SchemaRef {
284        if !compaction {
285            self.input_arrow_schema.clone()
286        } else {
287            // For compaction, we need to build a different schema from encoding.
288            let mut options = FlatSchemaOptions::from_encoding(self.metadata.primary_key_encoding);
289            options.concretized_json_types = self
290                .input_arrow_schema
291                .fields()
292                .iter()
293                .filter(|&field| is_json2_extension_type(field))
294                .map(|field| (field.name().clone(), field.data_type().clone()))
295                .collect();
296            to_flat_sst_arrow_schema(&self.metadata, &options)
297        }
298    }
299
300    /// Returns the schema of converted [RecordBatch].
301    /// This is the schema that the stream will output. This schema may contain
302    /// less columns than [FlatProjectionMapper::column_ids()].
303    pub(crate) fn output_schema(&self) -> SchemaRef {
304        self.output_schema.clone()
305    }
306
307    /// Converts a flat format [RecordBatch] to a normal [RecordBatch].
308    ///
309    /// The batch must match the `projection` using to build the mapper.
310    pub(crate) fn convert(
311        &self,
312        batch: &datatypes::arrow::record_batch::RecordBatch,
313        cache_strategy: &CacheStrategy,
314    ) -> common_recordbatch::error::Result<RecordBatch> {
315        if self.is_empty_projection {
316            return RecordBatch::new_with_count(self.output_schema.clone(), batch.num_rows());
317        }
318        // Construct output record batch directly from Arrow arrays to avoid
319        // Arrow -> Vector -> Arrow roundtrips in the hot path.
320        let mut arrays = Vec::with_capacity(self.output_schema.num_columns());
321        for (output_idx, index) in self.batch_indices.iter().enumerate() {
322            let mut array = batch.column(*index).clone();
323            if matches!(
324                self.output_schema.arrow_schema().fields()[output_idx].data_type(),
325                ArrowDataType::Dictionary(_, _)
326            ) && let Some(dictionary) = array.as_any_dictionary_opt()
327                && dictionary.values().len() > dictionary.len()
328            {
329                // Small scan slices must not retain an entire SST's tag dictionary in merges.
330                array = arrow_select::dictionary::garbage_collect_any_dictionary(dictionary)
331                    .context(ArrowComputeSnafu)?;
332            }
333            // Cast dictionary values to the target type.
334            if let ArrowDataType::Dictionary(_key_type, value_type) = array.data_type()
335                && !matches!(
336                    self.output_schema.arrow_schema().fields()[output_idx].data_type(),
337                    ArrowDataType::Dictionary(_, _)
338                )
339            {
340                // When a string dictionary column contains only a single value, reuse a cached
341                // repeated vector to avoid repeatedly expanding the dictionary.
342                if let Some(dict_array) = single_value_string_dictionary(
343                    &array,
344                    &self.output_schema.column_schemas()[output_idx].data_type,
345                    value_type.as_ref(),
346                ) {
347                    let dict_values = dict_array.values();
348                    let value = if dict_values.is_null(0) {
349                        Value::Null
350                    } else {
351                        Value::from(datatypes::arrow_array::string_array_value(dict_values, 0))
352                    };
353
354                    let repeated = repeated_vector_with_cache(
355                        &self.output_schema.column_schemas()[output_idx].data_type,
356                        &value,
357                        batch.num_rows(),
358                        cache_strategy,
359                    )?;
360                    array = repeated.to_arrow_array();
361                } else {
362                    let casted = datatypes::arrow::compute::cast(&array, value_type)
363                        .context(ArrowComputeSnafu)?;
364                    array = casted;
365                }
366            }
367
368            let field = &self.output_schema.arrow_schema().fields()[output_idx];
369            if is_json2_extension_type(field) && array.data_type() != field.data_type() {
370                array = JsonArray::from(&array)
371                    .project_to_v2(batch.schema_ref().field(*index), field.data_type())
372                    .context(DataTypesSnafu)?;
373            }
374
375            arrays.push(array);
376        }
377
378        let df_record_batch =
379            DfRecordBatch::try_new(self.output_schema.arrow_schema().clone(), arrays)
380                .context(NewDfRecordBatchSnafu)?;
381        Ok(RecordBatch::from_df_record_batch(
382            self.output_schema.clone(),
383            df_record_batch,
384        ))
385    }
386
387    /// Projects columns from the input batch and converts them into vectors.
388    pub(crate) fn project_vectors(
389        &self,
390        batch: &datatypes::arrow::record_batch::RecordBatch,
391    ) -> common_recordbatch::error::Result<Vec<datatypes::vectors::VectorRef>> {
392        let mut columns = Vec::with_capacity(self.output_schema.num_columns());
393        for index in &self.batch_indices {
394            let mut array = batch.column(*index).clone();
395            // Casts dictionary values to the target type.
396            if let datatypes::arrow::datatypes::DataType::Dictionary(_key_type, value_type) =
397                array.data_type()
398            {
399                let casted = datatypes::arrow::compute::cast(&array, value_type)
400                    .context(ArrowComputeSnafu)?;
401                array = casted;
402            }
403            let vector = Helper::try_into_vector(array)
404                .map_err(BoxedError::new)
405                .context(ExternalSnafu)?;
406            columns.push(vector);
407        }
408        Ok(columns)
409    }
410}
411
412fn single_value_string_dictionary<'a>(
413    array: &'a Arc<dyn Array>,
414    output_type: &ConcreteDataType,
415    value_type: &ArrowDataType,
416) -> Option<&'a datatypes::arrow::array::DictionaryArray<datatypes::arrow::datatypes::UInt32Type>> {
417    if !matches!(
418        value_type,
419        ArrowDataType::Utf8 | ArrowDataType::LargeUtf8 | ArrowDataType::Utf8View
420    ) || !output_type.is_string()
421    {
422        return None;
423    }
424
425    let dict_array = array
426        .as_any()
427        .downcast_ref::<datatypes::arrow::array::DictionaryArray<
428            datatypes::arrow::datatypes::UInt32Type,
429        >>()?;
430
431    (dict_array.values().len() == 1 && dict_array.null_count() == 0).then_some(dict_array)
432}
433
434/// Returns ids and datatypes of columns after applying the projection and JSON2 target types.
435///
436/// It adds the time index column if it doesn't present in the projection.
437pub(crate) fn flat_projected_columns(
438    metadata: &RegionMetadata,
439    format_projection: &FormatProjection,
440    json_target_types: &JsonTargetTypes,
441) -> Vec<(ColumnId, ConcreteDataType)> {
442    let time_index = metadata.time_index_column();
443    let num_columns = if format_projection
444        .column_id_to_projected_index
445        .contains_key(&time_index.column_id)
446    {
447        format_projection.column_id_to_projected_index.len()
448    } else {
449        format_projection.column_id_to_projected_index.len() + 1
450    };
451    let mut schema = vec![None; num_columns];
452    for (column_id, index) in &format_projection.column_id_to_projected_index {
453        let data_type = if let Some(json_type) = json_target_types.get(column_id) {
454            ConcreteDataType::json2(json_type.clone())
455        } else {
456            // Safety: FormatProjection ensures the id is valid.
457            metadata
458                .column_by_id(*column_id)
459                .unwrap()
460                .column_schema
461                .data_type
462                .clone()
463        };
464        schema[*index] = Some((*column_id, data_type));
465    }
466    if num_columns != format_projection.column_id_to_projected_index.len() {
467        schema[num_columns - 1] = Some((
468            time_index.column_id,
469            time_index.column_schema.data_type.clone(),
470        ));
471    }
472
473    // Safety: FormatProjection ensures all indices can be unwrapped.
474    schema.into_iter().map(|id_type| id_type.unwrap()).collect()
475}
476
477/// Computes the Arrow schema for input batches.
478///
479/// # Panics
480/// Panics if it can't find the column by the column id in the batch_schema.
481pub(crate) fn compute_input_arrow_schema(
482    metadata: &RegionMetadata,
483    batch_schema: &[(ColumnId, ConcreteDataType)],
484    read_cols: &ReadColumns,
485    json2_rewrite_targets: &Json2RewriteTargets,
486) -> datatypes::arrow::datatypes::SchemaRef {
487    let mut new_fields = Vec::with_capacity(batch_schema.len() + 3);
488    for (column_id, data_type) in batch_schema {
489        let data_type = json2_rewrite_targets
490            .get(column_id)
491            .map(|x| json2_physical_data_type(&x.target_layout))
492            .or_else(|| {
493                read_cols
494                    .json_target_type(*column_id)
495                    .map(|x| x.as_arrow_type())
496            })
497            .unwrap_or_else(|| data_type.as_arrow_type());
498
499        let column_metadata = metadata.column_by_id(*column_id).unwrap();
500        let field = Field::new(
501            &column_metadata.column_schema.name,
502            data_type,
503            column_metadata.column_schema.is_nullable(),
504        )
505        .with_metadata(column_metadata.column_schema.metadata().clone());
506        let field = with_field_id(field, *column_id);
507        if column_metadata.semantic_type == SemanticType::Tag {
508            new_fields.push(tag_maybe_to_dictionary_field(
509                &column_metadata.column_schema.data_type,
510                &Arc::new(field),
511            ));
512        } else {
513            new_fields.push(Arc::new(field));
514        }
515    }
516    new_fields.extend_from_slice(&internal_fields());
517
518    Arc::new(datatypes::arrow::datatypes::Schema::new(new_fields))
519}
520
521/// Helper to project compaction batches into flat format columns
522/// (fields + time index + __primary_key + __sequence + __op_type).
523pub(crate) struct CompactionProjectionMapper {
524    mapper: FlatProjectionMapper,
525    assembler: DfBatchAssembler,
526}
527
528impl CompactionProjectionMapper {
529    pub(crate) fn try_new(metadata: &RegionMetadataRef) -> Result<Self> {
530        let projection = metadata
531            .column_metadatas
532            .iter()
533            .enumerate()
534            .filter_map(|(idx, col)| {
535                if matches!(col.semantic_type, SemanticType::Field) {
536                    Some(idx)
537                } else {
538                    None
539                }
540            })
541            .chain([metadata.time_index_column_pos()])
542            .collect::<Vec<_>>();
543
544        let read_col_ids = metadata.column_metadatas.iter().map(|col| col.column_id);
545        let read_cols = ReadColumns::new(read_col_ids);
546        let mapper = FlatProjectionMapper::new_with_read_columns(metadata, projection, read_cols)?;
547        let assembler = DfBatchAssembler::new(mapper.output_schema());
548
549        Ok(Self { mapper, assembler })
550    }
551
552    /// Projects columns and appends internal columns for compaction output.
553    ///
554    /// The input batch is expected to be in flat format with internal columns appended.
555    pub(crate) fn project(&self, batch: DfRecordBatch) -> Result<DfRecordBatch> {
556        let columns = self
557            .mapper
558            .project_vectors(&batch)
559            .context(RecordBatchSnafu)?;
560        self.assembler
561            .build_df_record_batch_with_internal(&batch, columns)
562            .context(RecordBatchSnafu)
563    }
564}
565
566/// Builds [DfRecordBatch] with internal columns appended.
567pub(crate) struct DfBatchAssembler {
568    output_arrow_schema_with_internal: datatypes::arrow::datatypes::SchemaRef,
569}
570
571impl DfBatchAssembler {
572    /// Precomputes the output schema with internal columns.
573    pub(crate) fn new(output_schema: SchemaRef) -> Self {
574        let fields = output_schema
575            .arrow_schema()
576            .fields()
577            .into_iter()
578            .chain(internal_fields().iter())
579            .cloned()
580            .collect::<Vec<_>>();
581        let output_arrow_schema_with_internal =
582            Arc::new(datatypes::arrow::datatypes::Schema::new(fields));
583        Self {
584            output_arrow_schema_with_internal,
585        }
586    }
587
588    /// Builds a [DfRecordBatch] from projected vectors plus internal columns.
589    ///
590    /// Assumes the input batch already contains internal columns as the last three fields
591    /// ("__primary_key", "__sequence", "__op_type").
592    pub(crate) fn build_df_record_batch_with_internal(
593        &self,
594        batch: &datatypes::arrow::record_batch::RecordBatch,
595        mut columns: Vec<datatypes::vectors::VectorRef>,
596    ) -> common_recordbatch::error::Result<DfRecordBatch> {
597        let num_columns = batch.columns().len();
598        // The last 3 columns are the internal columns.
599        let internal_indices = [num_columns - 3, num_columns - 2, num_columns - 1];
600        for index in internal_indices.iter() {
601            let array = batch.column(*index).clone();
602            let vector = Helper::try_into_vector(array)
603                .map_err(BoxedError::new)
604                .context(ExternalSnafu)?;
605            columns.push(vector);
606        }
607        RecordBatch::to_df_record_batch(self.output_arrow_schema_with_internal.clone(), columns)
608    }
609}
610
611#[cfg(test)]
612mod tests {
613    use datatypes::schema::ColumnSchema;
614    use store_api::metadata::{ColumnMetadata, RegionMetadataBuilder};
615    use store_api::storage::RegionId;
616
617    use super::*;
618
619    fn metadata_with_legacy_json() -> RegionMetadataRef {
620        let mut builder = RegionMetadataBuilder::new(RegionId::new(1024, 0));
621        builder
622            .push_column_metadata(ColumnMetadata {
623                column_schema: datatypes::schema::ColumnSchema::new(
624                    "j",
625                    ConcreteDataType::json_datatype(),
626                    true,
627                ),
628                semantic_type: SemanticType::Field,
629                column_id: 0,
630            })
631            .push_column_metadata(ColumnMetadata {
632                column_schema: datatypes::schema::ColumnSchema::new(
633                    "ts",
634                    ConcreteDataType::timestamp_millisecond_datatype(),
635                    false,
636                ),
637                semantic_type: SemanticType::Timestamp,
638                column_id: 1,
639            });
640        Arc::new(builder.build().unwrap())
641    }
642
643    #[test]
644    fn test_tag_projection_preserves_dictionary() {
645        let mut builder = RegionMetadataBuilder::new(RegionId::new(1024, 0));
646        builder
647            .push_column_metadata(ColumnMetadata {
648                column_schema: ColumnSchema::new("tag", ConcreteDataType::string_datatype(), true),
649                semantic_type: SemanticType::Tag,
650                column_id: 0,
651            })
652            .push_column_metadata(ColumnMetadata {
653                column_schema: ColumnSchema::new(
654                    "ts",
655                    ConcreteDataType::timestamp_millisecond_datatype(),
656                    false,
657                ),
658                semantic_type: SemanticType::Timestamp,
659                column_id: 1,
660            })
661            .primary_key(vec![0]);
662        let metadata = Arc::new(builder.build().unwrap());
663
664        let mapper = FlatProjectionMapper::new(&metadata, [0, 1]).unwrap();
665        assert_eq!(
666            &ArrowDataType::Utf8,
667            mapper.output_schema().arrow_schema().field(0).data_type()
668        );
669
670        let mapper = mapper.with_pk_dictionary_encoding();
671        assert_eq!(
672            &ArrowDataType::Dictionary(
673                Box::new(ArrowDataType::UInt32),
674                Box::new(ArrowDataType::Utf8),
675            ),
676            mapper.output_schema().arrow_schema().field(0).data_type()
677        );
678    }
679
680    #[test]
681    fn test_tag_projection_compacts_dictionary_slices() {
682        use datatypes::arrow::array::{
683            DictionaryArray, StringArray, TimestampMillisecondArray, UInt32Array,
684        };
685        use datatypes::arrow::datatypes::UInt32Type;
686
687        let mut builder = RegionMetadataBuilder::new(RegionId::new(1024, 0));
688        builder
689            .push_column_metadata(ColumnMetadata {
690                column_schema: ColumnSchema::new("tag", ConcreteDataType::string_datatype(), true),
691                semantic_type: SemanticType::Tag,
692                column_id: 0,
693            })
694            .push_column_metadata(ColumnMetadata {
695                column_schema: ColumnSchema::new(
696                    "ts",
697                    ConcreteDataType::timestamp_millisecond_datatype(),
698                    false,
699                ),
700                semantic_type: SemanticType::Timestamp,
701                column_id: 1,
702            })
703            .primary_key(vec![0]);
704        let metadata = Arc::new(builder.build().unwrap());
705        let mapper = FlatProjectionMapper::new(&metadata, [0, 1])
706            .unwrap()
707            .with_pk_dictionary_encoding();
708        let values = StringArray::from_iter(
709            (0..1024).map(|i| (i != 1).then(|| format!("{i}{}", "x".repeat(4096)))),
710        );
711        let dictionary = DictionaryArray::<UInt32Type>::try_new(
712            UInt32Array::from(vec![Some(1023), None, Some(1), Some(1023)]),
713            Arc::new(values),
714        )
715        .unwrap();
716        let expected = datatypes::arrow::compute::cast(&dictionary, &ArrowDataType::Utf8).unwrap();
717        let batch = DfRecordBatch::try_from_iter([
718            ("tag", Arc::new(dictionary) as Arc<dyn Array>),
719            (
720                "ts",
721                Arc::new(TimestampMillisecondArray::from(vec![1, 2, 3, 4])),
722            ),
723        ])
724        .unwrap();
725        let output = mapper.convert(&batch, &CacheStrategy::Disabled).unwrap();
726        let actual = output.column(0);
727        assert!(actual.get_array_memory_size() < 64 * 1024);
728        assert_eq!(actual.data_type(), batch.column(0).data_type());
729        assert_eq!(
730            datatypes::arrow::compute::cast(actual.as_ref(), &ArrowDataType::Utf8)
731                .unwrap()
732                .as_ref(),
733            expected.as_ref(),
734        );
735    }
736
737    #[test]
738    fn test_single_value_dictionary_preserves_string_type() {
739        use datatypes::arrow::array::{DictionaryArray, TimestampMillisecondArray, UInt32Array};
740        use datatypes::arrow::datatypes::UInt32Type;
741
742        for data_type in [
743            ConcreteDataType::large_string_datatype(),
744            ConcreteDataType::utf8_view_datatype(),
745        ] {
746            let mut builder = RegionMetadataBuilder::new(RegionId::new(1024, 0));
747            builder
748                .push_column_metadata(ColumnMetadata {
749                    column_schema: ColumnSchema::new("tag", data_type.clone(), true),
750                    semantic_type: SemanticType::Tag,
751                    column_id: 0,
752                })
753                .push_column_metadata(ColumnMetadata {
754                    column_schema: ColumnSchema::new(
755                        "ts",
756                        ConcreteDataType::timestamp_millisecond_datatype(),
757                        false,
758                    ),
759                    semantic_type: SemanticType::Timestamp,
760                    column_id: 1,
761                })
762                .primary_key(vec![0]);
763            let metadata = Arc::new(builder.build().unwrap());
764            let mapper = FlatProjectionMapper::new(&metadata, [0, 1]).unwrap();
765
766            for value in [Value::from("greptime"), Value::Null] {
767                let mut values = data_type.create_mutable_vector(1);
768                values.try_push_value_ref(&value.as_value_ref()).unwrap();
769                let dictionary = Arc::new(
770                    DictionaryArray::<UInt32Type>::try_new(
771                        UInt32Array::from(vec![0, 0, 0]),
772                        values.to_vector().to_arrow_array(),
773                    )
774                    .unwrap(),
775                );
776                let arrays: Vec<Arc<dyn Array>> = mapper
777                    .batch_schema()
778                    .iter()
779                    .map(|(id, _)| match id {
780                        0 => dictionary.clone() as Arc<dyn Array>,
781                        1 => Arc::new(TimestampMillisecondArray::from(vec![1, 2, 3])),
782                        _ => unreachable!(),
783                    })
784                    .collect();
785                let fields = mapper
786                    .batch_schema()
787                    .iter()
788                    .zip(&arrays)
789                    .map(|((id, _), array)| {
790                        Field::new(id.to_string(), array.data_type().clone(), true)
791                    })
792                    .collect::<Vec<_>>();
793                let batch = DfRecordBatch::try_new(
794                    Arc::new(datatypes::arrow::datatypes::Schema::new(fields)),
795                    arrays,
796                )
797                .unwrap();
798                let output = mapper.convert(&batch, &CacheStrategy::Disabled).unwrap();
799                let vector = Helper::try_into_vector(output.column(0)).unwrap();
800                assert_eq!(data_type, vector.data_type());
801                for row in 0..3 {
802                    assert_eq!(value, vector.get(row));
803                }
804            }
805        }
806    }
807
808    #[test]
809    fn test_json_type_hint_does_not_concretize_legacy_json() {
810        let metadata = metadata_with_legacy_json();
811        let mapper = FlatProjectionMapper::new_with_read_columns(
812            &metadata,
813            vec![0, 1],
814            ReadColumns::new([0, 1]),
815        )
816        .unwrap();
817
818        assert_eq!(
819            mapper.batch_schema()[0],
820            (0, ConcreteDataType::json_datatype())
821        );
822    }
823}