1use std::collections::{HashMap, HashSet};
16use std::sync::Arc;
17
18use arrow_schema::extension::ExtensionType;
19use datatypes::arrow::datatypes::{DataType as ArrowDataType, Field, Schema, SchemaRef};
20use datatypes::arrow::record_batch::RecordBatch;
21use datatypes::extension::json::{JSON2_REMAINDER_FIELD_NAME, Json2ExtensionType, JsonMetadata};
22use datatypes::json::{
23 JSON2_DEFAULT_MAX_AUTO_EXPANDED_PATHS, JsonSettings, JsonTypeHint, TypeHintMismatchPolicy,
24};
25use datatypes::prelude::ConcreteDataType;
26use datatypes::types::json_type::JsonNativeType;
27use datatypes::vectors::json::array::JsonArray;
28use datatypes::vectors::json::json2_physical_data_type;
29use parquet::arrow::parquet_to_arrow_schema;
30use parquet::file::metadata::ParquetMetaData;
31use snafu::{OptionExt, ResultExt, ensure};
32use store_api::metadata::RegionMetadataRef;
33
34use crate::error::{
35 ConvertValueSnafu, DataTypeMismatchSnafu, InvalidRecordBatchSnafu, NewRecordBatchSnafu, Result,
36};
37
38pub(crate) struct Json2RewritePlan {
45 logical_settings: JsonSettings,
47 pub(super) target_layout: JsonSettings,
49}
50
51pub(crate) type Json2RewritePlans = HashMap<String, Json2RewritePlan>;
53
54#[derive(Clone)]
55struct Json2LeafPathStats {
56 rows: u64,
57 sources: usize,
59 data_type: JsonNativeType,
60 is_type_conflicted: bool,
61}
62
63pub(super) fn collect_json2_rewrite_plans_from_parquet(
74 metadata: &RegionMetadataRef,
75 parquet_metadata: &[Arc<ParquetMetaData>],
76) -> Result<Json2RewritePlans> {
77 let schemas = parquet_metadata
78 .iter()
79 .map(|metadata| {
80 let file = metadata.file_metadata();
81 let schema = parquet_to_arrow_schema(file.schema_descr(), file.key_value_metadata())
82 .map_err(|error| {
83 InvalidRecordBatchSnafu {
84 reason: format!("Failed to read compaction input Arrow schema: {error}"),
85 }
86 .build()
87 })?;
88 let rows = metadata
89 .row_groups()
90 .iter()
91 .map(|x| x.num_rows())
92 .sum::<i64>() as u64;
93 Ok((Arc::new(schema), rows))
94 })
95 .collect::<Result<Vec<_>>>()?;
96
97 collect_json2_rewrite_plans(metadata, &schemas)
98}
99
100pub(crate) fn collect_json2_rewrite_plans(
105 metadata: &RegionMetadataRef,
106 schemas: &[(SchemaRef, u64)],
107) -> Result<Json2RewritePlans> {
108 let json2_columns = metadata
109 .column_metadatas
110 .iter()
111 .filter_map(|x| {
112 x.column_schema
113 .data_type
114 .is_json2()
115 .then_some(&x.column_schema)
116 })
117 .collect::<Vec<_>>();
118
119 let mut plans = HashMap::with_capacity(json2_columns.len());
120 for column in json2_columns {
121 let extension = column
122 .extension_type::<Json2ExtensionType>()
123 .context(DataTypeMismatchSnafu)?
124 .with_context(|| InvalidRecordBatchSnafu {
125 reason: format!("JSON2 column '{}' has no extension metadata", column.name),
126 })?;
127 ensure!(
131 extension.metadata().is_version_2(),
132 InvalidRecordBatchSnafu {
133 reason: format!("JSON2 column '{}' is not layout v2", column.name),
134 }
135 );
136
137 let settings = extension.metadata().json_settings();
138 let hint_paths = settings
139 .type_hints()
140 .iter()
141 .map(|hint| hint.path.iter().map(String::as_str).collect::<Vec<_>>())
142 .collect::<HashSet<_>>();
143 let mut stats = HashMap::new();
144 for (schema, rows) in schemas {
145 let Some((_, field)) = schema.fields().find(&column.name) else {
146 continue;
147 };
148 collect_json2_path_stats(field, *rows, &hint_paths, &mut stats)?;
149 }
150
151 for stat in stats.values_mut() {
157 stat.is_type_conflicted |= stat.sources != schemas.len();
158 }
159 let mut hints = settings.type_hints().to_vec();
160 hints.extend(select_dynamic_hints(settings, &hint_paths, &stats));
161 let target_layout = JsonSettings::try_new(hints, Some(0)).context(DataTypeMismatchSnafu)?;
162 plans.insert(
163 column.name.clone(),
164 Json2RewritePlan {
165 logical_settings: settings.clone(),
166 target_layout,
167 },
168 );
169 }
170 Ok(plans)
171}
172
173fn collect_json2_path_stats<'a>(
174 field: &'a Field,
175 rows: u64,
176 hint_paths: &HashSet<Vec<&str>>,
177 stats: &mut HashMap<Vec<&'a str>, Json2LeafPathStats>,
178) -> Result<()> {
179 let ArrowDataType::Struct(fields) = field.data_type() else {
180 return InvalidRecordBatchSnafu {
181 reason: format!("JSON2 column '{}' is not a struct", field.name()),
182 }
183 .fail();
184 };
185 let mut paths = Vec::new();
186 for field in fields {
187 if field.name() == JSON2_REMAINDER_FIELD_NAME {
188 continue;
189 }
190 collect_leaf_path_types(field, &mut Vec::new(), &mut paths)?;
191 }
192
193 for (path, data_type) in paths {
194 if hint_paths.contains(path.as_slice()) {
195 continue;
196 }
197 let Some(stat) = stats.get_mut(&path) else {
198 stats.insert(
199 path,
200 Json2LeafPathStats {
201 rows,
202 sources: 1,
203 data_type,
204 is_type_conflicted: false,
205 },
206 );
207 continue;
208 };
209 stat.sources += 1;
210 if stat.data_type != data_type {
211 stat.is_type_conflicted = true;
212 } else {
213 stat.rows += rows;
214 }
215 }
216 Ok(())
217}
218
219fn collect_leaf_path_types<'a>(
220 field: &'a Field,
221 path: &mut Vec<&'a str>,
222 paths: &mut Vec<(Vec<&'a str>, JsonNativeType)>,
223) -> Result<()> {
224 path.push(field.name());
225 if let ArrowDataType::Struct(fields) = field.data_type()
226 && !fields.is_empty()
227 {
228 for field in fields {
229 collect_leaf_path_types(field, path, paths)?;
230 }
231 } else {
232 let json_type =
233 JsonNativeType::try_from(field.data_type()).context(DataTypeMismatchSnafu)?;
234 paths.push((path.clone(), json_type));
235 }
236 path.pop();
237 Ok(())
238}
239
240fn select_dynamic_hints(
241 settings: &JsonSettings,
242 hint_paths: &HashSet<Vec<&str>>,
243 stats: &HashMap<Vec<&str>, Json2LeafPathStats>,
244) -> Vec<JsonTypeHint> {
245 let all_paths = stats
246 .keys()
247 .map(Vec::as_slice)
248 .chain(hint_paths.iter().map(Vec::as_slice))
249 .collect::<HashSet<_>>();
250 let has_ancestor_path =
251 |path: &[&str]| (1..path.len()).any(|len| all_paths.contains(&path[..len]));
252
253 let prefixes = all_paths
254 .iter()
255 .copied()
256 .flat_map(|path| (1..path.len()).map(|len| &path[..len]))
257 .collect::<HashSet<_>>();
258 let has_descendant_path = |path: &[&str]| prefixes.contains(path);
259
260 let mut candidates = stats
261 .iter()
262 .filter(|(path, stat)| {
263 !stat.is_type_conflicted
264 && stat.data_type.is_primitive()
268 && !has_ancestor_path(path)
269 && !has_descendant_path(path)
270 })
271 .collect::<Vec<_>>();
272 candidates.sort_unstable_by(|(x_path, x), (y_path, y)| {
273 y.rows.cmp(&x.rows).then_with(|| x_path.cmp(y_path))
274 });
275 candidates
276 .into_iter()
277 .take(
278 settings
279 .max_auto_expanded_paths()
280 .unwrap_or(JSON2_DEFAULT_MAX_AUTO_EXPANDED_PATHS) as usize,
281 )
282 .map(|(path, stat)| JsonTypeHint {
283 path: path.iter().map(|x| (*x).to_owned()).collect(),
284 data_type: ConcreteDataType::from_arrow_type(&stat.data_type.as_arrow_type()),
285 inverted_index: false,
286 skipping_index: None,
287 fulltext_index: None,
288 })
289 .collect()
290}
291
292pub(crate) fn rewrite_json2_schema(schema: &SchemaRef, plans: &Json2RewritePlans) -> SchemaRef {
294 if plans.is_empty() {
295 return schema.clone();
296 }
297 let fields = schema
298 .fields()
299 .iter()
300 .map(|field| {
301 let Some(plan) = plans.get(field.name()) else {
302 return field.clone();
303 };
304 let mut field = Field::clone(field);
305 field.set_data_type(json2_physical_data_type(&plan.target_layout));
306 field = field.with_extension_type(Json2ExtensionType::new(Arc::new(
307 JsonMetadata::new(plan.logical_settings.clone()),
308 )));
309 Arc::new(field)
310 })
311 .collect::<Vec<_>>();
312 Arc::new(Schema::new_with_metadata(fields, schema.metadata().clone()))
313}
314
315pub(crate) fn rewrite_json2_batch(
317 batch: RecordBatch,
318 plans: &Json2RewritePlans,
319) -> Result<RecordBatch> {
320 if plans.is_empty() {
321 return Ok(batch);
322 }
323 let mut fields = Vec::with_capacity(batch.num_columns());
324 let mut columns = Vec::with_capacity(batch.num_columns());
325
326 for (field, array) in batch.schema_ref().fields().iter().zip(batch.columns()) {
327 let Some(plan) = plans.get(field.name()) else {
328 fields.push(field.clone());
329 columns.push(array.clone());
330 continue;
331 };
332
333 let array = JsonArray::from(array)
334 .rewrite_to_v2_with_type_hint_mismatch_policy(
335 field,
336 &plan.logical_settings,
337 &plan.target_layout,
338 TypeHintMismatchPolicy::CoerceOrNull,
339 )
340 .context(ConvertValueSnafu)?;
341 debug_assert_eq!(
342 &json2_physical_data_type(&plan.target_layout),
343 array.data_type()
344 );
345
346 let mut field = Field::clone(field);
347 field.set_data_type(array.data_type().clone());
348 field = field.with_extension_type(Json2ExtensionType::new(Arc::new(JsonMetadata::new(
349 plan.logical_settings.clone(),
350 ))));
351 fields.push(Arc::new(field));
352 columns.push(array);
353 }
354
355 let schema = Arc::new(Schema::new_with_metadata(
356 fields,
357 batch.schema_ref().metadata().clone(),
358 ));
359 RecordBatch::try_new(schema, columns).context(NewRecordBatchSnafu)
360}
361
362#[cfg(test)]
363mod tests {
364 use datatypes::extension::json::{
365 JSON2_REMAINDER_FIELD_NAME, Json2PhysicalLayout, JsonMetadata,
366 };
367 use datatypes::json::JsonTypeHint;
368 use datatypes::prelude::{ConcreteDataType, DataType};
369 use datatypes::schema::ColumnSchema;
370 use datatypes::types::json_type::{JsonNativeType, JsonObjectType};
371 use serde_json::json;
372
373 use super::*;
374
375 #[test]
376 fn test_select_dynamic_hints_rejects_type_and_prefix_conflicts()
377 -> Result<(), Box<dyn std::error::Error>> {
378 let settings = JsonSettings::try_new(
379 vec![JsonTypeHint {
380 path: vec!["hint".to_string()],
381 data_type: ConcreteDataType::string_datatype(),
382 inverted_index: false,
383 skipping_index: None,
384 fulltext_index: None,
385 }],
386 Some(2),
387 )?;
388 let stat = |rows, data_type, is_type_conflicted| Json2LeafPathStats {
389 rows,
390 sources: 1,
391 data_type,
392 is_type_conflicted,
393 };
394 let stats = HashMap::from([
395 (
396 vec!["hint", "nested"],
397 stat(10, JsonNativeType::String, false),
398 ),
399 (vec!["popular"], stat(9, JsonNativeType::String, false)),
400 (
401 vec!["popular", "nested"],
402 stat(8, JsonNativeType::u64(), false),
403 ),
404 (
405 vec!["type_conflicted"],
406 stat(7, JsonNativeType::String, true),
407 ),
408 (
409 vec!["array"],
410 stat(
411 6,
412 JsonNativeType::Array(Box::new(JsonNativeType::String)),
413 false,
414 ),
415 ),
416 (vec!["variant"], stat(5, JsonNativeType::Variant, false)),
417 (vec!["tie_a"], stat(2, JsonNativeType::String, false)),
418 (vec!["tie_b"], stat(2, JsonNativeType::Bool, false)),
419 ]);
420
421 let hint_paths = settings
422 .type_hints()
423 .iter()
424 .map(|hint| hint.path.iter().map(String::as_str).collect::<Vec<_>>())
425 .collect::<HashSet<_>>();
426 let hints = select_dynamic_hints(&settings, &hint_paths, &stats);
427 assert_eq!(
428 vec![vec!["tie_a".to_string()], vec!["tie_b".to_string()]],
429 hints.into_iter().map(|x| x.path).collect::<Vec<_>>()
430 );
431 Ok(())
432 }
433
434 #[test]
435 fn test_rewrite_json2_v1_batch_to_target_layout() -> Result<(), Box<dyn std::error::Error>> {
436 let settings = JsonSettings::try_new(
437 vec![JsonTypeHint {
438 path: vec!["kind".to_string()],
439 data_type: ConcreteDataType::string_datatype(),
440 inverted_index: false,
441 skipping_index: None,
442 fulltext_index: None,
443 }],
444 Some(0),
445 )?;
446 let target = json2_physical_data_type(&settings);
447 let plans = HashMap::from([(
448 "j".to_string(),
449 Json2RewritePlan {
450 logical_settings: settings.clone(),
451 target_layout: settings,
452 },
453 )]);
454 let values = [
455 json!({"kind": "a", "extra": {"x": 1}}),
456 json!({"kind": "b", "extra": {"x": 2}}),
457 ];
458 let source_settings = JsonSettings::default();
459 let source_extension =
460 Json2ExtensionType::new(Arc::new(JsonMetadata::new_v1(source_settings.clone())));
461 let mut source_column = ColumnSchema::new(
462 "j",
463 ConcreteDataType::json2(JsonNativeType::Object(JsonObjectType::new())),
464 true,
465 );
466 source_column.with_extension_type(&source_extension);
467 let mut source_builder = source_column.data_type.create_mutable_vector(values.len());
468 for value in &values {
469 let value = source_settings.encode(value.clone())?;
470 source_builder.try_push_value_ref(&value.as_value_ref())?;
471 }
472 let source = source_builder.to_vector().to_arrow_array();
473 let field =
474 Field::new("j", source.data_type().clone(), true).with_extension_type(source_extension);
475 let batch = RecordBatch::try_new(Arc::new(Schema::new(vec![field])), vec![source])?;
476
477 let batch = rewrite_json2_batch(batch, &plans)?;
478 let field = batch.schema_ref().field(0);
479 assert!(Json2PhysicalLayout::try_from_root(field)?.is_version_2());
480 assert_eq!(&target, field.data_type());
481 let projected =
482 JsonArray::from(batch.column(0)).project_to_v2(field, &ArrowDataType::Binary)?;
483 let projected = JsonArray::from(&projected);
484 for (i, expected) in values.into_iter().enumerate() {
485 assert_eq!(expected, projected.try_get_value(i)?);
486 }
487 Ok(())
488 }
489
490 #[test]
491 fn test_rewrite_json2_v2_source_to_narrower_target() -> Result<(), Box<dyn std::error::Error>> {
492 let target_settings = JsonSettings::try_new(
493 vec![JsonTypeHint {
494 path: vec!["kind".to_string()],
495 data_type: ConcreteDataType::string_datatype(),
496 inverted_index: false,
497 skipping_index: None,
498 fulltext_index: None,
499 }],
500 Some(0),
501 )?;
502 let target_extension =
503 Json2ExtensionType::new(Arc::new(JsonMetadata::new(target_settings.clone())));
504 let target_type = json2_physical_data_type(&target_settings);
505 let plans = HashMap::from([(
506 "j".to_string(),
507 Json2RewritePlan {
508 logical_settings: target_settings.clone(),
509 target_layout: target_settings,
510 },
511 )]);
512
513 let source_settings = JsonSettings::try_new(
514 vec![
515 JsonTypeHint {
516 path: vec!["kind".to_string()],
517 data_type: ConcreteDataType::string_datatype(),
518 inverted_index: false,
519 skipping_index: None,
520 fulltext_index: None,
521 },
522 JsonTypeHint {
523 path: vec!["source_only".to_string()],
524 data_type: ConcreteDataType::int64_datatype(),
525 inverted_index: false,
526 skipping_index: None,
527 fulltext_index: None,
528 },
529 ],
530 None,
531 )?;
532 let source_extension =
533 Json2ExtensionType::new(Arc::new(JsonMetadata::new(source_settings.clone())));
534 let mut source_column = ColumnSchema::new(
535 "j",
536 ConcreteDataType::json2(JsonNativeType::Object(JsonObjectType::new())),
537 true,
538 );
539 source_column.with_extension_type(&source_extension);
540 let expected = json!({
541 "kind": "a",
542 "source_only": 7,
543 "dynamic": {"nested": true}
544 });
545 let mut builder = source_column.create_mutable_vector(1);
546 let value = source_settings.encode(expected.clone())?;
547 builder.try_push_value_ref(&value.as_value_ref())?;
548 let source = builder.to_vector().to_arrow_array();
549 let source_field =
550 Field::new("j", source.data_type().clone(), true).with_extension_type(source_extension);
551 let source =
552 JsonArray::from(&source).project_to_v2(&source_field, &ArrowDataType::Binary)?;
553 let field = Field::new("j", ArrowDataType::Binary, true)
554 .with_extension_type(target_extension.clone());
555 let batch = RecordBatch::try_new(Arc::new(Schema::new(vec![field])), vec![source])?;
556
557 let batch = rewrite_json2_batch(batch, &plans)?;
558 let field = batch.schema_ref().field(0);
559 assert!(Json2PhysicalLayout::try_from_root(field)?.is_version_2());
560 assert_eq!(&target_type, field.data_type());
561 let ArrowDataType::Struct(fields) = field.data_type() else {
562 unreachable!()
563 };
564 assert_eq!(
565 vec![JSON2_REMAINDER_FIELD_NAME, "kind"],
566 fields.iter().map(|x| x.name().as_str()).collect::<Vec<_>>()
567 );
568
569 let projected =
570 JsonArray::from(batch.column(0)).project_to_v2(field, &ArrowDataType::Binary)?;
571 assert_eq!(expected, JsonArray::from(&projected).try_get_value(0)?);
572
573 let first_schema = batch.schema();
574 let field =
575 Field::new("j", ArrowDataType::Binary, true).with_extension_type(target_extension);
576 let batch = RecordBatch::try_new(Arc::new(Schema::new(vec![field])), vec![projected])?;
577 let batch = rewrite_json2_batch(batch, &plans)?;
578 assert_eq!(first_schema, batch.schema());
579
580 let field = batch.schema_ref().field(0);
581 let projected =
582 JsonArray::from(batch.column(0)).project_to_v2(field, &ArrowDataType::Binary)?;
583 assert_eq!(expected, JsonArray::from(&projected).try_get_value(0)?);
584 Ok(())
585 }
586
587 #[test]
588 fn test_rewrite_json2_v2_source_to_wider_target() -> Result<(), Box<dyn std::error::Error>> {
589 let logical_settings = JsonSettings::try_new(
590 vec![JsonTypeHint {
591 path: vec!["kind".to_string()],
592 data_type: ConcreteDataType::string_datatype(),
593 inverted_index: false,
594 skipping_index: None,
595 fulltext_index: None,
596 }],
597 Some(0),
598 )?;
599 let target_layout = JsonSettings::try_new(
600 vec![
601 JsonTypeHint {
602 path: vec!["kind".to_string()],
603 data_type: ConcreteDataType::string_datatype(),
604 inverted_index: false,
605 skipping_index: None,
606 fulltext_index: None,
607 },
608 JsonTypeHint {
609 path: vec!["promoted".to_string()],
610 data_type: ConcreteDataType::int64_datatype(),
611 inverted_index: false,
612 skipping_index: None,
613 fulltext_index: None,
614 },
615 ],
616 Some(0),
617 )?;
618 let target_type = json2_physical_data_type(&target_layout);
619 let plans = HashMap::from([(
620 "j".to_string(),
621 Json2RewritePlan {
622 logical_settings: logical_settings.clone(),
623 target_layout,
624 },
625 )]);
626
627 let extension =
628 Json2ExtensionType::new(Arc::new(JsonMetadata::new(logical_settings.clone())));
629 let mut column = ColumnSchema::new(
630 "j",
631 ConcreteDataType::json2(JsonNativeType::Object(JsonObjectType::new())),
632 true,
633 );
634 column.with_extension_type(&extension);
635 let expected = json!({
636 "kind": "a",
637 "promoted": 7,
638 "dynamic": {"nested": true}
639 });
640 let mut builder = column.create_mutable_vector(1);
641 let value = logical_settings.encode(expected.clone())?;
642 builder.try_push_value_ref(&value.as_value_ref())?;
643 let source = builder.to_vector().to_arrow_array();
644 assert_eq!(
645 &json2_physical_data_type(&logical_settings),
646 source.data_type()
647 );
648 let field =
649 Field::new("j", source.data_type().clone(), true).with_extension_type(extension);
650 let batch = RecordBatch::try_new(Arc::new(Schema::new(vec![field])), vec![source])?;
651
652 let batch = rewrite_json2_batch(batch, &plans)?;
653 let field = batch.schema_ref().field(0);
654 assert_eq!(&target_type, field.data_type());
655 let ArrowDataType::Struct(fields) = field.data_type() else {
656 unreachable!()
657 };
658 assert_eq!(
659 vec![JSON2_REMAINDER_FIELD_NAME, "kind", "promoted"],
660 fields.iter().map(|x| x.name().as_str()).collect::<Vec<_>>()
661 );
662
663 let array = batch
664 .column(0)
665 .as_any()
666 .downcast_ref::<datatypes::arrow::array::StructArray>()
667 .unwrap();
668 let promoted = array
669 .column_by_name("promoted")
670 .unwrap()
671 .as_any()
672 .downcast_ref::<datatypes::arrow::array::Int64Array>()
673 .unwrap();
674 assert_eq!(7, promoted.value(0));
675
676 let projected =
677 JsonArray::from(batch.column(0)).project_to_v2(field, &ArrowDataType::Binary)?;
678 assert_eq!(expected, JsonArray::from(&projected).try_get_value(0)?);
679 Ok(())
680 }
681}