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