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