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