Skip to main content

query/
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::any::Any;
16use std::borrow::Cow;
17use std::collections::{HashMap, HashSet};
18use std::ops::ControlFlow;
19use std::str::FromStr;
20use std::sync::Arc;
21
22use arrow_schema::DataType;
23use async_trait::async_trait;
24use catalog::table_source::DfTableSourceProvider;
25use common_error::ext::BoxedError;
26use common_query::promql_annotations::promql_annotation_collector;
27use common_telemetry::tracing;
28use datafusion::common::{DFSchema, plan_err};
29use datafusion::execution::SessionStateBuilder;
30use datafusion::execution::context::SessionState;
31use datafusion::sql::planner::PlannerContext;
32use datafusion_common::tree_node::{TreeNode, TreeNodeRecursion};
33use datafusion_common::{ScalarValue, ToDFSchema};
34use datafusion_expr::expr::{Exists, InSubquery};
35use datafusion_expr::{
36    Analyze, Explain, ExplainFormat, Expr as DfExpr, LogicalPlan, LogicalPlanBuilder, PlanType,
37    ToStringifiedPlan, col,
38};
39use datafusion_sql::parser::Statement as DfStatement;
40use datafusion_sql::planner::{IdentNormalizer, ParserOptions, SqlToRel};
41use log_query::LogQuery;
42use promql_parser::parser::EvalStmt;
43use session::context::QueryContextRef;
44use snafu::{ResultExt, ensure};
45use sql::CteContent;
46use sql::ast::Expr as SqlExpr;
47use sql::statements::explain::ExplainStatement;
48use sql::statements::query::Query;
49use sql::statements::statement::Statement;
50use sql::statements::tql::Tql;
51use sqlparser::ast::{AccessExpr, Value, visit_expressions_mut};
52
53use crate::error::{
54    CteColumnSchemaMismatchSnafu, PlanSqlSnafu, QueryPlanSnafu, Result, SqlSnafu,
55    UnimplementedSnafu,
56};
57use crate::log_query::planner::LogQueryPlanner;
58use crate::parser::{DEFAULT_LOOKBACK_STRING, PromQuery, QueryLanguageParser, QueryStatement};
59use crate::promql::planner::PromPlanner;
60use crate::query_engine::{DefaultPlanDecoder, QueryEngineState};
61use crate::range_select::plan_rewrite::RangePlanRewriter;
62use crate::{DfContextProviderAdapter, QueryEngineContext};
63
64#[async_trait]
65pub trait LogicalPlanner: Send + Sync {
66    async fn plan(&self, stmt: &QueryStatement, query_ctx: QueryContextRef) -> Result<LogicalPlan>;
67
68    async fn plan_logs_query(
69        &self,
70        query: LogQuery,
71        query_ctx: QueryContextRef,
72    ) -> Result<LogicalPlan>;
73
74    fn optimize(&self, plan: LogicalPlan) -> Result<LogicalPlan>;
75
76    fn as_any(&self) -> &dyn Any;
77}
78
79pub struct DfLogicalPlanner {
80    engine_state: Arc<QueryEngineState>,
81    session_state: SessionState,
82}
83
84impl DfLogicalPlanner {
85    pub fn new(engine_state: Arc<QueryEngineState>) -> Self {
86        let session_state = engine_state.session_state();
87        Self {
88            engine_state,
89            session_state,
90        }
91    }
92
93    /// Derive a [`SessionState`] whose [`ExecutionProps`] includes
94    /// `query_execution_start_time` if a scheduled time extension is present
95    /// in the query context.
96    fn derive_session_state_with_scheduled_time(
97        &self,
98        query_ctx: &QueryContextRef,
99    ) -> Result<SessionState> {
100        let extensions = query_ctx.extensions();
101        match crate::options::parse_scheduled_time_datetime(&extensions)? {
102            Some(dt) => {
103                let execution_props = self
104                    .session_state
105                    .execution_props()
106                    .clone()
107                    .with_query_execution_start_time(dt);
108                Ok(
109                    SessionStateBuilder::new_from_existing(self.session_state.clone())
110                        .with_execution_props(execution_props)
111                        .build(),
112                )
113            }
114            None => Ok(self.session_state.clone()),
115        }
116    }
117
118    /// Basically the same with `explain_to_plan` in DataFusion, but adapted to Greptime's
119    /// `plan_sql` to support Greptime Statements.
120    async fn explain_to_plan(
121        &self,
122        explain: &ExplainStatement,
123        query_ctx: QueryContextRef,
124    ) -> Result<LogicalPlan> {
125        let plan = self.plan_sql(&explain.statement, query_ctx).await?;
126        if matches!(plan, LogicalPlan::Explain(_)) {
127            return plan_err!("Nested EXPLAINs are not supported").context(PlanSqlSnafu);
128        }
129
130        let verbose = explain.verbose;
131        let analyze = explain.analyze;
132        let format = explain.format.map(|f| f.to_string());
133
134        let plan = Arc::new(plan);
135        let schema = LogicalPlan::explain_schema();
136        let schema = ToDFSchema::to_dfschema_ref(schema)?;
137
138        if verbose && format.is_some() {
139            return plan_err!("EXPLAIN VERBOSE with FORMAT is not supported").context(PlanSqlSnafu);
140        }
141
142        if analyze {
143            // notice format is already set in query context, so can be ignore here
144            Ok(LogicalPlan::Analyze(Analyze {
145                verbose,
146                format: ExplainFormat::Indent,
147                input: plan,
148                schema,
149                analyze_level: None,
150                analyze_categories: None,
151            }))
152        } else {
153            let stringified_plans = vec![plan.to_stringified(PlanType::InitialLogicalPlan)];
154
155            let options = self.session_state.config().options();
156            let format = format
157                .map(|x| ExplainFormat::from_str(&x))
158                .transpose()?
159                .unwrap_or_else(|| options.explain.format.clone());
160
161            Ok(LogicalPlan::Explain(Explain {
162                verbose,
163                explain_format: format,
164                plan,
165                stringified_plans,
166                schema,
167                logical_optimization_succeeded: false,
168                show_statistics: None,
169            }))
170        }
171    }
172
173    #[tracing::instrument(skip_all)]
174    #[async_recursion::async_recursion]
175    async fn plan_sql(&self, stmt: &Statement, query_ctx: QueryContextRef) -> Result<LogicalPlan> {
176        let mut planner_context = PlannerContext::new();
177        let mut stmt = Cow::Borrowed(stmt);
178        let mut is_tql_cte = false;
179
180        // handle explain before normal processing so we can explain Greptime Statements
181        if let Statement::Explain(explain) = stmt.as_ref() {
182            return self.explain_to_plan(explain, query_ctx).await;
183        }
184
185        // Check for hybrid CTEs before normal processing
186        if self.has_hybrid_ctes(stmt.as_ref()) {
187            let stmt_owned = stmt.into_owned();
188            let mut query = match stmt_owned {
189                Statement::Query(query) => query.as_ref().clone(),
190                _ => unreachable!("has_hybrid_ctes should only return true for Query statements"),
191            };
192            self.plan_query_with_hybrid_ctes(&query, query_ctx.clone(), &mut planner_context)
193                .await?;
194
195            // remove the processed TQL CTEs from the query
196            query.hybrid_cte = None;
197            stmt = Cow::Owned(Statement::Query(Box::new(query)));
198            is_tql_cte = true;
199        }
200
201        let mut df_stmt = stmt.as_ref().try_into().context(SqlSnafu)?;
202        normalize_field_access_after_subscript(
203            &mut df_stmt,
204            self.session_state
205                .config_options()
206                .sql_parser
207                .enable_ident_normalization,
208        );
209
210        // TODO(LFC): Remove this when Datafusion supports **both** the syntax and implementation of "explain with format".
211        if let datafusion::sql::parser::Statement::Statement(
212            box datafusion::sql::sqlparser::ast::Statement::Explain { .. },
213        ) = &mut df_stmt
214        {
215            UnimplementedSnafu {
216                operation: "EXPLAIN with FORMAT using raw datafusion planner",
217            }
218            .fail()?;
219        }
220
221        let scheduled_state = self.derive_session_state_with_scheduled_time(&query_ctx)?;
222        let table_provider = DfTableSourceProvider::new(
223            self.engine_state.catalog_manager().clone(),
224            self.engine_state.disallow_cross_catalog_query(),
225            query_ctx.clone(),
226            Arc::new(DefaultPlanDecoder::new(
227                scheduled_state.clone(),
228                &query_ctx,
229            )?),
230            scheduled_state
231                .config_options()
232                .sql_parser
233                .enable_ident_normalization,
234        );
235
236        let context_provider = DfContextProviderAdapter::try_new(
237            self.engine_state.clone(),
238            scheduled_state.clone(),
239            Some(&df_stmt),
240            query_ctx.clone(),
241        )
242        .await?;
243
244        let config_options = self.session_state.config().options();
245        let parser_options = &config_options.sql_parser;
246        let parser_options = ParserOptions {
247            map_string_types_to_utf8view: false,
248            ..parser_options.into()
249        };
250
251        let sql_to_rel = SqlToRel::new_with_options(&context_provider, parser_options);
252
253        // this IF is to handle different version of ASTs
254        let result = if is_tql_cte {
255            let Statement::Query(query) = stmt.into_owned() else {
256                unreachable!("is_tql_cte should only be true for Query statements");
257            };
258            let sqlparser_stmt = sqlparser::ast::Statement::Query(Box::new(query.inner));
259            sql_to_rel
260                .sql_statement_to_plan_with_context(sqlparser_stmt, &mut planner_context)
261                .context(PlanSqlSnafu)?
262        } else {
263            sql_to_rel
264                .statement_to_plan(df_stmt)
265                .context(PlanSqlSnafu)?
266        };
267
268        common_telemetry::debug!("Logical planner, statement to plan result: {result}");
269        let plan = RangePlanRewriter::new(table_provider, query_ctx.clone())
270            .rewrite(result)
271            .await?;
272
273        // Optimize logical plan by extension rules
274        let context = QueryEngineContext::new(scheduled_state, query_ctx);
275        let plan = self
276            .engine_state
277            .optimize_by_extension_rules(plan, &context)?;
278        common_telemetry::debug!("Logical planner, optimize result: {plan}");
279
280        Ok(plan)
281    }
282
283    /// Generate a relational expression from a SQL expression
284    #[tracing::instrument(skip_all)]
285    pub(crate) async fn sql_to_expr(
286        &self,
287        sql: SqlExpr,
288        schema: &DFSchema,
289        normalize_ident: bool,
290        query_ctx: QueryContextRef,
291    ) -> Result<DfExpr> {
292        let scheduled_state = self.derive_session_state_with_scheduled_time(&query_ctx)?;
293        let context_provider = DfContextProviderAdapter::try_new(
294            self.engine_state.clone(),
295            scheduled_state,
296            None,
297            query_ctx,
298        )
299        .await?;
300
301        let config_options = self.session_state.config().options();
302        let parser_options = &config_options.sql_parser;
303        let parser_options: ParserOptions = ParserOptions {
304            map_string_types_to_utf8view: false,
305            enable_ident_normalization: normalize_ident,
306            ..parser_options.into()
307        };
308
309        let sql_to_rel = SqlToRel::new_with_options(&context_provider, parser_options);
310
311        Ok(sql_to_rel.sql_to_expr(sql, schema, &mut PlannerContext::new())?)
312    }
313
314    #[tracing::instrument(skip_all)]
315    async fn plan_pql(&self, stmt: &EvalStmt, query_ctx: QueryContextRef) -> Result<LogicalPlan> {
316        let mut scheduled_state = self.derive_session_state_with_scheduled_time(&query_ctx)?;
317        let promql_annotations = query_ctx.remote_query_id().map(promql_annotation_collector);
318        if let Some(collector) = &promql_annotations {
319            scheduled_state
320                .config_mut()
321                .options_mut()
322                .extensions
323                .insert(collector.clone());
324        }
325        let plan_decoder = Arc::new(DefaultPlanDecoder::new(
326            scheduled_state.clone(),
327            &query_ctx,
328        )?);
329        let table_provider = DfTableSourceProvider::new(
330            self.engine_state.catalog_manager().clone(),
331            self.engine_state.disallow_cross_catalog_query(),
332            query_ctx.clone(),
333            plan_decoder,
334            scheduled_state
335                .config_options()
336                .sql_parser
337                .enable_ident_normalization,
338        );
339        let plan = PromPlanner::stmt_to_plan_with_annotations(
340            table_provider,
341            stmt,
342            &self.engine_state,
343            promql_annotations,
344        )
345        .await
346        .map_err(BoxedError::new)
347        .context(QueryPlanSnafu)?;
348
349        let context = QueryEngineContext::new(scheduled_state, query_ctx);
350        Ok(self
351            .engine_state
352            .optimize_by_extension_rules(plan, &context)?)
353    }
354
355    #[tracing::instrument(skip_all)]
356    fn optimize_logical_plan(&self, plan: LogicalPlan) -> Result<LogicalPlan> {
357        Ok(self.engine_state.optimize_logical_plan(plan)?)
358    }
359
360    /// Check if a statement contains hybrid CTEs (mix of SQL and TQL)
361    fn has_hybrid_ctes(&self, stmt: &Statement) -> bool {
362        if let Statement::Query(query) = stmt {
363            query
364                .hybrid_cte
365                .as_ref()
366                .map(|hybrid_cte| !hybrid_cte.cte_tables.is_empty())
367                .unwrap_or(false)
368        } else {
369            false
370        }
371    }
372
373    /// Plan a query with hybrid CTEs using DataFusion's native PlannerContext
374    async fn plan_query_with_hybrid_ctes(
375        &self,
376        query: &Query,
377        query_ctx: QueryContextRef,
378        planner_context: &mut PlannerContext,
379    ) -> Result<()> {
380        let hybrid_cte = query.hybrid_cte.as_ref().unwrap();
381
382        for cte in &hybrid_cte.cte_tables {
383            match &cte.content {
384                CteContent::Tql(tql) => {
385                    // Plan TQL and register in PlannerContext
386                    let mut logical_plan = self.tql_to_logical_plan(tql, query_ctx.clone()).await?;
387                    if !cte.columns.is_empty() {
388                        let schema = logical_plan.schema();
389                        let schema_fields = schema.fields().to_vec();
390                        ensure!(
391                            schema_fields.len() == cte.columns.len(),
392                            CteColumnSchemaMismatchSnafu {
393                                cte_name: cte.name.value.clone(),
394                                original: schema_fields
395                                    .iter()
396                                    .map(|field| field.name().clone())
397                                    .collect::<Vec<_>>(),
398                                expected: cte
399                                    .columns
400                                    .iter()
401                                    .map(|column| column.to_string())
402                                    .collect::<Vec<_>>(),
403                            }
404                        );
405                        let aliases = cte
406                            .columns
407                            .iter()
408                            .zip(schema_fields.iter())
409                            .map(|(column, field)| col(field.name()).alias(column.to_string()));
410                        logical_plan = LogicalPlanBuilder::from(logical_plan)
411                            .project(aliases)
412                            .context(PlanSqlSnafu)?
413                            .build()
414                            .context(PlanSqlSnafu)?;
415                    }
416
417                    // Wrap in SubqueryAlias to ensure proper table qualification for CTE
418                    logical_plan = LogicalPlan::SubqueryAlias(
419                        datafusion_expr::SubqueryAlias::try_new(
420                            Arc::new(logical_plan),
421                            cte.name.value.clone(),
422                        )
423                        .context(PlanSqlSnafu)?,
424                    );
425
426                    planner_context.insert_cte(&cte.name.value, logical_plan);
427                }
428                CteContent::Sql(_) => {
429                    // SQL CTEs should have been moved to the main query's WITH clause
430                    // during parsing, so we shouldn't encounter them here
431                    unreachable!("SQL CTEs should not be in hybrid_cte.cte_tables");
432                }
433            }
434        }
435
436        Ok(())
437    }
438
439    /// Convert TQL to LogicalPlan directly
440    async fn tql_to_logical_plan(
441        &self,
442        tql: &Tql,
443        query_ctx: QueryContextRef,
444    ) -> Result<LogicalPlan> {
445        match tql {
446            Tql::Eval(eval) => {
447                // Convert TqlEval to PromQuery then to QueryStatement::Promql
448                let prom_query = PromQuery {
449                    query: eval.query.clone(),
450                    start: eval.start.clone(),
451                    end: eval.end.clone(),
452                    step: eval.step.clone(),
453                    lookback: eval
454                        .lookback
455                        .clone()
456                        .unwrap_or_else(|| DEFAULT_LOOKBACK_STRING.to_string()),
457                    alias: eval.alias.clone(),
458                };
459                let stmt = QueryLanguageParser::parse_promql(&prom_query, &query_ctx)?;
460
461                self.plan(&stmt, query_ctx).await
462            }
463            Tql::Explain(_) => UnimplementedSnafu {
464                operation: "TQL EXPLAIN in CTEs",
465            }
466            .fail(),
467            Tql::Analyze(_) => UnimplementedSnafu {
468                operation: "TQL ANALYZE in CTEs",
469            }
470            .fail(),
471        }
472    }
473
474    /// Extracts cast types for all placeholders in a logical plan.
475    /// Returns a map where each placeholder ID is mapped to:
476    /// - Some(DataType) if the placeholder is cast to a specific type
477    /// - None if the placeholder exists but has no cast
478    ///
479    /// Example: `$1::TEXT` returns `{"$1": Some(DataType::Utf8)}`
480    ///
481    /// This function walks through all expressions in the logical plan,
482    /// including subqueries, to identify placeholders and their cast types.
483    fn extract_placeholder_cast_types(
484        plan: &LogicalPlan,
485    ) -> Result<HashMap<String, Option<DataType>>> {
486        let mut placeholder_types = HashMap::new();
487        let mut casted_placeholders = HashSet::new();
488
489        Self::extract_from_plan(plan, &mut placeholder_types, &mut casted_placeholders)?;
490
491        Ok(placeholder_types)
492    }
493
494    fn extract_from_plan(
495        plan: &LogicalPlan,
496        placeholder_types: &mut HashMap<String, Option<DataType>>,
497        casted_placeholders: &mut HashSet<String>,
498    ) -> Result<()> {
499        plan.apply(|node| {
500            for expr in node.expressions() {
501                let _ = expr.apply(|e| {
502                    // Handle casted placeholders
503                    if let DfExpr::Cast(cast) = e
504                        && let DfExpr::Placeholder(ph) = &*cast.expr
505                    {
506                        placeholder_types
507                            .insert(ph.id.clone(), Some(cast.field.data_type().clone()));
508                        casted_placeholders.insert(ph.id.clone());
509                    }
510
511                    // Handle arrow_cast(Placeholder, 'type_string') generated by SQL rewriter
512                    if let DfExpr::ScalarFunction(scalar_func) = e
513                        && scalar_func.name() == "arrow_cast"
514                        && scalar_func.args.len() == 2
515                        && let DfExpr::Placeholder(ph) = &scalar_func.args[0]
516                        && let DfExpr::Literal(ScalarValue::Utf8(Some(type_str)), _) =
517                            &scalar_func.args[1]
518                        && let Ok(data_type) = type_str.parse::<DataType>()
519                    {
520                        placeholder_types.insert(ph.id.clone(), Some(data_type));
521                        casted_placeholders.insert(ph.id.clone());
522                    }
523
524                    // Handle bare (non-casted) placeholders
525                    if let DfExpr::Placeholder(ph) = e
526                        && !casted_placeholders.contains(&ph.id)
527                        && !placeholder_types.contains_key(&ph.id)
528                    {
529                        placeholder_types.insert(ph.id.clone(), None);
530                    }
531
532                    // Recurse into subquery plans embedded in expressions
533                    match e {
534                        DfExpr::Exists(Exists { subquery, .. })
535                        | DfExpr::InSubquery(InSubquery { subquery, .. })
536                        | DfExpr::ScalarSubquery(subquery) => {
537                            Self::extract_from_plan(
538                                &subquery.subquery,
539                                placeholder_types,
540                                casted_placeholders,
541                            )?;
542                        }
543                        _ => {}
544                    }
545
546                    Ok(TreeNodeRecursion::Continue)
547                });
548            }
549            Ok(TreeNodeRecursion::Continue)
550        })?;
551        Ok(())
552    }
553
554    fn infer_limit_placeholder_types(
555        plan: &LogicalPlan,
556        placeholder_types: &mut HashMap<String, Option<DataType>>,
557    ) -> Result<()> {
558        plan.apply(|node| {
559            if let LogicalPlan::Limit(limit) = node {
560                for expr in limit.skip.iter().chain(limit.fetch.iter()) {
561                    expr.apply(|e| {
562                        if let DfExpr::Placeholder(ph) = e {
563                            placeholder_types
564                                .entry(ph.id.clone())
565                                .and_modify(|existing| {
566                                    if existing.is_none() {
567                                        *existing = Some(DataType::Int64);
568                                    }
569                                })
570                                .or_insert(Some(DataType::Int64));
571                        }
572
573                        Ok(TreeNodeRecursion::Continue)
574                    })?;
575                }
576            }
577
578            Ok(TreeNodeRecursion::Continue)
579        })?;
580
581        Ok(())
582    }
583
584    /// Gets inferred parameter types from a logical plan.
585    /// Returns a map where each parameter ID is mapped to:
586    /// - Some(DataType) if the parameter type could be inferred
587    /// - None if the parameter type could not be inferred
588    ///
589    /// This function first uses DataFusion's `get_parameter_types()` to infer types.
590    /// If any parameters have `None` values (i.e., DataFusion couldn't infer their types),
591    /// it falls back to using `extract_placeholder_cast_types()` to detect explicit casts
592    /// and applies context-specific inference such as LIMIT/OFFSET placeholders.
593    ///
594    /// This is because datafusion can only infer types for a limited cases.
595    ///
596    /// Example: For query `WHERE $1::TEXT AND $2`, DataFusion may not infer `$2`'s type,
597    /// but this function will return `{"$1": Some(DataType::Utf8), "$2": None}`.
598    pub fn get_inferred_parameter_types(
599        plan: &LogicalPlan,
600    ) -> Result<HashMap<String, Option<DataType>>> {
601        let mut param_types = plan.get_parameter_types().context(PlanSqlSnafu)?;
602
603        let has_none = param_types.values().any(|v| v.is_none());
604
605        if has_none {
606            let cast_types = Self::extract_placeholder_cast_types(plan)?;
607
608            for (id, opt_type) in cast_types {
609                param_types
610                    .entry(id)
611                    .and_modify(|existing| {
612                        if existing.is_none() {
613                            *existing = opt_type.clone();
614                        }
615                    })
616                    .or_insert(opt_type);
617            }
618
619            Self::infer_limit_placeholder_types(plan, &mut param_types)?;
620        }
621
622        Ok(param_types)
623    }
624}
625
626/// Normalizes dot field accesses that follow a subscript for DataFusion.
627///
628/// sqlparser represents `j.o.l[1].inner.l[2]` as a compound field access with
629/// the following access chain:
630///
631/// ```text
632/// Dot(Identifier("o")),
633/// Dot(Identifier("l")),
634/// Subscript(1),
635/// Dot(Identifier("inner")),
636/// Dot(Identifier("l")),
637/// Subscript(2)
638/// ```
639///
640/// DataFusion first resolves the leading `j.o.l` through
641/// `JsonExprPlanner::plan_compound_identifier`, which produces an untyped
642/// `json_get` with path `$.o.l`. Before invoking `JsonExprPlanner::plan_field_access`,
643/// however, DataFusion eagerly converts every remaining access into a
644/// `GetFieldAccess`. It accepts string values but not [`SqlExpr::Identifier`]s
645/// in [`AccessExpr::Dot`] after a subscript. Without this normalization, that
646/// conversion fails at `.inner`, and `plan_field_access` is never called, even
647/// for the preceding `[1]`.
648///
649/// This function converts dot identifiers after the first subscript into
650/// `Dot(Value(SingleQuotedString(...)))`, applying DataFusion's identifier
651/// normalization before discarding whether each identifier was quoted. It
652/// changes neither the SQL text nor the dot accesses into subscript nodes: the
653/// resulting AST is conceptually `j.o.l[1].'inner'.'l'[2]`. DataFusion converts
654/// the string-valued dot accesses into named field accesses, which
655/// `plan_field_access` safely encodes as dot members, quoting names when needed. It can then extend the
656/// JSON path to `$.o.l[1].inner.l[2]`.
657///
658/// This behavior is unchanged in the latest upstream releases checked here:
659/// DataFusion 55.0.0 and sqlparser 0.62.0.
660///
661/// TODO(LFC): Remove this workaround after upstream supports dot identifiers after subscripts.
662fn normalize_field_access_after_subscript(stmt: &mut DfStatement, normalize_ident: bool) {
663    let DfStatement::Statement(stmt) = stmt else {
664        return;
665    };
666    let normalizer = IdentNormalizer::new(normalize_ident);
667
668    let _ = visit_expressions_mut(stmt.as_mut(), |expr| {
669        let SqlExpr::CompoundFieldAccess { access_chain, .. } = expr else {
670            return ControlFlow::<()>::Continue(());
671        };
672        let Some(index) = access_chain
673            .iter()
674            .position(|x| matches!(x, AccessExpr::Subscript(_)))
675        else {
676            return ControlFlow::Continue(());
677        };
678
679        for access in &mut access_chain[index + 1..] {
680            let AccessExpr::Dot(SqlExpr::Identifier(ident)) = access else {
681                continue;
682            };
683            let value = normalizer.normalize(ident.clone());
684            *access = AccessExpr::Dot(SqlExpr::Value(
685                Value::SingleQuotedString(value).with_span(ident.span),
686            ));
687        }
688        ControlFlow::Continue(())
689    });
690}
691
692#[async_trait]
693impl LogicalPlanner for DfLogicalPlanner {
694    #[tracing::instrument(skip_all)]
695    async fn plan(&self, stmt: &QueryStatement, query_ctx: QueryContextRef) -> Result<LogicalPlan> {
696        match stmt {
697            QueryStatement::Sql(stmt) => self.plan_sql(stmt, query_ctx).await,
698            QueryStatement::Promql(stmt, _alias) => self.plan_pql(stmt, query_ctx).await,
699        }
700    }
701
702    async fn plan_logs_query(
703        &self,
704        query: LogQuery,
705        query_ctx: QueryContextRef,
706    ) -> Result<LogicalPlan> {
707        let plan_decoder = Arc::new(DefaultPlanDecoder::new(
708            self.session_state.clone(),
709            &query_ctx,
710        )?);
711        let table_provider = DfTableSourceProvider::new(
712            self.engine_state.catalog_manager().clone(),
713            self.engine_state.disallow_cross_catalog_query(),
714            query_ctx,
715            plan_decoder,
716            self.session_state
717                .config_options()
718                .sql_parser
719                .enable_ident_normalization,
720        );
721
722        let mut planner = LogQueryPlanner::new(table_provider, self.session_state.clone());
723        planner
724            .query_to_plan(query)
725            .await
726            .map_err(BoxedError::new)
727            .context(QueryPlanSnafu)
728    }
729
730    fn optimize(&self, plan: LogicalPlan) -> Result<LogicalPlan> {
731        self.optimize_logical_plan(plan)
732    }
733
734    fn as_any(&self) -> &dyn Any {
735        self
736    }
737}
738
739#[cfg(test)]
740mod tests {
741    use std::sync::Arc;
742
743    use arrow_schema::DataType;
744    use catalog::RegisterTableRequest;
745    use catalog::memory::MemoryCatalogManager;
746    use common_catalog::consts::{DEFAULT_CATALOG_NAME, DEFAULT_SCHEMA_NAME};
747    use common_time::Timezone;
748    use datatypes::prelude::ConcreteDataType;
749    use datatypes::schema::{ColumnSchema, Schema};
750    use session::context::{QueryContext, QueryContextBuilder};
751    use store_api::metric_engine_consts::{
752        DATA_SCHEMA_TABLE_ID_COLUMN_NAME, DATA_SCHEMA_TSID_COLUMN_NAME, LOGICAL_TABLE_METADATA_KEY,
753        METRIC_ENGINE_NAME,
754    };
755    use table::metadata::{TableInfoBuilder, TableMetaBuilder};
756    use table::test_util::EmptyTable;
757
758    use super::*;
759    use crate::parser::{PromQuery, QueryLanguageParser};
760    use crate::{QueryEngineFactory, QueryEngineRef};
761
762    async fn create_test_engine() -> QueryEngineRef {
763        let columns = vec![
764            ColumnSchema::new("id", ConcreteDataType::int32_datatype(), false),
765            ColumnSchema::new("name", ConcreteDataType::string_datatype(), true),
766        ];
767        let schema = Arc::new(Schema::new(columns));
768        let table_meta = TableMetaBuilder::empty()
769            .schema(schema)
770            .primary_key_indices(vec![0])
771            .value_indices(vec![1])
772            .next_column_id(1024)
773            .build()
774            .unwrap();
775        let table_info = TableInfoBuilder::new("test", table_meta).build().unwrap();
776        let table = EmptyTable::from_table_info(&table_info);
777
778        crate::tests::new_query_engine_with_table(table)
779    }
780
781    async fn create_timestamp_test_engine() -> QueryEngineRef {
782        let columns = vec![
783            ColumnSchema::new(
784                "ts",
785                ConcreteDataType::timestamp_millisecond_datatype(),
786                false,
787            )
788            .with_time_index(true),
789            ColumnSchema::new(
790                "st",
791                ConcreteDataType::timestamp_millisecond_datatype(),
792                false,
793            ),
794            ColumnSchema::new("note", ConcreteDataType::string_datatype(), true),
795            ColumnSchema::new(
796                "ts_ns",
797                ConcreteDataType::timestamp_nanosecond_datatype(),
798                true,
799            ),
800        ];
801        let schema = Arc::new(Schema::new(columns));
802        let table_meta = TableMetaBuilder::empty()
803            .schema(schema)
804            .primary_key_indices(vec![])
805            .value_indices(vec![0, 1, 2, 3])
806            .next_column_id(1024)
807            .build()
808            .unwrap();
809        let table_info = TableInfoBuilder::new("timestamps", table_meta)
810            .build()
811            .unwrap();
812        let table = EmptyTable::from_table_info(&table_info);
813
814        crate::tests::new_query_engine_with_table(table)
815    }
816
817    fn create_promql_test_engine() -> QueryEngineRef {
818        let catalog_manager = MemoryCatalogManager::with_default_setup();
819        let physical_table_name = "phy";
820        let physical_table_id = 999u32;
821
822        let physical_schema = Arc::new(Schema::new(vec![
823            ColumnSchema::new(
824                DATA_SCHEMA_TABLE_ID_COLUMN_NAME.to_string(),
825                ConcreteDataType::uint32_datatype(),
826                false,
827            ),
828            ColumnSchema::new(
829                DATA_SCHEMA_TSID_COLUMN_NAME.to_string(),
830                ConcreteDataType::uint64_datatype(),
831                false,
832            ),
833            ColumnSchema::new("tag_0", ConcreteDataType::string_datatype(), false),
834            ColumnSchema::new("tag_1", ConcreteDataType::string_datatype(), false),
835            ColumnSchema::new(
836                "timestamp",
837                ConcreteDataType::timestamp_millisecond_datatype(),
838                false,
839            )
840            .with_time_index(true),
841            ColumnSchema::new("field_0", ConcreteDataType::float64_datatype(), true),
842        ]));
843        let physical_meta = TableMetaBuilder::empty()
844            .schema(physical_schema)
845            .primary_key_indices(vec![0, 1, 2, 3])
846            .value_indices(vec![4, 5])
847            .engine(METRIC_ENGINE_NAME.to_string())
848            .next_column_id(1024)
849            .build()
850            .unwrap();
851        let physical_info = TableInfoBuilder::default()
852            .table_id(physical_table_id)
853            .name(physical_table_name)
854            .meta(physical_meta)
855            .build()
856            .unwrap();
857        catalog_manager
858            .register_table_sync(RegisterTableRequest {
859                catalog: DEFAULT_CATALOG_NAME.to_string(),
860                schema: DEFAULT_SCHEMA_NAME.to_string(),
861                table_name: physical_table_name.to_string(),
862                table_id: physical_table_id,
863                table: EmptyTable::from_table_info(&physical_info),
864            })
865            .unwrap();
866
867        let mut options = table::requests::TableOptions::default();
868        options.extra_options.insert(
869            LOGICAL_TABLE_METADATA_KEY.to_string(),
870            physical_table_name.to_string(),
871        );
872        let logical_schema = Arc::new(Schema::new(vec![
873            ColumnSchema::new("tag_0", ConcreteDataType::string_datatype(), false),
874            ColumnSchema::new("tag_1", ConcreteDataType::string_datatype(), false),
875            ColumnSchema::new(
876                "timestamp",
877                ConcreteDataType::timestamp_millisecond_datatype(),
878                false,
879            )
880            .with_time_index(true),
881            ColumnSchema::new("field_0", ConcreteDataType::float64_datatype(), true),
882        ]));
883        let logical_meta = TableMetaBuilder::empty()
884            .schema(logical_schema)
885            .primary_key_indices(vec![0, 1])
886            .value_indices(vec![3])
887            .engine(METRIC_ENGINE_NAME.to_string())
888            .options(options)
889            .next_column_id(1024)
890            .build()
891            .unwrap();
892        let logical_info = TableInfoBuilder::default()
893            .table_id(1024)
894            .name("some_metric")
895            .meta(logical_meta)
896            .build()
897            .unwrap();
898        catalog_manager
899            .register_table_sync(RegisterTableRequest {
900                catalog: DEFAULT_CATALOG_NAME.to_string(),
901                schema: DEFAULT_SCHEMA_NAME.to_string(),
902                table_name: "some_metric".to_string(),
903                table_id: 1024,
904                table: EmptyTable::from_table_info(&logical_info),
905            })
906            .unwrap();
907
908        QueryEngineFactory::new(
909            catalog_manager,
910            None,
911            None,
912            None,
913            None,
914            false,
915            crate::options::QueryOptions::default(),
916        )
917        .query_engine()
918    }
919
920    async fn parse_sql_to_plan(sql: &str) -> LogicalPlan {
921        let stmt = QueryLanguageParser::parse_sql(sql, &QueryContext::arc()).unwrap();
922        let engine = create_test_engine().await;
923        engine
924            .planner()
925            .plan(&stmt, QueryContext::arc())
926            .await
927            .unwrap()
928    }
929
930    async fn parse_promql_to_plan(query: &str) -> LogicalPlan {
931        let engine = create_promql_test_engine();
932        let query_ctx = QueryContext::arc();
933        let stmt = QueryLanguageParser::parse_promql(
934            &PromQuery {
935                query: query.to_string(),
936                start: "0".to_string(),
937                end: "10".to_string(),
938                step: "5s".to_string(),
939                lookback: "300s".to_string(),
940                alias: None,
941            },
942            &query_ctx,
943        )
944        .unwrap();
945
946        engine.planner().plan(&stmt, query_ctx).await.unwrap()
947    }
948
949    /// Plans `sql` and runs the DataFusion analyzer, which is where
950    /// `InsertAssignmentRule` sits. Planning alone stops short of it, so these
951    /// assertions would not see the assignment rewrite at all.
952    async fn analyze_insert(
953        engine: &QueryEngineRef,
954        sql: &str,
955        query_ctx: &QueryContextRef,
956    ) -> String {
957        let stmt = QueryLanguageParser::parse_sql(sql, query_ctx).unwrap();
958        let plan = engine
959            .planner()
960            .plan(&stmt, query_ctx.clone())
961            .await
962            .unwrap();
963        let context = engine.engine_context(query_ctx.clone());
964        let state = context.state();
965        state
966            .analyzer()
967            .execute_and_check(plan, state.config_options(), |_, _| {})
968            .unwrap()
969            .display_indent_schema()
970            .to_string()
971    }
972
973    #[tokio::test]
974    async fn test_insert_timestamp_literals_use_query_timezone() {
975        let query_ctx = Arc::new(
976            QueryContextBuilder::default()
977                .timezone(Timezone::from_tz_string("Asia/Shanghai").unwrap())
978                .build(),
979        );
980        let engine = create_timestamp_test_engine().await;
981
982        for (sql, expected_timestamps) in [
983            (
984                "INSERT INTO timestamps (ts, st) \
985                 VALUES ('2026-08-02 12:00:00.001', now())",
986                &[1_785_643_200_001_i64][..],
987            ),
988            (
989                "INSERT INTO timestamps (ts, st) \
990                 SELECT '2026-08-03 12:00:00.001', now()",
991                &[1_785_729_600_001_i64][..],
992            ),
993            (
994                "INSERT INTO timestamps (ts, st) \
995                 SELECT '2026-08-04 12:00:00.001', now() LIMIT 1",
996                &[1_785_816_000_001_i64][..],
997            ),
998            (
999                "INSERT INTO timestamps (ts, st) \
1000                 SELECT * FROM (\
1001                     SELECT '2026-08-05 12:00:00.001', now()\
1002                 ) AS source",
1003                &[1_785_902_400_001_i64][..],
1004            ),
1005            (
1006                "INSERT INTO timestamps (ts, st) \
1007                 SELECT '2026-08-18 12:00:00.001', max(st) \
1008                 FROM timestamps GROUP BY note",
1009                &[1_787_025_600_001_i64][..],
1010            ),
1011            (
1012                "INSERT INTO timestamps (ts, st) \
1013                 SELECT c, s FROM (\
1014                     SELECT '2026-08-13 12:00:00.001' AS c, now() AS s\
1015                 ) AS t WHERE c > '2026-01-01'",
1016                &[1_786_593_600_001_i64][..],
1017            ),
1018            (
1019                "INSERT INTO timestamps (ts, st) \
1020                 SELECT c, s FROM (\
1021                     SELECT '2026-08-14 12:00:00.001' AS c, now() AS s\
1022                 ) AS t ORDER BY c",
1023                &[1_786_680_000_001_i64][..],
1024            ),
1025            (
1026                "INSERT INTO timestamps (ts, st) \
1027                 SELECT DISTINCT c, s FROM (\
1028                     SELECT '2026-08-15 12:00:00.001' AS c, now() AS s\
1029                 ) AS t",
1030                &[1_786_766_400_001_i64][..],
1031            ),
1032        ] {
1033            let plan = analyze_insert(&engine, sql, &query_ctx).await;
1034
1035            for expected_timestamp in expected_timestamps {
1036                assert!(
1037                    plan.contains(&format!("TimestampMillisecond({expected_timestamp}, None)")),
1038                    "{plan}"
1039                );
1040            }
1041        }
1042    }
1043
1044    #[tokio::test]
1045    async fn test_insert_explicit_timestamp_cast_keeps_datafusion_semantics() {
1046        let query_ctx = Arc::new(
1047            QueryContextBuilder::default()
1048                .timezone(Timezone::from_tz_string("Asia/Shanghai").unwrap())
1049                .build(),
1050        );
1051        let engine = create_timestamp_test_engine().await;
1052        let sql = "INSERT INTO timestamps (ts, st) \
1053                   VALUES (CAST('2026-08-08 12:00:00.001' AS TIMESTAMP), now())";
1054        let plan = analyze_insert(&engine, sql, &query_ctx).await;
1055
1056        // An explicit cast reaches the analyzer as an `arrow_cast` call rather
1057        // than an `Expr::Cast`, which is how it stays out of the rewrite.
1058        assert!(
1059            plan.contains("arrow_cast(Utf8(\"2026-08-08 12:00:00.001\")"),
1060            "{plan}"
1061        );
1062        // 12:00:00.001 read as Shanghai local time; the source query keeps UTC.
1063        assert!(
1064            !plan.contains("TimestampMillisecond(1786104000001, None)"),
1065            "{plan}"
1066        );
1067    }
1068
1069    #[tokio::test]
1070    async fn test_insert_converts_source_literal_shared_by_several_columns() {
1071        let query_ctx = Arc::new(
1072            QueryContextBuilder::default()
1073                .timezone(Timezone::from_tz_string("Asia/Shanghai").unwrap())
1074                .build(),
1075        );
1076        let engine = create_timestamp_test_engine().await;
1077
1078        // Rewriting `c` in place would also retype `note` and truncate `ts_ns`.
1079        for (sql, expected) in [
1080            (
1081                "INSERT INTO timestamps (ts, note) SELECT a, b FROM (\
1082                     SELECT c AS a, c AS b FROM (\
1083                         SELECT '2026-08-12 12:00:00.001' AS c\
1084                     ) AS t1\
1085                 ) AS t2",
1086                &[
1087                    "TimestampMillisecond(1786507200001, None)",
1088                    "[a:Utf8, b:Utf8]",
1089                ][..],
1090            ),
1091            (
1092                "INSERT INTO timestamps (ts, ts_ns) SELECT a, b FROM (\
1093                     SELECT c AS a, c AS b FROM (\
1094                         SELECT '2026-08-12 12:00:00.123456789' AS c\
1095                     ) AS t1\
1096                 ) AS t2",
1097                &[
1098                    "TimestampMillisecond(1786507200123, None)",
1099                    "TimestampNanosecond(1786507200123456789, None)",
1100                ][..],
1101            ),
1102        ] {
1103            let plan = analyze_insert(&engine, sql, &query_ctx).await;
1104
1105            for expected in expected {
1106                assert!(plan.contains(expected), "{plan}");
1107            }
1108        }
1109    }
1110
1111    #[tokio::test]
1112    async fn test_insert_union_converts_via_assignment_cast() {
1113        let query_ctx = Arc::new(
1114            QueryContextBuilder::default()
1115                .timezone(Timezone::from_tz_string("Asia/Shanghai").unwrap())
1116                .build(),
1117        );
1118        let engine = create_timestamp_test_engine().await;
1119        // Branches disagree, so the conversion stays as a cast on the
1120        // assignment instead of folding. One cast covers every branch, which is
1121        // why a NULL branch no longer cancels the conversion for the column and
1122        // why UNION's dedup keys stay on the original strings.
1123        for sql in [
1124            "INSERT INTO timestamps (ts, st) \
1125             SELECT '2026-08-06 12:00:00.001', now() \
1126             UNION ALL \
1127             SELECT '2026-08-07 12:00:00.001', NULL",
1128            "INSERT INTO timestamps (ts, st) \
1129             SELECT '2026-08-16 12:00:00.001', now() \
1130             UNION \
1131             SELECT '2026-08-17 12:00:00.001', now()",
1132        ] {
1133            let plan = analyze_insert(&engine, sql, &query_ctx).await;
1134
1135            assert!(
1136                plan.contains("AS Timestamp(ms, \"Asia/Shanghai\")"),
1137                "{plan}"
1138            );
1139            // The branches themselves are untouched.
1140            assert!(
1141                plan.contains("Utf8(\"2026-08-07 12:00:00.001\")")
1142                    || plan.contains("Utf8(\"2026-08-17 12:00:00.001\")"),
1143                "{plan}"
1144            );
1145        }
1146    }
1147
1148    #[tokio::test]
1149    async fn test_insert_mixed_union_keeps_source_coercion() {
1150        let query_ctx = Arc::new(
1151            QueryContextBuilder::default()
1152                .timezone(Timezone::from_tz_string("Asia/Shanghai").unwrap())
1153                .build(),
1154        );
1155        let engine = create_timestamp_test_engine().await;
1156        let sql = "INSERT INTO timestamps (ts, st) \
1157                   SELECT '2026-08-10 12:00:00.001', now() \
1158                   UNION ALL \
1159                   SELECT CAST('2026-08-11 12:00:00.001' AS TIMESTAMP), now()";
1160        let plan = analyze_insert(&engine, sql, &query_ctx).await;
1161
1162        assert!(
1163            !plan.contains("TimestampMillisecond(1786334400001, None)"),
1164            "{plan}"
1165        );
1166        assert!(
1167            plan.contains("arrow_cast(Utf8(\"2026-08-11 12:00:00.001\")"),
1168            "{plan}"
1169        );
1170        // TypeCoercion has already settled this union to timestamp, so the
1171        // assignment has nothing left to reinterpret. Retargeting the cast here
1172        // would leave a Timestamp(None) -> Timestamp(Some(tz)) step behind,
1173        // which shifts the value instead of relabelling it.
1174        assert!(!plan.contains("Asia/Shanghai"), "{plan}");
1175    }
1176
1177    #[tokio::test]
1178    async fn test_extract_placeholder_cast_types_multiple() {
1179        let plan = parse_sql_to_plan(
1180            "SELECT $1::INT, $2::TEXT, $3, $4::INTEGER FROM test WHERE $5::FLOAT > 0",
1181        )
1182        .await;
1183        let types = DfLogicalPlanner::extract_placeholder_cast_types(&plan).unwrap();
1184
1185        assert_eq!(types.len(), 5);
1186        assert_eq!(types.get("$1"), Some(&Some(DataType::Int32)));
1187        assert_eq!(types.get("$2"), Some(&Some(DataType::Utf8)));
1188        assert_eq!(types.get("$3"), Some(&None));
1189        assert_eq!(types.get("$4"), Some(&Some(DataType::Int32)));
1190        assert_eq!(types.get("$5"), Some(&Some(DataType::Float32)));
1191    }
1192
1193    #[tokio::test]
1194    async fn test_get_inferred_parameter_types_fallback_for_udf_args() {
1195        // datafusion is not able to infer type for scalar function arguments
1196        let plan = parse_sql_to_plan(
1197            "SELECT parse_ident($1), parse_ident($2::TEXT) FROM test WHERE id > $3",
1198        )
1199        .await;
1200        let types = DfLogicalPlanner::get_inferred_parameter_types(&plan).unwrap();
1201
1202        assert_eq!(types.len(), 3);
1203
1204        let type_1 = types.get("$1").unwrap();
1205        let type_2 = types.get("$2").unwrap();
1206        let type_3 = types.get("$3").unwrap();
1207
1208        assert!(type_1.is_none(), "Expected $1 to be None");
1209        assert_eq!(type_2, &Some(DataType::Utf8));
1210        assert_eq!(type_3, &Some(DataType::Int32));
1211    }
1212
1213    #[tokio::test]
1214    async fn test_get_inferred_parameter_types_limit_offset() {
1215        let plan = parse_sql_to_plan("SELECT id FROM test LIMIT $1 OFFSET $2").await;
1216        let types = DfLogicalPlanner::get_inferred_parameter_types(&plan).unwrap();
1217
1218        assert_eq!(types.get("$1"), Some(&Some(DataType::Int64)));
1219        assert_eq!(types.get("$2"), Some(&Some(DataType::Int64)));
1220    }
1221
1222    #[tokio::test]
1223    async fn test_plan_pql_applies_extension_rules() {
1224        for inner_agg in ["count", "sum", "avg", "min", "max", "stddev", "stdvar"] {
1225            let plan = parse_promql_to_plan(&format!(
1226                "sum(irate(some_metric[1h])) / scalar(count({inner_agg}(some_metric) by (tag_0)))"
1227            ))
1228            .await;
1229            let plan_str = plan.display_indent_schema().to_string();
1230            assert!(plan_str.contains("Distinct:"), "{inner_agg}: {plan_str}");
1231        }
1232    }
1233
1234    #[tokio::test]
1235    async fn test_plan_pql_filters_null_only_groups_for_non_count_inner_aggs() {
1236        let count_plan = parse_promql_to_plan("scalar(count(count(some_metric) by (tag_0)))").await;
1237        let count_plan_str = count_plan.display_indent_schema().to_string();
1238        assert!(
1239            !count_plan_str.contains("field_0 IS NOT NULL"),
1240            "{count_plan_str}"
1241        );
1242
1243        for inner_agg in ["sum", "avg", "min", "max", "stddev", "stdvar"] {
1244            let plan = parse_promql_to_plan(&format!(
1245                "scalar(count({inner_agg}(some_metric) by (tag_0)))"
1246            ))
1247            .await;
1248            let plan_str = plan.display_indent_schema().to_string();
1249            assert!(
1250                plan_str.contains("field_0 IS NOT NULL"),
1251                "{inner_agg}: {plan_str}"
1252            );
1253        }
1254    }
1255
1256    #[tokio::test]
1257    async fn test_plan_pql_skips_extension_rules_for_non_direct_or_unsupported_inner_agg() {
1258        for query in [
1259            "sum(irate(some_metric[1h])) / scalar(count(sum(irate(some_metric[1h])) by (tag_0)))",
1260            "sum(irate(some_metric[1h])) / scalar(count(group(some_metric) by (tag_0)))",
1261        ] {
1262            let plan = parse_promql_to_plan(query).await;
1263            let plan_str = plan.display_indent_schema().to_string();
1264            assert!(!plan_str.contains("Distinct:"), "{query}: {plan_str}");
1265        }
1266    }
1267
1268    #[tokio::test]
1269    async fn test_plan_sql_does_not_apply_nested_count_rule() {
1270        let plan = parse_sql_to_plan(
1271            "SELECT id, count(inner_count) \
1272             FROM ( \
1273                 SELECT id, count(name) AS inner_count \
1274                 FROM test \
1275                 GROUP BY id \
1276                 ORDER BY id \
1277                 LIMIT 1000000 \
1278             ) t \
1279             GROUP BY id \
1280             ORDER BY id",
1281        )
1282        .await;
1283
1284        let plan_str = plan.display_indent_schema().to_string();
1285        assert!(!plan_str.contains("Distinct:"), "{plan_str}");
1286    }
1287
1288    #[tokio::test]
1289    async fn test_get_inferred_parameter_types_subquery() {
1290        let plan = parse_sql_to_plan(
1291            r#"SELECT * FROM test WHERE id = (SELECT id FROM test CROSS JOIN (SELECT parse_ident($1::TEXT) AS parts) p LIMIT 1)"#,
1292        ).await;
1293        let types = DfLogicalPlanner::get_inferred_parameter_types(&plan).unwrap();
1294
1295        assert_eq!(types.len(), 1);
1296        let type_1 = types.get("$1").unwrap();
1297        assert_eq!(type_1, &Some(DataType::Utf8));
1298    }
1299
1300    #[tokio::test]
1301    async fn test_get_inferred_parameter_types_insert() {
1302        let plan = parse_sql_to_plan("INSERT INTO test (id, name) VALUES ($1, $2), ($3, $4)").await;
1303        let types = DfLogicalPlanner::get_inferred_parameter_types(&plan).unwrap();
1304
1305        assert_eq!(types.len(), 4);
1306        assert_eq!(types.get("$1"), Some(&Some(DataType::Int32)));
1307        assert_eq!(types.get("$2"), Some(&Some(DataType::Utf8)));
1308        assert_eq!(types.get("$3"), Some(&Some(DataType::Int32)));
1309        assert_eq!(types.get("$4"), Some(&Some(DataType::Utf8)));
1310    }
1311
1312    #[tokio::test]
1313    async fn test_get_inferred_parameter_types_arrow_cast() {
1314        let plan = parse_sql_to_plan("SELECT $1::INT64, $2::FLOAT64, $3::INT16, $4::INT32, $5::UINT8, $6::UINT16, $7::UINT32").await;
1315        let types = DfLogicalPlanner::get_inferred_parameter_types(&plan).unwrap();
1316
1317        assert_eq!(types.get("$1"), Some(&Some(DataType::Int64)));
1318        assert_eq!(types.get("$2"), Some(&Some(DataType::Float64)));
1319        assert_eq!(types.get("$3"), Some(&Some(DataType::Int16)));
1320        assert_eq!(types.get("$4"), Some(&Some(DataType::Int32)));
1321        assert_eq!(types.get("$5"), Some(&Some(DataType::UInt8)));
1322        assert_eq!(types.get("$6"), Some(&Some(DataType::UInt16)));
1323        assert_eq!(types.get("$7"), Some(&Some(DataType::UInt32)));
1324
1325        let plan = parse_sql_to_plan("SELECT $1::INT8, $2::FLOAT8, $3::INT2, $4::INT8").await;
1326        let types = DfLogicalPlanner::get_inferred_parameter_types(&plan).unwrap();
1327
1328        assert_eq!(types.get("$1"), Some(&Some(DataType::Int64)));
1329        assert_eq!(types.get("$2"), Some(&Some(DataType::Float64)));
1330        assert_eq!(types.get("$3"), Some(&Some(DataType::Int16)));
1331    }
1332}