1use 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 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 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 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 if let Statement::Explain(explain) = stmt.as_ref() {
200 return self.explain_to_plan(explain, query_ctx).await;
201 }
202
203 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 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 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 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 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 #[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 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 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 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 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 unreachable!("SQL CTEs should not be in hybrid_cte.cte_tables");
457 }
458 }
459 }
460
461 Ok(())
462 }
463
464 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 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 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 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 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 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 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 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
651fn 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 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 assert!(
1081 plan.contains("arrow_cast(Utf8(\"2026-08-08 12:00:00.001\")"),
1082 "{plan}"
1083 );
1084 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 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 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 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 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 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}