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