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