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