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::str::FromStr;
19use std::sync::Arc;
20
21use arrow_schema::DataType;
22use async_trait::async_trait;
23use catalog::table_source::DfTableSourceProvider;
24use common_error::ext::BoxedError;
25use common_telemetry::tracing;
26use datafusion::common::{DFSchema, plan_err};
27use datafusion::execution::SessionStateBuilder;
28use datafusion::execution::context::SessionState;
29use datafusion::sql::planner::PlannerContext;
30use datafusion_common::tree_node::{TreeNode, TreeNodeRecursion};
31use datafusion_common::{ScalarValue, ToDFSchema};
32use datafusion_expr::expr::{Exists, InSubquery};
33use datafusion_expr::{
34    Analyze, Explain, ExplainFormat, Expr as DfExpr, LogicalPlan, LogicalPlanBuilder, PlanType,
35    ToStringifiedPlan, col,
36};
37use datafusion_sql::planner::{ParserOptions, SqlToRel};
38use log_query::LogQuery;
39use promql_parser::parser::EvalStmt;
40use session::context::QueryContextRef;
41use snafu::{ResultExt, ensure};
42use sql::CteContent;
43use sql::ast::Expr as SqlExpr;
44use sql::statements::explain::ExplainStatement;
45use sql::statements::query::Query;
46use sql::statements::statement::Statement;
47use sql::statements::tql::Tql;
48
49use crate::error::{
50    CteColumnSchemaMismatchSnafu, PlanSqlSnafu, QueryPlanSnafu, Result, SqlSnafu,
51    UnimplementedSnafu,
52};
53use crate::log_query::planner::LogQueryPlanner;
54use crate::parser::{DEFAULT_LOOKBACK_STRING, PromQuery, QueryLanguageParser, QueryStatement};
55use crate::promql::planner::PromPlanner;
56use crate::query_engine::{DefaultPlanDecoder, QueryEngineState};
57use crate::range_select::plan_rewrite::RangePlanRewriter;
58use crate::{DfContextProviderAdapter, QueryEngineContext};
59
60#[async_trait]
61pub trait LogicalPlanner: Send + Sync {
62    async fn plan(&self, stmt: &QueryStatement, query_ctx: QueryContextRef) -> Result<LogicalPlan>;
63
64    async fn plan_logs_query(
65        &self,
66        query: LogQuery,
67        query_ctx: QueryContextRef,
68    ) -> Result<LogicalPlan>;
69
70    fn optimize(&self, plan: LogicalPlan) -> Result<LogicalPlan>;
71
72    fn as_any(&self) -> &dyn Any;
73}
74
75pub struct DfLogicalPlanner {
76    engine_state: Arc<QueryEngineState>,
77    session_state: SessionState,
78}
79
80impl DfLogicalPlanner {
81    pub fn new(engine_state: Arc<QueryEngineState>) -> Self {
82        let session_state = engine_state.session_state();
83        Self {
84            engine_state,
85            session_state,
86        }
87    }
88
89    /// Derive a [`SessionState`] whose [`ExecutionProps`] includes
90    /// `query_execution_start_time` if a scheduled time extension is present
91    /// in the query context.
92    fn derive_session_state_with_scheduled_time(
93        &self,
94        query_ctx: &QueryContextRef,
95    ) -> Result<SessionState> {
96        let extensions = query_ctx.extensions();
97        match crate::options::parse_scheduled_time_datetime(&extensions)? {
98            Some(dt) => {
99                let execution_props = self
100                    .session_state
101                    .execution_props()
102                    .clone()
103                    .with_query_execution_start_time(dt);
104                Ok(
105                    SessionStateBuilder::new_from_existing(self.session_state.clone())
106                        .with_execution_props(execution_props)
107                        .build(),
108                )
109            }
110            None => Ok(self.session_state.clone()),
111        }
112    }
113
114    /// Basically the same with `explain_to_plan` in DataFusion, but adapted to Greptime's
115    /// `plan_sql` to support Greptime Statements.
116    async fn explain_to_plan(
117        &self,
118        explain: &ExplainStatement,
119        query_ctx: QueryContextRef,
120    ) -> Result<LogicalPlan> {
121        let plan = self.plan_sql(&explain.statement, query_ctx).await?;
122        if matches!(plan, LogicalPlan::Explain(_)) {
123            return plan_err!("Nested EXPLAINs are not supported").context(PlanSqlSnafu);
124        }
125
126        let verbose = explain.verbose;
127        let analyze = explain.analyze;
128        let format = explain.format.map(|f| f.to_string());
129
130        let plan = Arc::new(plan);
131        let schema = LogicalPlan::explain_schema();
132        let schema = ToDFSchema::to_dfschema_ref(schema)?;
133
134        if verbose && format.is_some() {
135            return plan_err!("EXPLAIN VERBOSE with FORMAT is not supported").context(PlanSqlSnafu);
136        }
137
138        if analyze {
139            // notice format is already set in query context, so can be ignore here
140            Ok(LogicalPlan::Analyze(Analyze {
141                verbose,
142                input: plan,
143                schema,
144            }))
145        } else {
146            let stringified_plans = vec![plan.to_stringified(PlanType::InitialLogicalPlan)];
147
148            // default to configuration value
149            let options = self.session_state.config().options();
150            let format = format
151                .map(|x| ExplainFormat::from_str(&x))
152                .transpose()?
153                .unwrap_or_else(|| options.explain.format.clone());
154
155            Ok(LogicalPlan::Explain(Explain {
156                verbose,
157                explain_format: format,
158                plan,
159                stringified_plans,
160                schema,
161                logical_optimization_succeeded: false,
162            }))
163        }
164    }
165
166    #[tracing::instrument(skip_all)]
167    #[async_recursion::async_recursion]
168    async fn plan_sql(&self, stmt: &Statement, query_ctx: QueryContextRef) -> Result<LogicalPlan> {
169        let mut planner_context = PlannerContext::new();
170        let mut stmt = Cow::Borrowed(stmt);
171        let mut is_tql_cte = false;
172
173        // handle explain before normal processing so we can explain Greptime Statements
174        if let Statement::Explain(explain) = stmt.as_ref() {
175            return self.explain_to_plan(explain, query_ctx).await;
176        }
177
178        // Check for hybrid CTEs before normal processing
179        if self.has_hybrid_ctes(stmt.as_ref()) {
180            let stmt_owned = stmt.into_owned();
181            let mut query = match stmt_owned {
182                Statement::Query(query) => query.as_ref().clone(),
183                _ => unreachable!("has_hybrid_ctes should only return true for Query statements"),
184            };
185            self.plan_query_with_hybrid_ctes(&query, query_ctx.clone(), &mut planner_context)
186                .await?;
187
188            // remove the processed TQL CTEs from the query
189            query.hybrid_cte = None;
190            stmt = Cow::Owned(Statement::Query(Box::new(query)));
191            is_tql_cte = true;
192        }
193
194        let mut df_stmt = stmt.as_ref().try_into().context(SqlSnafu)?;
195
196        // TODO(LFC): Remove this when Datafusion supports **both** the syntax and implementation of "explain with format".
197        if let datafusion::sql::parser::Statement::Statement(
198            box datafusion::sql::sqlparser::ast::Statement::Explain { .. },
199        ) = &mut df_stmt
200        {
201            UnimplementedSnafu {
202                operation: "EXPLAIN with FORMAT using raw datafusion planner",
203            }
204            .fail()?;
205        }
206
207        let scheduled_state = self.derive_session_state_with_scheduled_time(&query_ctx)?;
208        let table_provider = DfTableSourceProvider::new(
209            self.engine_state.catalog_manager().clone(),
210            self.engine_state.disallow_cross_catalog_query(),
211            query_ctx.clone(),
212            Arc::new(DefaultPlanDecoder::new(
213                scheduled_state.clone(),
214                &query_ctx,
215            )?),
216            scheduled_state
217                .config_options()
218                .sql_parser
219                .enable_ident_normalization,
220        );
221
222        let context_provider = DfContextProviderAdapter::try_new(
223            self.engine_state.clone(),
224            scheduled_state.clone(),
225            Some(&df_stmt),
226            query_ctx.clone(),
227        )
228        .await?;
229
230        let config_options = self.session_state.config().options();
231        let parser_options = &config_options.sql_parser;
232        let parser_options = ParserOptions {
233            map_string_types_to_utf8view: false,
234            ..parser_options.into()
235        };
236
237        let sql_to_rel = SqlToRel::new_with_options(&context_provider, parser_options);
238
239        // this IF is to handle different version of ASTs
240        let result = if is_tql_cte {
241            let Statement::Query(query) = stmt.into_owned() else {
242                unreachable!("is_tql_cte should only be true for Query statements");
243            };
244            let sqlparser_stmt = sqlparser::ast::Statement::Query(Box::new(query.inner));
245            sql_to_rel
246                .sql_statement_to_plan_with_context(sqlparser_stmt, &mut planner_context)
247                .context(PlanSqlSnafu)?
248        } else {
249            sql_to_rel
250                .statement_to_plan(df_stmt)
251                .context(PlanSqlSnafu)?
252        };
253
254        common_telemetry::debug!("Logical planner, statement to plan result: {result}");
255        let plan = RangePlanRewriter::new(table_provider, query_ctx.clone())
256            .rewrite(result)
257            .await?;
258
259        // Optimize logical plan by extension rules
260        let context = QueryEngineContext::new(scheduled_state, query_ctx);
261        let plan = self
262            .engine_state
263            .optimize_by_extension_rules(plan, &context)?;
264        common_telemetry::debug!("Logical planner, optimize result: {plan}");
265
266        Ok(plan)
267    }
268
269    /// Generate a relational expression from a SQL expression
270    #[tracing::instrument(skip_all)]
271    pub(crate) async fn sql_to_expr(
272        &self,
273        sql: SqlExpr,
274        schema: &DFSchema,
275        normalize_ident: bool,
276        query_ctx: QueryContextRef,
277    ) -> Result<DfExpr> {
278        let scheduled_state = self.derive_session_state_with_scheduled_time(&query_ctx)?;
279        let context_provider = DfContextProviderAdapter::try_new(
280            self.engine_state.clone(),
281            scheduled_state,
282            None,
283            query_ctx,
284        )
285        .await?;
286
287        let config_options = self.session_state.config().options();
288        let parser_options = &config_options.sql_parser;
289        let parser_options: ParserOptions = ParserOptions {
290            map_string_types_to_utf8view: false,
291            enable_ident_normalization: normalize_ident,
292            ..parser_options.into()
293        };
294
295        let sql_to_rel = SqlToRel::new_with_options(&context_provider, parser_options);
296
297        Ok(sql_to_rel.sql_to_expr(sql, schema, &mut PlannerContext::new())?)
298    }
299
300    #[tracing::instrument(skip_all)]
301    async fn plan_pql(&self, stmt: &EvalStmt, query_ctx: QueryContextRef) -> Result<LogicalPlan> {
302        let scheduled_state = self.derive_session_state_with_scheduled_time(&query_ctx)?;
303        let plan_decoder = Arc::new(DefaultPlanDecoder::new(
304            scheduled_state.clone(),
305            &query_ctx,
306        )?);
307        let table_provider = DfTableSourceProvider::new(
308            self.engine_state.catalog_manager().clone(),
309            self.engine_state.disallow_cross_catalog_query(),
310            query_ctx.clone(),
311            plan_decoder,
312            scheduled_state
313                .config_options()
314                .sql_parser
315                .enable_ident_normalization,
316        );
317        let plan = PromPlanner::stmt_to_plan(table_provider, stmt, &self.engine_state)
318            .await
319            .map_err(BoxedError::new)
320            .context(QueryPlanSnafu)?;
321
322        let context = QueryEngineContext::new(scheduled_state, query_ctx);
323        Ok(self
324            .engine_state
325            .optimize_by_extension_rules(plan, &context)?)
326    }
327
328    #[tracing::instrument(skip_all)]
329    fn optimize_logical_plan(&self, plan: LogicalPlan) -> Result<LogicalPlan> {
330        Ok(self.engine_state.optimize_logical_plan(plan)?)
331    }
332
333    /// Check if a statement contains hybrid CTEs (mix of SQL and TQL)
334    fn has_hybrid_ctes(&self, stmt: &Statement) -> bool {
335        if let Statement::Query(query) = stmt {
336            query
337                .hybrid_cte
338                .as_ref()
339                .map(|hybrid_cte| !hybrid_cte.cte_tables.is_empty())
340                .unwrap_or(false)
341        } else {
342            false
343        }
344    }
345
346    /// Plan a query with hybrid CTEs using DataFusion's native PlannerContext
347    async fn plan_query_with_hybrid_ctes(
348        &self,
349        query: &Query,
350        query_ctx: QueryContextRef,
351        planner_context: &mut PlannerContext,
352    ) -> Result<()> {
353        let hybrid_cte = query.hybrid_cte.as_ref().unwrap();
354
355        for cte in &hybrid_cte.cte_tables {
356            match &cte.content {
357                CteContent::Tql(tql) => {
358                    // Plan TQL and register in PlannerContext
359                    let mut logical_plan = self.tql_to_logical_plan(tql, query_ctx.clone()).await?;
360                    if !cte.columns.is_empty() {
361                        let schema = logical_plan.schema();
362                        let schema_fields = schema.fields().to_vec();
363                        ensure!(
364                            schema_fields.len() == cte.columns.len(),
365                            CteColumnSchemaMismatchSnafu {
366                                cte_name: cte.name.value.clone(),
367                                original: schema_fields
368                                    .iter()
369                                    .map(|field| field.name().clone())
370                                    .collect::<Vec<_>>(),
371                                expected: cte
372                                    .columns
373                                    .iter()
374                                    .map(|column| column.to_string())
375                                    .collect::<Vec<_>>(),
376                            }
377                        );
378                        let aliases = cte
379                            .columns
380                            .iter()
381                            .zip(schema_fields.iter())
382                            .map(|(column, field)| col(field.name()).alias(column.to_string()));
383                        logical_plan = LogicalPlanBuilder::from(logical_plan)
384                            .project(aliases)
385                            .context(PlanSqlSnafu)?
386                            .build()
387                            .context(PlanSqlSnafu)?;
388                    }
389
390                    // Wrap in SubqueryAlias to ensure proper table qualification for CTE
391                    logical_plan = LogicalPlan::SubqueryAlias(
392                        datafusion_expr::SubqueryAlias::try_new(
393                            Arc::new(logical_plan),
394                            cte.name.value.clone(),
395                        )
396                        .context(PlanSqlSnafu)?,
397                    );
398
399                    planner_context.insert_cte(&cte.name.value, logical_plan);
400                }
401                CteContent::Sql(_) => {
402                    // SQL CTEs should have been moved to the main query's WITH clause
403                    // during parsing, so we shouldn't encounter them here
404                    unreachable!("SQL CTEs should not be in hybrid_cte.cte_tables");
405                }
406            }
407        }
408
409        Ok(())
410    }
411
412    /// Convert TQL to LogicalPlan directly
413    async fn tql_to_logical_plan(
414        &self,
415        tql: &Tql,
416        query_ctx: QueryContextRef,
417    ) -> Result<LogicalPlan> {
418        match tql {
419            Tql::Eval(eval) => {
420                // Convert TqlEval to PromQuery then to QueryStatement::Promql
421                let prom_query = PromQuery {
422                    query: eval.query.clone(),
423                    start: eval.start.clone(),
424                    end: eval.end.clone(),
425                    step: eval.step.clone(),
426                    lookback: eval
427                        .lookback
428                        .clone()
429                        .unwrap_or_else(|| DEFAULT_LOOKBACK_STRING.to_string()),
430                    alias: eval.alias.clone(),
431                };
432                let stmt = QueryLanguageParser::parse_promql(&prom_query, &query_ctx)?;
433
434                self.plan(&stmt, query_ctx).await
435            }
436            Tql::Explain(_) => UnimplementedSnafu {
437                operation: "TQL EXPLAIN in CTEs",
438            }
439            .fail(),
440            Tql::Analyze(_) => UnimplementedSnafu {
441                operation: "TQL ANALYZE in CTEs",
442            }
443            .fail(),
444        }
445    }
446
447    /// Extracts cast types for all placeholders in a logical plan.
448    /// Returns a map where each placeholder ID is mapped to:
449    /// - Some(DataType) if the placeholder is cast to a specific type
450    /// - None if the placeholder exists but has no cast
451    ///
452    /// Example: `$1::TEXT` returns `{"$1": Some(DataType::Utf8)}`
453    ///
454    /// This function walks through all expressions in the logical plan,
455    /// including subqueries, to identify placeholders and their cast types.
456    fn extract_placeholder_cast_types(
457        plan: &LogicalPlan,
458    ) -> Result<HashMap<String, Option<DataType>>> {
459        let mut placeholder_types = HashMap::new();
460        let mut casted_placeholders = HashSet::new();
461
462        Self::extract_from_plan(plan, &mut placeholder_types, &mut casted_placeholders)?;
463
464        Ok(placeholder_types)
465    }
466
467    fn extract_from_plan(
468        plan: &LogicalPlan,
469        placeholder_types: &mut HashMap<String, Option<DataType>>,
470        casted_placeholders: &mut HashSet<String>,
471    ) -> Result<()> {
472        plan.apply(|node| {
473            for expr in node.expressions() {
474                let _ = expr.apply(|e| {
475                    // Handle casted placeholders
476                    if let DfExpr::Cast(cast) = e
477                        && let DfExpr::Placeholder(ph) = &*cast.expr
478                    {
479                        placeholder_types.insert(ph.id.clone(), Some(cast.data_type.clone()));
480                        casted_placeholders.insert(ph.id.clone());
481                    }
482
483                    // Handle arrow_cast(Placeholder, 'type_string') generated by SQL rewriter
484                    if let DfExpr::ScalarFunction(scalar_func) = e
485                        && scalar_func.name() == "arrow_cast"
486                        && scalar_func.args.len() == 2
487                        && let DfExpr::Placeholder(ph) = &scalar_func.args[0]
488                        && let DfExpr::Literal(ScalarValue::Utf8(Some(type_str)), _) =
489                            &scalar_func.args[1]
490                        && let Ok(data_type) = type_str.parse::<DataType>()
491                    {
492                        placeholder_types.insert(ph.id.clone(), Some(data_type));
493                        casted_placeholders.insert(ph.id.clone());
494                    }
495
496                    // Handle bare (non-casted) placeholders
497                    if let DfExpr::Placeholder(ph) = e
498                        && !casted_placeholders.contains(&ph.id)
499                        && !placeholder_types.contains_key(&ph.id)
500                    {
501                        placeholder_types.insert(ph.id.clone(), None);
502                    }
503
504                    // Recurse into subquery plans embedded in expressions
505                    match e {
506                        DfExpr::Exists(Exists { subquery, .. })
507                        | DfExpr::InSubquery(InSubquery { subquery, .. })
508                        | DfExpr::ScalarSubquery(subquery) => {
509                            Self::extract_from_plan(
510                                &subquery.subquery,
511                                placeholder_types,
512                                casted_placeholders,
513                            )?;
514                        }
515                        _ => {}
516                    }
517
518                    Ok(TreeNodeRecursion::Continue)
519                });
520            }
521            Ok(TreeNodeRecursion::Continue)
522        })?;
523        Ok(())
524    }
525
526    fn infer_limit_placeholder_types(
527        plan: &LogicalPlan,
528        placeholder_types: &mut HashMap<String, Option<DataType>>,
529    ) -> Result<()> {
530        plan.apply(|node| {
531            if let LogicalPlan::Limit(limit) = node {
532                for expr in limit.skip.iter().chain(limit.fetch.iter()) {
533                    expr.apply(|e| {
534                        if let DfExpr::Placeholder(ph) = e {
535                            placeholder_types
536                                .entry(ph.id.clone())
537                                .and_modify(|existing| {
538                                    if existing.is_none() {
539                                        *existing = Some(DataType::Int64);
540                                    }
541                                })
542                                .or_insert(Some(DataType::Int64));
543                        }
544
545                        Ok(TreeNodeRecursion::Continue)
546                    })?;
547                }
548            }
549
550            Ok(TreeNodeRecursion::Continue)
551        })?;
552
553        Ok(())
554    }
555
556    /// Gets inferred parameter types from a logical plan.
557    /// Returns a map where each parameter ID is mapped to:
558    /// - Some(DataType) if the parameter type could be inferred
559    /// - None if the parameter type could not be inferred
560    ///
561    /// This function first uses DataFusion's `get_parameter_types()` to infer types.
562    /// If any parameters have `None` values (i.e., DataFusion couldn't infer their types),
563    /// it falls back to using `extract_placeholder_cast_types()` to detect explicit casts
564    /// and applies context-specific inference such as LIMIT/OFFSET placeholders.
565    ///
566    /// This is because datafusion can only infer types for a limited cases.
567    ///
568    /// Example: For query `WHERE $1::TEXT AND $2`, DataFusion may not infer `$2`'s type,
569    /// but this function will return `{"$1": Some(DataType::Utf8), "$2": None}`.
570    pub fn get_inferred_parameter_types(
571        plan: &LogicalPlan,
572    ) -> Result<HashMap<String, Option<DataType>>> {
573        let mut param_types = plan.get_parameter_types().context(PlanSqlSnafu)?;
574
575        let has_none = param_types.values().any(|v| v.is_none());
576
577        if has_none {
578            let cast_types = Self::extract_placeholder_cast_types(plan)?;
579
580            for (id, opt_type) in cast_types {
581                param_types
582                    .entry(id)
583                    .and_modify(|existing| {
584                        if existing.is_none() {
585                            *existing = opt_type.clone();
586                        }
587                    })
588                    .or_insert(opt_type);
589            }
590
591            Self::infer_limit_placeholder_types(plan, &mut param_types)?;
592        }
593
594        Ok(param_types)
595    }
596}
597
598#[async_trait]
599impl LogicalPlanner for DfLogicalPlanner {
600    #[tracing::instrument(skip_all)]
601    async fn plan(&self, stmt: &QueryStatement, query_ctx: QueryContextRef) -> Result<LogicalPlan> {
602        match stmt {
603            QueryStatement::Sql(stmt) => self.plan_sql(stmt, query_ctx).await,
604            QueryStatement::Promql(stmt, _alias) => self.plan_pql(stmt, query_ctx).await,
605        }
606    }
607
608    async fn plan_logs_query(
609        &self,
610        query: LogQuery,
611        query_ctx: QueryContextRef,
612    ) -> Result<LogicalPlan> {
613        let plan_decoder = Arc::new(DefaultPlanDecoder::new(
614            self.session_state.clone(),
615            &query_ctx,
616        )?);
617        let table_provider = DfTableSourceProvider::new(
618            self.engine_state.catalog_manager().clone(),
619            self.engine_state.disallow_cross_catalog_query(),
620            query_ctx,
621            plan_decoder,
622            self.session_state
623                .config_options()
624                .sql_parser
625                .enable_ident_normalization,
626        );
627
628        let mut planner = LogQueryPlanner::new(table_provider, self.session_state.clone());
629        planner
630            .query_to_plan(query)
631            .await
632            .map_err(BoxedError::new)
633            .context(QueryPlanSnafu)
634    }
635
636    fn optimize(&self, plan: LogicalPlan) -> Result<LogicalPlan> {
637        self.optimize_logical_plan(plan)
638    }
639
640    fn as_any(&self) -> &dyn Any {
641        self
642    }
643}
644
645#[cfg(test)]
646mod tests {
647    use std::sync::Arc;
648
649    use arrow_schema::DataType;
650    use catalog::RegisterTableRequest;
651    use catalog::memory::MemoryCatalogManager;
652    use common_catalog::consts::{DEFAULT_CATALOG_NAME, DEFAULT_SCHEMA_NAME};
653    use datatypes::prelude::ConcreteDataType;
654    use datatypes::schema::{ColumnSchema, Schema};
655    use session::context::QueryContext;
656    use store_api::metric_engine_consts::{
657        DATA_SCHEMA_TABLE_ID_COLUMN_NAME, DATA_SCHEMA_TSID_COLUMN_NAME, LOGICAL_TABLE_METADATA_KEY,
658        METRIC_ENGINE_NAME,
659    };
660    use table::metadata::{TableInfoBuilder, TableMetaBuilder};
661    use table::test_util::EmptyTable;
662
663    use super::*;
664    use crate::parser::{PromQuery, QueryLanguageParser};
665    use crate::{QueryEngineFactory, QueryEngineRef};
666
667    async fn create_test_engine() -> QueryEngineRef {
668        let columns = vec![
669            ColumnSchema::new("id", ConcreteDataType::int32_datatype(), false),
670            ColumnSchema::new("name", ConcreteDataType::string_datatype(), true),
671        ];
672        let schema = Arc::new(Schema::new(columns));
673        let table_meta = TableMetaBuilder::empty()
674            .schema(schema)
675            .primary_key_indices(vec![0])
676            .value_indices(vec![1])
677            .next_column_id(1024)
678            .build()
679            .unwrap();
680        let table_info = TableInfoBuilder::new("test", table_meta).build().unwrap();
681        let table = EmptyTable::from_table_info(&table_info);
682
683        crate::tests::new_query_engine_with_table(table)
684    }
685
686    fn create_promql_test_engine() -> QueryEngineRef {
687        let catalog_manager = MemoryCatalogManager::with_default_setup();
688        let physical_table_name = "phy";
689        let physical_table_id = 999u32;
690
691        let physical_schema = Arc::new(Schema::new(vec![
692            ColumnSchema::new(
693                DATA_SCHEMA_TABLE_ID_COLUMN_NAME.to_string(),
694                ConcreteDataType::uint32_datatype(),
695                false,
696            ),
697            ColumnSchema::new(
698                DATA_SCHEMA_TSID_COLUMN_NAME.to_string(),
699                ConcreteDataType::uint64_datatype(),
700                false,
701            ),
702            ColumnSchema::new("tag_0", ConcreteDataType::string_datatype(), false),
703            ColumnSchema::new("tag_1", ConcreteDataType::string_datatype(), false),
704            ColumnSchema::new(
705                "timestamp",
706                ConcreteDataType::timestamp_millisecond_datatype(),
707                false,
708            )
709            .with_time_index(true),
710            ColumnSchema::new("field_0", ConcreteDataType::float64_datatype(), true),
711        ]));
712        let physical_meta = TableMetaBuilder::empty()
713            .schema(physical_schema)
714            .primary_key_indices(vec![0, 1, 2, 3])
715            .value_indices(vec![4, 5])
716            .engine(METRIC_ENGINE_NAME.to_string())
717            .next_column_id(1024)
718            .build()
719            .unwrap();
720        let physical_info = TableInfoBuilder::default()
721            .table_id(physical_table_id)
722            .name(physical_table_name)
723            .meta(physical_meta)
724            .build()
725            .unwrap();
726        catalog_manager
727            .register_table_sync(RegisterTableRequest {
728                catalog: DEFAULT_CATALOG_NAME.to_string(),
729                schema: DEFAULT_SCHEMA_NAME.to_string(),
730                table_name: physical_table_name.to_string(),
731                table_id: physical_table_id,
732                table: EmptyTable::from_table_info(&physical_info),
733            })
734            .unwrap();
735
736        let mut options = table::requests::TableOptions::default();
737        options.extra_options.insert(
738            LOGICAL_TABLE_METADATA_KEY.to_string(),
739            physical_table_name.to_string(),
740        );
741        let logical_schema = Arc::new(Schema::new(vec![
742            ColumnSchema::new("tag_0", ConcreteDataType::string_datatype(), false),
743            ColumnSchema::new("tag_1", ConcreteDataType::string_datatype(), false),
744            ColumnSchema::new(
745                "timestamp",
746                ConcreteDataType::timestamp_millisecond_datatype(),
747                false,
748            )
749            .with_time_index(true),
750            ColumnSchema::new("field_0", ConcreteDataType::float64_datatype(), true),
751        ]));
752        let logical_meta = TableMetaBuilder::empty()
753            .schema(logical_schema)
754            .primary_key_indices(vec![0, 1])
755            .value_indices(vec![3])
756            .engine(METRIC_ENGINE_NAME.to_string())
757            .options(options)
758            .next_column_id(1024)
759            .build()
760            .unwrap();
761        let logical_info = TableInfoBuilder::default()
762            .table_id(1024)
763            .name("some_metric")
764            .meta(logical_meta)
765            .build()
766            .unwrap();
767        catalog_manager
768            .register_table_sync(RegisterTableRequest {
769                catalog: DEFAULT_CATALOG_NAME.to_string(),
770                schema: DEFAULT_SCHEMA_NAME.to_string(),
771                table_name: "some_metric".to_string(),
772                table_id: 1024,
773                table: EmptyTable::from_table_info(&logical_info),
774            })
775            .unwrap();
776
777        QueryEngineFactory::new(
778            catalog_manager,
779            None,
780            None,
781            None,
782            None,
783            false,
784            crate::options::QueryOptions::default(),
785        )
786        .query_engine()
787    }
788
789    async fn parse_sql_to_plan(sql: &str) -> LogicalPlan {
790        let stmt = QueryLanguageParser::parse_sql(sql, &QueryContext::arc()).unwrap();
791        let engine = create_test_engine().await;
792        engine
793            .planner()
794            .plan(&stmt, QueryContext::arc())
795            .await
796            .unwrap()
797    }
798
799    async fn parse_promql_to_plan(query: &str) -> LogicalPlan {
800        let engine = create_promql_test_engine();
801        let query_ctx = QueryContext::arc();
802        let stmt = QueryLanguageParser::parse_promql(
803            &PromQuery {
804                query: query.to_string(),
805                start: "0".to_string(),
806                end: "10".to_string(),
807                step: "5s".to_string(),
808                lookback: "300s".to_string(),
809                alias: None,
810            },
811            &query_ctx,
812        )
813        .unwrap();
814
815        engine.planner().plan(&stmt, query_ctx).await.unwrap()
816    }
817
818    #[tokio::test]
819    async fn test_extract_placeholder_cast_types_multiple() {
820        let plan = parse_sql_to_plan(
821            "SELECT $1::INT, $2::TEXT, $3, $4::INTEGER FROM test WHERE $5::FLOAT > 0",
822        )
823        .await;
824        let types = DfLogicalPlanner::extract_placeholder_cast_types(&plan).unwrap();
825
826        assert_eq!(types.len(), 5);
827        assert_eq!(types.get("$1"), Some(&Some(DataType::Int32)));
828        assert_eq!(types.get("$2"), Some(&Some(DataType::Utf8)));
829        assert_eq!(types.get("$3"), Some(&None));
830        assert_eq!(types.get("$4"), Some(&Some(DataType::Int32)));
831        assert_eq!(types.get("$5"), Some(&Some(DataType::Float32)));
832    }
833
834    #[tokio::test]
835    async fn test_get_inferred_parameter_types_fallback_for_udf_args() {
836        // datafusion is not able to infer type for scalar function arguments
837        let plan = parse_sql_to_plan(
838            "SELECT parse_ident($1), parse_ident($2::TEXT) FROM test WHERE id > $3",
839        )
840        .await;
841        let types = DfLogicalPlanner::get_inferred_parameter_types(&plan).unwrap();
842
843        assert_eq!(types.len(), 3);
844
845        let type_1 = types.get("$1").unwrap();
846        let type_2 = types.get("$2").unwrap();
847        let type_3 = types.get("$3").unwrap();
848
849        assert!(type_1.is_none(), "Expected $1 to be None");
850        assert_eq!(type_2, &Some(DataType::Utf8));
851        assert_eq!(type_3, &Some(DataType::Int32));
852    }
853
854    #[tokio::test]
855    async fn test_get_inferred_parameter_types_limit_offset() {
856        let plan = parse_sql_to_plan("SELECT id FROM test LIMIT $1 OFFSET $2").await;
857        let types = DfLogicalPlanner::get_inferred_parameter_types(&plan).unwrap();
858
859        assert_eq!(types.get("$1"), Some(&Some(DataType::Int64)));
860        assert_eq!(types.get("$2"), Some(&Some(DataType::Int64)));
861    }
862
863    #[tokio::test]
864    async fn test_plan_pql_applies_extension_rules() {
865        for inner_agg in ["count", "sum", "avg", "min", "max", "stddev", "stdvar"] {
866            let plan = parse_promql_to_plan(&format!(
867                "sum(irate(some_metric[1h])) / scalar(count({inner_agg}(some_metric) by (tag_0)))"
868            ))
869            .await;
870            let plan_str = plan.display_indent_schema().to_string();
871            assert!(plan_str.contains("Distinct:"), "{inner_agg}: {plan_str}");
872        }
873    }
874
875    #[tokio::test]
876    async fn test_plan_pql_filters_null_only_groups_for_non_count_inner_aggs() {
877        let count_plan = parse_promql_to_plan("scalar(count(count(some_metric) by (tag_0)))").await;
878        let count_plan_str = count_plan.display_indent_schema().to_string();
879        assert!(
880            !count_plan_str.contains("field_0 IS NOT NULL"),
881            "{count_plan_str}"
882        );
883
884        for inner_agg in ["sum", "avg", "min", "max", "stddev", "stdvar"] {
885            let plan = parse_promql_to_plan(&format!(
886                "scalar(count({inner_agg}(some_metric) by (tag_0)))"
887            ))
888            .await;
889            let plan_str = plan.display_indent_schema().to_string();
890            assert!(
891                plan_str.contains("field_0 IS NOT NULL"),
892                "{inner_agg}: {plan_str}"
893            );
894        }
895    }
896
897    #[tokio::test]
898    async fn test_plan_pql_skips_extension_rules_for_non_direct_or_unsupported_inner_agg() {
899        for query in [
900            "sum(irate(some_metric[1h])) / scalar(count(sum(irate(some_metric[1h])) by (tag_0)))",
901            "sum(irate(some_metric[1h])) / scalar(count(group(some_metric) by (tag_0)))",
902        ] {
903            let plan = parse_promql_to_plan(query).await;
904            let plan_str = plan.display_indent_schema().to_string();
905            assert!(!plan_str.contains("Distinct:"), "{query}: {plan_str}");
906        }
907    }
908
909    #[tokio::test]
910    async fn test_plan_sql_does_not_apply_nested_count_rule() {
911        let plan = parse_sql_to_plan(
912            "SELECT id, count(inner_count) \
913             FROM ( \
914                 SELECT id, count(name) AS inner_count \
915                 FROM test \
916                 GROUP BY id \
917                 ORDER BY id \
918                 LIMIT 1000000 \
919             ) t \
920             GROUP BY id \
921             ORDER BY id",
922        )
923        .await;
924
925        let plan_str = plan.display_indent_schema().to_string();
926        assert!(!plan_str.contains("Distinct:"), "{plan_str}");
927    }
928
929    #[tokio::test]
930    async fn test_get_inferred_parameter_types_subquery() {
931        let plan = parse_sql_to_plan(
932            r#"SELECT * FROM test WHERE id = (SELECT id FROM test CROSS JOIN (SELECT parse_ident($1::TEXT) AS parts) p LIMIT 1)"#,
933        ).await;
934        let types = DfLogicalPlanner::get_inferred_parameter_types(&plan).unwrap();
935
936        assert_eq!(types.len(), 1);
937        let type_1 = types.get("$1").unwrap();
938        assert_eq!(type_1, &Some(DataType::Utf8));
939    }
940
941    #[tokio::test]
942    async fn test_get_inferred_parameter_types_insert() {
943        let plan = parse_sql_to_plan("INSERT INTO test (id, name) VALUES ($1, $2), ($3, $4)").await;
944        let types = DfLogicalPlanner::get_inferred_parameter_types(&plan).unwrap();
945
946        assert_eq!(types.len(), 4);
947        assert_eq!(types.get("$1"), Some(&Some(DataType::Int32)));
948        assert_eq!(types.get("$2"), Some(&Some(DataType::Utf8)));
949        assert_eq!(types.get("$3"), Some(&Some(DataType::Int32)));
950        assert_eq!(types.get("$4"), Some(&Some(DataType::Utf8)));
951    }
952
953    #[tokio::test]
954    async fn test_get_inferred_parameter_types_arrow_cast() {
955        let plan = parse_sql_to_plan("SELECT $1::INT64, $2::FLOAT64, $3::INT16, $4::INT32, $5::UINT8, $6::UINT16, $7::UINT32").await;
956        let types = DfLogicalPlanner::get_inferred_parameter_types(&plan).unwrap();
957
958        assert_eq!(types.get("$1"), Some(&Some(DataType::Int64)));
959        assert_eq!(types.get("$2"), Some(&Some(DataType::Float64)));
960        assert_eq!(types.get("$3"), Some(&Some(DataType::Int16)));
961        assert_eq!(types.get("$4"), Some(&Some(DataType::Int32)));
962        assert_eq!(types.get("$5"), Some(&Some(DataType::UInt8)));
963        assert_eq!(types.get("$6"), Some(&Some(DataType::UInt16)));
964        assert_eq!(types.get("$7"), Some(&Some(DataType::UInt32)));
965
966        let plan = parse_sql_to_plan("SELECT $1::INT8, $2::FLOAT8, $3::INT2, $4::INT8").await;
967        let types = DfLogicalPlanner::get_inferred_parameter_types(&plan).unwrap();
968
969        assert_eq!(types.get("$1"), Some(&Some(DataType::Int64)));
970        assert_eq!(types.get("$2"), Some(&Some(DataType::Float64)));
971        assert_eq!(types.get("$3"), Some(&Some(DataType::Int16)));
972    }
973}