Skip to main content

query/datafusion/
json_expr_planner.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::sync::{Arc, LazyLock};
16
17use arrow_schema::Field;
18use arrow_schema::extension::ExtensionType;
19use common_function::scalars::json::json_get::{JsonGetWithType, parse_json_get_path};
20use common_function::scalars::udf::create_udf;
21use datafusion_common::arrow::datatypes::DataType;
22use datafusion_common::{
23    Column, DFSchema, DataFusionError, ExprSchema, Result, ScalarValue, TableReference,
24    plan_datafusion_err,
25};
26use datafusion_expr::expr::{BinaryExpr, ScalarFunction};
27use datafusion_expr::planner::{
28    ExprPlanner, PlannerResult, RawAggregateExpr, RawBinaryExpr, RawFieldAccessExpr, RawScalarExpr,
29    RawWindowExpr,
30};
31use datafusion_expr::type_coercion::functions::{UDFCoercionExt, fields_with_udf};
32use datafusion_expr::{
33    Expr, ExprSchemable, GetFieldAccess, Operator, ScalarUDF, WindowFunctionDefinition,
34};
35use datatypes::extension::json::{
36    Json2ExtensionType, is_any_json_extension_type, is_json2_extension_type,
37    parse_legacy_json2_settings,
38};
39use datatypes::types::json_type::JsonNativeType;
40use jsonb::jsonpath::Path;
41use sqlparser::ast::BinaryOperator;
42
43/// Rewrites JSON-aware SQL expressions into DataFusion expressions.
44///
45/// This planner handles the following cases:
46/// - Rewrites compound identifiers on JSON extension columns into `json_get` function.
47///   For example, `select a.b.c` => `select json_get(a, '$.b.c')`.
48/// - Extends a JSON path with list indexes and fields following an index.
49///   For example, `select a.b[0].c` => `select json_get(a, '$.b[0].c')`.
50/// - Applies JSON2 path type hints to both compound identifiers and explicit `json_get` calls
51///   before inferring types from their enclosing expressions.
52/// - Pushes an "expected type" argument into the `json_get` function when it participates in a
53///   binary operator. So that `json_get` knows the wanted data type when dealing with variant
54///   JSON values.
55///   For example, `select json_get(a, "b.c") + 1` => `select json_get(a, "b.c", NULL::Int64) + 1`.
56/// - Infers the expected type from scalar, aggregate, and window function signatures.
57///   For example, `select abs(a.b.c)` => `select abs(json_get(a, '$.b.c', NULL::Float64))`.
58#[derive(Debug)]
59pub(crate) struct JsonExprPlanner;
60
61impl ExprPlanner for JsonExprPlanner {
62    fn plan_binary_op(
63        &self,
64        expr: RawBinaryExpr,
65        schema: &DFSchema,
66    ) -> Result<PlannerResult<RawBinaryExpr>> {
67        let RawBinaryExpr {
68            op,
69            mut left,
70            mut right,
71        } = expr;
72
73        if !is_untyped_json_get(&left) && !is_untyped_json_get(&right) {
74            return Ok(PlannerResult::Original(RawBinaryExpr { op, left, right }));
75        }
76
77        let Some(expr_op) = parse_sql_op(&op) else {
78            return Ok(PlannerResult::Original(RawBinaryExpr { op, left, right }));
79        };
80
81        let left_type = left.get_type(schema)?;
82        let right_type = right.get_type(schema)?;
83        let left_changed = push_json_get_type_arg(&mut left, &right_type)?;
84        let right_changed = push_json_get_type_arg(&mut right, &left_type)?;
85        if left_changed || right_changed {
86            Ok(PlannerResult::Planned(Expr::BinaryExpr(BinaryExpr::new(
87                Box::new(left),
88                expr_op,
89                Box::new(right),
90            ))))
91        } else {
92            Ok(PlannerResult::Original(RawBinaryExpr { op, left, right }))
93        }
94    }
95
96    /// Extends the path of an untyped `json_get` with one field access.
97    ///
98    /// For `j.o.l[1].inner.l[2]`, `plan_compound_identifier` first produces
99    /// `json_get(j, '$.o.l')`. DataFusion then calls this method successively
100    /// with a list index, two named fields, and another list index, producing
101    /// the final path `$.o.l[1].inner.l[2]`.
102    fn plan_field_access(
103        &self,
104        mut expr: RawFieldAccessExpr,
105        _schema: &DFSchema,
106    ) -> Result<PlannerResult<RawFieldAccessExpr>> {
107        // See `normalize_field_access_after_subscript` for the reason why we construct the
108        // "suffix" like this.
109        let suffix = match &expr.field_access {
110            GetFieldAccess::ListIndex { key } => {
111                // DataFusion parses ordinary integer literals within the i64 range as Int64.
112                let Expr::Literal(ScalarValue::Int64(Some(index)), _) = key.as_ref() else {
113                    return Ok(PlannerResult::Original(expr));
114                };
115                format!("[{index}]")
116            }
117            GetFieldAccess::NamedStructField { name } => {
118                let Some(name) = name.try_as_str().flatten() else {
119                    return Ok(PlannerResult::Original(expr));
120                };
121                format!(".{}", json_path_field(name)?)
122            }
123            GetFieldAccess::ListRange { .. } => return Ok(PlannerResult::Original(expr)),
124        };
125        let Some(json_get) = extract_untyped_json_get(&mut expr.expr) else {
126            return Ok(PlannerResult::Original(expr));
127        };
128        let Some(Expr::Literal(ScalarValue::Utf8(Some(path)), _)) = json_get.args.get_mut(1) else {
129            return Ok(PlannerResult::Original(expr));
130        };
131
132        path.push_str(&suffix);
133        Ok(PlannerResult::Planned(expr.expr))
134    }
135
136    fn plan_compound_identifier(
137        &self,
138        field: &Field,
139        qualifier: Option<&TableReference>,
140        nested_names: &[String],
141    ) -> Result<PlannerResult<Vec<Expr>>> {
142        if !is_any_json_extension_type(field)
143            // SQL-created JSONB columns use Binary with greptime:type=Json, without an Arrow extension.
144            && field
145                .metadata()
146                .get(datatypes::schema::TYPE_KEY)
147                .is_none_or(|value| value != "Json")
148        {
149            return Ok(PlannerResult::Original(Vec::new()));
150        }
151
152        static JSON_GET_UDF: LazyLock<Arc<ScalarUDF>> =
153            LazyLock::new(|| Arc::new(create_udf(Arc::new(JsonGetWithType::default()))));
154
155        let json_get = JSON_GET_UDF.clone();
156        let mut path = "$".to_string();
157        for name in nested_names {
158            path.push('.');
159            path.push_str(&json_path_field(name)?);
160        }
161
162        let mut args = vec![
163            Expr::Column(Column::from((qualifier, field))),
164            Expr::Literal(ScalarValue::Utf8(Some(path)), None),
165        ];
166        apply_json_type_hint(field, nested_names, &mut args)?;
167
168        Ok(PlannerResult::Planned(Expr::ScalarFunction(
169            ScalarFunction::new_udf(json_get, args),
170        )))
171    }
172
173    /// Rewrites JSON2 arguments without taking over the final function planning.
174    ///
175    /// `Original` carries the possibly modified raw expression to subsequent planners and then
176    /// DataFusion's default function construction. Returning `Planned` would short-circuit both.
177    fn plan_scalar(
178        &self,
179        mut expr: RawScalarExpr,
180        schema: &DFSchema,
181    ) -> Result<PlannerResult<RawScalarExpr>> {
182        if expr.func.name().eq_ignore_ascii_case(JsonGetWithType::NAME) && expr.args.len() == 2 {
183            let field = match &expr.args[0] {
184                // Resolving an unqualified column may require JOIN USING information unavailable
185                // in this schema. Skip hint injection if lookup fails and let DataFusion resolve
186                // the column or report the error later.
187                Expr::Column(column) => schema.field_from_column(column).ok().cloned(),
188                // Outer references carry their field metadata from the outer query's schema.
189                Expr::OuterReferenceColumn(field, _) => Some(Arc::clone(field)),
190                _ => None,
191            };
192            if let Some(field) = field
193                && let Some(path) = json_get_object_path(&expr.args[1])
194            {
195                apply_json_type_hint(&field, &path, &mut expr.args)?;
196            }
197        }
198        push_function_arg_types(expr.func.as_ref(), &mut expr.args)?;
199        Ok(PlannerResult::Original(expr))
200    }
201
202    /// Rewrites JSON2 arguments while preserving subsequent aggregate planning.
203    fn plan_aggregate(
204        &self,
205        mut expr: RawAggregateExpr,
206    ) -> Result<PlannerResult<RawAggregateExpr>> {
207        push_function_arg_types(expr.func.as_ref(), &mut expr.args)?;
208        Ok(PlannerResult::Original(expr))
209    }
210
211    /// Rewrites JSON2 arguments while preserving subsequent window planning.
212    fn plan_window(&self, mut expr: RawWindowExpr) -> Result<PlannerResult<RawWindowExpr>> {
213        match &expr.func_def {
214            WindowFunctionDefinition::AggregateUDF(func) => {
215                push_function_arg_types(func.as_ref(), &mut expr.args)?;
216            }
217            WindowFunctionDefinition::WindowUDF(func) => {
218                push_function_arg_types(func.as_ref(), &mut expr.args)?;
219            }
220        }
221        Ok(PlannerResult::Original(expr))
222    }
223}
224
225/// Sets the native read type for an exact hinted object path. Both SQL syntaxes use this before
226/// an enclosing expression can infer a type or push down a cast.
227fn apply_json_type_hint(field: &Field, path: &[String], args: &mut Vec<Expr>) -> Result<()> {
228    if !is_json2_extension_type(field) || args.len() != 2 {
229        return Ok(());
230    }
231    if let Some(json_type) = json_type_hint(field, path)? {
232        args.push(Expr::Literal(
233            ScalarValue::try_new_null(&json_type.as_arrow_type())?,
234            None,
235        ));
236    }
237    Ok(())
238}
239
240/// Resolves `json_get`'s second argument expression (the path) to object field names for type hint
241/// lookup. For example, the string literal `'$.user.age'` resolves to `["user", "age"]`.
242///
243/// Returns `None` for non-string literals, non-literal expressions, invalid JSONPaths, or paths
244/// containing array indexes, wildcards, or filters.
245/// Returning `None` only skips hint injection; the original `json_get` expression is preserved.
246fn json_get_object_path(expr: &Expr) -> Option<Vec<String>> {
247    let path = expr.as_literal()?.try_as_str().flatten()?;
248    // Invalid paths skip hint lookup here. Later planning still validates the path and reports
249    // the error, preserving the existing JSONPath error behavior.
250    let json_path = parse_json_get_path(path).ok()?;
251    let mut names = Vec::new();
252    for segment in json_path.paths {
253        match segment {
254            Path::Root => {}
255            Path::DotField(name) | Path::ColonField(name) | Path::ObjectField(name) => {
256                names.push(name.into_owned());
257            }
258            // Array indexes, wildcards and filters do not identify a hinted object path.
259            _ => return None,
260        }
261    }
262    Some(names)
263}
264
265/// Returns the configured native type for an exact JSON2 object path.
266fn json_type_hint(field: &Field, path: &[String]) -> Result<Option<JsonNativeType>> {
267    let settings = if field.extension_type_name() == Some(Json2ExtensionType::NAME) {
268        let extension = field
269            .try_extension_type::<Json2ExtensionType>()
270            .map_err(|e| plan_datafusion_err!("invalid JSON2 extension metadata: {e}"))?;
271        Some(extension.metadata().json_settings().clone())
272    } else {
273        parse_legacy_json2_settings(field.metadata())
274            .map_err(|e| plan_datafusion_err!("invalid JSON2 extension metadata: {e}"))?
275    };
276
277    Ok(settings.and_then(|settings| {
278        settings
279            .type_hints()
280            .iter()
281            .find(|hint| hint.path == path)
282            .map(|hint| JsonNativeType::from(&hint.data_type))
283    }))
284}
285
286/// Quotes field names containing JSONPath punctuation, preserving literal keys.
287fn json_path_field(name: &str) -> Result<String> {
288    if !name.is_empty() && name.chars().all(|c| c.is_alphanumeric() || c == '_') {
289        Ok(name.to_string())
290    } else {
291        // Reuse serde_json's string escaping.
292        serde_json::to_string(name).map_err(|e| DataFusionError::External(Box::new(e)))
293    }
294}
295
296enum JsonGetTypeResolution {
297    Fallback,
298    Typed(Vec<(usize, DataType)>),
299}
300
301/// Infers static output types for untyped `json_get` arguments from a function signature.
302///
303/// DataFusion requires every expression to have one Arrow data type during planning. A JSON path
304/// may contain heterogeneous values across rows, but it cannot expose those values as different
305/// Arrow types in one result column. Preserving their runtime types would require a single
306/// Variant-like data type and Variant-aware functions instead. Maybe we can wait for
307/// https://github.com/apache/datafusion/issues/16116
308///
309/// This helper uses the function's coercion rules to select a supported output type, then appends
310/// a typed NULL argument to each relevant `json_get`. The typed argument makes `json_get` project
311/// compatible JSON values to that type and return NULL for incompatible values. Functions that
312/// accept json_get's default `Utf8View` output keep the two-argument form so later rewrites can
313/// still push down an outer cast.
314fn push_function_arg_types<F>(func: &F, args: &mut [Expr]) -> Result<()>
315where
316    F: UDFCoercionExt,
317{
318    if !args.iter().any(is_untyped_json_get) {
319        return Ok(());
320    }
321
322    let fields = args.iter().map(function_arg_field).collect::<Vec<_>>();
323    match infer_json_get_types(func, args, &fields) {
324        JsonGetTypeResolution::Fallback => {
325            let Some(data_type) = fallback_json_get_type(func, args, &fields) else {
326                return Ok(());
327            };
328            for arg in args.iter_mut() {
329                if is_untyped_json_get(arg) {
330                    let _ = push_json_get_type_arg(arg, &data_type)?;
331                }
332            }
333        }
334        JsonGetTypeResolution::Typed(types) => {
335            for (index, data_type) in types {
336                let _ = push_json_get_type_arg(&mut args[index], &data_type)?;
337            }
338        }
339    }
340    Ok(())
341}
342
343fn infer_json_get_types<F>(func: &F, args: &[Expr], fields: &[Arc<Field>]) -> JsonGetTypeResolution
344where
345    F: UDFCoercionExt,
346{
347    // Only untyped json_get arguments use Null placeholders; preserve every other known argument
348    // type. fields_with_udf performs contextual coercion rather than reverse inference from a
349    // signature alone. Numeric signatures may preserve all-Null inputs, while Comparable
350    // signatures may default them to Utf8. For example, retaining the Float64 peer in
351    // coalesce(json_get(...), 1.0) lets DataFusion resolve json_get to Float64 instead of Utf8.
352    //
353    // This is a best-effort probe: a failure does not mean the actual function call is invalid, so
354    // try concrete JSON types before leaving final validation to DataFusion's default planner.
355    let Ok(coerced) = fields_with_udf(fields, func) else {
356        return JsonGetTypeResolution::Fallback;
357    };
358
359    let mut inferred_types = Vec::with_capacity(coerced.len());
360    for (index, (arg, field)) in args.iter().zip(coerced).enumerate() {
361        if !is_untyped_json_get(arg) || field.data_type().is_null() {
362            continue;
363        }
364        let Some(data_type) = json_get_output_type(field.data_type()) else {
365            return JsonGetTypeResolution::Fallback;
366        };
367        inferred_types.push((index, data_type));
368    }
369    if inferred_types.is_empty() {
370        JsonGetTypeResolution::Fallback
371    } else {
372        JsonGetTypeResolution::Typed(inferred_types)
373    }
374}
375
376fn fallback_json_get_type<F>(func: &F, args: &[Expr], fields: &[Arc<Field>]) -> Option<DataType>
377where
378    F: UDFCoercionExt,
379{
380    // Prefer json_get's default Utf8View type. If the function rejects strings but accepts numeric
381    // values, prefer Float64 so both integers and fractions remain usable.
382    let mut candidate_fields = fields.to_vec();
383    for data_type in [
384        DataType::Utf8View,
385        DataType::Float64,
386        DataType::Int64,
387        DataType::Boolean,
388    ] {
389        for (index, arg) in args.iter().enumerate() {
390            if is_untyped_json_get(arg) {
391                candidate_fields[index] = Arc::new(
392                    fields[index]
393                        .as_ref()
394                        .clone()
395                        .with_data_type(data_type.clone()),
396                );
397            }
398        }
399        if fields_with_udf(&candidate_fields, func).is_ok() {
400            return Some(data_type);
401        }
402    }
403    None
404}
405
406fn function_arg_field(expr: &Expr) -> Arc<Field> {
407    let data_type = if is_untyped_json_get(expr) {
408        DataType::Null
409    } else if let Some(data_type) = extract_json_get_type(expr) {
410        data_type
411    } else {
412        // Treat unresolved expressions as untyped NULL. This lets signatures such as `power`
413        // infer a JSON type, while functions such as `coalesce` can leave it untyped for default
414        // planning. This is only best-effort: overloaded or user-defined functions may select a
415        // different signature for NULL than for the expression's actual type.
416        // TODO(LFC): Use the input schema once DataFusion passes it to ExprPlanner::plan_*().
417        expr.get_type(&DFSchema::empty()).unwrap_or(DataType::Null)
418    };
419    Arc::new(Field::new("", data_type, true))
420}
421
422fn json_get_output_type(data_type: &DataType) -> Option<DataType> {
423    let output_type = match data_type {
424        DataType::Boolean => DataType::Boolean,
425        data_type if data_type.is_integer() => DataType::Int64,
426        data_type if data_type.is_floating() => DataType::Float64,
427        DataType::Decimal128(_, _) | DataType::Decimal256(_, _) => DataType::Float64,
428        data_type if data_type.is_string() => DataType::Utf8View,
429        _ => return None,
430    };
431    Some(output_type)
432}
433
434macro_rules! is_untyped_json_get_func {
435    ($func:expr) => {
436        $func
437            .func
438            .name()
439            .eq_ignore_ascii_case(JsonGetWithType::NAME)
440            && $func.args.len() == 2
441    };
442}
443
444macro_rules! is_typed_json_get_func {
445    ($func:expr) => {
446        $func
447            .func
448            .name()
449            .eq_ignore_ascii_case(JsonGetWithType::NAME)
450            && $func.args.len() == 3
451    };
452}
453
454fn extract_untyped_json_get(expr: &mut Expr) -> Option<&mut ScalarFunction> {
455    match expr {
456        Expr::ScalarFunction(f) if is_untyped_json_get_func!(f) => Some(f),
457        _ => None,
458    }
459}
460
461fn extract_json_get_type(expr: &Expr) -> Option<DataType> {
462    match expr {
463        Expr::ScalarFunction(f) if is_typed_json_get_func!(f) => f
464            .args
465            .get(2)
466            .and_then(|x| x.as_literal())
467            .map(|x| x.data_type()),
468        _ => None,
469    }
470}
471
472fn is_untyped_json_get(expr: &Expr) -> bool {
473    matches!(
474        expr,
475        Expr::ScalarFunction(f) if is_untyped_json_get_func!(f)
476    )
477}
478
479fn push_json_get_type_arg(expr: &mut Expr, data_type: &DataType) -> Result<bool> {
480    let Some(json_get) = extract_untyped_json_get(expr) else {
481        return Ok(false);
482    };
483
484    // The two-argument form already returns Utf8View. Keep it so JsonGetRewriter can still absorb
485    // a cast added by subsequent function coercion.
486    if data_type.is_string() {
487        return Ok(false);
488    }
489    let with_type = ScalarValue::try_new_null(data_type).map(|x| Expr::Literal(x, None))?;
490    json_get.args.push(with_type);
491    Ok(true)
492}
493
494fn parse_sql_op(op: &BinaryOperator) -> Option<Operator> {
495    match *op {
496        BinaryOperator::Plus => Some(Operator::Plus),
497        BinaryOperator::Minus => Some(Operator::Minus),
498        BinaryOperator::Multiply => Some(Operator::Multiply),
499        BinaryOperator::Divide => Some(Operator::Divide),
500        BinaryOperator::Modulo => Some(Operator::Modulo),
501        BinaryOperator::Gt => Some(Operator::Gt),
502        BinaryOperator::GtEq => Some(Operator::GtEq),
503        BinaryOperator::Lt => Some(Operator::Lt),
504        BinaryOperator::LtEq => Some(Operator::LtEq),
505        BinaryOperator::Eq => Some(Operator::Eq),
506        BinaryOperator::NotEq => Some(Operator::NotEq),
507        BinaryOperator::And => Some(Operator::And),
508        BinaryOperator::Or => Some(Operator::Or),
509        BinaryOperator::BitwiseAnd => Some(Operator::BitwiseAnd),
510        BinaryOperator::BitwiseOr => Some(Operator::BitwiseOr),
511        BinaryOperator::BitwiseXor => Some(Operator::BitwiseXor),
512        _ => None,
513    }
514}
515
516#[cfg(test)]
517mod tests {
518    use arrow_schema::Fields;
519    use datafusion::functions_aggregate::count::count_udaf;
520    use datafusion::functions_aggregate::sum::sum_udaf;
521    use datafusion_expr::WindowFrame;
522    use datafusion_functions::core::coalesce;
523    use datafusion_functions::math::{abs, power};
524    use datatypes::extension::json::{Json2ExtensionType, JsonExtensionType, JsonMetadata};
525    use datatypes::json::{JsonSettings, JsonTypeHint};
526    use datatypes::prelude::ConcreteDataType;
527    use std::collections::HashMap;
528
529    use super::*;
530
531    fn json_get_expr(base: Expr, path: &str) -> Expr {
532        let json_get = Arc::new(create_udf(Arc::new(JsonGetWithType::default())));
533        Expr::ScalarFunction(ScalarFunction::new_udf(
534            json_get,
535            vec![
536                base,
537                Expr::Literal(ScalarValue::Utf8(Some(path.to_string())), None),
538            ],
539        ))
540    }
541
542    #[test]
543    fn test_plan_binary_op() -> Result<()> {
544        let planner = JsonExprPlanner;
545        let schema = DFSchema::from_unqualified_fields(
546            Fields::from(vec![Field::new("value", DataType::Int64, true)]),
547            Default::default(),
548        )?;
549
550        let planned = planner.plan_binary_op(
551            RawBinaryExpr {
552                op: BinaryOperator::Eq,
553                left: json_get_expr(
554                    Expr::Literal(ScalarValue::Binary(Some(b"{\"a\": 1}".to_vec())), None),
555                    "a",
556                ),
557                right: Expr::Column(Column::new_unqualified("value")),
558            },
559            &schema,
560        )?;
561
562        match planned {
563            PlannerResult::Planned(Expr::BinaryExpr(expr)) => {
564                assert_eq!(expr.op, Operator::Eq);
565
566                match expr.left.as_ref() {
567                    Expr::ScalarFunction(func) => {
568                        assert_eq!(func.func.name(), JsonGetWithType::NAME);
569                        assert_eq!(func.args.len(), 3);
570                        assert_eq!(func.args[2], Expr::Literal(ScalarValue::Int64(None), None));
571                    }
572                    other => panic!("expected json_get on left side, got {other:?}"),
573                }
574
575                assert_eq!(
576                    expr.right.as_ref(),
577                    &Expr::Column(Column::new_unqualified("value"))
578                );
579            }
580            other => panic!("expected planned binary expression, got {other:?}"),
581        }
582
583        let original = planner.plan_binary_op(
584            RawBinaryExpr {
585                op: BinaryOperator::StringConcat,
586                left: Expr::Column(Column::new_unqualified("value")),
587                right: Expr::Literal(ScalarValue::Utf8(Some("x".to_string())), None),
588            },
589            &schema,
590        )?;
591
592        match original {
593            PlannerResult::Original(expr) => {
594                assert!(matches!(expr.op, BinaryOperator::StringConcat));
595                assert_eq!(expr.left, Expr::Column(Column::new_unqualified("value")));
596                assert_eq!(
597                    expr.right,
598                    Expr::Literal(ScalarValue::Utf8(Some("x".to_string())), None)
599                );
600            }
601            other => panic!(
602                "expected original expression for unsupported operator, got {:?}",
603                other,
604            ),
605        }
606
607        Ok(())
608    }
609
610    #[test]
611    fn test_plan_list_index() -> Result<()> {
612        let planner = JsonExprPlanner;
613        let planned = planner.plan_field_access(
614            RawFieldAccessExpr {
615                field_access: GetFieldAccess::ListIndex {
616                    key: Box::new(Expr::Literal(ScalarValue::Int64(Some(0)), None)),
617                },
618                expr: json_get_expr(Expr::Column(Column::new_unqualified("j")), "list"),
619            },
620            &DFSchema::empty(),
621        )?;
622        let PlannerResult::Planned(Expr::ScalarFunction(func)) = planned else {
623            unreachable!()
624        };
625        assert_eq!(func.func.name(), JsonGetWithType::NAME);
626        assert_eq!(func.args.len(), 2);
627        assert_eq!(
628            func.args[1],
629            Expr::Literal(ScalarValue::Utf8(Some("list[0]".to_string())), None)
630        );
631        Ok(())
632    }
633
634    #[test]
635    fn test_plan_field_after_list_index() -> Result<()> {
636        let planner = JsonExprPlanner;
637        let planned = planner.plan_field_access(
638            RawFieldAccessExpr {
639                field_access: GetFieldAccess::NamedStructField {
640                    name: ScalarValue::Utf8(Some("a.b".to_string())),
641                },
642                expr: json_get_expr(Expr::Column(Column::new_unqualified("j")), "list[0]"),
643            },
644            &DFSchema::empty(),
645        )?;
646        let PlannerResult::Planned(Expr::ScalarFunction(func)) = planned else {
647            unreachable!()
648        };
649        assert_eq!(
650            func.args[1],
651            Expr::Literal(
652                ScalarValue::Utf8(Some(r#"list[0]."a.b""#.to_string())),
653                None
654            )
655        );
656        Ok(())
657    }
658
659    #[test]
660    fn test_plan_compound_identifier() -> Result<()> {
661        let planner = JsonExprPlanner;
662        let qualifier = TableReference::bare("events");
663        let nested_names = vec!["payload".to_string(), "cpu".to_string()];
664
665        for field in [
666            Field::new("labels", DataType::Struct(Fields::empty()), true)
667                .with_extension_type(Json2ExtensionType::default()),
668            Field::new("labels", DataType::Binary, true).with_extension_type(JsonExtensionType),
669            Field::new("labels", DataType::Binary, true).with_metadata(HashMap::from([(
670                datatypes::schema::TYPE_KEY.to_string(),
671                "Json".to_string(),
672            )])),
673        ] {
674            let planned =
675                planner.plan_compound_identifier(&field, Some(&qualifier), &nested_names)?;
676
677            match planned {
678                PlannerResult::Planned(Expr::ScalarFunction(func)) => {
679                    assert_eq!(func.func.name(), JsonGetWithType::NAME);
680                    assert_eq!(func.args.len(), 2);
681                    assert_eq!(
682                        func.args[0],
683                        Expr::Column(Column::new(Some(qualifier.clone()), "labels"))
684                    );
685                    assert_eq!(
686                        func.args[1],
687                        Expr::Literal(ScalarValue::Utf8(Some("$.payload.cpu".to_string())), None)
688                    );
689                }
690                other => panic!("expected json_get scalar function, got {other:?}"),
691            }
692
693            for (key, path) in [
694                ("http.status_code", r#"$."http.status_code""#),
695                ("a\"b", r#"$."a\"b""#),
696                ("a\\b", r#"$."a\\b""#),
697            ] {
698                let planned = planner.plan_compound_identifier(
699                    &field,
700                    Some(&qualifier),
701                    &[key.to_string()],
702                )?;
703                let PlannerResult::Planned(Expr::ScalarFunction(func)) = planned else {
704                    unreachable!()
705                };
706                assert_eq!(
707                    func.args[1],
708                    Expr::Literal(ScalarValue::Utf8(Some(path.to_string())), None)
709                );
710            }
711
712            let PlannerResult::Planned(Expr::ScalarFunction(func)) = planner
713                .plan_compound_identifier(
714                    &field,
715                    Some(&qualifier),
716                    &["resource".to_string(), "http.status_code".to_string()],
717                )?
718            else {
719                unreachable!()
720            };
721            assert_eq!(
722                func.args[1],
723                Expr::Literal(
724                    ScalarValue::Utf8(Some(r#"$.resource."http.status_code""#.to_string())),
725                    None
726                )
727            );
728        }
729
730        for data_type in [DataType::Utf8, DataType::Binary] {
731            let original = planner.plan_compound_identifier(
732                &Field::new("plain", data_type, true),
733                Some(&qualifier),
734                &nested_names,
735            )?;
736
737            match original {
738                PlannerResult::Original(exprs) => assert!(exprs.is_empty()),
739                other => panic!(
740                    "expected original empty result for non-json field, got {:?}",
741                    other,
742                ),
743            }
744        }
745
746        Ok(())
747    }
748
749    #[test]
750    fn test_plan_compound_identifier_applies_json2_type_hint() -> Result<()> {
751        let settings = JsonSettings::try_new(
752            vec![JsonTypeHint {
753                path: vec!["payload".to_string(), "cpu".to_string()],
754                data_type: ConcreteDataType::int64_datatype(),
755                inverted_index: false,
756            }],
757            None,
758        )
759        .unwrap();
760        let field = Field::new("labels", DataType::Struct(Fields::empty()), true)
761            .with_extension_type(Json2ExtensionType::new(Arc::new(JsonMetadata::new(
762                settings,
763            ))));
764
765        let PlannerResult::Planned(Expr::ScalarFunction(func)) = JsonExprPlanner
766            .plan_compound_identifier(
767                &field,
768                Some(&TableReference::bare("events")),
769                &["payload".to_string(), "cpu".to_string()],
770            )?
771        else {
772            unreachable!()
773        };
774
775        assert_eq!(
776            Some(DataType::Int64),
777            extract_json_get_type(&Expr::ScalarFunction(func))
778        );
779        Ok(())
780    }
781
782    #[test]
783    fn test_plan_json_get_applies_type_hint() -> Result<()> {
784        let settings = JsonSettings::try_new(
785            vec![
786                JsonTypeHint {
787                    path: vec!["payload".to_string(), "cpu".to_string()],
788                    data_type: ConcreteDataType::int64_datatype(),
789                    inverted_index: false,
790                },
791                JsonTypeHint {
792                    path: vec!["payload.cpu".to_string()],
793                    data_type: ConcreteDataType::string_datatype(),
794                    inverted_index: false,
795                },
796            ],
797            None,
798        )
799        .unwrap();
800        let field = Field::new("j", DataType::Struct(Fields::empty()), true).with_extension_type(
801            Json2ExtensionType::new(Arc::new(JsonMetadata::new(settings))),
802        );
803        let relation = TableReference::bare("events");
804        let schema = DFSchema::new_with_metadata(
805            vec![
806                (Some(relation.clone()), Arc::new(field.clone())),
807                (
808                    Some(TableReference::bare("other")),
809                    Arc::new(Field::new("j", DataType::Binary, true)),
810                ),
811            ],
812            Default::default(),
813        )?;
814        let base = Expr::Column(Column::new(Some(relation.clone()), "j"));
815
816        for (path, names, expected) in [
817            ("payload.cpu", vec!["payload", "cpu"], Some(DataType::Int64)),
818            (
819                "$.payload.cpu",
820                vec!["payload", "cpu"],
821                Some(DataType::Int64),
822            ),
823            (
824                r#"$."payload.cpu""#,
825                vec!["payload.cpu"],
826                Some(DataType::Utf8View),
827            ),
828            ("missing", vec!["missing"], None),
829        ] {
830            let Expr::ScalarFunction(function) = json_get_expr(base.clone(), path) else {
831                unreachable!();
832            };
833            let PlannerResult::Original(scalar) = JsonExprPlanner.plan_scalar(
834                RawScalarExpr {
835                    func: function.func,
836                    args: function.args,
837                },
838                &schema,
839            )?
840            else {
841                unreachable!();
842            };
843            let direct = Expr::ScalarFunction(ScalarFunction::new_udf(scalar.func, scalar.args));
844            let names = names.into_iter().map(String::from).collect::<Vec<_>>();
845            let PlannerResult::Planned(dotted) =
846                JsonExprPlanner.plan_compound_identifier(&field, Some(&relation), &names)?
847            else {
848                unreachable!();
849            };
850            assert_eq!(expected, extract_json_get_type(&direct), "{path}");
851            assert_eq!(
852                extract_json_get_type(&dotted),
853                extract_json_get_type(&direct)
854            );
855
856            // Outer function inference must not replace a declared hint.
857            if expected.is_some() {
858                let PlannerResult::Original(outer) = JsonExprPlanner.plan_scalar(
859                    RawScalarExpr {
860                        func: coalesce(),
861                        args: vec![
862                            direct.clone(),
863                            Expr::Literal(ScalarValue::Float64(Some(1.0)), None),
864                        ],
865                    },
866                    &schema,
867                )?
868                else {
869                    unreachable!();
870                };
871                assert_eq!(direct, outer.args[0]);
872            }
873        }
874
875        // Non-object paths cannot match an object type hint. A different table's unhinted
876        // column must not inherit the same-named JSON2 column's hints either.
877        for (base, path) in [
878            (base.clone(), "$"),
879            (base.clone(), "$.payload.*"),
880            (base.clone(), "$.payload.cpu[0]"),
881            (base.clone(), "$.payload["),
882            (Expr::Column(Column::new(Some("other"), "j")), "payload.cpu"),
883        ] {
884            let Expr::ScalarFunction(function) = json_get_expr(base, path) else {
885                unreachable!();
886            };
887            let PlannerResult::Original(scalar) = JsonExprPlanner.plan_scalar(
888                RawScalarExpr {
889                    func: function.func,
890                    args: function.args,
891                },
892                &schema,
893            )?
894            else {
895                unreachable!();
896            };
897            assert_eq!(2, scalar.args.len(), "{path}");
898        }
899        Ok(())
900    }
901
902    #[test]
903    fn test_plan_functions() -> Result<()> {
904        let planner = JsonExprPlanner;
905        let json_get = || json_get_expr(Expr::Column(Column::new_unqualified("j")), "a.b");
906
907        let PlannerResult::Original(scalar) = planner.plan_scalar(
908            RawScalarExpr {
909                func: abs(),
910                args: vec![json_get()],
911            },
912            &DFSchema::empty(),
913        )?
914        else {
915            unreachable!();
916        };
917        assert_eq!(
918            Some(DataType::Float64),
919            extract_json_get_type(&scalar.args[0])
920        );
921
922        let PlannerResult::Original(scalar) = planner.plan_scalar(
923            RawScalarExpr {
924                func: power(),
925                args: vec![
926                    json_get(),
927                    Expr::Column(Column::new_unqualified("exponent")),
928                ],
929            },
930            &DFSchema::empty(),
931        )?
932        else {
933            unreachable!();
934        };
935        assert_eq!(
936            Some(DataType::Float64),
937            extract_json_get_type(&scalar.args[0])
938        );
939
940        let PlannerResult::Original(aggregate) = planner.plan_aggregate(RawAggregateExpr {
941            func: sum_udaf(),
942            args: vec![json_get()],
943            distinct: false,
944            filter: None,
945            order_by: vec![],
946            null_treatment: None,
947        })?
948        else {
949            unreachable!();
950        };
951        assert_eq!(
952            Some(DataType::Float64),
953            extract_json_get_type(&aggregate.args[0])
954        );
955
956        let PlannerResult::Original(count) = planner.plan_aggregate(RawAggregateExpr {
957            func: count_udaf(),
958            args: vec![json_get()],
959            distinct: false,
960            filter: None,
961            order_by: vec![],
962            null_treatment: None,
963        })?
964        else {
965            unreachable!();
966        };
967        assert_eq!(None, extract_json_get_type(&count.args[0]));
968
969        let PlannerResult::Original(window) = planner.plan_window(RawWindowExpr {
970            func_def: WindowFunctionDefinition::AggregateUDF(sum_udaf()),
971            args: vec![json_get()],
972            partition_by: vec![],
973            order_by: vec![],
974            window_frame: WindowFrame::new(None),
975            filter: None,
976            null_treatment: None,
977            distinct: false,
978        })?
979        else {
980            unreachable!();
981        };
982        assert_eq!(
983            Some(DataType::Float64),
984            extract_json_get_type(&window.args[0])
985        );
986        Ok(())
987    }
988
989    #[test]
990    fn test_plan_function_with_mixed_json_get_types() -> Result<()> {
991        let planner = JsonExprPlanner;
992        let json_get = || json_get_expr(Expr::Column(Column::new_unqualified("j")), "a.b");
993        let mut typed = json_get();
994        push_json_get_type_arg(&mut typed, &DataType::Float64)?;
995
996        let PlannerResult::Original(scalar) = planner.plan_scalar(
997            RawScalarExpr {
998                func: coalesce(),
999                args: vec![json_get(), typed],
1000            },
1001            &DFSchema::empty(),
1002        )?
1003        else {
1004            unreachable!();
1005        };
1006        assert_eq!(
1007            Some(DataType::Float64),
1008            extract_json_get_type(&scalar.args[0])
1009        );
1010        assert_eq!(
1011            Some(DataType::Float64),
1012            extract_json_get_type(&scalar.args[1])
1013        );
1014        Ok(())
1015    }
1016}