Skip to main content

mito2/memtable/bulk/
json_align.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
15use std::collections::HashMap;
16use std::sync::Arc;
17
18use datatypes::arrow::datatypes::{DataType as ArrowDataType, Schema, SchemaRef};
19use datatypes::arrow::record_batch::RecordBatch;
20use datatypes::data_type::DataType;
21use datatypes::extension::json::is_structured_json_field;
22use datatypes::types::JsonType;
23use datatypes::vectors::json::array::JsonArray;
24use snafu::{OptionExt, ResultExt};
25
26use crate::error::{
27    ConvertValueSnafu, DataTypeMismatchSnafu, NewRecordBatchSnafu, Result, UnexpectedSnafu,
28};
29use crate::memtable::BoxedRecordBatchIterator;
30
31/// Aligns concrete JSON2 Arrow types across record batches.
32///
33/// JSON2 column concrete Arrow types are derived from data. Different memtable
34/// parts may therefore have different concrete types for the same JSON2 column.
35/// This helper merges those concrete types and aligns batches to the merged schema.
36#[derive(Clone)]
37pub(crate) struct Json2Aligner {
38    /// Schema after merging all JSON2 column concrete types.
39    schema: SchemaRef,
40    /// JSON2 columns that may need per-batch alignment.
41    json_columns: Vec<(usize, ArrowDataType)>,
42}
43
44impl Json2Aligner {
45    /// Builds an aligner from input schemas.
46    ///
47    /// Note: except for JSON2 columns, all input schemas must be identical.
48    pub(crate) fn try_new<I>(input_schemas: I) -> Result<Self>
49    where
50        I: IntoIterator<Item = SchemaRef>,
51    {
52        let mut input_schemas = input_schemas.into_iter();
53
54        // Use first schema as base: it defines column order and non-JSON types.
55        let base_schema = input_schemas.next().context(UnexpectedSnafu {
56            reason: "Json2Aligner requires at least one input schema",
57        })?;
58
59        // Init merged types from base schema.
60        let mut merged_types: HashMap<usize, JsonType> = base_schema
61            .fields()
62            .iter()
63            .enumerate()
64            .filter(|&(_idx, field)| is_structured_json_field(field))
65            .map(|(idx, field)| (idx, JsonType::from(field.data_type())))
66            .collect();
67
68        // No JSON2 columns, no alignment needed.
69        if merged_types.is_empty() {
70            return Ok(Self {
71                schema: base_schema,
72                json_columns: Vec::new(),
73            });
74        }
75
76        // Merge JSON2 types from remaining schemas.
77        for schema in input_schemas {
78            // Input schemas should only differ in JSON2 concrete types.
79            #[cfg(debug_assertions)]
80            assert_columns_match_except_json2(&base_schema, &schema);
81
82            for (idx, merged) in &mut merged_types {
83                if *idx >= schema.fields().len() {
84                    continue;
85                }
86                merged
87                    .merge(&JsonType::from(schema.field(*idx).data_type()))
88                    .context(DataTypeMismatchSnafu)?;
89            }
90        }
91
92        // Build output schema with merged JSON2 types.
93        let mut json_columns = Vec::with_capacity(merged_types.len());
94        let fields: Vec<_> = base_schema
95            .fields()
96            .iter()
97            .enumerate()
98            .map(|(idx, field)| {
99                if let Some(merged) = merged_types.get(&idx) {
100                    let data_type = merged.as_arrow_type();
101                    json_columns.push((idx, data_type.clone()));
102                    let mut field = (**field).clone();
103                    field.set_data_type(data_type);
104                    Arc::new(field)
105                } else {
106                    field.clone()
107                }
108            })
109            .collect();
110
111        let schema = Arc::new(Schema::new_with_metadata(
112            fields,
113            base_schema.metadata().clone(),
114        ));
115
116        Ok(Self {
117            schema,
118            json_columns,
119        })
120    }
121
122    /// Returns the aligned output schema.
123    pub(crate) fn schema(&self) -> &SchemaRef {
124        &self.schema
125    }
126
127    /// Aligns a [`RecordBatch`] to [`Self::schema`].
128    pub(crate) fn align_batch(&self, batch: RecordBatch) -> Result<RecordBatch> {
129        if self.json_columns.is_empty() {
130            return Ok(batch);
131        }
132        let mut cols = batch.columns().to_vec();
133        for (idx, expected_type) in &self.json_columns {
134            if batch.schema_ref().field(*idx).data_type() != expected_type {
135                cols[*idx] = JsonArray::from(batch.column(*idx))
136                    .try_align(expected_type)
137                    .context(ConvertValueSnafu)?;
138            }
139        }
140        RecordBatch::try_new(self.schema.clone(), cols).context(NewRecordBatchSnafu)
141    }
142
143    /// Aligns [`RecordBatch`]s to [`Self::schema`].
144    pub(crate) fn align_batches<I>(&self, batches: I) -> Result<Vec<RecordBatch>>
145    where
146        I: IntoIterator<Item = RecordBatch>,
147    {
148        batches
149            .into_iter()
150            .map(|batch| self.align_batch(batch))
151            .collect()
152    }
153
154    /// Wraps an iterator so each yielded [`RecordBatch`] is lazily aligned.
155    pub(crate) fn wrap_iter(&self, iter: BoxedRecordBatchIterator) -> BoxedRecordBatchIterator {
156        let aligner = self.clone();
157        Box::new(iter.map(move |batch| aligner.align_batch(batch?)))
158    }
159}
160
161#[cfg(debug_assertions)]
162fn assert_columns_match_except_json2(base_schema: &Schema, schema: &Schema) {
163    debug_assert_eq!(
164        base_schema.fields().len(),
165        schema.fields().len(),
166        "input schemas for Json2Aligner must have the same column count"
167    );
168    for (idx, (base_field, field)) in base_schema.fields().iter().zip(schema.fields()).enumerate() {
169        let base_is_json2 = is_structured_json_field(base_field);
170        let is_json2 = is_structured_json_field(field);
171        debug_assert_eq!(
172            base_is_json2, is_json2,
173            "column {idx} must be JSON2 in all input schemas or none"
174        );
175        if !base_is_json2 && !is_json2 {
176            debug_assert_eq!(
177                base_field, field,
178                "non-JSON2 column {idx} must be identical across input schemas"
179            );
180        }
181    }
182}
183
184#[cfg(test)]
185mod tests {
186    use std::sync::Arc;
187
188    use datatypes::arrow::array::{
189        Array, ArrayRef, Int64Array, StringViewArray, StructArray, UInt64Array,
190    };
191    use datatypes::arrow::datatypes::{DataType, Field, Fields, Schema};
192    use datatypes::extension::json::{JsonExtensionType, JsonMetadata};
193
194    use super::*;
195
196    #[test]
197    fn test_try_new_rejects_empty_input() {
198        let err = match Json2Aligner::try_new([]) {
199            Ok(_) => panic!("expected empty input to fail"),
200            Err(err) => err,
201        };
202        assert!(
203            err.to_string()
204                .contains("Json2Aligner requires at least one input schema")
205        );
206    }
207
208    #[test]
209    fn test_try_new_keeps_non_json_schema_unchanged() {
210        let schema = Arc::new(Schema::new(vec![
211            Arc::new(Field::new("ts", DataType::Int64, false)),
212            Arc::new(Field::new("value", DataType::UInt64, true)),
213        ]));
214        let batch = RecordBatch::try_new(
215            schema.clone(),
216            vec![
217                Arc::new(Int64Array::from_iter_values([1, 2])) as ArrayRef,
218                Arc::new(UInt64Array::from(vec![Some(10), None])) as ArrayRef,
219            ],
220        )
221        .unwrap();
222
223        let aligner = Json2Aligner::try_new([schema.clone()]).unwrap();
224        assert!(Arc::ptr_eq(aligner.schema(), &schema));
225
226        let aligned = aligner.align_batch(batch).unwrap();
227        assert!(Arc::ptr_eq(aligned.schema_ref(), &schema));
228    }
229
230    #[test]
231    fn test_try_new_ignores_legacy_jsonb_extension_field() {
232        let legacy_jsonb_field = Arc::new(
233            Field::new("data", DataType::Binary, true)
234                .with_extension_type(JsonExtensionType::new(Arc::new(JsonMetadata::default()))),
235        );
236        let schema = Arc::new(Schema::new(vec![
237            Arc::new(Field::new("ts", DataType::Int64, false)),
238            legacy_jsonb_field,
239        ]));
240
241        let aligner = Json2Aligner::try_new([schema.clone()]).unwrap();
242
243        assert!(Arc::ptr_eq(aligner.schema(), &schema));
244        assert!(aligner.json_columns.is_empty());
245    }
246
247    #[test]
248    fn test_try_new_merges_json2_object_fields() {
249        let id_fields = Fields::from(vec![id_field()]);
250        let name_fields = Fields::from(vec![name_field()]);
251        let schema_with_id = schema_with_json_field(json_field("data", id_fields));
252        let schema_with_name = schema_with_json_field(json_field("data", name_fields));
253
254        let aligner = Json2Aligner::try_new([schema_with_id, schema_with_name]).unwrap();
255        let data_field = aligner.schema().field(1);
256        let DataType::Struct(fields) = data_field.data_type() else {
257            panic!("expected JSON2 field to be a struct");
258        };
259
260        assert_eq!(2, fields.len());
261        assert_eq!("id", fields[0].name());
262        assert_eq!(&DataType::Int64, fields[0].data_type());
263        assert_eq!("name", fields[1].name());
264        assert_eq!(&DataType::Utf8View, fields[1].data_type());
265        assert!(is_structured_json_field(&aligner.schema().fields()[1]));
266    }
267
268    #[test]
269    fn test_align_batch_fills_missing_json2_fields() {
270        let id_fields = Fields::from(vec![id_field()]);
271        let name_fields = Fields::from(vec![name_field()]);
272        let schema_with_id = schema_with_json_field(json_field("data", id_fields.clone()));
273        let schema_with_name = schema_with_json_field(json_field("data", name_fields.clone()));
274
275        let batch_with_id = RecordBatch::try_new(
276            schema_with_id.clone(),
277            vec![
278                Arc::new(Int64Array::from_iter_values([1, 2])) as ArrayRef,
279                struct_array(
280                    id_fields,
281                    vec![Arc::new(Int64Array::from_iter_values([10, 20])) as ArrayRef],
282                ),
283            ],
284        )
285        .unwrap();
286        let batch_with_name = RecordBatch::try_new(
287            schema_with_name.clone(),
288            vec![
289                Arc::new(Int64Array::from_iter_values([3, 4])) as ArrayRef,
290                struct_array(
291                    name_fields,
292                    vec![
293                        Arc::new(StringViewArray::from(vec![Some("alice"), Some("bob")]))
294                            as ArrayRef,
295                    ],
296                ),
297            ],
298        )
299        .unwrap();
300
301        let aligner = Json2Aligner::try_new([schema_with_id, schema_with_name]).unwrap();
302        let aligned_with_id = aligner.align_batch(batch_with_id).unwrap();
303        let aligned_with_name = aligner.align_batch(batch_with_name).unwrap();
304
305        let data_with_id = aligned_with_id
306            .column(1)
307            .as_any()
308            .downcast_ref::<StructArray>()
309            .unwrap();
310        let id_values = data_with_id
311            .column(0)
312            .as_any()
313            .downcast_ref::<Int64Array>()
314            .unwrap();
315        let missing_names = data_with_id.column(1);
316        assert_eq!(10, id_values.value(0));
317        assert_eq!(20, id_values.value(1));
318        assert!(missing_names.is_null(0));
319        assert!(missing_names.is_null(1));
320
321        let data_with_name = aligned_with_name
322            .column(1)
323            .as_any()
324            .downcast_ref::<StructArray>()
325            .unwrap();
326        let missing_ids = data_with_name.column(0);
327        let name_values = data_with_name
328            .column(1)
329            .as_any()
330            .downcast_ref::<StringViewArray>()
331            .unwrap();
332        assert!(missing_ids.is_null(0));
333        assert!(missing_ids.is_null(1));
334        assert_eq!("alice", name_values.value(0));
335        assert_eq!("bob", name_values.value(1));
336    }
337
338    #[test]
339    fn test_wrap_iter_aligns_each_batch() {
340        let id_fields = Fields::from(vec![id_field()]);
341        let name_fields = Fields::from(vec![name_field()]);
342        let schema_with_id = schema_with_json_field(json_field("data", id_fields.clone()));
343        let schema_with_name = schema_with_json_field(json_field("data", name_fields.clone()));
344
345        let batch_with_id = RecordBatch::try_new(
346            schema_with_id.clone(),
347            vec![
348                Arc::new(Int64Array::from_iter_values([1])) as ArrayRef,
349                struct_array(
350                    id_fields,
351                    vec![Arc::new(Int64Array::from_iter_values([10])) as ArrayRef],
352                ),
353            ],
354        )
355        .unwrap();
356        let batch_with_name = RecordBatch::try_new(
357            schema_with_name.clone(),
358            vec![
359                Arc::new(Int64Array::from_iter_values([2])) as ArrayRef,
360                struct_array(
361                    name_fields,
362                    vec![Arc::new(StringViewArray::from(vec![Some("alice")])) as ArrayRef],
363                ),
364            ],
365        )
366        .unwrap();
367
368        let aligner = Json2Aligner::try_new([schema_with_id, schema_with_name]).unwrap();
369        let iter: BoxedRecordBatchIterator =
370            Box::new(vec![Ok(batch_with_id), Ok(batch_with_name)].into_iter());
371        let aligned = aligner.wrap_iter(iter).collect::<Result<Vec<_>>>().unwrap();
372
373        assert_eq!(2, aligned.len());
374        assert!(Arc::ptr_eq(aligned[0].schema_ref(), aligner.schema()));
375        assert!(Arc::ptr_eq(aligned[1].schema_ref(), aligner.schema()));
376    }
377
378    fn json_field(name: &str, fields: Fields) -> Arc<Field> {
379        Arc::new(
380            Field::new(name, DataType::Struct(fields), true)
381                .with_extension_type(JsonExtensionType::new(Arc::new(JsonMetadata::default()))),
382        )
383    }
384
385    fn schema_with_json_field(json_field: Arc<Field>) -> SchemaRef {
386        Arc::new(Schema::new(vec![
387            Arc::new(Field::new("ts", DataType::Int64, false)),
388            json_field,
389        ]))
390    }
391
392    fn id_field() -> Arc<Field> {
393        Arc::new(Field::new("id", DataType::Int64, true))
394    }
395
396    fn name_field() -> Arc<Field> {
397        Arc::new(Field::new("name", DataType::Utf8View, true))
398    }
399
400    fn struct_array(fields: Fields, columns: Vec<ArrayRef>) -> ArrayRef {
401        Arc::new(StructArray::new(fields, columns, None))
402    }
403}