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