1use 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 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 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 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 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 if let Statement::Explain(explain) = stmt.as_ref() {
175 return self.explain_to_plan(explain, query_ctx).await;
176 }
177
178 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 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 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 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 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 #[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 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 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 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 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 unreachable!("SQL CTEs should not be in hybrid_cte.cte_tables");
405 }
406 }
407 }
408
409 Ok(())
410 }
411
412 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 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 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 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 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 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 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 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 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}