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