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;
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            // Cast dictionary values to the target type.
324            if let ArrowDataType::Dictionary(_key_type, value_type) = array.data_type()
325                && !matches!(
326                    self.output_schema.arrow_schema().fields()[output_idx].data_type(),
327                    ArrowDataType::Dictionary(_, _)
328                )
329            {
330                // When a string dictionary column contains only a single value, reuse a cached
331                // repeated vector to avoid repeatedly expanding the dictionary.
332                if let Some(dict_array) = single_value_string_dictionary(
333                    &array,
334                    &self.output_schema.column_schemas()[output_idx].data_type,
335                    value_type.as_ref(),
336                ) {
337                    let dict_values = dict_array.values();
338                    let value = if dict_values.is_null(0) {
339                        Value::Null
340                    } else {
341                        Value::from(datatypes::arrow_array::string_array_value(dict_values, 0))
342                    };
343
344                    let repeated = repeated_vector_with_cache(
345                        &self.output_schema.column_schemas()[output_idx].data_type,
346                        &value,
347                        batch.num_rows(),
348                        cache_strategy,
349                    )?;
350                    array = repeated.to_arrow_array();
351                } else {
352                    let casted = datatypes::arrow::compute::cast(&array, value_type)
353                        .context(ArrowComputeSnafu)?;
354                    array = casted;
355                }
356            }
357
358            let field = &self.output_schema.arrow_schema().fields()[output_idx];
359            if is_json2_extension_type(field) && array.data_type() != field.data_type() {
360                array = JsonArray::from(&array)
361                    .project_to_v2(batch.schema_ref().field(*index), field.data_type())
362                    .context(DataTypesSnafu)?;
363            }
364
365            arrays.push(array);
366        }
367
368        let df_record_batch =
369            DfRecordBatch::try_new(self.output_schema.arrow_schema().clone(), arrays)
370                .context(NewDfRecordBatchSnafu)?;
371        Ok(RecordBatch::from_df_record_batch(
372            self.output_schema.clone(),
373            df_record_batch,
374        ))
375    }
376
377    /// Projects columns from the input batch and converts them into vectors.
378    pub(crate) fn project_vectors(
379        &self,
380        batch: &datatypes::arrow::record_batch::RecordBatch,
381    ) -> common_recordbatch::error::Result<Vec<datatypes::vectors::VectorRef>> {
382        let mut columns = Vec::with_capacity(self.output_schema.num_columns());
383        for index in &self.batch_indices {
384            let mut array = batch.column(*index).clone();
385            // Casts dictionary values to the target type.
386            if let datatypes::arrow::datatypes::DataType::Dictionary(_key_type, value_type) =
387                array.data_type()
388            {
389                let casted = datatypes::arrow::compute::cast(&array, value_type)
390                    .context(ArrowComputeSnafu)?;
391                array = casted;
392            }
393            let vector = Helper::try_into_vector(array)
394                .map_err(BoxedError::new)
395                .context(ExternalSnafu)?;
396            columns.push(vector);
397        }
398        Ok(columns)
399    }
400}
401
402fn single_value_string_dictionary<'a>(
403    array: &'a Arc<dyn Array>,
404    output_type: &ConcreteDataType,
405    value_type: &ArrowDataType,
406) -> Option<&'a datatypes::arrow::array::DictionaryArray<datatypes::arrow::datatypes::UInt32Type>> {
407    if !matches!(
408        value_type,
409        ArrowDataType::Utf8 | ArrowDataType::LargeUtf8 | ArrowDataType::Utf8View
410    ) || !output_type.is_string()
411    {
412        return None;
413    }
414
415    let dict_array = array
416        .as_any()
417        .downcast_ref::<datatypes::arrow::array::DictionaryArray<
418            datatypes::arrow::datatypes::UInt32Type,
419        >>()?;
420
421    (dict_array.values().len() == 1 && dict_array.null_count() == 0).then_some(dict_array)
422}
423
424/// Returns ids and datatypes of columns after applying the projection and JSON2 target types.
425///
426/// It adds the time index column if it doesn't present in the projection.
427pub(crate) fn flat_projected_columns(
428    metadata: &RegionMetadata,
429    format_projection: &FormatProjection,
430    json_target_types: &JsonTargetTypes,
431) -> Vec<(ColumnId, ConcreteDataType)> {
432    let time_index = metadata.time_index_column();
433    let num_columns = if format_projection
434        .column_id_to_projected_index
435        .contains_key(&time_index.column_id)
436    {
437        format_projection.column_id_to_projected_index.len()
438    } else {
439        format_projection.column_id_to_projected_index.len() + 1
440    };
441    let mut schema = vec![None; num_columns];
442    for (column_id, index) in &format_projection.column_id_to_projected_index {
443        let data_type = if let Some(json_type) = json_target_types.get(column_id) {
444            ConcreteDataType::json2(json_type.clone())
445        } else {
446            // Safety: FormatProjection ensures the id is valid.
447            metadata
448                .column_by_id(*column_id)
449                .unwrap()
450                .column_schema
451                .data_type
452                .clone()
453        };
454        schema[*index] = Some((*column_id, data_type));
455    }
456    if num_columns != format_projection.column_id_to_projected_index.len() {
457        schema[num_columns - 1] = Some((
458            time_index.column_id,
459            time_index.column_schema.data_type.clone(),
460        ));
461    }
462
463    // Safety: FormatProjection ensures all indices can be unwrapped.
464    schema.into_iter().map(|id_type| id_type.unwrap()).collect()
465}
466
467/// Computes the Arrow schema for input batches.
468///
469/// # Panics
470/// Panics if it can't find the column by the column id in the batch_schema.
471pub(crate) fn compute_input_arrow_schema(
472    metadata: &RegionMetadata,
473    batch_schema: &[(ColumnId, ConcreteDataType)],
474    read_cols: &ReadColumns,
475    json2_rewrite_targets: &Json2RewriteTargets,
476) -> datatypes::arrow::datatypes::SchemaRef {
477    let mut new_fields = Vec::with_capacity(batch_schema.len() + 3);
478    for (column_id, data_type) in batch_schema {
479        let data_type = json2_rewrite_targets
480            .get(column_id)
481            .map(|x| json2_physical_data_type(&x.target_layout))
482            .or_else(|| {
483                read_cols
484                    .json_target_type(*column_id)
485                    .map(|x| x.as_arrow_type())
486            })
487            .unwrap_or_else(|| data_type.as_arrow_type());
488
489        let column_metadata = metadata.column_by_id(*column_id).unwrap();
490        let field = Field::new(
491            &column_metadata.column_schema.name,
492            data_type,
493            column_metadata.column_schema.is_nullable(),
494        )
495        .with_metadata(column_metadata.column_schema.metadata().clone());
496        let field = with_field_id(field, *column_id);
497        if column_metadata.semantic_type == SemanticType::Tag {
498            new_fields.push(tag_maybe_to_dictionary_field(
499                &column_metadata.column_schema.data_type,
500                &Arc::new(field),
501            ));
502        } else {
503            new_fields.push(Arc::new(field));
504        }
505    }
506    new_fields.extend_from_slice(&internal_fields());
507
508    Arc::new(datatypes::arrow::datatypes::Schema::new(new_fields))
509}
510
511/// Helper to project compaction batches into flat format columns
512/// (fields + time index + __primary_key + __sequence + __op_type).
513pub(crate) struct CompactionProjectionMapper {
514    mapper: FlatProjectionMapper,
515    assembler: DfBatchAssembler,
516}
517
518impl CompactionProjectionMapper {
519    pub(crate) fn try_new(metadata: &RegionMetadataRef) -> Result<Self> {
520        let projection = metadata
521            .column_metadatas
522            .iter()
523            .enumerate()
524            .filter_map(|(idx, col)| {
525                if matches!(col.semantic_type, SemanticType::Field) {
526                    Some(idx)
527                } else {
528                    None
529                }
530            })
531            .chain([metadata.time_index_column_pos()])
532            .collect::<Vec<_>>();
533
534        let read_col_ids = metadata.column_metadatas.iter().map(|col| col.column_id);
535        let read_cols = ReadColumns::new(read_col_ids);
536        let mapper = FlatProjectionMapper::new_with_read_columns(metadata, projection, read_cols)?;
537        let assembler = DfBatchAssembler::new(mapper.output_schema());
538
539        Ok(Self { mapper, assembler })
540    }
541
542    /// Projects columns and appends internal columns for compaction output.
543    ///
544    /// The input batch is expected to be in flat format with internal columns appended.
545    pub(crate) fn project(&self, batch: DfRecordBatch) -> Result<DfRecordBatch> {
546        let columns = self
547            .mapper
548            .project_vectors(&batch)
549            .context(RecordBatchSnafu)?;
550        self.assembler
551            .build_df_record_batch_with_internal(&batch, columns)
552            .context(RecordBatchSnafu)
553    }
554}
555
556/// Builds [DfRecordBatch] with internal columns appended.
557pub(crate) struct DfBatchAssembler {
558    output_arrow_schema_with_internal: datatypes::arrow::datatypes::SchemaRef,
559}
560
561impl DfBatchAssembler {
562    /// Precomputes the output schema with internal columns.
563    pub(crate) fn new(output_schema: SchemaRef) -> Self {
564        let fields = output_schema
565            .arrow_schema()
566            .fields()
567            .into_iter()
568            .chain(internal_fields().iter())
569            .cloned()
570            .collect::<Vec<_>>();
571        let output_arrow_schema_with_internal =
572            Arc::new(datatypes::arrow::datatypes::Schema::new(fields));
573        Self {
574            output_arrow_schema_with_internal,
575        }
576    }
577
578    /// Builds a [DfRecordBatch] from projected vectors plus internal columns.
579    ///
580    /// Assumes the input batch already contains internal columns as the last three fields
581    /// ("__primary_key", "__sequence", "__op_type").
582    pub(crate) fn build_df_record_batch_with_internal(
583        &self,
584        batch: &datatypes::arrow::record_batch::RecordBatch,
585        mut columns: Vec<datatypes::vectors::VectorRef>,
586    ) -> common_recordbatch::error::Result<DfRecordBatch> {
587        let num_columns = batch.columns().len();
588        // The last 3 columns are the internal columns.
589        let internal_indices = [num_columns - 3, num_columns - 2, num_columns - 1];
590        for index in internal_indices.iter() {
591            let array = batch.column(*index).clone();
592            let vector = Helper::try_into_vector(array)
593                .map_err(BoxedError::new)
594                .context(ExternalSnafu)?;
595            columns.push(vector);
596        }
597        RecordBatch::to_df_record_batch(self.output_arrow_schema_with_internal.clone(), columns)
598    }
599}
600
601#[cfg(test)]
602mod tests {
603    use datatypes::schema::ColumnSchema;
604    use store_api::metadata::{ColumnMetadata, RegionMetadataBuilder};
605    use store_api::storage::RegionId;
606
607    use super::*;
608
609    fn metadata_with_legacy_json() -> RegionMetadataRef {
610        let mut builder = RegionMetadataBuilder::new(RegionId::new(1024, 0));
611        builder
612            .push_column_metadata(ColumnMetadata {
613                column_schema: datatypes::schema::ColumnSchema::new(
614                    "j",
615                    ConcreteDataType::json_datatype(),
616                    true,
617                ),
618                semantic_type: SemanticType::Field,
619                column_id: 0,
620            })
621            .push_column_metadata(ColumnMetadata {
622                column_schema: datatypes::schema::ColumnSchema::new(
623                    "ts",
624                    ConcreteDataType::timestamp_millisecond_datatype(),
625                    false,
626                ),
627                semantic_type: SemanticType::Timestamp,
628                column_id: 1,
629            });
630        Arc::new(builder.build().unwrap())
631    }
632
633    #[test]
634    fn test_tag_projection_preserves_dictionary() {
635        let mut builder = RegionMetadataBuilder::new(RegionId::new(1024, 0));
636        builder
637            .push_column_metadata(ColumnMetadata {
638                column_schema: ColumnSchema::new("tag", ConcreteDataType::string_datatype(), true),
639                semantic_type: SemanticType::Tag,
640                column_id: 0,
641            })
642            .push_column_metadata(ColumnMetadata {
643                column_schema: ColumnSchema::new(
644                    "ts",
645                    ConcreteDataType::timestamp_millisecond_datatype(),
646                    false,
647                ),
648                semantic_type: SemanticType::Timestamp,
649                column_id: 1,
650            })
651            .primary_key(vec![0]);
652        let metadata = Arc::new(builder.build().unwrap());
653
654        let mapper = FlatProjectionMapper::new(&metadata, [0, 1]).unwrap();
655        assert_eq!(
656            &ArrowDataType::Utf8,
657            mapper.output_schema().arrow_schema().field(0).data_type()
658        );
659
660        let mapper = mapper.with_pk_dictionary_encoding();
661        assert_eq!(
662            &ArrowDataType::Dictionary(
663                Box::new(ArrowDataType::UInt32),
664                Box::new(ArrowDataType::Utf8),
665            ),
666            mapper.output_schema().arrow_schema().field(0).data_type()
667        );
668    }
669
670    #[test]
671    fn test_json_type_hint_does_not_concretize_legacy_json() {
672        let metadata = metadata_with_legacy_json();
673        let mapper = FlatProjectionMapper::new_with_read_columns(
674            &metadata,
675            vec![0, 1],
676            ReadColumns::new([0, 1]),
677        )
678        .unwrap();
679
680        assert_eq!(
681            mapper.batch_schema()[0],
682            (0, ConcreteDataType::json_datatype())
683        );
684    }
685}