1use std::collections::HashMap;
16
17use arrow_schema::DataType;
18use common_function::scalars::json::json_get::JsonGetWithType;
19use datafusion::datasource::{DefaultTableSource, TableProvider};
20use datafusion_common::tree_node::{Transformed, TreeNode, TreeNodeRecursion};
21use datafusion_common::{Result, plan_datafusion_err, plan_err};
22use datafusion_expr::{Expr, LogicalPlan};
23use datafusion_optimizer::{OptimizerConfig, OptimizerRule};
24use datatypes::extension::json::is_json2_extension_type;
25use datatypes::types::json_type::{JsonNativeType, JsonObjectType};
26use table::table::adapter::DfTableProviderAdapter;
27
28use crate::dummy_catalog::DummyTableProvider;
29
30#[derive(Debug)]
34pub(crate) struct JsonTypeConcretizeRule;
35
36impl OptimizerRule for JsonTypeConcretizeRule {
37 fn name(&self) -> &str {
38 "JsonTypeConcretizeRule"
39 }
40
41 fn rewrite(
42 &self,
43 plan: LogicalPlan,
44 _config: &dyn OptimizerConfig,
45 ) -> Result<Transformed<LogicalPlan>> {
46 let json_types = deduce_json_types(&plan)?;
47 if json_types.is_empty() {
48 return Ok(Transformed::no(plan));
49 }
50
51 plan.transform_down(|plan| match &plan {
52 LogicalPlan::TableScan(table_scan) => {
53 let Some(source) = table_scan
54 .source
55 .as_any()
56 .downcast_ref::<DefaultTableSource>()
57 else {
58 return Ok(Transformed::no(plan));
59 };
60
61 if apply_json_type_hint(source.table_provider.as_ref(), &json_types) {
62 Ok(Transformed::yes(plan))
63 } else {
64 Ok(Transformed::no(plan))
65 }
66 }
67 _ => Ok(Transformed::no(plan)),
68 })
69 }
70}
71
72fn apply_json_type_hint(
79 provider: &dyn TableProvider,
80 json_types: &HashMap<String, JsonNativeType>,
81) -> bool {
82 let schema = provider.schema();
83 let json_types = json_types
84 .iter()
85 .filter(|(column, _)| {
86 schema
87 .fields()
88 .iter()
89 .any(|field| field.name() == *column && is_json2_extension_type(field))
90 })
91 .map(|(column, json_type)| (column.clone(), json_type.clone()))
92 .collect::<HashMap<_, _>>();
93
94 if json_types.is_empty() {
95 return false;
96 }
97
98 if let Some(adapter) = provider.as_any().downcast_ref::<DummyTableProvider>() {
99 adapter.with_json_type_hint(json_types);
100 return true;
101 }
102
103 if let Some(adapter) = provider.as_any().downcast_ref::<DfTableProviderAdapter>() {
104 adapter.with_json_type_hint(json_types);
105 return true;
106 }
107
108 false
109}
110
111pub(crate) fn deduce_json_types(plan: &LogicalPlan) -> Result<HashMap<String, JsonNativeType>> {
112 let mut json_types = HashMap::<String, JsonNativeType>::new();
113
114 plan.schema()
118 .fields()
119 .iter()
120 .filter(|field| is_json2_extension_type(field))
121 .for_each(|field| {
122 json_types.insert(field.name().clone(), JsonNativeType::Variant);
123 });
124
125 plan.apply(|plan| {
126 for expr in plan.expressions() {
127 if matches!(plan, LogicalPlan::Projection(_)) && is_same_name_column_projection(&expr) {
132 continue;
133 }
134 expr.apply(|expr| {
135 if let Some((column, json_type)) = deduce_json_type(expr)? {
136 json_types.entry(column).or_default().merge(&json_type);
137 Ok(TreeNodeRecursion::Jump)
138 } else {
139 Ok(TreeNodeRecursion::Continue)
140 }
141 })?;
142 }
143 Ok(TreeNodeRecursion::Continue)
144 })?;
145 Ok(json_types)
146}
147
148fn is_same_name_column_projection(expr: &Expr) -> bool {
149 match expr {
150 Expr::Column(_) => true,
151 Expr::Alias(alias) => {
152 matches!(alias.expr.as_ref(), Expr::Column(column) if column.name == alias.name)
153 }
154 _ => false,
155 }
156}
157
158fn deduce_json_type(expr: &Expr) -> Result<Option<(String, JsonNativeType)>> {
159 let f = match expr {
160 Expr::ScalarFunction(f) if f.name().eq_ignore_ascii_case(JsonGetWithType::NAME) => f,
161 Expr::Column(c) => return Ok(Some((c.name.clone(), JsonNativeType::Variant))),
162 _ => return Ok(None),
163 };
164
165 let Some(Expr::Column(column)) = f.args.first() else {
166 return plan_err!(
167 "First argument of {} is expected to be a column expr, actual: {:?}",
168 JsonGetWithType::NAME,
169 f.args.first()
170 );
171 };
172
173 let Some(path) = f
174 .args
175 .get(1)
176 .and_then(|expr| expr.as_literal())
177 .and_then(|x| x.try_as_str())
178 .flatten()
179 else {
180 return plan_err!(
181 "Second argument of {} is expected to be a string literal, actual: {:?}",
182 JsonGetWithType::NAME,
183 f.args.get(1)
184 );
185 };
186
187 if path.contains('[') {
190 return Ok(Some((column.name.clone(), JsonNativeType::Variant)));
191 }
192
193 let with_type = f
194 .args
195 .get(2)
196 .and_then(|expr| expr.as_literal())
197 .map(|x| x.data_type())
198 .unwrap_or(DataType::Utf8View);
199 let with_type =
200 JsonNativeType::try_from(&with_type).map_err(|e| plan_datafusion_err!("{e:?}"))?;
201
202 let mut split = path.rsplit(".");
203 let Some(leaf) = split.next().filter(|&x| !x.is_empty() && x != "$") else {
204 return Ok(Some((column.name.clone(), JsonNativeType::String)));
205 };
206
207 let mut object = JsonObjectType::new();
208 object.insert(leaf.to_string(), with_type);
209 let mut root = JsonNativeType::Object(object);
210
211 for s in split {
212 let mut object = JsonObjectType::new();
213 object.insert(s.to_string(), root);
214 root = JsonNativeType::Object(object);
215 }
216
217 Ok(Some((column.name.clone(), root)))
218}
219
220#[cfg(test)]
221mod tests {
222 use std::sync::Arc;
223
224 use api::v1::SemanticType;
225 use common_function::scalars::udf::create_udf;
226 use datafusion::datasource::provider_as_source;
227 use datafusion::functions_aggregate::expr_fn::count;
228 use datafusion_common::{Column, ScalarValue};
229 use datafusion_expr::expr::ScalarFunction;
230 use datafusion_expr::{LogicalPlanBuilder, col, lit};
231 use datafusion_optimizer::OptimizerContext;
232 use datatypes::extension::json::Json2ExtensionType;
233 use datatypes::schema::ColumnSchema;
234 use store_api::metadata::{ColumnMetadata, RegionMetadataBuilder};
235 use store_api::storage::{ConcreteDataType, RegionId};
236
237 use super::*;
238 use crate::optimizer::test_util::{MetaRegionEngine, mock_table_provider};
239
240 fn json_get_expr(base: Expr, path: Expr, with_type: Option<DataType>) -> Result<Expr> {
241 let json_get = Arc::new(create_udf(Arc::new(JsonGetWithType::default())));
242 let mut args = vec![base, path];
243 if let Some(with_type) = with_type {
244 let with_type = ScalarValue::try_new_null(&with_type)?;
245 args.push(Expr::Literal(with_type, None));
246 }
247 Ok(Expr::ScalarFunction(ScalarFunction::new_udf(
248 json_get, args,
249 )))
250 }
251
252 fn path_expr(path: &str) -> Expr {
253 Expr::Literal(ScalarValue::Utf8(Some(path.to_string())), None)
254 }
255
256 fn build_plan(exprs: Vec<Expr>) -> Result<(Arc<DummyTableProvider>, LogicalPlan)> {
257 let provider = Arc::new(mock_table_provider(RegionId::new(1024, 1)));
258 let plan = LogicalPlanBuilder::scan("t", provider_as_source(provider.clone()), None)?
259 .project(exprs)?
260 .build()?;
261 Ok((provider, plan))
262 }
263
264 fn build_json2_scan() -> Result<(Arc<DummyTableProvider>, LogicalPlanBuilder)> {
265 let region_id = RegionId::new(1024, 2);
266 let mut builder = RegionMetadataBuilder::new(region_id);
267 let mut json_column = ColumnSchema::new(
268 "j",
269 ConcreteDataType::json2(JsonNativeType::Object(JsonObjectType::new())),
270 true,
271 );
272 json_column.with_extension_type(&Json2ExtensionType::default());
273 builder
274 .push_column_metadata(ColumnMetadata {
275 column_schema: json_column,
276 semantic_type: SemanticType::Field,
277 column_id: 1,
278 })
279 .push_column_metadata(ColumnMetadata {
280 column_schema: ColumnSchema::new(
281 "ts",
282 ConcreteDataType::timestamp_millisecond_datatype(),
283 false,
284 ),
285 semantic_type: SemanticType::Timestamp,
286 column_id: 2,
287 });
288 let metadata = Arc::new(builder.build().unwrap());
289 let engine = Arc::new(MetaRegionEngine::with_metadata(metadata.clone()));
290 let provider = Arc::new(DummyTableProvider::new(region_id, engine, metadata));
291 let plan = LogicalPlanBuilder::scan("t", provider_as_source(provider.clone()), None)?;
292 Ok((provider, plan))
293 }
294
295 fn build_json2_plan(exprs: Vec<Expr>) -> Result<(Arc<DummyTableProvider>, LogicalPlan)> {
296 let (provider, plan) = build_json2_scan()?;
297 let plan = plan.project(exprs)?.build()?;
298 Ok((provider, plan))
299 }
300
301 #[test]
302 fn test_json_type_concretize_rule_rewrite() -> Result<()> {
303 let exprs = vec![
304 json_get_expr(col("j"), path_expr("a.b"), Some(DataType::Int64))?.alias("ab"),
305 json_get_expr(col("j"), path_expr("a.c"), None)?.alias("ac"),
306 json_get_expr(col("j"), path_expr("d"), Some(DataType::Boolean))?.alias("d"),
307 ];
308 let (provider, plan) = build_json2_plan(exprs)?;
309
310 assert!(
311 JsonTypeConcretizeRule
312 .rewrite(plan, &OptimizerContext::default())?
313 .transformed
314 );
315
316 let expected = JsonNativeType::Object(JsonObjectType::from([
317 (
318 "a".to_string(),
319 JsonNativeType::Object(JsonObjectType::from([
320 ("b".to_string(), JsonNativeType::i64()),
321 ("c".to_string(), JsonNativeType::String),
322 ])),
323 ),
324 ("d".to_string(), JsonNativeType::Bool),
325 ]));
326
327 let request = provider.scan_request();
328 assert_eq!(1, request.json_type_hint.len());
329 assert_eq!(Some(&expected), request.json_type_hint.get("j"));
330 Ok(())
331 }
332
333 #[test]
334 fn test_deduce_json_type_with_list_index() -> Result<()> {
335 let expr = json_get_expr(col("j"), path_expr("l[0]"), Some(DataType::Int64))?;
336
337 assert_eq!(
338 Some(("j".to_string(), JsonNativeType::Variant)),
339 deduce_json_type(&expr)?
340 );
341 Ok(())
342 }
343
344 #[test]
345 fn test_json_type_concretize_rule_conflict_to_variant() -> Result<()> {
346 let exprs = vec![
347 json_get_expr(col("j"), path_expr("a"), Some(DataType::Int64))?.alias("a_num"),
348 json_get_expr(col("j"), path_expr("a.b"), Some(DataType::Boolean))?.alias("a_obj"),
349 ];
350 let (provider, plan) = build_json2_plan(exprs)?;
351
352 assert!(
353 JsonTypeConcretizeRule
354 .rewrite(plan, &OptimizerContext::default())?
355 .transformed
356 );
357
358 let expected = JsonNativeType::Object(JsonObjectType::from([(
359 "a".to_string(),
360 JsonNativeType::Variant,
361 )]));
362 assert_eq!(
363 Some(&expected),
364 provider.scan_request().json_type_hint.get("j")
365 );
366 Ok(())
367 }
368
369 #[test]
370 fn test_json_type_concretize_rule_ignores_non_json2_columns() -> Result<()> {
371 let exprs =
372 vec![json_get_expr(col("k0"), path_expr("a.b"), Some(DataType::Int64))?.alias("ab")];
373 let (provider, plan) = build_plan(exprs)?;
374
375 assert!(
376 !JsonTypeConcretizeRule
377 .rewrite(plan, &OptimizerContext::default())?
378 .transformed
379 );
380 assert!(provider.scan_request().json_type_hint.is_empty());
381 Ok(())
382 }
383
384 #[test]
385 fn test_json_type_concretize_rule_no_json_get() -> Result<()> {
386 let (provider, plan) = build_plan(vec![col("k0"), col("v0")])?;
387
388 assert!(
389 !JsonTypeConcretizeRule
390 .rewrite(plan, &OptimizerContext::default())?
391 .transformed
392 );
393 assert!(provider.scan_request().json_type_hint.is_empty());
394 Ok(())
395 }
396
397 #[test]
398 fn test_allow_json2_path_use_in_intermediate_plan() -> Result<()> {
399 let json_get = json_get_expr(col("j"), path_expr("a"), Some(DataType::Int64))?;
400 let (provider, plan) = build_json2_scan()?;
401 let plan = plan
402 .aggregate(vec![json_get], Vec::<Expr>::new())?
403 .aggregate(Vec::<Expr>::new(), vec![count(lit(1))])?
404 .build()?;
405
406 assert!(
407 JsonTypeConcretizeRule
408 .rewrite(plan, &OptimizerContext::default())?
409 .transformed
410 );
411 assert_eq!(
412 Some(&JsonNativeType::Object(JsonObjectType::from([(
413 "a".to_string(),
414 JsonNativeType::i64(),
415 )]))),
416 provider.scan_request().json_type_hint.get("j")
417 );
418 Ok(())
419 }
420
421 #[test]
422 fn test_allow_json2_projection_by_path() -> Result<()> {
423 let expr = json_get_expr(col("j"), path_expr("a"), Some(DataType::Int64))?;
424 let (provider, plan) = build_json2_plan(vec![expr])?;
425
426 assert!(
427 JsonTypeConcretizeRule
428 .rewrite(plan, &OptimizerContext::default())?
429 .transformed
430 );
431 assert_eq!(
432 Some(&JsonNativeType::Object(JsonObjectType::from([(
433 "a".to_string(),
434 JsonNativeType::i64(),
435 )]))),
436 provider.scan_request().json_type_hint.get("j")
437 );
438 Ok(())
439 }
440
441 #[test]
442 fn test_allow_json2_filter_with_root_projection() -> Result<()> {
443 let predicate =
444 json_get_expr(col("j"), path_expr("a"), Some(DataType::Int64))?.eq(lit(1_i64));
445 let (provider, plan) = build_json2_scan()?;
446 let plan = plan.filter(predicate)?.build()?;
447
448 assert!(
449 JsonTypeConcretizeRule
450 .rewrite(plan, &OptimizerContext::default())?
451 .transformed
452 );
453 assert_eq!(
454 Some(&JsonNativeType::Variant),
455 provider.scan_request().json_type_hint.get("j")
456 );
457 Ok(())
458 }
459
460 #[test]
461 fn test_deduce_json_type_with_non_column_base() -> Result<()> {
462 let expr = json_get_expr(
463 Expr::Literal(ScalarValue::Utf8(Some("{}".to_string())), None),
464 path_expr("a"),
465 Some(DataType::Int64),
466 )?;
467
468 let err = deduce_json_type(&expr).unwrap_err();
469 assert!(
470 err.to_string()
471 .contains("First argument of json_get is expected to be a column expr")
472 );
473 Ok(())
474 }
475
476 #[test]
477 fn test_deduce_json_type_with_non_literal_path() -> Result<()> {
478 let expr = json_get_expr(
479 Expr::Column(Column::new_unqualified("k0")),
480 Expr::Column(Column::new_unqualified("path_col")),
481 Some(DataType::Int64),
482 )?;
483
484 let err = deduce_json_type(&expr).unwrap_err();
485 assert!(
486 err.to_string()
487 .contains("Second argument of json_get is expected to be a string literal")
488 );
489 Ok(())
490 }
491
492 #[test]
493 fn test_deduce_json_type_default_string() -> Result<()> {
494 let expr = json_get_expr(
495 Expr::Column(Column::new_unqualified("k0")),
496 path_expr("a.b"),
497 None,
498 )?;
499
500 let deduced = deduce_json_type(&expr)?;
501 let expected = JsonNativeType::Object(JsonObjectType::from([(
502 "a".to_string(),
503 JsonNativeType::Object(JsonObjectType::from([(
504 "b".to_string(),
505 JsonNativeType::String,
506 )])),
507 )]));
508
509 assert_eq!(Some(("k0".to_string(), expected)), deduced);
510 Ok(())
511 }
512}