1use std::collections::{HashMap, HashSet};
16use std::sync::Arc;
17use std::time::Duration;
18
19use api::helper::ColumnDataTypeWrapper;
20use api::v1::alter_table_expr::Kind;
21use api::v1::meta::CreateFlowTask as PbCreateFlowTask;
22use api::v1::repartition::Source;
23use api::v1::{
24 AlterDatabaseExpr, AlterTableExpr, CreateFlowExpr, CreateTableExpr, CreateViewExpr,
25 PartitionedSource, Repartition, TargetPartitionColumns, UnpartitionedSource, column_def,
26};
27#[cfg(feature = "enterprise")]
28use api::v1::{
29 CreateTriggerExpr as PbCreateTriggerExpr, meta::CreateTriggerTask as PbCreateTriggerTask,
30};
31use catalog::CatalogManagerRef;
32use chrono::Utc;
33use common_base::regex_pattern::NAME_PATTERN_REG;
34use common_catalog::consts::{
35 DEFAULT_CATALOG_NAME, DEFAULT_SCHEMA_NAME, is_ddl_reserved_table, is_readonly_schema,
36 is_readonly_table,
37};
38use common_catalog::{format_full_flow_name, format_full_table_name};
39use common_error::ext::BoxedError;
40#[cfg(feature = "enterprise")]
41use common_meta::cache_invalidator::CacheInvalidatorRef;
42use common_meta::cache_invalidator::Context;
43use common_meta::ddl::create_flow::{
44 DEFER_ON_MISSING_SOURCE_KEY, FLOW_EXPERIMENTAL_ENABLE_INCREMENTAL_READ_KEY, FlowType,
45 INTERNAL_EVAL_OFFSET_KEY, INTERNAL_EVAL_SCHEDULE_KEY,
46};
47use common_meta::instruction::CacheIdent;
48#[cfg(feature = "enterprise")]
49use common_meta::key::TableMetadataManagerRef;
50use common_meta::key::schema_name::{SchemaName, SchemaNameKey};
51#[cfg(feature = "enterprise")]
52use common_meta::procedure_executor::ProcedureExecutorRef;
53#[cfg(feature = "enterprise")]
54use common_meta::rpc::ddl::trigger::CreateTriggerTask;
55#[cfg(feature = "enterprise")]
56use common_meta::rpc::ddl::trigger::DropTriggerTask;
57use common_meta::rpc::ddl::{
58 CreateFlowTask, CreatorGrantIntent, DdlTask, DropFlowTask, DropViewTask, SubmitDdlTaskRequest,
59 SubmitDdlTaskResponse, TriggerReason,
60};
61use common_query::Output;
62use common_recordbatch::{RecordBatch, RecordBatches};
63use common_sql::convert::sql_value_to_value;
64use common_telemetry::{debug, info, tracing, warn};
65use common_time::{Timestamp, Timezone};
66use datafusion_common::tree_node::TreeNodeVisitor;
67use datafusion_expr::LogicalPlan;
68use datafusion_expr::logical_plan::Distinct;
69use datatypes::prelude::ConcreteDataType;
70use datatypes::schema::{ColumnSchema, Schema};
71use datatypes::value::Value;
72use datatypes::vectors::{StringVector, VectorRef};
73use humantime::parse_duration;
74use partition::expr::{Operand, PartitionExpr, RestrictedOp};
75use partition::function::IcebergHash;
76use partition::multi_dim::MultiDimPartitionRule;
77use query::parser::QueryStatement;
78use query::plan::extract_and_rewrite_full_table_names;
79use query::query_engine::DefaultSerializer;
80use query::sql::create_table_stmt;
81use session::context::QueryContextRef;
82use session::table_name::table_idents_to_full_name;
83use snafu::{OptionExt, ResultExt, ensure};
84use sql::parser::{ParseOptions, ParserContext};
85use sql::parsers::utils::is_tql;
86use sql::statements::OptionMap;
87#[cfg(feature = "enterprise")]
88use sql::statements::alter::trigger::AlterTrigger;
89use sql::statements::alter::{AlterDatabase, AlterTable, AlterTableOperation};
90#[cfg(feature = "enterprise")]
91use sql::statements::create::trigger::CreateTrigger;
92use sql::statements::create::{
93 CreateExternalTable, CreateFlow, CreateTable, CreateTableLike, CreateView, Partitions,
94};
95use sql::statements::statement::Statement;
96use sqlparser::ast::{
97 Expr, Function, FunctionArg, FunctionArgExpr, FunctionArguments, Ident, UnaryOperator,
98 Value as ParserValue,
99};
100use store_api::metric_engine_consts::{
101 LOGICAL_TABLE_METADATA_KEY, METRIC_ENGINE_NAME, PHYSICAL_TABLE_METADATA_KEY,
102};
103use store_api::mito_engine_options::APPEND_MODE_KEY;
104use substrait::{DFLogicalSubstraitConvertor, SubstraitPlan};
105use table::TableRef;
106use table::dist_table::DistTable;
107use table::metadata::{self, TableId, TableInfo, TableMeta, TableType};
108use table::requests::{
109 AlterKind, AlterTableRequest, COMMENT_KEY, DDL_TIMEOUT, DDL_WAIT, INGEST_ROWS_RATE_LIMIT_KEY,
110 TableOptions, validate_and_normalize_annotation_options,
111};
112use table::table_name::TableName;
113use table::table_reference::TableReference;
114
115use crate::error::{
116 self, AlterExprToRequestSnafu, BuildDfLogicalPlanSnafu, CatalogSnafu, ColumnDataTypeSnafu,
117 ColumnNotFoundSnafu, ConvertSchemaSnafu, CreateLogicalTablesSnafu,
118 DeserializePartitionExprSnafu, EmptyDdlExprSnafu, ExternalSnafu, ExtractTableNamesSnafu,
119 FlowNotFoundSnafu, InvalidPartitionRuleSnafu, InvalidPartitionSnafu, InvalidSqlSnafu,
120 InvalidTableNameSnafu, InvalidViewNameSnafu, InvalidViewStmtSnafu, NotSupportedSnafu,
121 PartitionExprToPbSnafu, Result, SchemaInUseSnafu, SchemaNotFoundSnafu, SchemaReadOnlySnafu,
122 SerializePartitionExprSnafu, SubstraitCodecSnafu, TableAlreadyExistsSnafu,
123 TableDdlReservedSnafu, TableMetadataManagerSnafu, TableNotFoundSnafu, TableReadOnlySnafu,
124 UnrecognizedTableOptionSnafu, ViewAlreadyExistsSnafu,
125};
126use crate::expr_helper::{self, RepartitionRequest, RepartitionSource};
127use crate::statement::StatementExecutor;
128use crate::statement::show::create_partitions_stmt;
129use crate::utils::{to_executor_context, to_executor_context_with_origin_frontend};
130
131#[derive(Debug, Clone, Copy)]
133pub(crate) struct DdlSubmitOptions {
134 wait: bool,
135 timeout: Duration,
136}
137
138impl Default for DdlSubmitOptions {
139 fn default() -> Self {
140 Self {
141 wait: SubmitDdlTaskRequest::default_wait(),
142 timeout: SubmitDdlTaskRequest::default_timeout(),
143 }
144 }
145}
146
147#[cfg(not(feature = "enterprise"))]
148const ALLOWED_FLOW_OPTIONS: [&str; 2] = [
149 DEFER_ON_MISSING_SOURCE_KEY,
150 FLOW_EXPERIMENTAL_ENABLE_INCREMENTAL_READ_KEY,
151];
152
153fn build_procedure_id_output(procedure_id: &str) -> Result<Output> {
154 let vector: VectorRef = Arc::new(StringVector::from(vec![procedure_id]));
155 let schema = Arc::new(Schema::new(vec![ColumnSchema::new(
156 "Procedure ID",
157 vector.data_type(),
158 false,
159 )]));
160 let batch =
161 RecordBatch::new(schema.clone(), vec![vector]).context(error::BuildRecordBatchSnafu)?;
162 let batches =
163 RecordBatches::try_new(schema, vec![batch]).context(error::BuildRecordBatchSnafu)?;
164 Ok(Output::new_with_record_batches(batches))
165}
166
167pub(crate) fn parse_ddl_options(options: &OptionMap) -> Result<DdlSubmitOptions> {
169 let defaults = DdlSubmitOptions::default();
170 let wait = match options.get(DDL_WAIT) {
171 Some(value) => value.parse::<bool>().map_err(|_| {
172 InvalidSqlSnafu {
173 err_msg: format!("invalid DDL option '{DDL_WAIT}': '{value}'"),
174 }
175 .build()
176 })?,
177 None => defaults.wait,
178 };
179
180 let timeout = match options.get(DDL_TIMEOUT) {
181 Some(value) => parse_duration(value).map_err(|err| {
182 InvalidSqlSnafu {
183 err_msg: format!("invalid DDL option '{DDL_TIMEOUT}': '{value}': {err}"),
184 }
185 .build()
186 })?,
187 None => defaults.timeout,
188 };
189
190 Ok(DdlSubmitOptions { wait, timeout })
191}
192
193#[cfg(not(feature = "enterprise"))]
194fn supported_flow_options() -> String {
195 ALLOWED_FLOW_OPTIONS.join(", ")
196}
197
198fn normalize_flow_bool_option(key: &str, value: &str) -> Result<String> {
199 value
200 .trim()
201 .to_ascii_lowercase()
202 .parse::<bool>()
203 .map(|value| value.to_string())
204 .map_err(|_| {
205 InvalidSqlSnafu {
206 err_msg: format!("invalid flow option '{key}': '{value}'"),
207 }
208 .build()
209 })
210}
211
212fn validate_and_normalize_flow_options(
213 options: HashMap<String, String>,
214 eval_interval: Option<i64>,
215) -> Result<HashMap<String, String>> {
216 if let Some(secs) = eval_interval
218 && secs <= 0
219 {
220 return InvalidSqlSnafu {
221 err_msg: format!("EVAL INTERVAL must be positive, got {secs} seconds"),
222 }
223 .fail();
224 }
225
226 options
227 .into_iter()
228 .map(|(key, value)| {
229 if matches!(
230 key.as_str(),
231 FlowType::FLOW_TYPE_KEY | INTERNAL_EVAL_OFFSET_KEY | INTERNAL_EVAL_SCHEDULE_KEY
232 ) {
233 return InvalidSqlSnafu {
234 err_msg: format!("flow option '{key}' is reserved for internal use"),
235 }
236 .fail();
237 }
238
239 let normalized_value = match key.as_str() {
240 DEFER_ON_MISSING_SOURCE_KEY | FLOW_EXPERIMENTAL_ENABLE_INCREMENTAL_READ_KEY => {
241 normalize_flow_bool_option(&key, &value)?
242 }
243 #[cfg(feature = "enterprise")]
244 _ => value,
245 #[cfg(not(feature = "enterprise"))]
246 _ => {
247 return InvalidSqlSnafu {
248 err_msg: format!(
249 "unknown flow option '{key}', supported options: {}",
250 supported_flow_options()
251 ),
252 }
253 .fail();
254 }
255 };
256
257 Ok((key, normalized_value))
258 })
259 .collect()
260}
261
262fn validate_eval_offset(
266 eval_offset_secs: Option<i64>,
267 eval_interval_secs: Option<i64>,
268) -> Result<()> {
269 if let Some(offset_secs) = eval_offset_secs {
270 let Some(eval_interval_secs) = eval_interval_secs else {
271 return InvalidSqlSnafu {
272 err_msg: "EVAL OFFSET requires EVAL INTERVAL to be specified".to_string(),
273 }
274 .fail();
275 };
276 if !(0..eval_interval_secs).contains(&offset_secs) {
277 return InvalidSqlSnafu {
278 err_msg: format!(
279 "EVAL OFFSET must be in range [0, EVAL INTERVAL), got {offset_secs} seconds with EVAL INTERVAL {eval_interval_secs} seconds"
280 ),
281 }
282 .fail();
283 }
284 }
285 Ok(())
286}
287
288fn determine_flow_type_for_source_state(
289 flow_name: &str,
290 flow_options: &HashMap<String, String>,
291 has_missing_source_table: bool,
292 has_instant_ttl_source_table: bool,
293 force_batching: bool,
294) -> Result<Option<FlowType>> {
295 if has_instant_ttl_source_table && force_batching {
296 return InvalidSqlSnafu {
299 err_msg: format!(
300 "flow '{}' with EVAL INTERVAL requires the batching scheduler, but source tables with ttl=instant are not supported under batching mode; use a TTL longer than the flush interval",
301 flow_name
302 ),
303 }
304 .fail();
305 }
306
307 if has_missing_source_table {
308 let defer_on_missing_source = flow_options
309 .get(DEFER_ON_MISSING_SOURCE_KEY)
310 .is_some_and(|value| value == "true");
311 ensure!(
312 defer_on_missing_source,
313 InvalidSqlSnafu {
314 err_msg: format!(
315 "missing source tables for flow '{}'; use WITH ({DEFER_ON_MISSING_SOURCE_KEY} = true) to create a pending flow",
316 flow_name
317 )
318 }
319 );
320 info!(
321 "Flow `{}` is created as a pending batching flow because source tables are missing and defer_on_missing_source=true",
322 flow_name
323 );
324 return Ok(Some(FlowType::Batching));
325 }
326
327 Ok(None)
328}
329
330fn is_stateless_flow_plan(plan: &LogicalPlan) -> bool {
334 let mut scans = 0;
335 let mut supported_nodes = true;
336 let result = plan.apply_with_subqueries(|node| {
337 match node {
338 LogicalPlan::TableScan(_) => scans += 1,
339 LogicalPlan::Projection(_) | LogicalPlan::Filter(_) => {}
340 LogicalPlan::Distinct(Distinct::All(_)) => {}
341 _ => supported_nodes = false,
342 }
343 Ok(datafusion_common::tree_node::TreeNodeRecursion::Continue)
344 });
345
346 result.is_ok() && supported_nodes && scans == 1
347}
348
349const INSTANT_TTL_FLOW_QUERY_ERROR: &str = "instant-TTL flow sources support only stateless projection/filter queries over one source; non-stateless queries require a persisted source";
350
351const STATELESS_FLOW_QUERY_ERROR: &str =
352 "flow streaming supports only stateless projection/filter queries over one source";
353
354impl StatementExecutor {
355 pub fn catalog_manager(&self) -> CatalogManagerRef {
356 self.catalog_manager.clone()
357 }
358
359 #[tracing::instrument(skip_all)]
360 pub async fn create_table(&self, stmt: CreateTable, ctx: QueryContextRef) -> Result<TableRef> {
361 let mut create_expr = self.prepare_create_table(&stmt, &ctx).await?;
362 self.create_table_inner(
363 &mut create_expr,
364 stmt.partitions,
365 ctx,
366 TriggerReason::Manual,
367 )
368 .await
369 }
370
371 async fn prepare_create_table(
372 &self,
373 stmt: &CreateTable,
374 ctx: &QueryContextRef,
375 ) -> Result<CreateTableExpr> {
376 let (catalog, schema, _table) = table_idents_to_full_name(&stmt.name, ctx)
377 .map_err(BoxedError::new)
378 .context(error::ExternalSnafu)?;
379
380 let schema_options = self
381 .table_metadata_manager
382 .schema_manager()
383 .get(SchemaNameKey {
384 catalog: &catalog,
385 schema: &schema,
386 })
387 .await
388 .context(TableMetadataManagerSnafu)?
389 .map(|v| v.into_inner());
390
391 let mut create_expr = expr_helper::create_to_expr(stmt, ctx)?;
392 if let Some(schema_options) = schema_options {
394 for (key, value) in schema_options.extra_options.iter() {
395 if key == INGEST_ROWS_RATE_LIMIT_KEY || key.starts_with("compaction.") {
396 continue;
397 }
398 create_expr
399 .table_options
400 .entry(key.clone())
401 .or_insert(value.clone());
402 }
403 }
404
405 Ok(create_expr)
406 }
407
408 pub async fn batch_create_logical_tables(
410 &self,
411 statements: Vec<CreateTable>,
412 ctx: QueryContextRef,
413 ) -> Result<()> {
414 let mut expressions = Vec::with_capacity(statements.len());
415 let mut targets = std::collections::HashSet::new();
416 let mut group = None;
417 for statement in statements {
418 ensure!(
419 statement.engine == METRIC_ENGINE_NAME
420 && statement.options.get(LOGICAL_TABLE_METADATA_KEY).is_some()
421 && statement
422 .options
423 .value(PHYSICAL_TABLE_METADATA_KEY)
424 .is_none(),
425 InvalidSqlSnafu {
426 err_msg: "batch requires explicit Metric logical CREATE TABLE statements"
427 }
428 );
429 let expr = self.prepare_create_table(&statement, &ctx).await?;
430 let key = (
431 expr.catalog_name.clone(),
432 expr.schema_name.clone(),
433 expr.table_options.get(LOGICAL_TABLE_METADATA_KEY).cloned(),
434 );
435 ensure!(
436 group.as_ref().is_none_or(|group| group == &key),
437 InvalidSqlSnafu {
438 err_msg: "batch must share catalog, schema and literal on_physical_table"
439 }
440 );
441 group = Some(key);
442 ensure!(
443 targets.insert(expr.table_name.clone()),
444 InvalidSqlSnafu {
445 err_msg: "duplicate target in logical-table batch"
446 }
447 );
448 self.validate_logical_create(&expr, statement.partitions.as_ref(), &ctx)
449 .await?;
450 expressions.push(expr);
451 }
452 self.create_logical_tables(&expressions, ctx, TriggerReason::Manual)
453 .await?;
454 Ok(())
455 }
456
457 async fn validate_logical_create(
458 &self,
459 expr: &CreateTableExpr,
460 partitions: Option<&Partitions>,
461 ctx: &QueryContextRef,
462 ) -> Result<()> {
463 ensure_table_definition_writable(&expr.schema_name, &expr.table_name)?;
464 if let Some(partitions) = partitions
465 && !partitions.exprs.is_empty()
466 {
467 self.validate_logical_table_partition_rule(expr, partitions, ctx)
468 .await?;
469 }
470 Ok(())
471 }
472
473 #[tracing::instrument(skip_all)]
474 pub async fn create_table_like(
475 &self,
476 stmt: CreateTableLike,
477 ctx: QueryContextRef,
478 ) -> Result<TableRef> {
479 let (catalog, schema, table) = table_idents_to_full_name(&stmt.source_name, &ctx)
480 .map_err(BoxedError::new)
481 .context(error::ExternalSnafu)?;
482 let table_ref = self
483 .catalog_manager
484 .table(&catalog, &schema, &table, Some(&ctx))
485 .await
486 .context(CatalogSnafu)?
487 .context(TableNotFoundSnafu { table_name: &table })?;
488 let partition_info = self
489 .partition_manager
490 .find_physical_partition_info(table_ref.table_info().table_id())
491 .await
492 .context(error::FindTablePartitionRuleSnafu { table_name: table })?;
493
494 let schema_options = self
496 .table_metadata_manager
497 .schema_manager()
498 .get(SchemaNameKey {
499 catalog: &catalog,
500 schema: &schema,
501 })
502 .await
503 .context(TableMetadataManagerSnafu)?
504 .map(|v| v.into_inner());
505
506 let quote_style = ctx.quote_style();
507 let mut create_stmt =
508 create_table_stmt(&table_ref.table_info(), schema_options, quote_style)
509 .context(error::ParseQuerySnafu)?;
510 create_stmt.name = stmt.table_name;
511 create_stmt.if_not_exists = false;
512
513 let table_info = table_ref.table_info();
514 let partitions = create_partitions_stmt(&table_info, &partition_info.partitions)?.and_then(
515 |mut partitions| {
516 if !partitions.column_list.is_empty() {
517 partitions.set_quote(quote_style);
518 Some(partitions)
519 } else {
520 None
521 }
522 },
523 );
524
525 let create_expr = &mut expr_helper::create_to_expr(&create_stmt, &ctx)?;
526 self.create_table_inner(create_expr, partitions, ctx, TriggerReason::Manual)
527 .await
528 }
529
530 #[tracing::instrument(skip_all)]
531 pub async fn create_external_table(
532 &self,
533 create_expr: CreateExternalTable,
534 ctx: QueryContextRef,
535 ) -> Result<TableRef> {
536 let create_expr =
537 &mut expr_helper::create_external_expr(create_expr, &ctx, &self.local_file_access)
538 .await?;
539 self.create_table_inner(create_expr, None, ctx, TriggerReason::Manual)
540 .await
541 }
542
543 pub async fn create_declared_relationships_table(
547 &self,
548 catalog: &str,
549 query_ctx: QueryContextRef,
550 ) -> Result<TableRef> {
551 let mut expr = super::semantic_graph::build_declared_relationships_expr(catalog);
552 self.create_non_logic_table(&mut expr, None, query_ctx, TriggerReason::AutoCreate)
553 .await
554 }
555
556 #[tracing::instrument(skip_all)]
557 pub async fn create_table_inner(
558 &self,
559 create_table: &mut CreateTableExpr,
560 partitions: Option<Partitions>,
561 query_ctx: QueryContextRef,
562 trigger_reason: TriggerReason,
563 ) -> Result<TableRef> {
564 ensure_table_definition_writable(&create_table.schema_name, &create_table.table_name)?;
565
566 if create_table.engine == METRIC_ENGINE_NAME
567 && create_table
568 .table_options
569 .contains_key(LOGICAL_TABLE_METADATA_KEY)
570 {
571 self.validate_logical_create(create_table, partitions.as_ref(), &query_ctx)
572 .await?;
573 self.create_logical_tables(
575 std::slice::from_ref(create_table),
576 query_ctx,
577 trigger_reason,
578 )
579 .await?
580 .into_iter()
581 .next()
582 .context(error::UnexpectedSnafu {
583 violated: "expected to create logical tables",
584 })
585 } else {
586 self.create_non_logic_table(create_table, partitions, query_ctx, trigger_reason)
588 .await
589 }
590 }
591
592 #[tracing::instrument(skip_all)]
593 pub async fn create_non_logic_table(
594 &self,
595 create_table: &mut CreateTableExpr,
596 partitions: Option<Partitions>,
597 query_ctx: QueryContextRef,
598 trigger_reason: TriggerReason,
599 ) -> Result<TableRef> {
600 let _timer = crate::metrics::DIST_CREATE_TABLE.start_timer();
601
602 let schema = self
604 .table_metadata_manager
605 .schema_manager()
606 .get(SchemaNameKey::new(
607 &create_table.catalog_name,
608 &create_table.schema_name,
609 ))
610 .await
611 .context(TableMetadataManagerSnafu)?;
612 ensure!(
613 schema.is_some(),
614 SchemaNotFoundSnafu {
615 schema_info: &create_table.schema_name,
616 }
617 );
618
619 if let Some(table) = self
621 .catalog_manager
622 .table(
623 &create_table.catalog_name,
624 &create_table.schema_name,
625 &create_table.table_name,
626 Some(&query_ctx),
627 )
628 .await
629 .context(CatalogSnafu)?
630 {
631 return if create_table.create_if_not_exists {
632 Ok(table)
633 } else {
634 TableAlreadyExistsSnafu {
635 table: format_full_table_name(
636 &create_table.catalog_name,
637 &create_table.schema_name,
638 &create_table.table_name,
639 ),
640 }
641 .fail()
642 };
643 }
644
645 ensure!(
646 NAME_PATTERN_REG.is_match(&create_table.table_name),
647 InvalidTableNameSnafu {
648 table_name: &create_table.table_name,
649 }
650 );
651
652 let table_name = TableName::new(
653 &create_table.catalog_name,
654 &create_table.schema_name,
655 &create_table.table_name,
656 );
657
658 let (partitions, partition_cols) = parse_partitions(create_table, partitions, &query_ctx)?;
659 let mut table_info = create_table_info(create_table, partition_cols)?;
660
661 let resp = self
662 .create_table_procedure(
663 create_table.clone(),
664 partitions,
665 table_info.clone(),
666 query_ctx,
667 trigger_reason,
668 )
669 .await?;
670
671 let table_id = resp
672 .table_ids
673 .into_iter()
674 .next()
675 .context(error::UnexpectedSnafu {
676 violated: "expected table_id",
677 })?;
678 info!("Successfully created table '{table_name}' with table id {table_id}");
679
680 table_info.ident.table_id = table_id;
681
682 let table_info = Arc::new(table_info);
683 create_table.table_id = Some(api::v1::TableId { id: table_id });
684
685 let table = DistTable::table(table_info);
686
687 Ok(table)
688 }
689
690 #[tracing::instrument(skip_all)]
691 pub async fn create_logical_tables(
692 &self,
693 create_table_exprs: &[CreateTableExpr],
694 query_context: QueryContextRef,
695 trigger_reason: TriggerReason,
696 ) -> Result<Vec<TableRef>> {
697 let _timer = crate::metrics::DIST_CREATE_TABLES.start_timer();
698 ensure!(
699 !create_table_exprs.is_empty(),
700 EmptyDdlExprSnafu {
701 name: "create logic tables"
702 }
703 );
704
705 for create_table in create_table_exprs {
707 ensure!(
708 NAME_PATTERN_REG.is_match(&create_table.table_name),
709 InvalidTableNameSnafu {
710 table_name: &create_table.table_name,
711 }
712 );
713 }
714
715 let raw_tables_info = create_table_exprs
716 .iter()
717 .map(|create| create_table_info(create, vec![]))
718 .collect::<Result<Vec<_>>>()?;
719 let tables_data = create_table_exprs
720 .iter()
721 .cloned()
722 .zip(raw_tables_info.iter().cloned())
723 .collect::<Vec<_>>();
724
725 let resp = self
726 .create_logical_tables_procedure(tables_data, query_context.clone(), trigger_reason)
727 .await?;
728
729 let table_ids = resp.table_ids;
730 ensure!(
731 table_ids.len() == raw_tables_info.len(),
732 CreateLogicalTablesSnafu {
733 reason: format!(
734 "The number of tables is inconsistent with the expected number to be created, expected: {}, actual: {}",
735 raw_tables_info.len(),
736 table_ids.len()
737 )
738 }
739 );
740 info!("Successfully created logical tables: {:?}", table_ids);
741
742 let mut tables_info = Vec::with_capacity(table_ids.len());
746 for (table_id, create_table) in table_ids.iter().zip(create_table_exprs.iter()) {
747 let table = self
748 .catalog_manager
749 .table(
750 &create_table.catalog_name,
751 &create_table.schema_name,
752 &create_table.table_name,
753 Some(&query_context),
754 )
755 .await
756 .context(CatalogSnafu)?
757 .with_context(|| TableNotFoundSnafu {
758 table_name: format_full_table_name(
759 &create_table.catalog_name,
760 &create_table.schema_name,
761 &create_table.table_name,
762 ),
763 })?;
764
765 let table_info = table.table_info();
766 ensure!(
768 table_info.table_id() == *table_id,
769 CreateLogicalTablesSnafu {
770 reason: format!(
771 "Table id mismatch after creation, expected {}, got {} for table {}",
772 table_id,
773 table_info.table_id(),
774 format_full_table_name(
775 &create_table.catalog_name,
776 &create_table.schema_name,
777 &create_table.table_name
778 )
779 )
780 }
781 );
782
783 tables_info.push(table_info);
784 }
785
786 Ok(tables_info.into_iter().map(DistTable::table).collect())
787 }
788
789 async fn validate_logical_table_partition_rule(
790 &self,
791 create_table: &CreateTableExpr,
792 partitions: &Partitions,
793 query_ctx: &QueryContextRef,
794 ) -> Result<()> {
795 let (_, mut logical_partition_exprs) =
796 parse_partitions_for_logical_validation(create_table, partitions, query_ctx)?;
797
798 let physical_table_name = create_table
799 .table_options
800 .get(LOGICAL_TABLE_METADATA_KEY)
801 .with_context(|| CreateLogicalTablesSnafu {
802 reason: format!(
803 "expect `{LOGICAL_TABLE_METADATA_KEY}` option on creating logical table"
804 ),
805 })?;
806
807 let physical_table = self
808 .catalog_manager
809 .table(
810 &create_table.catalog_name,
811 &create_table.schema_name,
812 physical_table_name,
813 Some(query_ctx),
814 )
815 .await
816 .context(CatalogSnafu)?
817 .context(TableNotFoundSnafu {
818 table_name: physical_table_name.clone(),
819 })?;
820
821 let physical_table_info = physical_table.table_info();
822 let (partition_rule, _) = self
823 .partition_manager
824 .find_table_partition_rule(&physical_table_info)
825 .await
826 .context(error::FindTablePartitionRuleSnafu {
827 table_name: physical_table_name.clone(),
828 })?;
829
830 let multi_dim_rule = partition_rule
831 .as_ref()
832 .as_any()
833 .downcast_ref::<MultiDimPartitionRule>()
834 .context(InvalidPartitionRuleSnafu {
835 reason: "physical table partition rule is not range-based",
836 })?;
837
838 let mut physical_partition_exprs = multi_dim_rule.exprs().to_vec();
840 logical_partition_exprs.sort_unstable();
841 physical_partition_exprs.sort_unstable();
842
843 ensure!(
844 physical_partition_exprs == logical_partition_exprs,
845 InvalidPartitionRuleSnafu {
846 reason: format!(
847 "logical table partition rule must match the corresponding physical table's\n logical table partition exprs:\t\t {:?}\n physical table partition exprs:\t {:?}",
848 logical_partition_exprs, physical_partition_exprs
849 ),
850 }
851 );
852
853 Ok(())
854 }
855
856 #[cfg(feature = "enterprise")]
857 #[tracing::instrument(skip_all)]
858 pub async fn create_trigger(
859 &self,
860 stmt: CreateTrigger,
861 query_context: QueryContextRef,
862 ) -> Result<Output> {
863 let expr = expr_helper::to_create_trigger_task_expr(stmt, &query_context)?;
864 self.create_trigger_inner(expr, query_context).await
865 }
866
867 #[cfg(feature = "enterprise")]
868 pub async fn create_trigger_inner(
869 &self,
870 expr: PbCreateTriggerExpr,
871 query_context: QueryContextRef,
872 ) -> Result<Output> {
873 self.create_trigger_procedure(expr, query_context).await?;
874 Ok(Output::new_with_affected_rows(0))
875 }
876
877 #[cfg(feature = "enterprise")]
878 async fn create_trigger_procedure(
879 &self,
880 expr: PbCreateTriggerExpr,
881 query_context: QueryContextRef,
882 ) -> Result<SubmitDdlTaskResponse> {
883 let task = CreateTriggerTask::try_from(PbCreateTriggerTask {
884 create_trigger: Some(expr),
885 })
886 .context(error::InvalidExprSnafu)?;
887
888 let executor_context = to_executor_context(query_context, TriggerReason::Manual);
889 let request = SubmitDdlTaskRequest::new(DdlTask::new_create_trigger(task));
890
891 self.procedure_executor
892 .submit_ddl_task(executor_context, request)
893 .await
894 .context(error::ExecuteDdlSnafu)
895 }
896
897 #[tracing::instrument(skip_all)]
898 pub async fn create_flow(
899 &self,
900 stmt: CreateFlow,
901 query_context: QueryContextRef,
902 ) -> Result<Output> {
903 let eval_offset_secs = stmt.eval_offset;
905 let expr = expr_helper::to_create_flow_task_expr(stmt, &query_context)?;
906
907 self.create_flow_procedure(expr, eval_offset_secs, query_context)
910 .await?;
911 Ok(Output::new_with_affected_rows(0))
912 }
913
914 pub async fn create_flow_inner(
916 &self,
917 expr: CreateFlowExpr,
918 query_context: QueryContextRef,
919 ) -> Result<Output> {
920 self.create_flow_procedure(expr, None, query_context)
921 .await?;
922 Ok(Output::new_with_affected_rows(0))
923 }
924
925 async fn create_flow_procedure(
926 &self,
927 mut expr: CreateFlowExpr,
928 eval_offset_secs: Option<i64>,
929 query_context: QueryContextRef,
930 ) -> Result<SubmitDdlTaskResponse> {
931 let eval_interval_secs = expr.eval_interval.as_ref().map(|e| e.seconds);
932
933 if let Some(secs) = eval_interval_secs
935 && secs <= 0
936 {
937 return InvalidSqlSnafu {
938 err_msg: format!("EVAL INTERVAL must be positive, got {secs} seconds"),
939 }
940 .fail();
941 }
942
943 validate_eval_offset(eval_offset_secs, eval_interval_secs)?;
947
948 expr.flow_options =
952 validate_and_normalize_flow_options(expr.flow_options, eval_interval_secs)?;
953
954 let flow_type = self
955 .determine_flow_type(&expr, query_context.clone())
956 .await?;
957 info!("determined flow={} type: {:#?}", expr.flow_name, flow_type);
958
959 expr.flow_options
960 .insert(FlowType::FLOW_TYPE_KEY.to_string(), flow_type.to_string());
961
962 let mut task = CreateFlowTask::try_from(PbCreateFlowTask {
963 create_flow: Some(expr),
964 })
965 .context(error::InvalidExprSnafu)?;
966 task.eval_offset_secs = eval_offset_secs;
967 let executor_context = to_executor_context(query_context, TriggerReason::Manual);
968 let request = SubmitDdlTaskRequest::new(DdlTask::new_create_flow(task));
969
970 self.procedure_executor
971 .submit_ddl_task(executor_context, request)
972 .await
973 .context(error::ExecuteDdlSnafu)
974 }
975
976 async fn determine_flow_type(
982 &self,
983 expr: &CreateFlowExpr,
984 query_ctx: QueryContextRef,
985 ) -> Result<FlowType> {
986 let has_eval_interval = expr.eval_interval.is_some();
987
988 let mut has_missing_source_table = false;
989 let mut has_instant_ttl_source_table = false;
990
991 for src_table_name in &expr.source_table_names {
992 let table = self
993 .catalog_manager()
994 .table(
995 &src_table_name.catalog_name,
996 &src_table_name.schema_name,
997 &src_table_name.table_name,
998 Some(&query_ctx),
999 )
1000 .await
1001 .map_err(BoxedError::new)
1002 .context(ExternalSnafu)?;
1003
1004 let Some(table) = table else {
1005 has_missing_source_table = true;
1006 continue;
1007 };
1008
1009 if table.table_info().meta.options.ttl == Some(common_time::TimeToLive::Instant) {
1010 warn!(
1011 "Source table `{}` for flow `{}`'s ttl=instant, {}",
1012 format_full_table_name(
1013 &src_table_name.catalog_name,
1014 &src_table_name.schema_name,
1015 &src_table_name.table_name
1016 ),
1017 expr.flow_name,
1018 if has_eval_interval {
1019 "rejecting flow because EVAL INTERVAL requires the batching scheduler which does not support instant TTL"
1020 } else {
1021 "fallback to streaming mode"
1022 }
1023 );
1024 has_instant_ttl_source_table = true;
1025 }
1026 }
1027
1028 if let Some(flow_type) = determine_flow_type_for_source_state(
1029 &expr.flow_name,
1030 &expr.flow_options,
1031 has_missing_source_table,
1032 has_instant_ttl_source_table,
1033 has_eval_interval,
1034 )? {
1035 return Ok(flow_type);
1036 }
1037
1038 if has_eval_interval {
1044 return Ok(FlowType::Batching);
1045 }
1046
1047 let engine = &self.query_engine;
1048 let stmts = ParserContext::create_with_dialect(
1049 &expr.sql,
1050 query_ctx.sql_dialect(),
1051 ParseOptions::default(),
1052 )
1053 .map_err(BoxedError::new)
1054 .context(ExternalSnafu)?;
1055
1056 ensure!(
1057 stmts.len() == 1,
1058 InvalidSqlSnafu {
1059 err_msg: format!("Expect only one statement, found {}", stmts.len())
1060 }
1061 );
1062 let stmt = &stmts[0];
1063
1064 let is_tql_query = is_tql(query_ctx.sql_dialect(), &expr.sql)
1065 .map_err(BoxedError::new)
1066 .context(ExternalSnafu)?;
1067 if is_tql_query {
1068 if has_instant_ttl_source_table {
1069 return InvalidSqlSnafu {
1070 err_msg: INSTANT_TTL_FLOW_QUERY_ERROR.to_string(),
1071 }
1072 .fail();
1073 }
1074 return Ok(FlowType::Batching);
1075 }
1076
1077 let plan = match stmt {
1080 Statement::Tql(_) => {
1081 if has_instant_ttl_source_table {
1082 return InvalidSqlSnafu {
1083 err_msg: INSTANT_TTL_FLOW_QUERY_ERROR.to_string(),
1084 }
1085 .fail();
1086 }
1087 return Ok(FlowType::Batching);
1088 }
1089 _ => engine
1090 .planner()
1091 .plan(&QueryStatement::Sql(stmt.clone()), query_ctx)
1092 .await
1093 .map_err(BoxedError::new)
1094 .context(ExternalSnafu)?,
1095 };
1096
1097 struct FindQueryShape {
1100 is_aggr: bool,
1101 is_distinct: bool,
1102 has_distinct_on: bool,
1103 }
1104
1105 impl TreeNodeVisitor<'_> for FindQueryShape {
1106 type Node = LogicalPlan;
1107 fn f_down(
1108 &mut self,
1109 node: &Self::Node,
1110 ) -> datafusion_common::Result<datafusion_common::tree_node::TreeNodeRecursion>
1111 {
1112 match node {
1113 LogicalPlan::Aggregate(_) => self.is_aggr = true,
1114 LogicalPlan::Distinct(Distinct::All(_)) => self.is_distinct = true,
1115 LogicalPlan::Distinct(Distinct::On(_)) => self.has_distinct_on = true,
1116 _ => (),
1117 }
1118 Ok(datafusion_common::tree_node::TreeNodeRecursion::Continue)
1119 }
1120 }
1121
1122 let mut query_shape = FindQueryShape {
1123 is_aggr: false,
1124 is_distinct: false,
1125 has_distinct_on: false,
1126 };
1127
1128 plan.visit_with_subqueries(&mut query_shape)
1129 .context(BuildDfLogicalPlanSnafu)?;
1130 if query_shape.has_distinct_on {
1131 return InvalidSqlSnafu {
1132 err_msg: if has_instant_ttl_source_table {
1133 INSTANT_TTL_FLOW_QUERY_ERROR.to_string()
1134 } else {
1135 STATELESS_FLOW_QUERY_ERROR.to_string()
1136 },
1137 }
1138 .fail();
1139 }
1140 if query_shape.is_aggr {
1141 return if has_instant_ttl_source_table {
1142 InvalidSqlSnafu {
1143 err_msg: INSTANT_TTL_FLOW_QUERY_ERROR.to_string(),
1144 }
1145 .fail()
1146 } else {
1147 Ok(FlowType::Batching)
1148 };
1149 }
1150 if query_shape.is_distinct && !has_instant_ttl_source_table {
1151 return Ok(FlowType::Batching);
1152 }
1153
1154 ensure!(
1155 is_stateless_flow_plan(&plan),
1156 InvalidSqlSnafu {
1157 err_msg: if has_instant_ttl_source_table {
1158 INSTANT_TTL_FLOW_QUERY_ERROR.to_string()
1159 } else {
1160 STATELESS_FLOW_QUERY_ERROR.to_string()
1161 },
1162 }
1163 );
1164 Ok(FlowType::Streaming)
1165 }
1166
1167 #[tracing::instrument(skip_all)]
1168 pub async fn create_view(
1169 &self,
1170 create_view: CreateView,
1171 ctx: QueryContextRef,
1172 ) -> Result<TableRef> {
1173 let logical_plan = match &*create_view.query {
1175 Statement::Query(query) => {
1176 self.plan(
1177 &QueryStatement::Sql(Statement::Query(query.clone())),
1178 ctx.clone(),
1179 )
1180 .await?
1181 }
1182 Statement::Tql(query) => self.plan_tql(query.clone(), &ctx).await?,
1183 _ => {
1184 return InvalidViewStmtSnafu {}.fail();
1185 }
1186 };
1187 let definition = create_view.to_string();
1189
1190 let schema: Schema = logical_plan
1193 .schema()
1194 .clone()
1195 .try_into()
1196 .context(ConvertSchemaSnafu)?;
1197 let plan_columns: Vec<_> = schema
1198 .column_schemas()
1199 .iter()
1200 .map(|c| c.name.clone())
1201 .collect();
1202
1203 let columns: Vec<_> = create_view
1204 .columns
1205 .iter()
1206 .map(|ident| ident.to_string())
1207 .collect();
1208
1209 if !columns.is_empty() {
1211 ensure!(
1212 columns.len() == plan_columns.len(),
1213 error::ViewColumnsMismatchSnafu {
1214 view_name: create_view.name.to_string(),
1215 expected: plan_columns.len(),
1216 actual: columns.len(),
1217 }
1218 );
1219 }
1220
1221 let (table_names, plan) = extract_and_rewrite_full_table_names(logical_plan, ctx.clone())
1224 .context(ExtractTableNamesSnafu)?;
1225
1226 let table_names = table_names.into_iter().map(|t| t.into()).collect();
1227
1228 let encoded_plan = DFLogicalSubstraitConvertor
1235 .encode(&plan, DefaultSerializer)
1236 .context(SubstraitCodecSnafu)?;
1237
1238 let expr = expr_helper::to_create_view_expr(
1239 create_view,
1240 encoded_plan.to_vec(),
1241 table_names,
1242 columns,
1243 plan_columns,
1244 definition,
1245 ctx.clone(),
1246 )?;
1247
1248 self.create_view_by_expr(expr, ctx).await
1250 }
1251
1252 pub async fn create_view_by_expr(
1253 &self,
1254 expr: CreateViewExpr,
1255 ctx: QueryContextRef,
1256 ) -> Result<TableRef> {
1257 ensure_table_definition_writable(&expr.schema_name, &expr.view_name)?;
1260 ensure! {
1261 !(expr.create_if_not_exists & expr.or_replace),
1262 InvalidSqlSnafu {
1263 err_msg: "syntax error Create Or Replace and If Not Exist cannot be used together",
1264 }
1265 };
1266 let _timer = crate::metrics::DIST_CREATE_VIEW.start_timer();
1267
1268 let schema_exists = self
1269 .table_metadata_manager
1270 .schema_manager()
1271 .exists(SchemaNameKey::new(&expr.catalog_name, &expr.schema_name))
1272 .await
1273 .context(TableMetadataManagerSnafu)?;
1274
1275 ensure!(
1276 schema_exists,
1277 SchemaNotFoundSnafu {
1278 schema_info: &expr.schema_name,
1279 }
1280 );
1281
1282 if let Some(table) = self
1284 .catalog_manager
1285 .table(
1286 &expr.catalog_name,
1287 &expr.schema_name,
1288 &expr.view_name,
1289 Some(&ctx),
1290 )
1291 .await
1292 .context(CatalogSnafu)?
1293 {
1294 let table_type = table.table_info().table_type;
1295
1296 match (table_type, expr.create_if_not_exists, expr.or_replace) {
1297 (TableType::View, true, false) => {
1298 return Ok(table);
1299 }
1300 (TableType::View, false, false) => {
1301 return ViewAlreadyExistsSnafu {
1302 name: format_full_table_name(
1303 &expr.catalog_name,
1304 &expr.schema_name,
1305 &expr.view_name,
1306 ),
1307 }
1308 .fail();
1309 }
1310 (TableType::View, _, true) => {
1311 }
1313 _ => {
1314 return TableAlreadyExistsSnafu {
1315 table: format_full_table_name(
1316 &expr.catalog_name,
1317 &expr.schema_name,
1318 &expr.view_name,
1319 ),
1320 }
1321 .fail();
1322 }
1323 }
1324 }
1325
1326 ensure!(
1327 NAME_PATTERN_REG.is_match(&expr.view_name),
1328 InvalidViewNameSnafu {
1329 name: expr.view_name.clone(),
1330 }
1331 );
1332
1333 let view_name = TableName::new(&expr.catalog_name, &expr.schema_name, &expr.view_name);
1334
1335 let mut view_info = TableInfo {
1336 ident: metadata::TableIdent {
1337 table_id: 0,
1339 version: 0,
1340 },
1341 name: expr.view_name.clone(),
1342 desc: None,
1343 catalog_name: expr.catalog_name.clone(),
1344 schema_name: expr.schema_name.clone(),
1345 meta: TableMeta::empty(),
1347 table_type: TableType::View,
1348 };
1349
1350 let executor_context = to_executor_context(ctx, TriggerReason::Manual);
1351 let request = SubmitDdlTaskRequest::new(DdlTask::new_create_view(expr, view_info.clone()));
1352
1353 let resp = self
1354 .procedure_executor
1355 .submit_ddl_task(executor_context, request)
1356 .await
1357 .context(error::ExecuteDdlSnafu)?;
1358
1359 debug!(
1360 "Submit creating view '{view_name}' task response: {:?}",
1361 resp
1362 );
1363
1364 let view_id = resp
1365 .table_ids
1366 .into_iter()
1367 .next()
1368 .context(error::UnexpectedSnafu {
1369 violated: "expected table_id",
1370 })?;
1371 info!("Successfully created view '{view_name}' with view id {view_id}");
1372
1373 view_info.ident.table_id = view_id;
1374
1375 let view_info = Arc::new(view_info);
1376
1377 let table = DistTable::table(view_info);
1378
1379 self.cache_invalidator
1381 .invalidate(
1382 &Context::default(),
1383 &[
1384 CacheIdent::TableId(view_id),
1385 CacheIdent::TableName(view_name.clone()),
1386 ],
1387 )
1388 .await
1389 .context(error::InvalidateTableCacheSnafu)?;
1390
1391 Ok(table)
1392 }
1393
1394 #[tracing::instrument(skip_all)]
1395 pub async fn drop_flow(
1396 &self,
1397 catalog_name: String,
1398 flow_name: String,
1399 drop_if_exists: bool,
1400 query_context: QueryContextRef,
1401 ) -> Result<Output> {
1402 if let Some(flow) = self
1403 .flow_metadata_manager
1404 .flow_name_manager()
1405 .get(&catalog_name, &flow_name)
1406 .await
1407 .context(error::TableMetadataManagerSnafu)?
1408 {
1409 let flow_id = flow.flow_id();
1410 let task = DropFlowTask {
1411 catalog_name,
1412 flow_name,
1413 flow_id,
1414 drop_if_exists,
1415 };
1416 self.drop_flow_procedure(task, query_context).await?;
1417
1418 Ok(Output::new_with_affected_rows(0))
1419 } else if drop_if_exists {
1420 Ok(Output::new_with_affected_rows(0))
1421 } else {
1422 FlowNotFoundSnafu {
1423 flow_name: format_full_flow_name(&catalog_name, &flow_name),
1424 }
1425 .fail()
1426 }
1427 }
1428
1429 async fn drop_flow_procedure(
1430 &self,
1431 expr: DropFlowTask,
1432 query_context: QueryContextRef,
1433 ) -> Result<SubmitDdlTaskResponse> {
1434 let executor_context = to_executor_context(query_context, TriggerReason::Manual);
1435 let request = SubmitDdlTaskRequest::new(DdlTask::new_drop_flow(expr));
1436
1437 self.procedure_executor
1438 .submit_ddl_task(executor_context, request)
1439 .await
1440 .context(error::ExecuteDdlSnafu)
1441 }
1442
1443 #[cfg(feature = "enterprise")]
1444 #[tracing::instrument(skip_all)]
1445 pub(super) async fn drop_trigger(
1446 &self,
1447 catalog_name: String,
1448 trigger_name: String,
1449 drop_if_exists: bool,
1450 query_context: QueryContextRef,
1451 ) -> Result<Output> {
1452 let task = DropTriggerTask {
1453 catalog_name,
1454 trigger_name,
1455 drop_if_exists,
1456 };
1457 self.drop_trigger_procedure(task, query_context).await?;
1458 Ok(Output::new_with_affected_rows(0))
1459 }
1460
1461 #[cfg(feature = "enterprise")]
1462 async fn drop_trigger_procedure(
1463 &self,
1464 expr: DropTriggerTask,
1465 query_context: QueryContextRef,
1466 ) -> Result<SubmitDdlTaskResponse> {
1467 let executor_context = to_executor_context(query_context, TriggerReason::Manual);
1468 let request = SubmitDdlTaskRequest::new(DdlTask::new_drop_trigger(expr));
1469
1470 self.procedure_executor
1471 .submit_ddl_task(executor_context, request)
1472 .await
1473 .context(error::ExecuteDdlSnafu)
1474 }
1475
1476 #[tracing::instrument(skip_all)]
1478 pub async fn drop_view(
1479 &self,
1480 catalog: String,
1481 schema: String,
1482 view: String,
1483 drop_if_exists: bool,
1484 query_context: QueryContextRef,
1485 ) -> Result<Output> {
1486 let view_info = if let Some(view) = self
1487 .catalog_manager
1488 .table(&catalog, &schema, &view, None)
1489 .await
1490 .context(CatalogSnafu)?
1491 {
1492 view.table_info()
1493 } else if drop_if_exists {
1494 return Ok(Output::new_with_affected_rows(0));
1496 } else {
1497 return TableNotFoundSnafu {
1498 table_name: format_full_table_name(&catalog, &schema, &view),
1499 }
1500 .fail();
1501 };
1502
1503 ensure!(
1505 view_info.table_type == TableType::View,
1506 error::InvalidViewSnafu {
1507 msg: "not a view",
1508 view_name: format_full_table_name(&catalog, &schema, &view),
1509 }
1510 );
1511
1512 let view_id = view_info.table_id();
1513 let view_name = TableName::new(&catalog, &schema, &view);
1514
1515 let task = DropViewTask {
1516 catalog,
1517 schema,
1518 view,
1519 view_id,
1520 drop_if_exists,
1521 };
1522
1523 self.drop_view_procedure(task, query_context).await?;
1524
1525 self.cache_invalidator
1527 .invalidate(
1528 &Context::default(),
1529 &[
1530 CacheIdent::TableId(view_id),
1531 CacheIdent::TableName(view_name),
1532 ],
1533 )
1534 .await
1535 .context(error::InvalidateTableCacheSnafu)?;
1536
1537 Ok(Output::new_with_affected_rows(0))
1538 }
1539
1540 async fn drop_view_procedure(
1542 &self,
1543 expr: DropViewTask,
1544 query_context: QueryContextRef,
1545 ) -> Result<SubmitDdlTaskResponse> {
1546 let executor_context = to_executor_context(query_context, TriggerReason::Manual);
1547 let request = SubmitDdlTaskRequest::new(DdlTask::new_drop_view(expr));
1548
1549 self.procedure_executor
1550 .submit_ddl_task(executor_context, request)
1551 .await
1552 .context(error::ExecuteDdlSnafu)
1553 }
1554
1555 #[tracing::instrument(skip_all)]
1556 pub async fn alter_logical_tables(
1557 &self,
1558 alter_table_exprs: Vec<AlterTableExpr>,
1559 query_context: QueryContextRef,
1560 trigger_reason: TriggerReason,
1561 ) -> Result<Output> {
1562 let _timer = crate::metrics::DIST_ALTER_TABLES.start_timer();
1563 ensure!(
1564 !alter_table_exprs.is_empty(),
1565 EmptyDdlExprSnafu {
1566 name: "alter logical tables"
1567 }
1568 );
1569
1570 let mut groups: HashMap<TableId, Vec<AlterTableExpr>> = HashMap::new();
1572 for expr in alter_table_exprs {
1573 let catalog = if expr.catalog_name.is_empty() {
1575 query_context.current_catalog()
1576 } else {
1577 &expr.catalog_name
1578 };
1579 let schema = if expr.schema_name.is_empty() {
1580 query_context.current_schema()
1581 } else {
1582 expr.schema_name.clone()
1583 };
1584 let table_name = &expr.table_name;
1585 let table = self
1586 .catalog_manager
1587 .table(catalog, &schema, table_name, Some(&query_context))
1588 .await
1589 .context(CatalogSnafu)?
1590 .with_context(|| TableNotFoundSnafu {
1591 table_name: format_full_table_name(catalog, &schema, table_name),
1592 })?;
1593 let table_id = table.table_info().ident.table_id;
1594 let physical_table_id = self
1595 .table_metadata_manager
1596 .table_route_manager()
1597 .get_physical_table_id(table_id)
1598 .await
1599 .context(TableMetadataManagerSnafu)?;
1600 groups.entry(physical_table_id).or_default().push(expr);
1601 }
1602
1603 let mut handles = Vec::with_capacity(groups.len());
1605 for (_physical_table_id, exprs) in groups {
1606 let fut =
1607 self.alter_logical_tables_procedure(exprs, query_context.clone(), trigger_reason);
1608 handles.push(fut);
1609 }
1610 let _results = futures::future::try_join_all(handles).await?;
1611
1612 Ok(Output::new_with_affected_rows(0))
1613 }
1614
1615 #[tracing::instrument(skip_all)]
1616 pub async fn drop_table(
1617 &self,
1618 table_name: TableName,
1619 drop_if_exists: bool,
1620 query_context: QueryContextRef,
1621 ) -> Result<Output> {
1622 self.drop_tables(&[table_name], drop_if_exists, query_context)
1624 .await
1625 }
1626
1627 #[tracing::instrument(skip_all)]
1628 pub async fn drop_tables(
1629 &self,
1630 table_names: &[TableName],
1631 drop_if_exists: bool,
1632 query_context: QueryContextRef,
1633 ) -> Result<Output> {
1634 let mut tables = Vec::with_capacity(table_names.len());
1635 for table_name in table_names {
1636 ensure_table_writable(&table_name.schema_name, &table_name.table_name)?;
1637
1638 if let Some(table) = self
1639 .catalog_manager
1640 .table(
1641 &table_name.catalog_name,
1642 &table_name.schema_name,
1643 &table_name.table_name,
1644 Some(&query_context),
1645 )
1646 .await
1647 .context(CatalogSnafu)?
1648 {
1649 tables.push(table.table_info().table_id());
1650 } else if drop_if_exists {
1651 continue;
1653 } else {
1654 return TableNotFoundSnafu {
1655 table_name: table_name.to_string(),
1656 }
1657 .fail();
1658 }
1659 }
1660
1661 for (table_name, table_id) in table_names.iter().zip(tables.into_iter()) {
1662 self.drop_table_procedure(table_name, table_id, drop_if_exists, query_context.clone())
1663 .await?;
1664
1665 self.cache_invalidator
1667 .invalidate(
1668 &Context::default(),
1669 &[
1670 CacheIdent::TableId(table_id),
1671 CacheIdent::TableName(table_name.clone()),
1672 ],
1673 )
1674 .await
1675 .context(error::InvalidateTableCacheSnafu)?;
1676 }
1677 Ok(Output::new_with_affected_rows(0))
1678 }
1679
1680 #[cfg(feature = "enterprise")]
1681 #[tracing::instrument(skip_all)]
1682 pub async fn undrop_table(
1683 &self,
1684 table_name: TableName,
1685 query_context: QueryContextRef,
1686 ) -> Result<Output> {
1687 execute_undrop_table(
1688 &self.table_metadata_manager,
1689 &self.procedure_executor,
1690 &self.cache_invalidator,
1691 table_name,
1692 query_context,
1693 )
1694 .await
1695 }
1696
1697 #[tracing::instrument(skip_all)]
1698 pub async fn drop_database(
1699 &self,
1700 catalog: String,
1701 schema: String,
1702 drop_if_exists: bool,
1703 query_context: QueryContextRef,
1704 ) -> Result<Output> {
1705 ensure!(
1706 !is_readonly_schema(&schema),
1707 SchemaReadOnlySnafu { name: schema }
1708 );
1709
1710 if self
1711 .catalog_manager
1712 .schema_exists(&catalog, &schema, None)
1713 .await
1714 .context(CatalogSnafu)?
1715 {
1716 if schema == query_context.current_schema() {
1717 SchemaInUseSnafu { name: schema }.fail()
1718 } else {
1719 self.drop_database_procedure(catalog, schema, drop_if_exists, query_context)
1720 .await?;
1721
1722 Ok(Output::new_with_affected_rows(0))
1723 }
1724 } else if drop_if_exists {
1725 Ok(Output::new_with_affected_rows(0))
1727 } else {
1728 SchemaNotFoundSnafu {
1729 schema_info: schema,
1730 }
1731 .fail()
1732 }
1733 }
1734
1735 pub async fn truncate_table(
1736 &self,
1737 table_name: TableName,
1738 time_ranges: Vec<(Timestamp, Timestamp)>,
1739 query_context: QueryContextRef,
1740 ) -> Result<Output> {
1741 self.truncate_table_with_options(
1742 table_name,
1743 time_ranges,
1744 DdlSubmitOptions::default(),
1745 query_context,
1746 )
1747 .await
1748 }
1749
1750 #[tracing::instrument(name = "truncate_table", skip_all)]
1752 pub(crate) async fn truncate_table_with_options(
1753 &self,
1754 table_name: TableName,
1755 time_ranges: Vec<(Timestamp, Timestamp)>,
1756 ddl_options: DdlSubmitOptions,
1757 query_context: QueryContextRef,
1758 ) -> Result<Output> {
1759 ensure_table_writable(&table_name.schema_name, &table_name.table_name)?;
1760
1761 let table = self
1762 .catalog_manager
1763 .table(
1764 &table_name.catalog_name,
1765 &table_name.schema_name,
1766 &table_name.table_name,
1767 Some(&query_context),
1768 )
1769 .await
1770 .context(CatalogSnafu)?
1771 .with_context(|| TableNotFoundSnafu {
1772 table_name: table_name.to_string(),
1773 })?;
1774 let table_id = table.table_info().table_id();
1775 let response = self
1776 .truncate_table_procedure(
1777 &table_name,
1778 table_id,
1779 time_ranges,
1780 ddl_options,
1781 query_context.clone(),
1782 )
1783 .await?;
1784
1785 if !ddl_options.wait {
1786 let procedure_id = String::from_utf8_lossy(&response.key).into_owned();
1787 let output = build_procedure_id_output(&procedure_id)?;
1788 query_context.set_last_procedure_id(procedure_id);
1789 return Ok(output);
1790 }
1791
1792 Ok(Output::new_with_affected_rows(0))
1793 }
1794
1795 #[tracing::instrument(skip_all)]
1796 pub async fn alter_table(
1797 &self,
1798 alter_table: AlterTable,
1799 query_context: QueryContextRef,
1800 ) -> Result<Output> {
1801 if matches!(
1802 alter_table.alter_operation(),
1803 AlterTableOperation::Repartition { .. } | AlterTableOperation::Partition { .. }
1804 ) {
1805 let request = expr_helper::to_repartition_request(alter_table, &query_context)?;
1806 return self.repartition_table(request, &query_context).await;
1807 }
1808
1809 let expr = expr_helper::to_alter_table_expr(alter_table, &query_context)?;
1810 self.alter_table_inner(expr, query_context, TriggerReason::Manual)
1811 .await
1812 }
1813
1814 #[tracing::instrument(skip_all)]
1815 pub async fn repartition_table(
1816 &self,
1817 request: RepartitionRequest,
1818 query_context: &QueryContextRef,
1819 ) -> Result<Output> {
1820 ensure_table_definition_writable(&request.schema_name, &request.table_name)?;
1822
1823 let table_ref = TableReference::full(
1824 &request.catalog_name,
1825 &request.schema_name,
1826 &request.table_name,
1827 );
1828 let table = self
1830 .catalog_manager
1831 .table(
1832 &request.catalog_name,
1833 &request.schema_name,
1834 &request.table_name,
1835 Some(query_context),
1836 )
1837 .await
1838 .context(CatalogSnafu)?
1839 .with_context(|| TableNotFoundSnafu {
1840 table_name: table_ref.to_string(),
1841 })?;
1842 let table_id = table.table_info().ident.table_id;
1843 let (physical_table_id, physical_table_route) = self
1845 .table_metadata_manager
1846 .table_route_manager()
1847 .get_physical_table_route(table_id)
1848 .await
1849 .context(TableMetadataManagerSnafu)?;
1850
1851 ensure!(
1852 physical_table_id == table_id,
1853 NotSupportedSnafu {
1854 feat: "REPARTITION on logical tables"
1855 }
1856 );
1857
1858 let table_info = table.table_info();
1859 let existing_partition_columns = table_info.meta.partition_columns().collect::<Vec<_>>();
1860 let column_schemas = table_info.meta.schema.column_schemas();
1861 let target_partition_columns = match &request.source {
1867 RepartitionSource::Partitions {
1868 target_partition_columns,
1869 ..
1870 } => {
1871 ensure!(
1872 !existing_partition_columns.is_empty(),
1873 InvalidPartitionRuleSnafu {
1874 reason: format!(
1875 "table {} does not have partition columns, cannot repartition",
1876 table_ref
1877 )
1878 }
1879 );
1880
1881 if let Some(target_partition_columns) = target_partition_columns {
1882 ensure!(
1883 !target_partition_columns.is_empty(),
1884 InvalidPartitionRuleSnafu {
1885 reason: "ON COLUMNS requires at least one partition column"
1886 }
1887 );
1888 validate_and_collect_partition_columns(
1889 target_partition_columns,
1890 column_schemas,
1891 )?
1892 } else {
1893 existing_partition_columns.clone()
1894 }
1895 }
1896 RepartitionSource::Unpartitioned { partition_columns } => {
1897 ensure!(
1898 !partition_columns.is_empty(),
1899 InvalidPartitionRuleSnafu {
1900 reason: "PARTITION ON COLUMNS requires at least one partition column"
1901 }
1902 );
1903 ensure!(
1904 existing_partition_columns.is_empty(),
1905 InvalidPartitionRuleSnafu {
1906 reason: format!("table {} already has partition columns", table_ref)
1907 }
1908 );
1909 partition_columns
1910 .iter()
1911 .map(|column_name| {
1912 column_schemas
1913 .iter()
1914 .find(|column| &column.name == column_name)
1915 .with_context(|| ColumnNotFoundSnafu { msg: column_name })
1916 })
1917 .collect::<Result<Vec<_>>>()?
1918 }
1919 };
1920
1921 let from_column_name_and_type = column_name_and_type(&existing_partition_columns);
1922 let target_column_name_and_type = column_name_and_type(&target_partition_columns);
1923 let target_partition_column_names = target_partition_columns
1924 .iter()
1925 .map(|column| column.name.clone())
1926 .collect::<Vec<_>>();
1927 let timezone = query_context.timezone();
1928 let from_partition_exprs = match &request.source {
1930 RepartitionSource::Partitions { from_exprs, .. } => from_exprs
1931 .iter()
1932 .map(|expr| convert_one_expr(expr, &from_column_name_and_type, &timezone))
1933 .collect::<Result<Vec<_>>>()?,
1934 RepartitionSource::Unpartitioned { .. } => vec![],
1935 };
1936
1937 let mut into_partition_exprs = request
1938 .into_exprs
1939 .iter()
1940 .map(|expr| convert_one_expr(expr, &target_column_name_and_type, &timezone))
1941 .collect::<Result<Vec<_>>>()?;
1942
1943 if matches!(&request.source, RepartitionSource::Partitions { .. })
1946 && from_partition_exprs.len() > 1
1947 && into_partition_exprs.len() == 1
1948 && let Some(expr) = into_partition_exprs.pop()
1949 {
1950 into_partition_exprs.push(partition::simplify::simplify_merged_partition_expr(expr));
1951 }
1952
1953 let mut existing_partition_exprs =
1955 Vec::with_capacity(physical_table_route.region_routes.len());
1956 for route in &physical_table_route.region_routes {
1957 let expr_json = route.region.partition_expr();
1958 if !expr_json.is_empty() {
1959 match PartitionExpr::from_json_str(&expr_json) {
1960 Ok(Some(expr)) => existing_partition_exprs.push(expr),
1961 Ok(None) => {
1962 }
1964 Err(e) => {
1965 return Err(e).context(DeserializePartitionExprSnafu);
1966 }
1967 }
1968 }
1969 }
1970
1971 if matches!(&request.source, RepartitionSource::Partitions { .. }) {
1974 for from_expr in &from_partition_exprs {
1975 ensure!(
1976 existing_partition_exprs.contains(from_expr),
1977 InvalidPartitionRuleSnafu {
1978 reason: format!(
1979 "partition expression '{}' does not exist in table {}",
1980 from_expr, table_ref
1981 )
1982 }
1983 );
1984 }
1985 }
1986
1987 let new_partition_exprs: Vec<PartitionExpr> = match &request.source {
1990 RepartitionSource::Partitions { .. } => existing_partition_exprs
1991 .into_iter()
1992 .filter(|expr| !from_partition_exprs.contains(expr))
1993 .chain(into_partition_exprs.clone().into_iter())
1994 .collect(),
1995 RepartitionSource::Unpartitioned { .. } => into_partition_exprs.clone(),
1996 };
1997 ensure_partition_expr_columns_in_target(
1998 &new_partition_exprs,
1999 &target_partition_column_names.iter().collect(),
2000 )?;
2001 let new_partition_exprs_len = new_partition_exprs.len();
2002 let from_partition_exprs_len = from_partition_exprs.len();
2003
2004 let _ = MultiDimPartitionRule::try_new(
2006 target_partition_column_names,
2007 vec![],
2008 new_partition_exprs,
2009 true,
2010 )
2011 .context(InvalidPartitionSnafu)?;
2012
2013 let ddl_options = parse_ddl_options(&request.options)?;
2014 let serialize_exprs = |exprs: Vec<PartitionExpr>| -> Result<Vec<String>> {
2015 let mut json_exprs = Vec::with_capacity(exprs.len());
2016 for expr in exprs {
2017 json_exprs.push(expr.as_json_str().context(SerializePartitionExprSnafu)?);
2018 }
2019 Ok(json_exprs)
2020 };
2021 let from_partition_exprs_json = serialize_exprs(from_partition_exprs)?;
2022 let into_partition_exprs_json = serialize_exprs(into_partition_exprs)?;
2023 let source = match &request.source {
2024 RepartitionSource::Partitions {
2025 target_partition_columns,
2026 ..
2027 } => Source::PartitionExprs(PartitionedSource {
2028 exprs: from_partition_exprs_json,
2029 target_partition_columns: target_partition_columns
2030 .clone()
2031 .map(|columns| TargetPartitionColumns { columns }),
2032 }),
2033 RepartitionSource::Unpartitioned { partition_columns } => {
2034 Source::Unpartitioned(UnpartitionedSource {
2035 partition_columns: partition_columns.clone(),
2036 })
2037 }
2038 };
2039 let repartition = Repartition {
2040 into_partition_exprs: into_partition_exprs_json,
2041 source: Some(source),
2042 ..Default::default()
2043 };
2044 let executor_context = to_executor_context(query_context.clone(), TriggerReason::Manual);
2045 let mut req = SubmitDdlTaskRequest::new(DdlTask::new_alter_table(AlterTableExpr {
2046 catalog_name: request.catalog_name.clone(),
2047 schema_name: request.schema_name.clone(),
2048 table_name: request.table_name.clone(),
2049 kind: Some(Kind::Repartition(repartition)),
2050 }));
2051 req.wait = ddl_options.wait;
2052 req.timeout = ddl_options.timeout;
2053
2054 info!(
2055 "Submitting repartition task for table {} (table_id={}), from {} to {} partitions, timeout: {:?}, wait: {}",
2056 table_ref,
2057 table_id,
2058 from_partition_exprs_len,
2059 new_partition_exprs_len,
2060 ddl_options.timeout,
2061 ddl_options.wait
2062 );
2063
2064 let response = self
2065 .procedure_executor
2066 .submit_ddl_task(executor_context, req)
2067 .await
2068 .context(error::ExecuteDdlSnafu)?;
2069
2070 if !ddl_options.wait {
2071 let procedure_id = String::from_utf8_lossy(&response.key).into_owned();
2072 let output = build_procedure_id_output(&procedure_id)?;
2073 query_context.set_last_procedure_id(procedure_id);
2074 return Ok(output);
2075 }
2076
2077 let invalidate_keys = vec![
2079 CacheIdent::TableId(table_id),
2080 CacheIdent::TableName(TableName::new(
2081 request.catalog_name,
2082 request.schema_name,
2083 request.table_name,
2084 )),
2085 ];
2086
2087 self.cache_invalidator
2089 .invalidate(&Context::default(), &invalidate_keys)
2090 .await
2091 .context(error::InvalidateTableCacheSnafu)?;
2092
2093 Ok(Output::new_with_affected_rows(0))
2094 }
2095
2096 #[tracing::instrument(skip_all)]
2097 pub async fn alter_table_inner(
2098 &self,
2099 expr: AlterTableExpr,
2100 query_context: QueryContextRef,
2101 trigger_reason: TriggerReason,
2102 ) -> Result<Output> {
2103 ensure_table_definition_writable(&expr.schema_name, &expr.table_name)?;
2104
2105 let catalog_name = if expr.catalog_name.is_empty() {
2106 DEFAULT_CATALOG_NAME.to_string()
2107 } else {
2108 expr.catalog_name.clone()
2109 };
2110
2111 let schema_name = if expr.schema_name.is_empty() {
2112 DEFAULT_SCHEMA_NAME.to_string()
2113 } else {
2114 expr.schema_name.clone()
2115 };
2116
2117 let table_name = expr.table_name.clone();
2118
2119 let table = self
2120 .catalog_manager
2121 .table(
2122 &catalog_name,
2123 &schema_name,
2124 &table_name,
2125 Some(&query_context),
2126 )
2127 .await
2128 .context(CatalogSnafu)?
2129 .with_context(|| TableNotFoundSnafu {
2130 table_name: format_full_table_name(&catalog_name, &schema_name, &table_name),
2131 })?;
2132
2133 let table_id = table.table_info().ident.table_id;
2134 let need_alter = verify_alter(table_id, table.table_info(), expr.clone())?;
2135 if !need_alter {
2136 return Ok(Output::new_with_affected_rows(0));
2137 }
2138 info!(
2139 "Table info before alter is {:?}, expr: {:?}",
2140 table.table_info(),
2141 expr
2142 );
2143
2144 let physical_table_id = self
2145 .table_metadata_manager
2146 .table_route_manager()
2147 .get_physical_table_id(table_id)
2148 .await
2149 .context(TableMetadataManagerSnafu)?;
2150
2151 let executor_context = to_executor_context(query_context, trigger_reason);
2152
2153 let (req, invalidate_keys) = if physical_table_id == table_id {
2154 let req = SubmitDdlTaskRequest::new(DdlTask::new_alter_table(expr));
2156
2157 let invalidate_keys = vec![
2158 CacheIdent::TableId(table_id),
2159 CacheIdent::TableName(TableName::new(catalog_name, schema_name, table_name)),
2160 ];
2161
2162 (req, invalidate_keys)
2163 } else {
2164 let annotation_alter = match expr.kind.as_ref() {
2167 Some(kind) => common_grpc_expr::annotation_alter_family(kind)
2168 .context(AlterExprToRequestSnafu)?
2169 .is_some_and(|family| family.allows_logical_tables()),
2170 None => false,
2171 };
2172 let task = if annotation_alter {
2173 DdlTask::new_alter_table(expr)
2174 } else {
2175 DdlTask::new_alter_logical_tables(vec![expr])
2176 };
2177 let req = SubmitDdlTaskRequest::new(task);
2178
2179 let mut invalidate_keys = vec![
2180 CacheIdent::TableId(physical_table_id),
2181 CacheIdent::TableId(table_id),
2182 CacheIdent::TableName(TableName::new(catalog_name, schema_name, table_name)),
2183 ];
2184
2185 let physical_table = self
2186 .table_metadata_manager
2187 .table_info_manager()
2188 .get(physical_table_id)
2189 .await
2190 .context(TableMetadataManagerSnafu)?
2191 .map(|x| x.into_inner());
2192 if let Some(physical_table) = physical_table {
2193 let physical_table_name = TableName::new(
2194 physical_table.table_info.catalog_name,
2195 physical_table.table_info.schema_name,
2196 physical_table.table_info.name,
2197 );
2198 invalidate_keys.push(CacheIdent::TableName(physical_table_name));
2199 }
2200
2201 (req, invalidate_keys)
2202 };
2203
2204 self.procedure_executor
2205 .submit_ddl_task(executor_context, req)
2206 .await
2207 .context(error::ExecuteDdlSnafu)?;
2208
2209 self.cache_invalidator
2211 .invalidate(&Context::default(), &invalidate_keys)
2212 .await
2213 .context(error::InvalidateTableCacheSnafu)?;
2214
2215 Ok(Output::new_with_affected_rows(0))
2216 }
2217
2218 #[cfg(feature = "enterprise")]
2219 #[tracing::instrument(skip_all)]
2220 pub async fn alter_trigger(
2221 &self,
2222 _alter_expr: AlterTrigger,
2223 _query_context: QueryContextRef,
2224 ) -> Result<Output> {
2225 crate::error::NotSupportedSnafu {
2226 feat: "alter trigger",
2227 }
2228 .fail()
2229 }
2230
2231 #[tracing::instrument(skip_all)]
2232 pub async fn alter_database(
2233 &self,
2234 alter_expr: AlterDatabase,
2235 query_context: QueryContextRef,
2236 ) -> Result<Output> {
2237 let alter_expr = expr_helper::to_alter_database_expr(alter_expr, &query_context)?;
2238 self.alter_database_inner(alter_expr, query_context).await
2239 }
2240
2241 #[tracing::instrument(skip_all)]
2242 pub async fn alter_database_inner(
2243 &self,
2244 alter_expr: AlterDatabaseExpr,
2245 query_context: QueryContextRef,
2246 ) -> Result<Output> {
2247 ensure!(
2248 !is_readonly_schema(&alter_expr.schema_name),
2249 SchemaReadOnlySnafu {
2250 name: query_context.current_schema().clone()
2251 }
2252 );
2253
2254 let exists = self
2255 .catalog_manager
2256 .schema_exists(&alter_expr.catalog_name, &alter_expr.schema_name, None)
2257 .await
2258 .context(CatalogSnafu)?;
2259 ensure!(
2260 exists,
2261 SchemaNotFoundSnafu {
2262 schema_info: alter_expr.schema_name,
2263 }
2264 );
2265
2266 let cache_ident = [CacheIdent::SchemaName(SchemaName {
2267 catalog_name: alter_expr.catalog_name.clone(),
2268 schema_name: alter_expr.schema_name.clone(),
2269 })];
2270
2271 self.alter_database_procedure(alter_expr, query_context)
2272 .await?;
2273
2274 self.cache_invalidator
2276 .invalidate(&Context::default(), &cache_ident)
2277 .await
2278 .context(error::InvalidateTableCacheSnafu)?;
2279
2280 Ok(Output::new_with_affected_rows(0))
2281 }
2282
2283 async fn create_table_procedure(
2284 &self,
2285 create_table: CreateTableExpr,
2286 partitions: Vec<PartitionExpr>,
2287 table_info: TableInfo,
2288 query_context: QueryContextRef,
2289 trigger_reason: TriggerReason,
2290 ) -> Result<SubmitDdlTaskResponse> {
2291 let partitions = partitions
2292 .into_iter()
2293 .map(|expr| expr.as_pb_partition().context(PartitionExprToPbSnafu))
2294 .collect::<Result<Vec<_>>>()?;
2295
2296 let executor_context = to_executor_context_with_origin_frontend(
2297 query_context,
2298 &self.origin_frontend_addr,
2299 trigger_reason,
2300 );
2301 let request = SubmitDdlTaskRequest::new(DdlTask::new_create_table(
2302 create_table,
2303 partitions,
2304 table_info,
2305 ));
2306
2307 self.procedure_executor
2308 .submit_ddl_task(executor_context, request)
2309 .await
2310 .context(error::ExecuteDdlSnafu)
2311 }
2312
2313 async fn create_logical_tables_procedure(
2314 &self,
2315 tables_data: Vec<(CreateTableExpr, TableInfo)>,
2316 query_context: QueryContextRef,
2317 trigger_reason: TriggerReason,
2318 ) -> Result<SubmitDdlTaskResponse> {
2319 let executor_context = to_executor_context_with_origin_frontend(
2320 query_context,
2321 &self.origin_frontend_addr,
2322 trigger_reason,
2323 );
2324 let request = SubmitDdlTaskRequest::new(DdlTask::new_create_logical_tables(tables_data));
2325
2326 self.procedure_executor
2327 .submit_ddl_task(executor_context, request)
2328 .await
2329 .context(error::ExecuteDdlSnafu)
2330 }
2331
2332 async fn alter_logical_tables_procedure(
2333 &self,
2334 tables_data: Vec<AlterTableExpr>,
2335 query_context: QueryContextRef,
2336 trigger_reason: TriggerReason,
2337 ) -> Result<SubmitDdlTaskResponse> {
2338 let executor_context = to_executor_context(query_context, trigger_reason);
2339 let request = SubmitDdlTaskRequest::new(DdlTask::new_alter_logical_tables(tables_data));
2340
2341 self.procedure_executor
2342 .submit_ddl_task(executor_context, request)
2343 .await
2344 .context(error::ExecuteDdlSnafu)
2345 }
2346
2347 async fn drop_table_procedure(
2348 &self,
2349 table_name: &TableName,
2350 table_id: TableId,
2351 drop_if_exists: bool,
2352 query_context: QueryContextRef,
2353 ) -> Result<SubmitDdlTaskResponse> {
2354 let executor_context = to_executor_context(query_context, TriggerReason::Manual);
2355 let request = SubmitDdlTaskRequest::new(DdlTask::new_drop_table(
2356 table_name.catalog_name.clone(),
2357 table_name.schema_name.clone(),
2358 table_name.table_name.clone(),
2359 table_id,
2360 drop_if_exists,
2361 ));
2362
2363 self.procedure_executor
2364 .submit_ddl_task(executor_context, request)
2365 .await
2366 .context(error::ExecuteDdlSnafu)
2367 }
2368
2369 async fn drop_database_procedure(
2370 &self,
2371 catalog: String,
2372 schema: String,
2373 drop_if_exists: bool,
2374 query_context: QueryContextRef,
2375 ) -> Result<SubmitDdlTaskResponse> {
2376 let executor_context = to_executor_context(query_context, TriggerReason::Manual);
2377 let request =
2378 SubmitDdlTaskRequest::new(DdlTask::new_drop_database(catalog, schema, drop_if_exists));
2379
2380 self.procedure_executor
2381 .submit_ddl_task(executor_context, request)
2382 .await
2383 .context(error::ExecuteDdlSnafu)
2384 }
2385
2386 async fn alter_database_procedure(
2387 &self,
2388 alter_expr: AlterDatabaseExpr,
2389 query_context: QueryContextRef,
2390 ) -> Result<SubmitDdlTaskResponse> {
2391 let executor_context = to_executor_context(query_context, TriggerReason::Manual);
2392 let request = SubmitDdlTaskRequest::new(DdlTask::new_alter_database(alter_expr));
2393
2394 self.procedure_executor
2395 .submit_ddl_task(executor_context, request)
2396 .await
2397 .context(error::ExecuteDdlSnafu)
2398 }
2399
2400 async fn truncate_table_procedure(
2401 &self,
2402 table_name: &TableName,
2403 table_id: TableId,
2404 time_ranges: Vec<(Timestamp, Timestamp)>,
2405 ddl_options: DdlSubmitOptions,
2406 query_context: QueryContextRef,
2407 ) -> Result<SubmitDdlTaskResponse> {
2408 let executor_context = to_executor_context(query_context, TriggerReason::Manual);
2409 let mut request = SubmitDdlTaskRequest::new(DdlTask::new_truncate_table(
2410 table_name.catalog_name.clone(),
2411 table_name.schema_name.clone(),
2412 table_name.table_name.clone(),
2413 table_id,
2414 time_ranges,
2415 ));
2416 request.wait = ddl_options.wait;
2417 request.timeout = ddl_options.timeout;
2418
2419 self.procedure_executor
2420 .submit_ddl_task(executor_context, request)
2421 .await
2422 .context(error::ExecuteDdlSnafu)
2423 }
2424
2425 #[tracing::instrument(skip_all)]
2426 pub async fn create_database(
2427 &self,
2428 database: &str,
2429 create_if_not_exists: bool,
2430 options: HashMap<String, String>,
2431 query_context: QueryContextRef,
2432 ) -> Result<Output> {
2433 let catalog = query_context.current_catalog();
2434 ensure!(
2435 NAME_PATTERN_REG.is_match(catalog),
2436 error::UnexpectedSnafu {
2437 violated: format!("Invalid catalog name: {}", catalog)
2438 }
2439 );
2440
2441 ensure!(
2442 NAME_PATTERN_REG.is_match(database),
2443 error::UnexpectedSnafu {
2444 violated: format!("Invalid database name: {}", database)
2445 }
2446 );
2447
2448 #[cfg(feature = "enterprise")]
2449 let creator = match &self.create_database_handler {
2450 Some(handler) => handler.creator(&query_context).context(ExternalSnafu)?,
2451 None => None,
2452 };
2453 #[cfg(not(feature = "enterprise"))]
2454 let creator = None;
2455
2456 let output = if !self
2457 .catalog_manager
2458 .schema_exists(catalog, database, None)
2459 .await
2460 .context(CatalogSnafu)?
2461 && !self.catalog_manager.is_reserved_schema_name(database)
2462 {
2463 self.create_database_procedure(
2464 catalog.to_string(),
2465 database.to_string(),
2466 create_if_not_exists,
2467 options,
2468 query_context.clone(),
2469 creator,
2470 )
2471 .await?;
2472
2473 Output::new_with_affected_rows(1)
2474 } else if create_if_not_exists {
2475 Output::new_with_affected_rows(1)
2476 } else {
2477 return error::SchemaExistsSnafu { name: database }.fail();
2478 };
2479
2480 #[cfg(feature = "enterprise")]
2481 if let Some(handler) = &self.create_database_handler {
2482 handler
2483 .refresh_current_user(&query_context)
2484 .await
2485 .context(ExternalSnafu)?;
2486 }
2487
2488 Ok(output)
2489 }
2490
2491 async fn create_database_procedure(
2492 &self,
2493 catalog: String,
2494 database: String,
2495 create_if_not_exists: bool,
2496 options: HashMap<String, String>,
2497 query_context: QueryContextRef,
2498 creator: Option<CreatorGrantIntent>,
2499 ) -> Result<SubmitDdlTaskResponse> {
2500 let executor_context = to_executor_context(query_context, TriggerReason::Manual);
2501 let request = SubmitDdlTaskRequest::new(DdlTask::new_create_database(
2502 catalog,
2503 database,
2504 create_if_not_exists,
2505 options,
2506 creator,
2507 ));
2508
2509 self.procedure_executor
2510 .submit_ddl_task(executor_context, request)
2511 .await
2512 .context(error::ExecuteDdlSnafu)
2513 }
2514}
2515
2516pub fn parse_partitions(
2518 create_table: &CreateTableExpr,
2519 partitions: Option<Partitions>,
2520 query_ctx: &QueryContextRef,
2521) -> Result<(Vec<PartitionExpr>, Vec<String>)> {
2522 let partition_columns = find_partition_columns(&partitions)?;
2525 let partition_exprs =
2526 find_partition_entries(create_table, &partitions, &partition_columns, query_ctx)?;
2527
2528 let exprs = partition_exprs.clone();
2530 MultiDimPartitionRule::try_new(partition_columns.clone(), vec![], exprs, true)
2531 .context(InvalidPartitionSnafu)?;
2532
2533 Ok((partition_exprs, partition_columns))
2534}
2535
2536fn parse_partitions_for_logical_validation(
2537 create_table: &CreateTableExpr,
2538 partitions: &Partitions,
2539 query_ctx: &QueryContextRef,
2540) -> Result<(Vec<String>, Vec<PartitionExpr>)> {
2541 let partition_columns = partitions
2542 .column_list
2543 .iter()
2544 .map(|ident| ident.value.clone())
2545 .collect::<Vec<_>>();
2546
2547 let column_name_and_type = partition_columns
2548 .iter()
2549 .map(|pc| {
2550 let column = create_table
2551 .column_defs
2552 .iter()
2553 .find(|c| &c.name == pc)
2554 .context(ColumnNotFoundSnafu { msg: pc.clone() })?;
2555 let column_name = &column.name;
2556 let data_type = ConcreteDataType::from(
2557 ColumnDataTypeWrapper::try_new(column.data_type, column.datatype_extension.clone())
2558 .context(ColumnDataTypeSnafu)?,
2559 );
2560 Ok((column_name, data_type))
2561 })
2562 .collect::<Result<HashMap<_, _>>>()?;
2563
2564 let mut partition_exprs = Vec::with_capacity(partitions.exprs.len());
2565 for expr in &partitions.exprs {
2566 let partition_expr = convert_one_expr(expr, &column_name_and_type, &query_ctx.timezone())?;
2567 partition_exprs.push(partition_expr);
2568 }
2569
2570 MultiDimPartitionRule::try_new(
2571 partition_columns.clone(),
2572 vec![],
2573 partition_exprs.clone(),
2574 true,
2575 )
2576 .context(InvalidPartitionSnafu)?;
2577
2578 Ok((partition_columns, partition_exprs))
2579}
2580
2581pub fn verify_alter(
2587 table_id: TableId,
2588 table_info: Arc<TableInfo>,
2589 expr: AlterTableExpr,
2590) -> Result<bool> {
2591 let request: AlterTableRequest =
2592 common_grpc_expr::alter_expr_to_request(table_id, expr, Some(&table_info.meta))
2593 .context(AlterExprToRequestSnafu)?;
2594
2595 let AlterTableRequest {
2596 table_name,
2597 alter_kind,
2598 ..
2599 } = &request;
2600
2601 if let AlterKind::RenameTable { new_table_name } = alter_kind {
2602 ensure!(
2603 NAME_PATTERN_REG.is_match(new_table_name),
2604 error::UnexpectedSnafu {
2605 violated: format!("Invalid table name: {}", new_table_name)
2606 }
2607 );
2608 ensure_table_definition_writable(&table_info.schema_name, new_table_name)?;
2611 } else if let AlterKind::AddColumns { columns } = alter_kind {
2612 let column_names: HashSet<_> = table_info
2615 .meta
2616 .schema
2617 .column_schemas()
2618 .iter()
2619 .map(|schema| &schema.name)
2620 .collect();
2621 if columns.iter().all(|column| {
2622 column_names.contains(&column.column_schema.name) && column.add_if_not_exists
2623 }) {
2624 return Ok(false);
2625 }
2626 }
2627
2628 let new_meta = table_info
2629 .meta
2630 .builder_with_alter_kind(table_name, &request.alter_kind)
2631 .context(error::TableSnafu)?
2632 .build()
2633 .context(error::BuildTableMetaSnafu { table_name })?;
2634
2635 validate_json2_columns_append_mode(&new_meta.schema, &new_meta.options)?;
2636
2637 Ok(true)
2638}
2639
2640pub fn create_table_info(
2641 create_table: &CreateTableExpr,
2642 partition_columns: Vec<String>,
2643) -> Result<TableInfo> {
2644 let mut column_schemas = Vec::with_capacity(create_table.column_defs.len());
2645 let mut column_name_to_index_map = HashMap::new();
2646
2647 for (idx, column) in create_table.column_defs.iter().enumerate() {
2648 let schema =
2649 column_def::try_as_column_schema(column).context(error::InvalidColumnDefSnafu {
2650 column: &column.name,
2651 })?;
2652 let schema = schema.with_time_index(column.name == create_table.time_index);
2653
2654 column_schemas.push(schema);
2655 let _ = column_name_to_index_map.insert(column.name.clone(), idx);
2656 }
2657
2658 let next_column_id = column_schemas.len() as u32;
2659 let schema = Arc::new(Schema::try_new(column_schemas).context(ConvertSchemaSnafu)?);
2660
2661 let primary_key_indices = create_table
2662 .primary_keys
2663 .iter()
2664 .map(|name| {
2665 column_name_to_index_map
2666 .get(name)
2667 .cloned()
2668 .context(ColumnNotFoundSnafu { msg: name })
2669 })
2670 .collect::<Result<Vec<_>>>()?;
2671
2672 let partition_key_indices = partition_columns
2673 .into_iter()
2674 .map(|col_name| {
2675 column_name_to_index_map
2676 .get(&col_name)
2677 .cloned()
2678 .context(ColumnNotFoundSnafu { msg: col_name })
2679 })
2680 .collect::<Result<Vec<_>>>()?;
2681
2682 let mut table_options = TableOptions::try_from_iter(&create_table.table_options)
2683 .context(UnrecognizedTableOptionSnafu)?;
2684
2685 validate_json2_columns_append_mode(&schema, &table_options)?;
2686
2687 validate_and_normalize_annotations(&mut table_options, &schema, &partition_key_indices)?;
2688
2689 let meta = TableMeta {
2690 schema,
2691 primary_key_indices,
2692 value_indices: vec![],
2693 engine: create_table.engine.clone(),
2694 next_column_id,
2695 options: table_options,
2696 created_on: Utc::now(),
2697 updated_on: Utc::now(),
2698 partition_key_indices,
2699 column_ids: vec![],
2700 };
2701
2702 let desc = if create_table.desc.is_empty() {
2703 create_table.table_options.get(COMMENT_KEY).cloned()
2704 } else {
2705 Some(create_table.desc.clone())
2706 };
2707
2708 let table_info = TableInfo {
2709 ident: metadata::TableIdent {
2710 table_id: 0,
2712 version: 0,
2713 },
2714 name: create_table.table_name.clone(),
2715 desc,
2716 catalog_name: create_table.catalog_name.clone(),
2717 schema_name: create_table.schema_name.clone(),
2718 meta,
2719 table_type: TableType::Base,
2720 };
2721 Ok(table_info)
2722}
2723
2724fn validate_json2_columns_append_mode(schema: &Schema, table_options: &TableOptions) -> Result<()> {
2725 let append_mode = table_options
2726 .extra_options
2727 .get(APPEND_MODE_KEY)
2728 .is_some_and(|value| value == "true");
2729
2730 for column in schema.column_schemas() {
2731 if column.data_type.is_json2() {
2732 ensure!(
2733 append_mode,
2734 InvalidSqlSnafu {
2735 err_msg: format!(
2736 "JSON2 column `{}` requires {}='true'",
2737 column.name, APPEND_MODE_KEY
2738 ),
2739 }
2740 );
2741 }
2742 }
2743
2744 Ok(())
2745}
2746
2747fn ensure_table_writable(schema: &str, table: &str) -> Result<()> {
2749 ensure!(
2750 !is_readonly_schema(schema),
2751 SchemaReadOnlySnafu {
2752 name: schema.to_string()
2753 }
2754 );
2755 ensure!(
2756 !is_readonly_table(schema, table),
2757 TableReadOnlySnafu {
2758 name: table.to_string()
2759 }
2760 );
2761 Ok(())
2762}
2763
2764fn ensure_table_definition_writable(schema: &str, table: &str) -> Result<()> {
2770 ensure_table_writable(schema, table)?;
2771 ensure!(
2772 !is_ddl_reserved_table(schema, table),
2773 TableDdlReservedSnafu {
2774 name: table.to_string()
2775 }
2776 );
2777 Ok(())
2778}
2779
2780fn validate_and_normalize_annotations(
2784 options: &mut TableOptions,
2785 schema: &Schema,
2786 partition_key_indices: &[usize],
2787) -> Result<()> {
2788 use table::requests::AnnotationValidationError as CheckError;
2789 let handle_error = |e: CheckError| match e {
2790 CheckError::ColumnNotFound { column } => ColumnNotFoundSnafu { msg: column }.build(),
2791 e @ (CheckError::UnknownKey { .. }
2792 | CheckError::InvalidValue { .. }
2793 | CheckError::InvalidPartitionNumHint { .. }
2794 | CheckError::ColumnNotStringForm { .. }) => InvalidSqlSnafu {
2795 err_msg: e.to_string(),
2796 }
2797 .build(),
2798 e @ (CheckError::NotSingleColumn
2799 | CheckError::PartitionMetadataConflict
2800 | CheckError::TimeIndexConflict) => InvalidPartitionRuleSnafu {
2801 reason: e.to_string(),
2802 }
2803 .build(),
2804 };
2805 validate_and_normalize_annotation_options(options, schema, partition_key_indices)
2806 .map_err(handle_error)
2807}
2808
2809fn find_partition_columns(partitions: &Option<Partitions>) -> Result<Vec<String>> {
2810 let columns = if let Some(partitions) = partitions {
2811 partitions
2812 .column_list
2813 .iter()
2814 .map(|x| x.value.clone())
2815 .collect::<Vec<_>>()
2816 } else {
2817 vec![]
2818 };
2819 Ok(columns)
2820}
2821
2822fn find_partition_entries(
2826 create_table: &CreateTableExpr,
2827 partitions: &Option<Partitions>,
2828 partition_columns: &[String],
2829 query_ctx: &QueryContextRef,
2830) -> Result<Vec<PartitionExpr>> {
2831 let Some(partitions) = partitions else {
2832 return Ok(vec![]);
2833 };
2834
2835 let column_name_and_type = partition_columns
2837 .iter()
2838 .map(|pc| {
2839 let column = create_table
2840 .column_defs
2841 .iter()
2842 .find(|c| &c.name == pc)
2843 .unwrap();
2845 let column_name = &column.name;
2846 let data_type = ConcreteDataType::from(
2847 ColumnDataTypeWrapper::try_new(column.data_type, column.datatype_extension.clone())
2848 .context(ColumnDataTypeSnafu)?,
2849 );
2850 Ok((column_name, data_type))
2851 })
2852 .collect::<Result<HashMap<_, _>>>()?;
2853
2854 let mut partition_exprs = Vec::with_capacity(partitions.exprs.len());
2856 for partition in &partitions.exprs {
2857 let partition_expr =
2858 convert_one_expr(partition, &column_name_and_type, &query_ctx.timezone())?;
2859 partition_exprs.push(partition_expr);
2860 }
2861
2862 Ok(partition_exprs)
2863}
2864
2865fn column_name_and_type<'a>(
2866 partition_columns: &'a [&'a ColumnSchema],
2867) -> HashMap<&'a String, ConcreteDataType> {
2868 partition_columns
2869 .iter()
2870 .map(|column| (&column.name, column.data_type.clone()))
2871 .collect()
2872}
2873
2874fn validate_and_collect_partition_columns<'a>(
2875 column_names: &[String],
2876 column_schemas: &'a [ColumnSchema],
2877) -> Result<Vec<&'a ColumnSchema>> {
2878 let mut seen = HashSet::with_capacity(column_names.len());
2879 column_names
2880 .iter()
2881 .map(|column_name| {
2882 ensure!(
2883 seen.insert(column_name),
2884 InvalidPartitionRuleSnafu {
2885 reason: format!("duplicate partition column '{}'", column_name)
2886 }
2887 );
2888 column_schemas
2889 .iter()
2890 .find(|column| &column.name == column_name)
2891 .with_context(|| ColumnNotFoundSnafu { msg: column_name })
2892 })
2893 .collect()
2894}
2895
2896fn ensure_partition_expr_columns_in_target(
2897 partition_exprs: &[PartitionExpr],
2898 target_partition_columns: &HashSet<&String>,
2899) -> Result<()> {
2900 for expr in partition_exprs {
2901 ensure_partition_operand_columns_in_target(&expr.lhs, target_partition_columns)?;
2902 ensure_partition_operand_columns_in_target(&expr.rhs, target_partition_columns)?;
2903 }
2904
2905 Ok(())
2906}
2907
2908fn ensure_partition_operand_columns_in_target(
2909 operand: &Operand,
2910 target_partition_columns: &HashSet<&String>,
2911) -> Result<()> {
2912 match operand {
2913 Operand::Column(column) => ensure!(
2914 target_partition_columns.contains(column),
2915 InvalidPartitionRuleSnafu {
2916 reason: format!(
2917 "partition expression references column '{}' that is not in target partition columns",
2918 column
2919 )
2920 }
2921 ),
2922 Operand::Expr(expr) => {
2923 ensure_partition_operand_columns_in_target(&expr.lhs, target_partition_columns)?;
2924 ensure_partition_operand_columns_in_target(&expr.rhs, target_partition_columns)?;
2925 }
2926 Operand::IcebergHash(hash) => ensure!(
2927 target_partition_columns.contains(&hash.column().to_string()),
2928 InvalidPartitionRuleSnafu {
2929 reason: format!(
2930 "partition expression references column '{}' that is not in target partition columns",
2931 hash.column()
2932 )
2933 }
2934 ),
2935 Operand::Value(_) => {}
2936 }
2937
2938 Ok(())
2939}
2940
2941fn convert_one_expr(
2942 expr: &Expr,
2943 column_name_and_type: &HashMap<&String, ConcreteDataType>,
2944 timezone: &Timezone,
2945) -> Result<PartitionExpr> {
2946 let Expr::BinaryOp { left, op, right } = expr else {
2947 return InvalidPartitionRuleSnafu {
2948 reason: "partition rule must be a binary expression",
2949 }
2950 .fail();
2951 };
2952
2953 let op =
2954 RestrictedOp::try_from_parser(&op.clone()).with_context(|| InvalidPartitionRuleSnafu {
2955 reason: format!("unsupported operator in partition expr {op}"),
2956 })?;
2957
2958 let (lhs, op, rhs) = match (left.as_ref(), right.as_ref()) {
2960 (Expr::Identifier(ident), Expr::Value(value)) => {
2962 let (column_name, data_type) = convert_identifier(ident, column_name_and_type)?;
2963 let value = convert_value(&value.value, data_type, timezone, None)?;
2964 (Operand::Column(column_name), op, Operand::Value(value))
2965 }
2966 (Expr::Identifier(ident), Expr::UnaryOp { op: unary_op, expr })
2967 if let Expr::Value(v) = &**expr =>
2968 {
2969 let (column_name, data_type) = convert_identifier(ident, column_name_and_type)?;
2970 let value = convert_value(&v.value, data_type, timezone, Some(*unary_op))?;
2971 (Operand::Column(column_name), op, Operand::Value(value))
2972 }
2973 (Expr::Value(value), Expr::Identifier(ident)) => {
2975 let (column_name, data_type) = convert_identifier(ident, column_name_and_type)?;
2976 let value = convert_value(&value.value, data_type, timezone, None)?;
2977 (Operand::Value(value), op, Operand::Column(column_name))
2978 }
2979 (Expr::UnaryOp { op: unary_op, expr }, Expr::Identifier(ident))
2980 if let Expr::Value(v) = &**expr =>
2981 {
2982 let (column_name, data_type) = convert_identifier(ident, column_name_and_type)?;
2983 let value = convert_value(&v.value, data_type, timezone, Some(*unary_op))?;
2984 (Operand::Value(value), op, Operand::Column(column_name))
2985 }
2986 (Expr::Function(function), literal)
2988 if !matches!(op, RestrictedOp::And | RestrictedOp::Or) =>
2989 {
2990 let hash = convert_iceberg_hash(function, column_name_and_type)?;
2991 let value = convert_literal(literal, IcebergHash::output_type(), timezone)?;
2992 (Operand::IcebergHash(hash), op, Operand::Value(value))
2993 }
2994 (literal, Expr::Function(function))
2996 if !matches!(op, RestrictedOp::And | RestrictedOp::Or) =>
2997 {
2998 let hash = convert_iceberg_hash(function, column_name_and_type)?;
2999 let value = convert_literal(literal, IcebergHash::output_type(), timezone)?;
3000 (Operand::Value(value), op, Operand::IcebergHash(hash))
3001 }
3002 (Expr::BinaryOp { .. }, Expr::BinaryOp { .. }) => {
3003 let lhs = convert_one_expr(left, column_name_and_type, timezone)?;
3005 let rhs = convert_one_expr(right, column_name_and_type, timezone)?;
3006 (Operand::Expr(lhs), op, Operand::Expr(rhs))
3007 }
3008 _ => {
3009 return InvalidPartitionRuleSnafu {
3010 reason: format!("invalid partition expr {expr}"),
3011 }
3012 .fail();
3013 }
3014 };
3015
3016 Ok(PartitionExpr::new(lhs, op, rhs))
3017}
3018
3019fn convert_iceberg_hash(
3021 function: &Function,
3022 column_name_and_type: &HashMap<&String, ConcreteDataType>,
3023) -> Result<IcebergHash> {
3024 let invalid = || {
3025 InvalidPartitionRuleSnafu {
3026 reason: format!("unsupported partition function call {function}"),
3027 }
3028 .build()
3029 };
3030 let FunctionArguments::List(args) = &function.args else {
3031 return Err(invalid());
3032 };
3033 let [FunctionArg::Unnamed(FunctionArgExpr::Expr(Expr::Identifier(ident)))] =
3034 args.args.as_slice()
3035 else {
3036 return Err(invalid());
3037 };
3038 if function.filter.is_some()
3039 || function.over.is_some()
3040 || function.null_treatment.is_some()
3041 || !function.within_group.is_empty()
3042 || !matches!(function.parameters, FunctionArguments::None)
3043 || args.duplicate_treatment.is_some()
3044 || !args.clauses.is_empty()
3045 {
3046 return Err(invalid());
3047 }
3048 ensure!(
3049 function
3050 .name
3051 .to_string()
3052 .eq_ignore_ascii_case(IcebergHash::NAME),
3053 InvalidPartitionRuleSnafu {
3054 reason: format!("unsupported partition function {}", function.name),
3055 }
3056 );
3057 let (column_name, data_type) = convert_identifier(ident, column_name_and_type)?;
3058 ensure!(
3059 IcebergHash::accepts(&data_type),
3060 InvalidPartitionRuleSnafu {
3061 reason: format!(
3062 "partition function {} does not support column type {data_type}",
3063 IcebergHash::NAME
3064 ),
3065 }
3066 );
3067 Ok(IcebergHash::new(column_name))
3068}
3069
3070fn convert_literal(expr: &Expr, data_type: ConcreteDataType, timezone: &Timezone) -> Result<Value> {
3071 match expr {
3072 Expr::Value(value) => convert_value(&value.value, data_type, timezone, None),
3073 Expr::UnaryOp { op, expr } if let Expr::Value(value) = &**expr => {
3074 convert_value(&value.value, data_type, timezone, Some(*op))
3075 }
3076 _ => InvalidPartitionRuleSnafu {
3077 reason: format!("expect a literal in partition expr, got {expr}"),
3078 }
3079 .fail(),
3080 }
3081}
3082
3083fn convert_identifier(
3084 ident: &Ident,
3085 column_name_and_type: &HashMap<&String, ConcreteDataType>,
3086) -> Result<(String, ConcreteDataType)> {
3087 let column_name = ident.value.clone();
3088 let data_type = column_name_and_type
3089 .get(&column_name)
3090 .cloned()
3091 .with_context(|| ColumnNotFoundSnafu { msg: &column_name })?;
3092 Ok((column_name, data_type))
3093}
3094
3095fn convert_value(
3096 value: &ParserValue,
3097 data_type: ConcreteDataType,
3098 timezone: &Timezone,
3099 unary_op: Option<UnaryOperator>,
3100) -> Result<Value> {
3101 sql_value_to_value(
3102 &ColumnSchema::new("<NONAME>", data_type, true),
3103 value,
3104 Some(timezone),
3105 unary_op,
3106 false,
3107 )
3108 .context(error::SqlCommonSnafu)
3109}
3110
3111#[cfg(feature = "enterprise")]
3112async fn execute_undrop_table(
3113 table_metadata_manager: &TableMetadataManagerRef,
3114 procedure_executor: &ProcedureExecutorRef,
3115 cache_invalidator: &CacheInvalidatorRef,
3116 table_name: TableName,
3117 query_context: QueryContextRef,
3118) -> Result<Output> {
3119 ensure_table_definition_writable(&table_name.schema_name, &table_name.table_name)?;
3122
3123 let dropped = table_metadata_manager
3124 .get_dropped_table(&table_name)
3125 .await
3126 .context(TableMetadataManagerSnafu)?
3127 .with_context(|| TableNotFoundSnafu {
3128 table_name: table_name.to_string(),
3129 })?;
3130
3131 let executor_context = to_executor_context(query_context, TriggerReason::Manual);
3132 let request = SubmitDdlTaskRequest::new(DdlTask::new_undrop_table(dropped.table_id));
3133 procedure_executor
3134 .submit_ddl_task(executor_context, request)
3135 .await
3136 .context(error::ExecuteDdlSnafu)?;
3137
3138 if let Err(err) = cache_invalidator
3139 .invalidate(
3140 &Context::default(),
3141 &[
3142 CacheIdent::TableId(dropped.table_id),
3143 CacheIdent::TableName(table_name),
3144 ],
3145 )
3146 .await
3147 {
3148 warn!(
3149 "Failed to invalidate cache after restoring table '{}' (id={}): {}",
3150 dropped.table_name, dropped.table_id, err
3151 );
3152 }
3153
3154 Ok(Output::new_with_affected_rows(0))
3155}
3156
3157#[cfg(test)]
3158mod test {
3159 #[cfg(feature = "enterprise")]
3160 use std::sync::{Arc, Mutex};
3161 use std::time::Duration;
3162
3163 #[cfg(feature = "enterprise")]
3164 use api::v1::meta::{ProcedureDetailResponse, ReconcileRequest, ReconcileResponse};
3165 use arrow::datatypes::{DataType as ArrowDataType, Field as ArrowField};
3166 #[cfg(feature = "enterprise")]
3167 use common_meta::cache_invalidator::{CacheInvalidator, CacheInvalidatorRef};
3168 #[cfg(feature = "enterprise")]
3169 use common_meta::instruction::CacheIdent;
3170 #[cfg(feature = "enterprise")]
3171 use common_meta::key::table_route::TableRouteValue;
3172 #[cfg(feature = "enterprise")]
3173 use common_meta::key::test_utils::new_test_table_info_with_name;
3174 #[cfg(feature = "enterprise")]
3175 use common_meta::key::{TableMetadataManager, TableMetadataManagerRef};
3176 #[cfg(feature = "enterprise")]
3177 use common_meta::kv_backend::memory::MemoryKvBackend;
3178 #[cfg(feature = "enterprise")]
3179 use common_meta::procedure_executor::{
3180 ExecutorContext, ProcedureExecutor, ProcedureExecutorRef,
3181 };
3182 #[cfg(feature = "enterprise")]
3183 use common_meta::rpc::ddl::{DdlTask, SubmitDdlTaskRequest, SubmitDdlTaskResponse};
3184 #[cfg(feature = "enterprise")]
3185 use common_meta::rpc::procedure::{
3186 MigrateRegionRequest, MigrateRegionResponse, ProcedureStateResponse,
3187 };
3188 use datafusion::functions_aggregate::expr_fn::count;
3189 use datafusion::logical_expr::builder::LogicalTableSource;
3190 use datafusion::logical_expr::{LogicalPlanBuilder, col};
3191 use session::context::QueryContext;
3192 use sql::dialect::GreptimeDbDialect;
3193 use sql::parser::{ParseOptions, ParserContext};
3194 use sql::statements::statement::Statement;
3195 use sqlparser::parser::Parser;
3196 use table::requests::REPARTITION_COLUMN_HINT_KEY;
3197
3198 use super::*;
3199 use crate::expr_helper;
3200
3201 #[cfg(feature = "enterprise")]
3202 #[derive(Default)]
3203 struct RecordingProcedureExecutor {
3204 requests: Mutex<Vec<SubmitDdlTaskRequest>>,
3205 contexts: Mutex<Vec<ExecutorContext>>,
3206 fail: bool,
3207 }
3208
3209 #[cfg(feature = "enterprise")]
3210 #[async_trait::async_trait]
3211 impl ProcedureExecutor for RecordingProcedureExecutor {
3212 async fn submit_ddl_task(
3213 &self,
3214 ctx: ExecutorContext,
3215 request: SubmitDdlTaskRequest,
3216 ) -> common_meta::error::Result<SubmitDdlTaskResponse> {
3217 self.contexts.lock().unwrap().push(ctx);
3218 self.requests.lock().unwrap().push(request);
3219 if self.fail {
3220 return common_meta::error::UnsupportedSnafu {
3221 operation: "test submit failure",
3222 }
3223 .fail();
3224 }
3225 Ok(SubmitDdlTaskResponse::default())
3226 }
3227
3228 async fn migrate_region(
3229 &self,
3230 _: &ExecutorContext,
3231 _: MigrateRegionRequest,
3232 ) -> common_meta::error::Result<MigrateRegionResponse> {
3233 unimplemented!()
3234 }
3235 async fn reconcile(
3236 &self,
3237 _: &ExecutorContext,
3238 _: ReconcileRequest,
3239 ) -> common_meta::error::Result<ReconcileResponse> {
3240 unimplemented!()
3241 }
3242 async fn query_procedure_state(
3243 &self,
3244 _: &ExecutorContext,
3245 _: &str,
3246 ) -> common_meta::error::Result<ProcedureStateResponse> {
3247 unimplemented!()
3248 }
3249 async fn list_procedures(
3250 &self,
3251 _: &ExecutorContext,
3252 ) -> common_meta::error::Result<ProcedureDetailResponse> {
3253 unimplemented!()
3254 }
3255 }
3256
3257 #[cfg(feature = "enterprise")]
3258 #[derive(Default)]
3259 struct RecordingCacheInvalidator {
3260 invalidations: Mutex<Vec<Vec<CacheIdent>>>,
3261 fail: bool,
3262 }
3263
3264 #[cfg(feature = "enterprise")]
3265 #[async_trait::async_trait]
3266 impl CacheInvalidator for RecordingCacheInvalidator {
3267 async fn invalidate(
3268 &self,
3269 _: &Context,
3270 caches: &[CacheIdent],
3271 ) -> common_meta::error::Result<()> {
3272 self.invalidations.lock().unwrap().push(caches.to_vec());
3273 if self.fail {
3274 return common_meta::error::UnsupportedSnafu {
3275 operation: "test cache failure",
3276 }
3277 .fail();
3278 }
3279 Ok(())
3280 }
3281
3282 fn invalidate_all(&self) -> common_meta::error::Result<()> {
3283 Ok(())
3284 }
3285 }
3286
3287 #[cfg(feature = "enterprise")]
3288 async fn dropped_table_manager(table_id: TableId, name: &TableName) -> TableMetadataManagerRef {
3289 let backend = Arc::new(MemoryKvBackend::default());
3290 let manager = Arc::new(TableMetadataManager::new(backend));
3291 let mut info = new_test_table_info_with_name(table_id, &name.table_name);
3292 info.catalog_name = name.catalog_name.clone();
3293 info.schema_name = name.schema_name.clone();
3294 let route = TableRouteValue::physical(vec![]);
3295 manager
3296 .create_table_metadata(info, route.clone(), HashMap::new())
3297 .await
3298 .unwrap();
3299 manager
3300 .delete_table_metadata(table_id, name, &route, &HashMap::new(), None)
3301 .await
3302 .unwrap();
3303 manager
3304 }
3305
3306 #[cfg(feature = "enterprise")]
3307 #[tokio::test]
3308 async fn test_undrop_table_execution_path() {
3309 let name = TableName::new("greptime", "public", "metrics");
3310 let manager = dropped_table_manager(42, &name).await;
3311 let mut live = new_test_table_info_with_name(99, "metrics");
3313 live.catalog_name = "greptime".to_string();
3314 live.schema_name = "public".to_string();
3315 manager
3316 .create_table_metadata(live, TableRouteValue::physical(vec![]), HashMap::new())
3317 .await
3318 .unwrap();
3319 let procedure = Arc::new(RecordingProcedureExecutor::default());
3320 let cache = Arc::new(RecordingCacheInvalidator::default());
3321
3322 execute_undrop_table(
3323 &manager,
3324 &(procedure.clone() as ProcedureExecutorRef),
3325 &(cache.clone() as CacheInvalidatorRef),
3326 name.clone(),
3327 QueryContext::arc(),
3328 )
3329 .await
3330 .unwrap();
3331
3332 let requests = procedure.requests.lock().unwrap();
3333 assert!(matches!(&requests[0].task, DdlTask::UndropTable(task) if task.table_id == 42));
3334 let contexts = procedure.contexts.lock().unwrap();
3335 assert_eq!(
3336 contexts[0].event_input.as_ref().map(|input| input.reason),
3337 Some(TriggerReason::Manual)
3338 );
3339 assert_eq!(
3340 cache.invalidations.lock().unwrap()[0],
3341 vec![CacheIdent::TableId(42), CacheIdent::TableName(name)]
3342 );
3343 }
3344
3345 #[cfg(feature = "enterprise")]
3346 #[tokio::test]
3347 async fn test_undrop_table_missing_tombstone_submits_nothing() {
3348 let manager = Arc::new(TableMetadataManager::new(Arc::new(
3349 MemoryKvBackend::default(),
3350 )));
3351 let procedure = Arc::new(RecordingProcedureExecutor::default());
3352 let cache = Arc::new(RecordingCacheInvalidator::default());
3353 let err = execute_undrop_table(
3354 &manager,
3355 &(procedure.clone() as ProcedureExecutorRef),
3356 &(cache as CacheInvalidatorRef),
3357 TableName::new("greptime", "public", "missing"),
3358 QueryContext::arc(),
3359 )
3360 .await
3361 .unwrap_err();
3362 assert!(matches!(err, crate::error::Error::TableNotFound { .. }));
3363 assert!(procedure.requests.lock().unwrap().is_empty());
3364 }
3365
3366 #[cfg(feature = "enterprise")]
3367 #[tokio::test]
3368 async fn test_undrop_table_submit_failure_does_not_invalidate() {
3369 let name = TableName::new("greptime", "public", "metrics");
3370 let manager = dropped_table_manager(42, &name).await;
3371 let procedure = Arc::new(RecordingProcedureExecutor {
3372 fail: true,
3373 ..Default::default()
3374 });
3375 let cache = Arc::new(RecordingCacheInvalidator::default());
3376 execute_undrop_table(
3377 &manager,
3378 &(procedure as ProcedureExecutorRef),
3379 &(cache.clone() as CacheInvalidatorRef),
3380 name,
3381 QueryContext::arc(),
3382 )
3383 .await
3384 .unwrap_err();
3385 assert!(cache.invalidations.lock().unwrap().is_empty());
3386 }
3387
3388 #[cfg(feature = "enterprise")]
3389 #[tokio::test]
3390 async fn test_undrop_table_cache_failure_returns_success_after_submit() {
3391 let name = TableName::new("greptime", "public", "metrics");
3392 let manager = dropped_table_manager(42, &name).await;
3393 let procedure = Arc::new(RecordingProcedureExecutor::default());
3394 let cache = Arc::new(RecordingCacheInvalidator {
3395 fail: true,
3396 ..Default::default()
3397 });
3398 execute_undrop_table(
3399 &manager,
3400 &(procedure.clone() as ProcedureExecutorRef),
3401 &(cache.clone() as CacheInvalidatorRef),
3402 name.clone(),
3403 QueryContext::arc(),
3404 )
3405 .await
3406 .unwrap();
3407 assert_eq!(procedure.requests.lock().unwrap().len(), 1);
3408 assert_eq!(
3409 cache.invalidations.lock().unwrap()[0],
3410 vec![CacheIdent::TableId(42), CacheIdent::TableName(name)]
3411 );
3412 }
3413
3414 #[test]
3415 fn test_parse_ddl_options() {
3416 let defaults = parse_ddl_options(&OptionMap::default()).unwrap();
3417 assert!(defaults.wait);
3418 assert_eq!(defaults.timeout, SubmitDdlTaskRequest::default_timeout());
3419
3420 let options = OptionMap::from([
3421 ("timeout".to_string(), "5m".to_string()),
3422 ("wait".to_string(), "false".to_string()),
3423 ]);
3424 let ddl_options = parse_ddl_options(&options).unwrap();
3425 assert!(!ddl_options.wait);
3426 assert_eq!(Duration::from_secs(300), ddl_options.timeout);
3427
3428 let options = OptionMap::from([("timeout".to_string(), "1500ms".to_string())]);
3429 assert_eq!(
3430 parse_ddl_options(&options).unwrap().timeout,
3431 Duration::from_millis(1500)
3432 );
3433 for (key, value) in [
3434 ("wait", "yes"),
3435 ("timeout", "not-a-duration"),
3436 ("timeout", "-1s"),
3437 ] {
3438 let options = OptionMap::from([(key.to_string(), value.to_string())]);
3439 assert!(matches!(
3440 parse_ddl_options(&options),
3441 Err(error::Error::InvalidSql { .. })
3442 ));
3443 }
3444 }
3445
3446 #[test]
3447 fn test_validate_and_normalize_annotations() {
3448 let schema = Schema::new(vec![
3449 ColumnSchema::new("service_name", ConcreteDataType::string_datatype(), true),
3450 ColumnSchema::new("host_id", ConcreteDataType::string_datatype(), true),
3451 ColumnSchema::new("value", ConcreteDataType::float64_datatype(), true),
3452 ColumnSchema::new("payload", ConcreteDataType::binary_datatype(), true),
3453 ]);
3454 let opts = |pairs: &[(&str, &str)]| TableOptions {
3455 extra_options: pairs
3456 .iter()
3457 .map(|(k, v)| (k.to_string(), v.to_string()))
3458 .collect(),
3459 ..Default::default()
3460 };
3461 let check = |pairs: &[(&str, &str)]| {
3462 let mut options = opts(pairs);
3463 validate_and_normalize_annotations(&mut options, &schema, &[]).map(|()| options)
3464 };
3465
3466 for pairs in [
3468 &[(
3469 "greptime.semantic.entity.process.id",
3470 "service_name,host_id",
3471 )][..],
3472 &[("greptime.semantic.entity.service.id", "value")][..],
3473 ] {
3474 assert!(check(pairs).is_ok());
3475 }
3476
3477 let missing = check(&[("greptime.semantic.entity.service.id", "nope")]).unwrap_err();
3479 assert!(matches!(missing, error::Error::ColumnNotFound { .. }));
3480 assert_eq!(
3481 common_error::status_code::StatusCode::InvalidArguments,
3482 common_error::ext::ErrorExt::status_code(&missing)
3483 );
3484
3485 let binary_id = check(&[("greptime.semantic.entity.service.id", "payload")]).unwrap_err();
3486 assert!(matches!(binary_id, error::Error::InvalidSql { .. }));
3487
3488 let bad_value = check(&[("greptime.semantic.signal_type", "garbage")]).unwrap_err();
3491 assert!(matches!(bad_value, error::Error::InvalidSql { .. }));
3492
3493 let unknown_key = check(&[("greptime.semantic.nonsense", "x")]).unwrap_err();
3494 assert!(matches!(unknown_key, error::Error::InvalidSql { .. }));
3495 }
3496
3497 #[test]
3498 fn test_validate_and_normalize_flow_options_empty() {
3499 assert!(
3500 validate_and_normalize_flow_options(HashMap::new(), None)
3501 .unwrap()
3502 .is_empty()
3503 );
3504 }
3505
3506 #[test]
3507 fn test_validate_and_normalize_flow_options_valid() {
3508 let options = HashMap::from([
3509 (DEFER_ON_MISSING_SOURCE_KEY.to_string(), "TRUE".to_string()),
3510 (
3511 FLOW_EXPERIMENTAL_ENABLE_INCREMENTAL_READ_KEY.to_string(),
3512 "FALSE".to_string(),
3513 ),
3514 ]);
3515
3516 assert_eq!(
3517 validate_and_normalize_flow_options(options, None).unwrap(),
3518 HashMap::from([
3519 (DEFER_ON_MISSING_SOURCE_KEY.to_string(), "true".to_string(),),
3520 (
3521 FLOW_EXPERIMENTAL_ENABLE_INCREMENTAL_READ_KEY.to_string(),
3522 "false".to_string(),
3523 )
3524 ])
3525 );
3526 }
3527
3528 #[cfg(not(feature = "enterprise"))]
3529 #[test]
3530 fn test_validate_and_normalize_flow_options_unknown_option() {
3531 let err = validate_and_normalize_flow_options(
3532 HashMap::from([("foo".to_string(), "bar".to_string())]),
3533 None,
3534 )
3535 .unwrap_err();
3536
3537 assert!(
3538 err.to_string()
3539 .contains("unknown flow option 'foo', supported options:")
3540 );
3541 }
3542
3543 #[test]
3544 fn test_validate_and_normalize_flow_options_reserved_option() {
3545 let err = validate_and_normalize_flow_options(
3546 HashMap::from([(
3547 FlowType::FLOW_TYPE_KEY.to_string(),
3548 FlowType::BATCHING.to_string(),
3549 )]),
3550 None,
3551 )
3552 .unwrap_err();
3553
3554 assert!(
3555 err.to_string()
3556 .contains("flow option 'flow_type' is reserved for internal use")
3557 );
3558 }
3559
3560 #[test]
3561 fn test_validate_and_normalize_flow_options_invalid_bool() {
3562 let err = validate_and_normalize_flow_options(
3563 HashMap::from([(
3564 DEFER_ON_MISSING_SOURCE_KEY.to_string(),
3565 "not-a-bool".to_string(),
3566 )]),
3567 None,
3568 )
3569 .unwrap_err();
3570
3571 assert!(
3572 err.to_string()
3573 .contains("invalid flow option 'defer_on_missing_source': 'not-a-bool'")
3574 );
3575 }
3576
3577 #[test]
3578 fn test_parser_flow_option_access_key_id() {
3579 let sql = r"
3580CREATE FLOW task_6
3581SINK TO schema_1.table_1
3582WITH (access_key_id = [true])
3583AS
3584SELECT max(c1), min(c2) FROM schema_2.table_2;";
3585 let stmt =
3586 ParserContext::create_with_dialect(sql, &GreptimeDbDialect {}, ParseOptions::default())
3587 .unwrap()
3588 .pop()
3589 .unwrap();
3590
3591 let Statement::CreateFlow(create_flow) = stmt else {
3592 unreachable!()
3593 };
3594 let expr =
3595 expr_helper::to_create_flow_task_expr(create_flow, &QueryContext::arc()).unwrap();
3596
3597 #[cfg(not(feature = "enterprise"))]
3598 {
3599 let err = validate_and_normalize_flow_options(expr.flow_options, None).unwrap_err();
3600 assert!(
3601 err.to_string()
3602 .contains("unknown flow option 'access_key_id'")
3603 );
3604 }
3605 #[cfg(feature = "enterprise")]
3606 assert_eq!(
3607 validate_and_normalize_flow_options(expr.flow_options, None).unwrap(),
3608 HashMap::from([("access_key_id".to_string(), "['true']".to_string())])
3609 );
3610 }
3611
3612 fn stateless_test_scan(name: &str) -> LogicalPlan {
3613 let schema = arrow::datatypes::Schema::new(vec![ArrowField::new(
3614 "value",
3615 ArrowDataType::Int32,
3616 true,
3617 )]);
3618 LogicalPlanBuilder::scan(
3619 name,
3620 Arc::new(LogicalTableSource::new(Arc::new(schema))),
3621 None,
3622 )
3623 .unwrap()
3624 .build()
3625 .unwrap()
3626 }
3627
3628 #[test]
3629 fn test_is_stateless_flow_plan_accepts_scan_projection_and_filter() {
3630 let scan = stateless_test_scan("source");
3631 assert!(is_stateless_flow_plan(&scan));
3632
3633 let projection = LogicalPlanBuilder::from(scan.clone())
3634 .project(vec![col("value")])
3635 .unwrap()
3636 .build()
3637 .unwrap();
3638 assert!(is_stateless_flow_plan(&projection));
3639
3640 let filter = LogicalPlanBuilder::from(projection)
3641 .filter(col("value").gt(datafusion_expr::lit(0)))
3642 .unwrap()
3643 .build()
3644 .unwrap();
3645 assert!(is_stateless_flow_plan(&filter));
3646 }
3647
3648 #[test]
3649 fn test_is_stateless_flow_plan_rejects_aggregate_and_multiple_scans() {
3650 let scan = stateless_test_scan("source");
3651 let aggregate = LogicalPlanBuilder::from(scan.clone())
3652 .aggregate(
3653 Vec::<datafusion_expr::Expr>::new(),
3654 vec![count(col("value"))],
3655 )
3656 .unwrap()
3657 .build()
3658 .unwrap();
3659 assert!(!is_stateless_flow_plan(&aggregate));
3660
3661 let distinct = LogicalPlanBuilder::from(scan.clone())
3662 .distinct()
3663 .unwrap()
3664 .build()
3665 .unwrap();
3666 assert!(is_stateless_flow_plan(&distinct));
3667
3668 let distinct_on_aggregate = LogicalPlanBuilder::from(aggregate)
3669 .distinct()
3670 .unwrap()
3671 .build()
3672 .unwrap();
3673 assert!(!is_stateless_flow_plan(&distinct_on_aggregate));
3674
3675 let multiple_scans = LogicalPlanBuilder::from(stateless_test_scan("left"))
3676 .cross_join(stateless_test_scan("right"))
3677 .unwrap()
3678 .build()
3679 .unwrap();
3680 assert!(!is_stateless_flow_plan(&multiple_scans));
3681 }
3682
3683 #[test]
3686 fn test_eval_interval_rejected_non_positive() {
3687 let err = validate_and_normalize_flow_options(HashMap::new(), Some(0)).unwrap_err();
3689 assert!(err.to_string().contains("EVAL INTERVAL must be positive"));
3690
3691 let err = validate_and_normalize_flow_options(HashMap::new(), Some(-5)).unwrap_err();
3693 assert!(err.to_string().contains("EVAL INTERVAL must be positive"));
3694
3695 let result = validate_and_normalize_flow_options(HashMap::new(), Some(300));
3697 assert!(result.is_ok());
3698 }
3699
3700 #[test]
3701 fn test_internal_schedule_key_rejected() {
3702 let err = validate_and_normalize_flow_options(
3703 HashMap::from([(INTERNAL_EVAL_SCHEDULE_KEY.to_string(), "value".to_string())]),
3704 Some(300),
3705 )
3706 .unwrap_err();
3707 assert!(err.to_string().contains("reserved for internal use"));
3708 }
3709
3710 #[cfg(not(feature = "enterprise"))]
3711 #[test]
3712 fn test_schedule_keys_rejected_as_unknown_options() {
3713 for key in [
3714 "eval_interval_anchor",
3715 "eval_interval_start",
3716 "eval_interval_missed_tick_policy",
3717 "eval_interval_catchup_max_runs",
3718 "eval_interval_catchup_max_lag",
3719 ] {
3720 let err = validate_and_normalize_flow_options(
3721 HashMap::from([(key.to_string(), "value".to_string())]),
3722 Some(300),
3723 )
3724 .unwrap_err();
3725 assert!(
3726 err.to_string()
3727 .contains(&format!("unknown flow option '{key}'"))
3728 );
3729 }
3730 }
3731
3732 #[cfg(feature = "enterprise")]
3733 #[test]
3734 fn test_schedule_keys_preserved() {
3735 let options = HashMap::from([
3736 ("eval_interval_anchor".to_string(), "anchor".to_string()),
3737 ("eval_interval_start".to_string(), "start".to_string()),
3738 (
3739 "eval_interval_missed_tick_policy".to_string(),
3740 "missed".to_string(),
3741 ),
3742 (
3743 "eval_interval_catchup_max_runs".to_string(),
3744 "runs".to_string(),
3745 ),
3746 (
3747 "eval_interval_catchup_max_lag".to_string(),
3748 "lag".to_string(),
3749 ),
3750 ]);
3751 assert_eq!(
3752 validate_and_normalize_flow_options(options.clone(), Some(300)).unwrap(),
3753 options
3754 );
3755 }
3756
3757 #[test]
3758 fn test_internal_transport_keys_rejected_as_reserved() {
3759 for key in [
3760 "__greptime_internal_eval_schedule",
3761 "__greptime_internal_eval_offset_secs",
3762 ] {
3763 let err = validate_and_normalize_flow_options(
3764 HashMap::from([(key.to_string(), "120".to_string())]),
3765 Some(300),
3766 )
3767 .unwrap_err();
3768
3769 assert!(
3770 err.to_string()
3771 .contains(&format!("flow option '{key}' is reserved for internal use")),
3772 "unexpected error for {key}: {err}"
3773 );
3774 }
3775 }
3776
3777 #[test]
3778 fn test_determine_flow_type_for_source_state_missing_sources_require_opt_in() {
3779 let err =
3780 determine_flow_type_for_source_state("my_flow", &HashMap::new(), true, false, false)
3781 .unwrap_err();
3782
3783 assert!(err.to_string().contains(
3784 "missing source tables for flow 'my_flow'; use WITH (defer_on_missing_source = true) to create a pending flow"
3785 ));
3786 }
3787
3788 #[test]
3789 fn test_determine_flow_type_for_source_state_missing_sources_prefer_batching() {
3790 let flow_options =
3791 HashMap::from([(DEFER_ON_MISSING_SOURCE_KEY.to_string(), "true".to_string())]);
3792
3793 assert_eq!(
3794 determine_flow_type_for_source_state("my_flow", &flow_options, true, true, false)
3795 .unwrap(),
3796 Some(FlowType::Batching)
3797 );
3798 let err = determine_flow_type_for_source_state("my_flow", &flow_options, true, true, true)
3799 .unwrap_err();
3800 assert!(err.to_string().contains(
3801 "flow 'my_flow' with EVAL INTERVAL requires the batching scheduler, but source tables with ttl=instant are not supported under batching mode"
3802 ));
3803 }
3804
3805 #[test]
3806 fn test_determine_flow_type_for_source_state_existing_sources_require_plan() {
3807 assert_eq!(
3808 determine_flow_type_for_source_state("my_flow", &HashMap::new(), false, true, false)
3809 .unwrap(),
3810 None
3811 );
3812 }
3813
3814 #[test]
3815 fn test_determine_flow_type_for_source_state_instant_ttl_with_eval_interval_is_rejected() {
3816 let err =
3821 determine_flow_type_for_source_state("my_flow", &HashMap::new(), false, true, true)
3822 .unwrap_err();
3823 assert!(
3824 err.to_string().contains(
3825 "flow 'my_flow' with EVAL INTERVAL requires the batching scheduler, but source tables with ttl=instant are not supported under batching mode"
3826 ),
3827 "unexpected error: {err}"
3828 );
3829 }
3830
3831 #[test]
3832 fn test_name_is_match() {
3833 assert!(!NAME_PATTERN_REG.is_match("/adaf"));
3834 assert!(!NAME_PATTERN_REG.is_match("🈲"));
3835 assert!(NAME_PATTERN_REG.is_match("hello"));
3836 assert!(NAME_PATTERN_REG.is_match("test@"));
3837 assert!(!NAME_PATTERN_REG.is_match("@test"));
3838 assert!(NAME_PATTERN_REG.is_match("test#"));
3839 assert!(!NAME_PATTERN_REG.is_match("#test"));
3840 assert!(!NAME_PATTERN_REG.is_match("@"));
3841 assert!(!NAME_PATTERN_REG.is_match("#"));
3842 }
3843
3844 #[test]
3845 fn test_convert_iceberg_hash() {
3846 let names = ["host", "count", "device", "measure", "host-name"].map(String::from);
3847 let column_name_and_type = HashMap::from([
3848 (&names[0], ConcreteDataType::string_datatype()),
3849 (&names[1], ConcreteDataType::int16_datatype()),
3850 (&names[2], ConcreteDataType::uint64_datatype()),
3851 (&names[3], ConcreteDataType::float64_datatype()),
3852 (&names[4], ConcreteDataType::string_datatype()),
3853 ]);
3854 let timezone = Timezone::from_tz_string("UTC").unwrap();
3855 let dialect = GreptimeDbDialect {};
3856 let parse = |sql: &str| {
3857 Parser::new(&dialect)
3858 .try_with_sql(sql)
3859 .unwrap()
3860 .parse_expr()
3861 .unwrap()
3862 };
3863
3864 for sql in [
3865 "iceberg_hash(host) < 0",
3866 "ICEBERG_HASH(device) >= -1073741824",
3867 "0 > iceberg_hash(count)",
3868 "iceberg_hash(host) < 100 AND host < 'm'",
3869 r#"iceberg_hash("host-name") < 0 AND "host-name" < 'm'"#,
3870 ] {
3871 let expr = convert_one_expr(&parse(sql), &column_name_and_type, &timezone).unwrap();
3872 let mut partitions = Partitions {
3874 column_list: vec![],
3875 exprs: vec![expr.to_parser_expr()],
3876 };
3877 partitions.set_quote('"');
3878 let restored = parse(&partitions.exprs[0].to_string());
3879 assert_eq!(
3880 convert_one_expr(&restored, &column_name_and_type, &timezone).unwrap(),
3881 expr,
3882 "{sql}"
3883 );
3884 }
3885
3886 for sql in [
3887 "iceberg_hash(measure) < 0",
3888 "iceberg_hash(host, count) < 0",
3889 "iceberg_hash('a') < 0",
3890 "iceberg_hash(missing) < 0",
3891 "iceberg_hash(DISTINCT host) < 0",
3892 "md5(host) < 0",
3893 "iceberg_hash(host) < host",
3894 "iceberg_hash(host) < 'abc'",
3895 "iceberg_hash(host) < 2147483648",
3896 ] {
3897 assert!(
3898 convert_one_expr(&parse(sql), &column_name_and_type, &timezone).is_err(),
3899 "{sql}"
3900 );
3901 }
3902 }
3903
3904 #[test]
3905 fn test_partition_expr_equivalence_with_swapped_operands() {
3906 let column_name = "device_id".to_string();
3907 let column_name_and_type =
3908 HashMap::from([(&column_name, ConcreteDataType::int32_datatype())]);
3909 let timezone = Timezone::from_tz_string("UTC").unwrap();
3910 let dialect = GreptimeDbDialect {};
3911
3912 let mut parser = Parser::new(&dialect)
3913 .try_with_sql("device_id < 100")
3914 .unwrap();
3915 let expr_left = parser.parse_expr().unwrap();
3916
3917 let mut parser = Parser::new(&dialect)
3918 .try_with_sql("100 > device_id")
3919 .unwrap();
3920 let expr_right = parser.parse_expr().unwrap();
3921
3922 let partition_left =
3923 convert_one_expr(&expr_left, &column_name_and_type, &timezone).unwrap();
3924 let partition_right =
3925 convert_one_expr(&expr_right, &column_name_and_type, &timezone).unwrap();
3926
3927 assert_eq!(partition_left, partition_right);
3928 assert!([partition_left.clone()].contains(&partition_right));
3929
3930 let mut physical_partition_exprs = vec![partition_left];
3931 let mut logical_partition_exprs = vec![partition_right];
3932 physical_partition_exprs.sort_unstable();
3933 logical_partition_exprs.sort_unstable();
3934 assert_eq!(physical_partition_exprs, logical_partition_exprs);
3935 }
3936
3937 #[test]
3938 fn test_repartition_target_partition_columns_are_overwrite_context() {
3939 let device_id = ColumnSchema::new("device_id", ConcreteDataType::int32_datatype(), true);
3940 let area = ColumnSchema::new("area", ConcreteDataType::string_datatype(), true);
3941 let existing_partition_columns = vec![&device_id];
3942 let target_partition_columns = vec![&device_id, &area];
3943 let existing_column_name_and_type = column_name_and_type(&existing_partition_columns);
3944 let target_column_name_and_type = column_name_and_type(&target_partition_columns);
3945 let timezone = Timezone::from_tz_string("UTC").unwrap();
3946 let dialect = GreptimeDbDialect {};
3947
3948 let mut parser = Parser::new(&dialect)
3949 .try_with_sql("device_id < 100 AND area < 'South'")
3950 .unwrap();
3951 let expr = parser.parse_expr().unwrap();
3952
3953 let err = convert_one_expr(&expr, &existing_column_name_and_type, &timezone).unwrap_err();
3954 assert!(err.to_string().contains("area"));
3955
3956 let partition_expr = convert_one_expr(&expr, &target_column_name_and_type, &timezone)
3957 .expect("target columns should overwrite the conversion context");
3958 let partition_expr = partition_expr.to_string();
3959 assert!(partition_expr.contains("device_id"));
3960 assert!(partition_expr.contains("area"));
3961 assert!(partition_expr.contains("South"));
3962 }
3963
3964 #[test]
3965 fn test_repartition_rejects_remaining_expr_outside_target_columns() {
3966 let device_id = "device_id".to_string();
3967 let area = "area".to_string();
3968 let timezone = Timezone::from_tz_string("UTC").unwrap();
3969 let column_name_and_type = HashMap::from([
3970 (&device_id, ConcreteDataType::int32_datatype()),
3971 (&area, ConcreteDataType::string_datatype()),
3972 ]);
3973 let dialect = GreptimeDbDialect {};
3974 let target_partition_columns = HashSet::from([&area]);
3975 for sql in ["device_id >= 100", "iceberg_hash(device_id) >= 0"] {
3976 let mut parser = Parser::new(&dialect).try_with_sql(sql).unwrap();
3977 let remaining_old_expr = convert_one_expr(
3978 &parser.parse_expr().unwrap(),
3979 &column_name_and_type,
3980 &timezone,
3981 )
3982 .unwrap();
3983
3984 let err = ensure_partition_expr_columns_in_target(
3985 &[remaining_old_expr],
3986 &target_partition_columns,
3987 )
3988 .unwrap_err();
3989
3990 assert!(err.to_string().contains("device_id"), "{sql}");
3991 assert!(
3992 err.to_string().contains("target partition columns"),
3993 "{sql}"
3994 );
3995 }
3996 }
3997
3998 #[test]
3999 fn test_repartition_rejects_duplicate_target_partition_columns() {
4000 let device_id = ColumnSchema::new("device_id", ConcreteDataType::int32_datatype(), true);
4001 let column_schemas = vec![device_id];
4002 let target_partition_columns = vec!["device_id".to_string(), "device_id".to_string()];
4003
4004 let err =
4005 validate_and_collect_partition_columns(&target_partition_columns, &column_schemas)
4006 .unwrap_err();
4007
4008 assert!(err.to_string().contains("duplicate partition column"));
4009 assert!(err.to_string().contains("device_id"));
4010 }
4011
4012 fn create_expr_from_sql(sql: &str) -> CreateTableExpr {
4013 let result =
4014 ParserContext::create_with_dialect(sql, &GreptimeDbDialect {}, ParseOptions::default())
4015 .unwrap();
4016
4017 match &result[0] {
4018 Statement::CreateTable(create) => {
4019 expr_helper::create_to_expr(create, &QueryContext::arc()).unwrap()
4020 }
4021 _ => unreachable!(),
4022 }
4023 }
4024
4025 #[test]
4026 fn test_create_table_with_repartition_partition_num_hint() {
4027 let key = table::requests::REPARTITION_PARTITION_NUM_HINT_KEY;
4028 for (value, expected) in [
4029 (" 8 ", Some("8")),
4030 ("0", None),
4031 ("-1", None),
4032 ("4294967296", None),
4033 ] {
4034 let expr = create_expr_from_sql(&format!(
4035 "CREATE TABLE metrics (host STRING, ts TIMESTAMP TIME INDEX, PRIMARY KEY(host)) WITH ('{key}' = '{value}')"
4036 ));
4037 let result = create_table_info(&expr, vec![]);
4038 if let Some(expected) = expected {
4039 let info = result.unwrap();
4040 assert_eq!(
4041 info.meta.options.extra_options.get(key).map(String::as_str),
4042 Some(expected)
4043 );
4044 } else {
4045 assert!(
4046 result
4047 .unwrap_err()
4048 .to_string()
4049 .contains("expects a positive integer")
4050 );
4051 }
4052 }
4053 }
4054
4055 #[test]
4056 fn test_create_table_with_repartition_column_hint() {
4057 let expr = create_expr_from_sql(
4058 r"
4059CREATE TABLE metrics (
4060 host STRING,
4061 ts TIMESTAMP TIME INDEX,
4062 cpu DOUBLE,
4063 PRIMARY KEY(host)
4064)
4065WITH ('repartition.column.hint' = ' host ')",
4066 );
4067
4068 let table_info = create_table_info(&expr, vec![]).unwrap();
4069 assert_eq!(
4070 table_info
4071 .meta
4072 .options
4073 .extra_options
4074 .get(REPARTITION_COLUMN_HINT_KEY),
4075 Some(&"host".to_string())
4076 );
4077 }
4078
4079 #[test]
4080 fn test_create_table_info_rejects_non_timestamp_time_index() {
4081 let expr = CreateTableExpr {
4082 catalog_name: "greptime".to_string(),
4083 schema_name: "public".to_string(),
4084 table_name: "demo".to_string(),
4085 desc: String::new(),
4086 column_defs: vec![api::v1::ColumnDef {
4087 name: "host".to_string(),
4088 data_type: api::v1::ColumnDataType::String as i32,
4089 is_nullable: true,
4090 default_constraint: vec![],
4091 semantic_type: 0,
4092 comment: String::new(),
4093 datatype_extension: None,
4094 options: None,
4095 }],
4096 time_index: "host".to_string(),
4097 primary_keys: vec![],
4098 create_if_not_exists: false,
4099 table_options: HashMap::new(),
4100 table_id: None,
4101 engine: "mito".to_string(),
4102 };
4103
4104 let err = create_table_info(&expr, vec![]).unwrap_err();
4105 assert_eq!(
4106 common_error::ext::ErrorExt::status_code(&err),
4107 common_error::status_code::StatusCode::InvalidArguments
4108 );
4109 }
4110
4111 #[test]
4112 fn test_verify_alter_guards_entity_column_types() {
4113 let expr = create_expr_from_sql(
4114 "CREATE TABLE t (svc STRING, ts TIMESTAMP TIME INDEX) \
4115 WITH ('greptime.semantic.entity.service.id'='svc');",
4116 );
4117 let table_info = Arc::new(create_table_info(&expr, vec![]).unwrap());
4118 let alter = |kind| AlterTableExpr {
4119 catalog_name: "greptime".to_string(),
4120 schema_name: "public".to_string(),
4121 table_name: "t".to_string(),
4122 kind: Some(kind),
4123 };
4124
4125 let modify = alter(Kind::ModifyColumnTypes(api::v1::ModifyColumnTypes {
4126 modify_column_types: vec![api::v1::ModifyColumnType {
4127 column_name: "svc".to_string(),
4128 target_type: api::v1::ColumnDataType::Binary as i32,
4129 target_type_extension: None,
4130 }],
4131 }));
4132 let err = verify_alter(1, table_info.clone(), modify).unwrap_err();
4133 let msg = common_error::ext::ErrorExt::output_msg(&err);
4134 assert!(
4135 msg.contains("must keep a type that renders as a string"),
4136 "{msg}"
4137 );
4138
4139 let drop = alter(Kind::DropColumns(api::v1::DropColumns {
4142 drop_columns: vec![api::v1::DropColumn {
4143 name: "svc".to_string(),
4144 }],
4145 }));
4146 assert!(verify_alter(1, table_info, drop).unwrap());
4147 }
4148
4149 #[test]
4150 fn test_json2_requires_append_mode() {
4151 let cases = [
4152 "CREATE TABLE monitor (payload JSON2, ts TIMESTAMP TIME INDEX);",
4153 "CREATE TABLE monitor (payload JSON2, ts TIMESTAMP TIME INDEX) WITH (append_mode='false');",
4154 ];
4155
4156 for sql in cases {
4157 let expr = create_expr_from_sql(sql);
4158 let err = create_table_info(&expr, vec![]).unwrap_err();
4159 assert!(
4160 err.to_string()
4161 .contains("JSON2 column `payload` requires append_mode='true'"),
4162 "{err}"
4163 );
4164 }
4165
4166 let expr = create_expr_from_sql(
4167 "CREATE TABLE monitor (payload JSON2, ts TIMESTAMP TIME INDEX) WITH (append_mode='true');",
4168 );
4169 create_table_info(&expr, vec![]).unwrap();
4170 }
4171
4172 #[test]
4173 fn test_create_table_with_empty_repartition_column_hint() {
4174 let expr = create_expr_from_sql(
4175 r"
4176CREATE TABLE metrics (
4177 host STRING,
4178 ts TIMESTAMP TIME INDEX,
4179 cpu DOUBLE,
4180 PRIMARY KEY(host)
4181)
4182WITH ('repartition.column.hint' = '')",
4183 );
4184
4185 let err = create_table_info(&expr, vec![]).unwrap_err();
4186 assert!(
4187 err.to_string()
4188 .contains("repartition.column.hint expects exactly one column name")
4189 );
4190 }
4191
4192 #[test]
4193 fn test_create_table_with_multiple_repartition_column_hints() {
4194 let expr = create_expr_from_sql(
4195 r"
4196CREATE TABLE metrics (
4197 host STRING,
4198 region_id STRING,
4199 ts TIMESTAMP TIME INDEX,
4200 cpu DOUBLE,
4201 PRIMARY KEY(host)
4202)
4203WITH ('repartition.column.hint' = 'host,region_id')",
4204 );
4205
4206 let err = create_table_info(&expr, vec![]).unwrap_err();
4207 assert!(
4208 err.to_string()
4209 .contains("repartition.column.hint expects exactly one column name")
4210 );
4211 }
4212
4213 #[test]
4214 fn test_create_table_with_missing_repartition_column_hint() {
4215 let expr = create_expr_from_sql(
4216 r"
4217CREATE TABLE metrics (
4218 host STRING,
4219 ts TIMESTAMP TIME INDEX,
4220 cpu DOUBLE,
4221 PRIMARY KEY(host)
4222)
4223WITH ('repartition.column.hint' = 'region_id')",
4224 );
4225
4226 let err = create_table_info(&expr, vec![]).unwrap_err();
4227 assert!(
4228 err.to_string()
4229 .contains("Cannot find column by name: region")
4230 );
4231 }
4232
4233 #[test]
4234 fn test_create_table_with_time_index_repartition_column_hint() {
4235 let expr = create_expr_from_sql(
4236 r"
4237CREATE TABLE metrics (
4238 host STRING,
4239 ts TIMESTAMP TIME INDEX,
4240 cpu DOUBLE,
4241 PRIMARY KEY(host)
4242)
4243WITH ('repartition.column.hint' = 'ts')",
4244 );
4245
4246 let err = create_table_info(&expr, vec![]).unwrap_err();
4247 assert!(
4248 err.to_string()
4249 .contains("cannot set repartition.column.hint to the time index column")
4250 );
4251 }
4252
4253 #[test]
4254 fn test_create_partitioned_table_with_repartition_column_hint() {
4255 let expr = create_expr_from_sql(
4256 r"
4257CREATE TABLE metrics (
4258 host STRING,
4259 ts TIMESTAMP TIME INDEX,
4260 cpu DOUBLE,
4261 PRIMARY KEY(host)
4262)
4263WITH ('repartition.column.hint' = 'host')",
4264 );
4265
4266 let err = create_table_info(&expr, vec!["host".to_string()]).unwrap_err();
4267 assert!(
4268 err.to_string()
4269 .contains("cannot set repartition.column.hint on a table with partition metadata")
4270 );
4271 }
4272}