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