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