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