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