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::{DEFAULT_CATALOG_NAME, DEFAULT_SCHEMA_NAME, is_readonly_schema};
35use common_catalog::{format_full_flow_name, format_full_table_name};
36use common_error::ext::BoxedError;
37use common_meta::cache_invalidator::{CacheInvalidatorRef, Context};
38use common_meta::ddl::create_flow::{
39 DEFER_ON_MISSING_SOURCE_KEY, FLOW_EXPERIMENTAL_ENABLE_INCREMENTAL_READ_KEY, FlowType,
40};
41use common_meta::instruction::CacheIdent;
42use common_meta::key::TableMetadataManagerRef;
43use common_meta::key::schema_name::{SchemaName, SchemaNameKey};
44use common_meta::procedure_executor::{ExecutorContext, ProcedureExecutorRef};
45#[cfg(feature = "enterprise")]
46use common_meta::rpc::ddl::trigger::CreateTriggerTask;
47#[cfg(feature = "enterprise")]
48use common_meta::rpc::ddl::trigger::DropTriggerTask;
49use common_meta::rpc::ddl::{
50 CreateFlowTask, CreatorGrantIntent, DdlTask, DropFlowTask, DropViewTask, SubmitDdlTaskRequest,
51 SubmitDdlTaskResponse,
52};
53use common_query::Output;
54use common_recordbatch::{RecordBatch, RecordBatches};
55use common_sql::convert::sql_value_to_value;
56use common_telemetry::{debug, info, tracing, warn};
57use common_time::{Timestamp, Timezone};
58use datafusion_common::tree_node::TreeNodeVisitor;
59use datafusion_expr::LogicalPlan;
60use datatypes::prelude::ConcreteDataType;
61use datatypes::schema::{ColumnSchema, Schema};
62use datatypes::value::Value;
63use datatypes::vectors::{StringVector, VectorRef};
64use humantime::parse_duration;
65use partition::expr::{Operand, PartitionExpr, RestrictedOp};
66use partition::multi_dim::MultiDimPartitionRule;
67use query::parser::QueryStatement;
68use query::plan::extract_and_rewrite_full_table_names;
69use query::query_engine::DefaultSerializer;
70use query::sql::create_table_stmt;
71use session::context::QueryContextRef;
72use session::table_name::table_idents_to_full_name;
73use snafu::{OptionExt, ResultExt, ensure};
74use sql::parser::{ParseOptions, ParserContext};
75use sql::parsers::utils::is_tql;
76use sql::statements::OptionMap;
77#[cfg(feature = "enterprise")]
78use sql::statements::alter::trigger::AlterTrigger;
79use sql::statements::alter::{AlterDatabase, AlterTable, AlterTableOperation};
80#[cfg(feature = "enterprise")]
81use sql::statements::create::trigger::CreateTrigger;
82use sql::statements::create::{
83 CreateExternalTable, CreateFlow, CreateTable, CreateTableLike, CreateView, Partitions,
84};
85use sql::statements::statement::Statement;
86use sqlparser::ast::{Expr, Ident, UnaryOperator, Value as ParserValue};
87use store_api::metric_engine_consts::{LOGICAL_TABLE_METADATA_KEY, METRIC_ENGINE_NAME};
88use store_api::mito_engine_options::APPEND_MODE_KEY;
89use substrait::{DFLogicalSubstraitConvertor, SubstraitPlan};
90use table::TableRef;
91use table::dist_table::DistTable;
92use table::metadata::{self, TableId, TableInfo, TableMeta, TableType};
93use table::requests::{
94 AlterKind, AlterTableRequest, COMMENT_KEY, DDL_TIMEOUT, DDL_WAIT, REPARTITION_COLUMN_HINT_KEY,
95 TableOptions,
96};
97use table::table_name::TableName;
98use table::table_reference::TableReference;
99
100use crate::error::{
101 self, AlterExprToRequestSnafu, BuildDfLogicalPlanSnafu, CatalogSnafu, ColumnDataTypeSnafu,
102 ColumnNotFoundSnafu, ConvertSchemaSnafu, CreateLogicalTablesSnafu,
103 DeserializePartitionExprSnafu, EmptyDdlExprSnafu, ExternalSnafu, ExtractTableNamesSnafu,
104 FlowNotFoundSnafu, InvalidPartitionRuleSnafu, InvalidPartitionSnafu, InvalidSqlSnafu,
105 InvalidTableNameSnafu, InvalidViewNameSnafu, InvalidViewStmtSnafu, NotSupportedSnafu,
106 PartitionExprToPbSnafu, Result, SchemaInUseSnafu, SchemaNotFoundSnafu, SchemaReadOnlySnafu,
107 SerializePartitionExprSnafu, SubstraitCodecSnafu, TableAlreadyExistsSnafu,
108 TableMetadataManagerSnafu, TableNotFoundSnafu, UnrecognizedTableOptionSnafu,
109 ViewAlreadyExistsSnafu,
110};
111use crate::expr_helper::{self, RepartitionRequest, RepartitionSource};
112use crate::statement::StatementExecutor;
113use crate::statement::show::create_partitions_stmt;
114use crate::utils::{to_meta_query_context, to_meta_query_context_with_origin_frontend};
115
116#[derive(Debug, Clone, Copy)]
117struct DdlSubmitOptions {
118 wait: bool,
119 timeout: Duration,
120}
121
122const ALLOWED_FLOW_OPTIONS: [&str; 2] = [
123 DEFER_ON_MISSING_SOURCE_KEY,
124 FLOW_EXPERIMENTAL_ENABLE_INCREMENTAL_READ_KEY,
125];
126
127fn build_procedure_id_output(procedure_id: Vec<u8>) -> Result<Output> {
128 let procedure_id = String::from_utf8_lossy(&procedure_id).to_string();
129 let vector: VectorRef = Arc::new(StringVector::from(vec![procedure_id]));
130 let schema = Arc::new(Schema::new(vec![ColumnSchema::new(
131 "Procedure ID",
132 vector.data_type(),
133 false,
134 )]));
135 let batch =
136 RecordBatch::new(schema.clone(), vec![vector]).context(error::BuildRecordBatchSnafu)?;
137 let batches =
138 RecordBatches::try_new(schema, vec![batch]).context(error::BuildRecordBatchSnafu)?;
139 Ok(Output::new_with_record_batches(batches))
140}
141
142fn parse_ddl_options(options: &OptionMap) -> Result<DdlSubmitOptions> {
143 let wait = match options.get(DDL_WAIT) {
144 Some(value) => value.parse::<bool>().map_err(|_| {
145 InvalidSqlSnafu {
146 err_msg: format!("invalid DDL option '{DDL_WAIT}': '{value}'"),
147 }
148 .build()
149 })?,
150 None => SubmitDdlTaskRequest::default_wait(),
151 };
152
153 let timeout = match options.get(DDL_TIMEOUT) {
154 Some(value) => parse_duration(value).map_err(|err| {
155 InvalidSqlSnafu {
156 err_msg: format!("invalid DDL option '{DDL_TIMEOUT}': '{value}': {err}"),
157 }
158 .build()
159 })?,
160 None => SubmitDdlTaskRequest::default_timeout(),
161 };
162
163 Ok(DdlSubmitOptions { wait, timeout })
164}
165
166fn supported_flow_options() -> String {
167 ALLOWED_FLOW_OPTIONS.join(", ")
168}
169
170fn normalize_flow_bool_option(key: &str, value: &str) -> Result<String> {
171 value
172 .trim()
173 .to_ascii_lowercase()
174 .parse::<bool>()
175 .map(|value| value.to_string())
176 .map_err(|_| {
177 InvalidSqlSnafu {
178 err_msg: format!("invalid flow option '{key}': '{value}'"),
179 }
180 .build()
181 })
182}
183
184fn validate_and_normalize_flow_options(
185 options: HashMap<String, String>,
186 eval_interval: Option<i64>,
187) -> Result<HashMap<String, String>> {
188 if let Some(secs) = eval_interval
190 && secs <= 0
191 {
192 return InvalidSqlSnafu {
193 err_msg: format!("EVAL INTERVAL must be positive, got {secs} seconds"),
194 }
195 .fail();
196 }
197
198 options
199 .into_iter()
200 .map(|(key, value)| {
201 if key == FlowType::FLOW_TYPE_KEY {
202 return InvalidSqlSnafu {
203 err_msg: format!("flow option '{key}' is reserved for internal use"),
204 }
205 .fail();
206 }
207
208 let normalized_value = match key.as_str() {
209 DEFER_ON_MISSING_SOURCE_KEY | FLOW_EXPERIMENTAL_ENABLE_INCREMENTAL_READ_KEY => {
210 normalize_flow_bool_option(&key, &value)?
211 }
212 _ => {
213 return InvalidSqlSnafu {
214 err_msg: format!(
215 "unknown flow option '{key}', supported options: {}",
216 supported_flow_options()
217 ),
218 }
219 .fail();
220 }
221 };
222
223 Ok((key, normalized_value))
224 })
225 .collect()
226}
227
228fn determine_flow_type_for_source_state(
229 flow_name: &str,
230 flow_options: &HashMap<String, String>,
231 has_missing_source_table: bool,
232 has_instant_ttl_source_table: bool,
233) -> Result<Option<FlowType>> {
234 if has_missing_source_table {
235 let defer_on_missing_source = flow_options
236 .get(DEFER_ON_MISSING_SOURCE_KEY)
237 .is_some_and(|value| value == "true");
238 ensure!(
239 defer_on_missing_source,
240 InvalidSqlSnafu {
241 err_msg: format!(
242 "missing source tables for flow '{}'; use WITH ({DEFER_ON_MISSING_SOURCE_KEY} = true) to create a pending flow",
243 flow_name
244 )
245 }
246 );
247 info!(
248 "Flow `{}` is created as a pending batching flow because source tables are missing and defer_on_missing_source=true",
249 flow_name
250 );
251 return Ok(Some(FlowType::Batching));
252 }
253
254 if has_instant_ttl_source_table {
255 return Ok(Some(FlowType::Streaming));
256 }
257
258 Ok(None)
259}
260
261impl StatementExecutor {
262 pub fn catalog_manager(&self) -> CatalogManagerRef {
263 self.catalog_manager.clone()
264 }
265
266 #[tracing::instrument(skip_all)]
267 pub async fn create_table(&self, stmt: CreateTable, ctx: QueryContextRef) -> Result<TableRef> {
268 let (catalog, schema, _table) = table_idents_to_full_name(&stmt.name, &ctx)
269 .map_err(BoxedError::new)
270 .context(error::ExternalSnafu)?;
271
272 let schema_options = self
273 .table_metadata_manager
274 .schema_manager()
275 .get(SchemaNameKey {
276 catalog: &catalog,
277 schema: &schema,
278 })
279 .await
280 .context(TableMetadataManagerSnafu)?
281 .map(|v| v.into_inner());
282
283 let create_expr = &mut expr_helper::create_to_expr(&stmt, &ctx)?;
284 if let Some(schema_options) = schema_options {
287 for (key, value) in schema_options.extra_options.iter() {
288 if key.starts_with("compaction.") {
289 continue;
290 }
291 create_expr
292 .table_options
293 .entry(key.clone())
294 .or_insert(value.clone());
295 }
296 }
297
298 self.create_table_inner(create_expr, stmt.partitions, ctx)
299 .await
300 }
301
302 #[tracing::instrument(skip_all)]
303 pub async fn create_table_like(
304 &self,
305 stmt: CreateTableLike,
306 ctx: QueryContextRef,
307 ) -> Result<TableRef> {
308 let (catalog, schema, table) = table_idents_to_full_name(&stmt.source_name, &ctx)
309 .map_err(BoxedError::new)
310 .context(error::ExternalSnafu)?;
311 let table_ref = self
312 .catalog_manager
313 .table(&catalog, &schema, &table, Some(&ctx))
314 .await
315 .context(CatalogSnafu)?
316 .context(TableNotFoundSnafu { table_name: &table })?;
317 let partition_info = self
318 .partition_manager
319 .find_physical_partition_info(table_ref.table_info().table_id())
320 .await
321 .context(error::FindTablePartitionRuleSnafu { table_name: table })?;
322
323 let schema_options = self
325 .table_metadata_manager
326 .schema_manager()
327 .get(SchemaNameKey {
328 catalog: &catalog,
329 schema: &schema,
330 })
331 .await
332 .context(TableMetadataManagerSnafu)?
333 .map(|v| v.into_inner());
334
335 let quote_style = ctx.quote_style();
336 let mut create_stmt =
337 create_table_stmt(&table_ref.table_info(), schema_options, quote_style)
338 .context(error::ParseQuerySnafu)?;
339 create_stmt.name = stmt.table_name;
340 create_stmt.if_not_exists = false;
341
342 let table_info = table_ref.table_info();
343 let partitions = create_partitions_stmt(&table_info, &partition_info.partitions)?.and_then(
344 |mut partitions| {
345 if !partitions.column_list.is_empty() {
346 partitions.set_quote(quote_style);
347 Some(partitions)
348 } else {
349 None
350 }
351 },
352 );
353
354 let create_expr = &mut expr_helper::create_to_expr(&create_stmt, &ctx)?;
355 self.create_table_inner(create_expr, partitions, ctx).await
356 }
357
358 #[tracing::instrument(skip_all)]
359 pub async fn create_external_table(
360 &self,
361 create_expr: CreateExternalTable,
362 ctx: QueryContextRef,
363 ) -> Result<TableRef> {
364 let create_expr = &mut expr_helper::create_external_expr(create_expr, &ctx).await?;
365 self.create_table_inner(create_expr, None, ctx).await
366 }
367
368 #[tracing::instrument(skip_all)]
369 pub async fn create_table_inner(
370 &self,
371 create_table: &mut CreateTableExpr,
372 partitions: Option<Partitions>,
373 query_ctx: QueryContextRef,
374 ) -> Result<TableRef> {
375 ensure!(
376 !is_readonly_schema(&create_table.schema_name),
377 SchemaReadOnlySnafu {
378 name: create_table.schema_name.clone()
379 }
380 );
381
382 if create_table.engine == METRIC_ENGINE_NAME
383 && create_table
384 .table_options
385 .contains_key(LOGICAL_TABLE_METADATA_KEY)
386 {
387 if let Some(partitions) = partitions.as_ref()
388 && !partitions.exprs.is_empty()
389 {
390 self.validate_logical_table_partition_rule(create_table, partitions, &query_ctx)
391 .await?;
392 }
393 self.create_logical_tables(std::slice::from_ref(create_table), query_ctx)
395 .await?
396 .into_iter()
397 .next()
398 .context(error::UnexpectedSnafu {
399 violated: "expected to create logical tables",
400 })
401 } else {
402 self.create_non_logic_table(create_table, partitions, query_ctx)
404 .await
405 }
406 }
407
408 #[tracing::instrument(skip_all)]
409 pub async fn create_non_logic_table(
410 &self,
411 create_table: &mut CreateTableExpr,
412 partitions: Option<Partitions>,
413 query_ctx: QueryContextRef,
414 ) -> Result<TableRef> {
415 let _timer = crate::metrics::DIST_CREATE_TABLE.start_timer();
416
417 let schema = self
419 .table_metadata_manager
420 .schema_manager()
421 .get(SchemaNameKey::new(
422 &create_table.catalog_name,
423 &create_table.schema_name,
424 ))
425 .await
426 .context(TableMetadataManagerSnafu)?;
427 ensure!(
428 schema.is_some(),
429 SchemaNotFoundSnafu {
430 schema_info: &create_table.schema_name,
431 }
432 );
433
434 if let Some(table) = self
436 .catalog_manager
437 .table(
438 &create_table.catalog_name,
439 &create_table.schema_name,
440 &create_table.table_name,
441 Some(&query_ctx),
442 )
443 .await
444 .context(CatalogSnafu)?
445 {
446 return if create_table.create_if_not_exists {
447 Ok(table)
448 } else {
449 TableAlreadyExistsSnafu {
450 table: format_full_table_name(
451 &create_table.catalog_name,
452 &create_table.schema_name,
453 &create_table.table_name,
454 ),
455 }
456 .fail()
457 };
458 }
459
460 ensure!(
461 NAME_PATTERN_REG.is_match(&create_table.table_name),
462 InvalidTableNameSnafu {
463 table_name: &create_table.table_name,
464 }
465 );
466
467 let table_name = TableName::new(
468 &create_table.catalog_name,
469 &create_table.schema_name,
470 &create_table.table_name,
471 );
472
473 let (partitions, partition_cols) = parse_partitions(create_table, partitions, &query_ctx)?;
474 let mut table_info = create_table_info(create_table, partition_cols)?;
475
476 let resp = self
477 .create_table_procedure(
478 create_table.clone(),
479 partitions,
480 table_info.clone(),
481 query_ctx,
482 )
483 .await?;
484
485 let table_id = resp
486 .table_ids
487 .into_iter()
488 .next()
489 .context(error::UnexpectedSnafu {
490 violated: "expected table_id",
491 })?;
492 info!("Successfully created table '{table_name}' with table id {table_id}");
493
494 table_info.ident.table_id = table_id;
495
496 let table_info = Arc::new(table_info);
497 create_table.table_id = Some(api::v1::TableId { id: table_id });
498
499 let table = DistTable::table(table_info);
500
501 Ok(table)
502 }
503
504 #[tracing::instrument(skip_all)]
505 pub async fn create_logical_tables(
506 &self,
507 create_table_exprs: &[CreateTableExpr],
508 query_context: QueryContextRef,
509 ) -> Result<Vec<TableRef>> {
510 let _timer = crate::metrics::DIST_CREATE_TABLES.start_timer();
511 ensure!(
512 !create_table_exprs.is_empty(),
513 EmptyDdlExprSnafu {
514 name: "create logic tables"
515 }
516 );
517
518 for create_table in create_table_exprs {
520 ensure!(
521 NAME_PATTERN_REG.is_match(&create_table.table_name),
522 InvalidTableNameSnafu {
523 table_name: &create_table.table_name,
524 }
525 );
526 }
527
528 let raw_tables_info = create_table_exprs
529 .iter()
530 .map(|create| create_table_info(create, vec![]))
531 .collect::<Result<Vec<_>>>()?;
532 let tables_data = create_table_exprs
533 .iter()
534 .cloned()
535 .zip(raw_tables_info.iter().cloned())
536 .collect::<Vec<_>>();
537
538 let resp = self
539 .create_logical_tables_procedure(tables_data, query_context.clone())
540 .await?;
541
542 let table_ids = resp.table_ids;
543 ensure!(
544 table_ids.len() == raw_tables_info.len(),
545 CreateLogicalTablesSnafu {
546 reason: format!(
547 "The number of tables is inconsistent with the expected number to be created, expected: {}, actual: {}",
548 raw_tables_info.len(),
549 table_ids.len()
550 )
551 }
552 );
553 info!("Successfully created logical tables: {:?}", table_ids);
554
555 let mut tables_info = Vec::with_capacity(table_ids.len());
559 for (table_id, create_table) in table_ids.iter().zip(create_table_exprs.iter()) {
560 let table = self
561 .catalog_manager
562 .table(
563 &create_table.catalog_name,
564 &create_table.schema_name,
565 &create_table.table_name,
566 Some(&query_context),
567 )
568 .await
569 .context(CatalogSnafu)?
570 .with_context(|| TableNotFoundSnafu {
571 table_name: format_full_table_name(
572 &create_table.catalog_name,
573 &create_table.schema_name,
574 &create_table.table_name,
575 ),
576 })?;
577
578 let table_info = table.table_info();
579 ensure!(
581 table_info.table_id() == *table_id,
582 CreateLogicalTablesSnafu {
583 reason: format!(
584 "Table id mismatch after creation, expected {}, got {} for table {}",
585 table_id,
586 table_info.table_id(),
587 format_full_table_name(
588 &create_table.catalog_name,
589 &create_table.schema_name,
590 &create_table.table_name
591 )
592 )
593 }
594 );
595
596 tables_info.push(table_info);
597 }
598
599 Ok(tables_info.into_iter().map(DistTable::table).collect())
600 }
601
602 async fn validate_logical_table_partition_rule(
603 &self,
604 create_table: &CreateTableExpr,
605 partitions: &Partitions,
606 query_ctx: &QueryContextRef,
607 ) -> Result<()> {
608 let (_, mut logical_partition_exprs) =
609 parse_partitions_for_logical_validation(create_table, partitions, query_ctx)?;
610
611 let physical_table_name = create_table
612 .table_options
613 .get(LOGICAL_TABLE_METADATA_KEY)
614 .with_context(|| CreateLogicalTablesSnafu {
615 reason: format!(
616 "expect `{LOGICAL_TABLE_METADATA_KEY}` option on creating logical table"
617 ),
618 })?;
619
620 let physical_table = self
621 .catalog_manager
622 .table(
623 &create_table.catalog_name,
624 &create_table.schema_name,
625 physical_table_name,
626 Some(query_ctx),
627 )
628 .await
629 .context(CatalogSnafu)?
630 .context(TableNotFoundSnafu {
631 table_name: physical_table_name.clone(),
632 })?;
633
634 let physical_table_info = physical_table.table_info();
635 let (partition_rule, _) = self
636 .partition_manager
637 .find_table_partition_rule(&physical_table_info)
638 .await
639 .context(error::FindTablePartitionRuleSnafu {
640 table_name: physical_table_name.clone(),
641 })?;
642
643 let multi_dim_rule = partition_rule
644 .as_ref()
645 .as_any()
646 .downcast_ref::<MultiDimPartitionRule>()
647 .context(InvalidPartitionRuleSnafu {
648 reason: "physical table partition rule is not range-based",
649 })?;
650
651 let mut physical_partition_exprs = multi_dim_rule.exprs().to_vec();
653 logical_partition_exprs.sort_unstable();
654 physical_partition_exprs.sort_unstable();
655
656 ensure!(
657 physical_partition_exprs == logical_partition_exprs,
658 InvalidPartitionRuleSnafu {
659 reason: format!(
660 "logical table partition rule must match the corresponding physical table's\n logical table partition exprs:\t\t {:?}\n physical table partition exprs:\t {:?}",
661 logical_partition_exprs, physical_partition_exprs
662 ),
663 }
664 );
665
666 Ok(())
667 }
668
669 #[cfg(feature = "enterprise")]
670 #[tracing::instrument(skip_all)]
671 pub async fn create_trigger(
672 &self,
673 stmt: CreateTrigger,
674 query_context: QueryContextRef,
675 ) -> Result<Output> {
676 let expr = expr_helper::to_create_trigger_task_expr(stmt, &query_context)?;
677 self.create_trigger_inner(expr, query_context).await
678 }
679
680 #[cfg(feature = "enterprise")]
681 pub async fn create_trigger_inner(
682 &self,
683 expr: PbCreateTriggerExpr,
684 query_context: QueryContextRef,
685 ) -> Result<Output> {
686 self.create_trigger_procedure(expr, query_context).await?;
687 Ok(Output::new_with_affected_rows(0))
688 }
689
690 #[cfg(feature = "enterprise")]
691 async fn create_trigger_procedure(
692 &self,
693 expr: PbCreateTriggerExpr,
694 query_context: QueryContextRef,
695 ) -> Result<SubmitDdlTaskResponse> {
696 let task = CreateTriggerTask::try_from(PbCreateTriggerTask {
697 create_trigger: Some(expr),
698 })
699 .context(error::InvalidExprSnafu)?;
700
701 let request = SubmitDdlTaskRequest::new(
702 to_meta_query_context(query_context),
703 DdlTask::new_create_trigger(task),
704 );
705
706 self.procedure_executor
707 .submit_ddl_task(&ExecutorContext::default(), request)
708 .await
709 .context(error::ExecuteDdlSnafu)
710 }
711
712 #[tracing::instrument(skip_all)]
713 pub async fn create_flow(
714 &self,
715 stmt: CreateFlow,
716 query_context: QueryContextRef,
717 ) -> Result<Output> {
718 let expr = expr_helper::to_create_flow_task_expr(stmt, &query_context)?;
720
721 self.create_flow_inner(expr, query_context).await
722 }
723
724 pub async fn create_flow_inner(
725 &self,
726 expr: CreateFlowExpr,
727 query_context: QueryContextRef,
728 ) -> Result<Output> {
729 self.create_flow_procedure(expr, query_context).await?;
730 Ok(Output::new_with_affected_rows(0))
731 }
732
733 async fn create_flow_procedure(
734 &self,
735 mut expr: CreateFlowExpr,
736 query_context: QueryContextRef,
737 ) -> Result<SubmitDdlTaskResponse> {
738 let eval_interval_secs = expr.eval_interval.as_ref().map(|e| e.seconds);
739
740 if let Some(secs) = eval_interval_secs
742 && secs <= 0
743 {
744 return InvalidSqlSnafu {
745 err_msg: format!("EVAL INTERVAL must be positive, got {secs} seconds"),
746 }
747 .fail();
748 }
749
750 expr.flow_options =
751 validate_and_normalize_flow_options(expr.flow_options, eval_interval_secs)?;
752
753 let flow_type = self
754 .determine_flow_type(&expr, query_context.clone())
755 .await?;
756 info!("determined flow={} type: {:#?}", expr.flow_name, flow_type);
757
758 expr.flow_options
759 .insert(FlowType::FLOW_TYPE_KEY.to_string(), flow_type.to_string());
760
761 let task = CreateFlowTask::try_from(PbCreateFlowTask {
762 create_flow: Some(expr),
763 })
764 .context(error::InvalidExprSnafu)?;
765 let request = SubmitDdlTaskRequest::new(
766 to_meta_query_context(query_context),
767 DdlTask::new_create_flow(task),
768 );
769
770 self.procedure_executor
771 .submit_ddl_task(&ExecutorContext::default(), request)
772 .await
773 .context(error::ExecuteDdlSnafu)
774 }
775
776 async fn determine_flow_type(
780 &self,
781 expr: &CreateFlowExpr,
782 query_ctx: QueryContextRef,
783 ) -> Result<FlowType> {
784 let mut has_missing_source_table = false;
785 let mut has_instant_ttl_source_table = false;
786
787 for src_table_name in &expr.source_table_names {
788 let table = self
789 .catalog_manager()
790 .table(
791 &src_table_name.catalog_name,
792 &src_table_name.schema_name,
793 &src_table_name.table_name,
794 Some(&query_ctx),
795 )
796 .await
797 .map_err(BoxedError::new)
798 .context(ExternalSnafu)?;
799
800 let Some(table) = table else {
801 has_missing_source_table = true;
802 continue;
803 };
804
805 if table.table_info().meta.options.ttl == Some(common_time::TimeToLive::Instant) {
806 warn!(
807 "Source table `{}` for flow `{}`'s ttl=instant, fallback to streaming mode",
808 format_full_table_name(
809 &src_table_name.catalog_name,
810 &src_table_name.schema_name,
811 &src_table_name.table_name
812 ),
813 expr.flow_name
814 );
815 has_instant_ttl_source_table = true;
816 }
817 }
818
819 if let Some(flow_type) = determine_flow_type_for_source_state(
820 &expr.flow_name,
821 &expr.flow_options,
822 has_missing_source_table,
823 has_instant_ttl_source_table,
824 )? {
825 return Ok(flow_type);
826 }
827
828 let engine = &self.query_engine;
829 let stmts = ParserContext::create_with_dialect(
830 &expr.sql,
831 query_ctx.sql_dialect(),
832 ParseOptions::default(),
833 )
834 .map_err(BoxedError::new)
835 .context(ExternalSnafu)?;
836
837 ensure!(
838 stmts.len() == 1,
839 InvalidSqlSnafu {
840 err_msg: format!("Expect only one statement, found {}", stmts.len())
841 }
842 );
843 let stmt = &stmts[0];
844
845 if is_tql(query_ctx.sql_dialect(), &expr.sql)
846 .map_err(BoxedError::new)
847 .context(ExternalSnafu)?
848 {
849 return Ok(FlowType::Batching);
850 }
851
852 let plan = match stmt {
854 Statement::Tql(_) => return Ok(FlowType::Batching),
856 _ => engine
857 .planner()
858 .plan(&QueryStatement::Sql(stmt.clone()), query_ctx)
859 .await
860 .map_err(BoxedError::new)
861 .context(ExternalSnafu)?,
862 };
863
864 struct FindAggr {
866 is_aggr: bool,
867 }
868
869 impl TreeNodeVisitor<'_> for FindAggr {
870 type Node = LogicalPlan;
871 fn f_down(
872 &mut self,
873 node: &Self::Node,
874 ) -> datafusion_common::Result<datafusion_common::tree_node::TreeNodeRecursion>
875 {
876 match node {
877 LogicalPlan::Aggregate(_) | LogicalPlan::Distinct(_) => {
878 self.is_aggr = true;
879 return Ok(datafusion_common::tree_node::TreeNodeRecursion::Stop);
880 }
881 _ => (),
882 }
883 Ok(datafusion_common::tree_node::TreeNodeRecursion::Continue)
884 }
885 }
886
887 let mut find_aggr = FindAggr { is_aggr: false };
888
889 plan.visit_with_subqueries(&mut find_aggr)
890 .context(BuildDfLogicalPlanSnafu)?;
891 if find_aggr.is_aggr {
892 Ok(FlowType::Batching)
893 } else {
894 Ok(FlowType::Streaming)
895 }
896 }
897
898 #[tracing::instrument(skip_all)]
899 pub async fn create_view(
900 &self,
901 create_view: CreateView,
902 ctx: QueryContextRef,
903 ) -> Result<TableRef> {
904 let logical_plan = match &*create_view.query {
906 Statement::Query(query) => {
907 self.plan(
908 &QueryStatement::Sql(Statement::Query(query.clone())),
909 ctx.clone(),
910 )
911 .await?
912 }
913 Statement::Tql(query) => self.plan_tql(query.clone(), &ctx).await?,
914 _ => {
915 return InvalidViewStmtSnafu {}.fail();
916 }
917 };
918 let definition = create_view.to_string();
920
921 let schema: Schema = logical_plan
924 .schema()
925 .clone()
926 .try_into()
927 .context(ConvertSchemaSnafu)?;
928 let plan_columns: Vec<_> = schema
929 .column_schemas()
930 .iter()
931 .map(|c| c.name.clone())
932 .collect();
933
934 let columns: Vec<_> = create_view
935 .columns
936 .iter()
937 .map(|ident| ident.to_string())
938 .collect();
939
940 if !columns.is_empty() {
942 ensure!(
943 columns.len() == plan_columns.len(),
944 error::ViewColumnsMismatchSnafu {
945 view_name: create_view.name.to_string(),
946 expected: plan_columns.len(),
947 actual: columns.len(),
948 }
949 );
950 }
951
952 let (table_names, plan) = extract_and_rewrite_full_table_names(logical_plan, ctx.clone())
955 .context(ExtractTableNamesSnafu)?;
956
957 let table_names = table_names.into_iter().map(|t| t.into()).collect();
958
959 let encoded_plan = DFLogicalSubstraitConvertor
966 .encode(&plan, DefaultSerializer)
967 .context(SubstraitCodecSnafu)?;
968
969 let expr = expr_helper::to_create_view_expr(
970 create_view,
971 encoded_plan.to_vec(),
972 table_names,
973 columns,
974 plan_columns,
975 definition,
976 ctx.clone(),
977 )?;
978
979 self.create_view_by_expr(expr, ctx).await
981 }
982
983 pub async fn create_view_by_expr(
984 &self,
985 expr: CreateViewExpr,
986 ctx: QueryContextRef,
987 ) -> Result<TableRef> {
988 ensure! {
989 !(expr.create_if_not_exists & expr.or_replace),
990 InvalidSqlSnafu {
991 err_msg: "syntax error Create Or Replace and If Not Exist cannot be used together",
992 }
993 };
994 let _timer = crate::metrics::DIST_CREATE_VIEW.start_timer();
995
996 let schema_exists = self
997 .table_metadata_manager
998 .schema_manager()
999 .exists(SchemaNameKey::new(&expr.catalog_name, &expr.schema_name))
1000 .await
1001 .context(TableMetadataManagerSnafu)?;
1002
1003 ensure!(
1004 schema_exists,
1005 SchemaNotFoundSnafu {
1006 schema_info: &expr.schema_name,
1007 }
1008 );
1009
1010 if let Some(table) = self
1012 .catalog_manager
1013 .table(
1014 &expr.catalog_name,
1015 &expr.schema_name,
1016 &expr.view_name,
1017 Some(&ctx),
1018 )
1019 .await
1020 .context(CatalogSnafu)?
1021 {
1022 let table_type = table.table_info().table_type;
1023
1024 match (table_type, expr.create_if_not_exists, expr.or_replace) {
1025 (TableType::View, true, false) => {
1026 return Ok(table);
1027 }
1028 (TableType::View, false, false) => {
1029 return ViewAlreadyExistsSnafu {
1030 name: format_full_table_name(
1031 &expr.catalog_name,
1032 &expr.schema_name,
1033 &expr.view_name,
1034 ),
1035 }
1036 .fail();
1037 }
1038 (TableType::View, _, true) => {
1039 }
1041 _ => {
1042 return TableAlreadyExistsSnafu {
1043 table: format_full_table_name(
1044 &expr.catalog_name,
1045 &expr.schema_name,
1046 &expr.view_name,
1047 ),
1048 }
1049 .fail();
1050 }
1051 }
1052 }
1053
1054 ensure!(
1055 NAME_PATTERN_REG.is_match(&expr.view_name),
1056 InvalidViewNameSnafu {
1057 name: expr.view_name.clone(),
1058 }
1059 );
1060
1061 let view_name = TableName::new(&expr.catalog_name, &expr.schema_name, &expr.view_name);
1062
1063 let mut view_info = TableInfo {
1064 ident: metadata::TableIdent {
1065 table_id: 0,
1067 version: 0,
1068 },
1069 name: expr.view_name.clone(),
1070 desc: None,
1071 catalog_name: expr.catalog_name.clone(),
1072 schema_name: expr.schema_name.clone(),
1073 meta: TableMeta::empty(),
1075 table_type: TableType::View,
1076 };
1077
1078 let request = SubmitDdlTaskRequest::new(
1079 to_meta_query_context(ctx),
1080 DdlTask::new_create_view(expr, view_info.clone()),
1081 );
1082
1083 let resp = self
1084 .procedure_executor
1085 .submit_ddl_task(&ExecutorContext::default(), request)
1086 .await
1087 .context(error::ExecuteDdlSnafu)?;
1088
1089 debug!(
1090 "Submit creating view '{view_name}' task response: {:?}",
1091 resp
1092 );
1093
1094 let view_id = resp
1095 .table_ids
1096 .into_iter()
1097 .next()
1098 .context(error::UnexpectedSnafu {
1099 violated: "expected table_id",
1100 })?;
1101 info!("Successfully created view '{view_name}' with view id {view_id}");
1102
1103 view_info.ident.table_id = view_id;
1104
1105 let view_info = Arc::new(view_info);
1106
1107 let table = DistTable::table(view_info);
1108
1109 self.cache_invalidator
1111 .invalidate(
1112 &Context::default(),
1113 &[
1114 CacheIdent::TableId(view_id),
1115 CacheIdent::TableName(view_name.clone()),
1116 ],
1117 )
1118 .await
1119 .context(error::InvalidateTableCacheSnafu)?;
1120
1121 Ok(table)
1122 }
1123
1124 #[tracing::instrument(skip_all)]
1125 pub async fn drop_flow(
1126 &self,
1127 catalog_name: String,
1128 flow_name: String,
1129 drop_if_exists: bool,
1130 query_context: QueryContextRef,
1131 ) -> Result<Output> {
1132 if let Some(flow) = self
1133 .flow_metadata_manager
1134 .flow_name_manager()
1135 .get(&catalog_name, &flow_name)
1136 .await
1137 .context(error::TableMetadataManagerSnafu)?
1138 {
1139 let flow_id = flow.flow_id();
1140 let task = DropFlowTask {
1141 catalog_name,
1142 flow_name,
1143 flow_id,
1144 drop_if_exists,
1145 };
1146 self.drop_flow_procedure(task, query_context).await?;
1147
1148 Ok(Output::new_with_affected_rows(0))
1149 } else if drop_if_exists {
1150 Ok(Output::new_with_affected_rows(0))
1151 } else {
1152 FlowNotFoundSnafu {
1153 flow_name: format_full_flow_name(&catalog_name, &flow_name),
1154 }
1155 .fail()
1156 }
1157 }
1158
1159 async fn drop_flow_procedure(
1160 &self,
1161 expr: DropFlowTask,
1162 query_context: QueryContextRef,
1163 ) -> Result<SubmitDdlTaskResponse> {
1164 let request = SubmitDdlTaskRequest::new(
1165 to_meta_query_context(query_context),
1166 DdlTask::new_drop_flow(expr),
1167 );
1168
1169 self.procedure_executor
1170 .submit_ddl_task(&ExecutorContext::default(), request)
1171 .await
1172 .context(error::ExecuteDdlSnafu)
1173 }
1174
1175 #[cfg(feature = "enterprise")]
1176 #[tracing::instrument(skip_all)]
1177 pub(super) async fn drop_trigger(
1178 &self,
1179 catalog_name: String,
1180 trigger_name: String,
1181 drop_if_exists: bool,
1182 query_context: QueryContextRef,
1183 ) -> Result<Output> {
1184 let task = DropTriggerTask {
1185 catalog_name,
1186 trigger_name,
1187 drop_if_exists,
1188 };
1189 self.drop_trigger_procedure(task, query_context).await?;
1190 Ok(Output::new_with_affected_rows(0))
1191 }
1192
1193 #[cfg(feature = "enterprise")]
1194 async fn drop_trigger_procedure(
1195 &self,
1196 expr: DropTriggerTask,
1197 query_context: QueryContextRef,
1198 ) -> Result<SubmitDdlTaskResponse> {
1199 let request = SubmitDdlTaskRequest::new(
1200 to_meta_query_context(query_context),
1201 DdlTask::new_drop_trigger(expr),
1202 );
1203
1204 self.procedure_executor
1205 .submit_ddl_task(&ExecutorContext::default(), request)
1206 .await
1207 .context(error::ExecuteDdlSnafu)
1208 }
1209
1210 #[tracing::instrument(skip_all)]
1212 pub(crate) async fn drop_view(
1213 &self,
1214 catalog: String,
1215 schema: String,
1216 view: String,
1217 drop_if_exists: bool,
1218 query_context: QueryContextRef,
1219 ) -> Result<Output> {
1220 let view_info = if let Some(view) = self
1221 .catalog_manager
1222 .table(&catalog, &schema, &view, None)
1223 .await
1224 .context(CatalogSnafu)?
1225 {
1226 view.table_info()
1227 } else if drop_if_exists {
1228 return Ok(Output::new_with_affected_rows(0));
1230 } else {
1231 return TableNotFoundSnafu {
1232 table_name: format_full_table_name(&catalog, &schema, &view),
1233 }
1234 .fail();
1235 };
1236
1237 ensure!(
1239 view_info.table_type == TableType::View,
1240 error::InvalidViewSnafu {
1241 msg: "not a view",
1242 view_name: format_full_table_name(&catalog, &schema, &view),
1243 }
1244 );
1245
1246 let view_id = view_info.table_id();
1247
1248 let task = DropViewTask {
1249 catalog,
1250 schema,
1251 view,
1252 view_id,
1253 drop_if_exists,
1254 };
1255
1256 self.drop_view_procedure(task, query_context).await?;
1257
1258 Ok(Output::new_with_affected_rows(0))
1259 }
1260
1261 async fn drop_view_procedure(
1263 &self,
1264 expr: DropViewTask,
1265 query_context: QueryContextRef,
1266 ) -> Result<SubmitDdlTaskResponse> {
1267 let request = SubmitDdlTaskRequest::new(
1268 to_meta_query_context(query_context),
1269 DdlTask::new_drop_view(expr),
1270 );
1271
1272 self.procedure_executor
1273 .submit_ddl_task(&ExecutorContext::default(), request)
1274 .await
1275 .context(error::ExecuteDdlSnafu)
1276 }
1277
1278 #[tracing::instrument(skip_all)]
1279 pub async fn alter_logical_tables(
1280 &self,
1281 alter_table_exprs: Vec<AlterTableExpr>,
1282 query_context: QueryContextRef,
1283 ) -> Result<Output> {
1284 let _timer = crate::metrics::DIST_ALTER_TABLES.start_timer();
1285 ensure!(
1286 !alter_table_exprs.is_empty(),
1287 EmptyDdlExprSnafu {
1288 name: "alter logical tables"
1289 }
1290 );
1291
1292 let mut groups: HashMap<TableId, Vec<AlterTableExpr>> = HashMap::new();
1294 for expr in alter_table_exprs {
1295 let catalog = if expr.catalog_name.is_empty() {
1297 query_context.current_catalog()
1298 } else {
1299 &expr.catalog_name
1300 };
1301 let schema = if expr.schema_name.is_empty() {
1302 query_context.current_schema()
1303 } else {
1304 expr.schema_name.clone()
1305 };
1306 let table_name = &expr.table_name;
1307 let table = self
1308 .catalog_manager
1309 .table(catalog, &schema, table_name, Some(&query_context))
1310 .await
1311 .context(CatalogSnafu)?
1312 .with_context(|| TableNotFoundSnafu {
1313 table_name: format_full_table_name(catalog, &schema, table_name),
1314 })?;
1315 let table_id = table.table_info().ident.table_id;
1316 let physical_table_id = self
1317 .table_metadata_manager
1318 .table_route_manager()
1319 .get_physical_table_id(table_id)
1320 .await
1321 .context(TableMetadataManagerSnafu)?;
1322 groups.entry(physical_table_id).or_default().push(expr);
1323 }
1324
1325 let mut handles = Vec::with_capacity(groups.len());
1327 for (_physical_table_id, exprs) in groups {
1328 let fut = self.alter_logical_tables_procedure(exprs, query_context.clone());
1329 handles.push(fut);
1330 }
1331 let _results = futures::future::try_join_all(handles).await?;
1332
1333 Ok(Output::new_with_affected_rows(0))
1334 }
1335
1336 #[tracing::instrument(skip_all)]
1337 pub async fn drop_table(
1338 &self,
1339 table_name: TableName,
1340 drop_if_exists: bool,
1341 query_context: QueryContextRef,
1342 ) -> Result<Output> {
1343 self.drop_tables(&[table_name], drop_if_exists, query_context)
1345 .await
1346 }
1347
1348 #[tracing::instrument(skip_all)]
1349 pub async fn drop_tables(
1350 &self,
1351 table_names: &[TableName],
1352 drop_if_exists: bool,
1353 query_context: QueryContextRef,
1354 ) -> Result<Output> {
1355 let mut tables = Vec::with_capacity(table_names.len());
1356 for table_name in table_names {
1357 ensure!(
1358 !is_readonly_schema(&table_name.schema_name),
1359 SchemaReadOnlySnafu {
1360 name: table_name.schema_name.clone()
1361 }
1362 );
1363
1364 if let Some(table) = self
1365 .catalog_manager
1366 .table(
1367 &table_name.catalog_name,
1368 &table_name.schema_name,
1369 &table_name.table_name,
1370 Some(&query_context),
1371 )
1372 .await
1373 .context(CatalogSnafu)?
1374 {
1375 tables.push(table.table_info().table_id());
1376 } else if drop_if_exists {
1377 continue;
1379 } else {
1380 return TableNotFoundSnafu {
1381 table_name: table_name.to_string(),
1382 }
1383 .fail();
1384 }
1385 }
1386
1387 for (table_name, table_id) in table_names.iter().zip(tables.into_iter()) {
1388 self.drop_table_procedure(table_name, table_id, drop_if_exists, query_context.clone())
1389 .await?;
1390
1391 self.cache_invalidator
1393 .invalidate(
1394 &Context::default(),
1395 &[
1396 CacheIdent::TableId(table_id),
1397 CacheIdent::TableName(table_name.clone()),
1398 ],
1399 )
1400 .await
1401 .context(error::InvalidateTableCacheSnafu)?;
1402 }
1403 Ok(Output::new_with_affected_rows(0))
1404 }
1405
1406 #[tracing::instrument(skip_all)]
1407 pub async fn undrop_table(
1408 &self,
1409 table_name: TableName,
1410 query_context: QueryContextRef,
1411 ) -> Result<Output> {
1412 execute_undrop_table(
1413 &self.table_metadata_manager,
1414 &self.procedure_executor,
1415 &self.cache_invalidator,
1416 table_name,
1417 query_context,
1418 )
1419 .await
1420 }
1421
1422 #[tracing::instrument(skip_all)]
1423 pub async fn drop_database(
1424 &self,
1425 catalog: String,
1426 schema: String,
1427 drop_if_exists: bool,
1428 query_context: QueryContextRef,
1429 ) -> Result<Output> {
1430 ensure!(
1431 !is_readonly_schema(&schema),
1432 SchemaReadOnlySnafu { name: schema }
1433 );
1434
1435 if self
1436 .catalog_manager
1437 .schema_exists(&catalog, &schema, None)
1438 .await
1439 .context(CatalogSnafu)?
1440 {
1441 if schema == query_context.current_schema() {
1442 SchemaInUseSnafu { name: schema }.fail()
1443 } else {
1444 self.drop_database_procedure(catalog, schema, drop_if_exists, query_context)
1445 .await?;
1446
1447 Ok(Output::new_with_affected_rows(0))
1448 }
1449 } else if drop_if_exists {
1450 Ok(Output::new_with_affected_rows(0))
1452 } else {
1453 SchemaNotFoundSnafu {
1454 schema_info: schema,
1455 }
1456 .fail()
1457 }
1458 }
1459
1460 #[tracing::instrument(skip_all)]
1461 pub async fn truncate_table(
1462 &self,
1463 table_name: TableName,
1464 time_ranges: Vec<(Timestamp, Timestamp)>,
1465 query_context: QueryContextRef,
1466 ) -> Result<Output> {
1467 ensure!(
1468 !is_readonly_schema(&table_name.schema_name),
1469 SchemaReadOnlySnafu {
1470 name: table_name.schema_name.clone()
1471 }
1472 );
1473
1474 let table = self
1475 .catalog_manager
1476 .table(
1477 &table_name.catalog_name,
1478 &table_name.schema_name,
1479 &table_name.table_name,
1480 Some(&query_context),
1481 )
1482 .await
1483 .context(CatalogSnafu)?
1484 .with_context(|| TableNotFoundSnafu {
1485 table_name: table_name.to_string(),
1486 })?;
1487 let table_id = table.table_info().table_id();
1488 self.truncate_table_procedure(&table_name, table_id, time_ranges, query_context)
1489 .await?;
1490
1491 Ok(Output::new_with_affected_rows(0))
1492 }
1493
1494 #[tracing::instrument(skip_all)]
1495 pub async fn alter_table(
1496 &self,
1497 alter_table: AlterTable,
1498 query_context: QueryContextRef,
1499 ) -> Result<Output> {
1500 if matches!(
1501 alter_table.alter_operation(),
1502 AlterTableOperation::Repartition { .. } | AlterTableOperation::Partition { .. }
1503 ) {
1504 let request = expr_helper::to_repartition_request(alter_table, &query_context)?;
1505 return self.repartition_table(request, &query_context).await;
1506 }
1507
1508 let expr = expr_helper::to_alter_table_expr(alter_table, &query_context)?;
1509 self.alter_table_inner(expr, query_context).await
1510 }
1511
1512 #[tracing::instrument(skip_all)]
1513 pub async fn repartition_table(
1514 &self,
1515 request: RepartitionRequest,
1516 query_context: &QueryContextRef,
1517 ) -> Result<Output> {
1518 ensure!(
1520 !is_readonly_schema(&request.schema_name),
1521 SchemaReadOnlySnafu {
1522 name: request.schema_name.clone()
1523 }
1524 );
1525
1526 let table_ref = TableReference::full(
1527 &request.catalog_name,
1528 &request.schema_name,
1529 &request.table_name,
1530 );
1531 let table = self
1533 .catalog_manager
1534 .table(
1535 &request.catalog_name,
1536 &request.schema_name,
1537 &request.table_name,
1538 Some(query_context),
1539 )
1540 .await
1541 .context(CatalogSnafu)?
1542 .with_context(|| TableNotFoundSnafu {
1543 table_name: table_ref.to_string(),
1544 })?;
1545 let table_id = table.table_info().ident.table_id;
1546 let (physical_table_id, physical_table_route) = self
1548 .table_metadata_manager
1549 .table_route_manager()
1550 .get_physical_table_route(table_id)
1551 .await
1552 .context(TableMetadataManagerSnafu)?;
1553
1554 ensure!(
1555 physical_table_id == table_id,
1556 NotSupportedSnafu {
1557 feat: "REPARTITION on logical tables"
1558 }
1559 );
1560
1561 let table_info = table.table_info();
1562 let existing_partition_columns = table_info.meta.partition_columns().collect::<Vec<_>>();
1563 let column_schemas = table_info.meta.schema.column_schemas();
1564 let target_partition_columns = match &request.source {
1570 RepartitionSource::Partitions {
1571 target_partition_columns,
1572 ..
1573 } => {
1574 ensure!(
1575 !existing_partition_columns.is_empty(),
1576 InvalidPartitionRuleSnafu {
1577 reason: format!(
1578 "table {} does not have partition columns, cannot repartition",
1579 table_ref
1580 )
1581 }
1582 );
1583
1584 if let Some(target_partition_columns) = target_partition_columns {
1585 ensure!(
1586 !target_partition_columns.is_empty(),
1587 InvalidPartitionRuleSnafu {
1588 reason: "ON COLUMNS requires at least one partition column"
1589 }
1590 );
1591 validate_and_collect_partition_columns(
1592 target_partition_columns,
1593 column_schemas,
1594 )?
1595 } else {
1596 existing_partition_columns.clone()
1597 }
1598 }
1599 RepartitionSource::Unpartitioned { partition_columns } => {
1600 ensure!(
1601 !partition_columns.is_empty(),
1602 InvalidPartitionRuleSnafu {
1603 reason: "PARTITION ON COLUMNS requires at least one partition column"
1604 }
1605 );
1606 ensure!(
1607 existing_partition_columns.is_empty(),
1608 InvalidPartitionRuleSnafu {
1609 reason: format!("table {} already has partition columns", table_ref)
1610 }
1611 );
1612 partition_columns
1613 .iter()
1614 .map(|column_name| {
1615 column_schemas
1616 .iter()
1617 .find(|column| &column.name == column_name)
1618 .with_context(|| ColumnNotFoundSnafu { msg: column_name })
1619 })
1620 .collect::<Result<Vec<_>>>()?
1621 }
1622 };
1623
1624 let from_column_name_and_type = column_name_and_type(&existing_partition_columns);
1625 let target_column_name_and_type = column_name_and_type(&target_partition_columns);
1626 let target_partition_column_names = target_partition_columns
1627 .iter()
1628 .map(|column| column.name.clone())
1629 .collect::<Vec<_>>();
1630 let timezone = query_context.timezone();
1631 let from_partition_exprs = match &request.source {
1633 RepartitionSource::Partitions { from_exprs, .. } => from_exprs
1634 .iter()
1635 .map(|expr| convert_one_expr(expr, &from_column_name_and_type, &timezone))
1636 .collect::<Result<Vec<_>>>()?,
1637 RepartitionSource::Unpartitioned { .. } => vec![],
1638 };
1639
1640 let mut into_partition_exprs = request
1641 .into_exprs
1642 .iter()
1643 .map(|expr| convert_one_expr(expr, &target_column_name_and_type, &timezone))
1644 .collect::<Result<Vec<_>>>()?;
1645
1646 if matches!(&request.source, RepartitionSource::Partitions { .. })
1649 && from_partition_exprs.len() > 1
1650 && into_partition_exprs.len() == 1
1651 && let Some(expr) = into_partition_exprs.pop()
1652 {
1653 into_partition_exprs.push(partition::simplify::simplify_merged_partition_expr(expr));
1654 }
1655
1656 let mut existing_partition_exprs =
1658 Vec::with_capacity(physical_table_route.region_routes.len());
1659 for route in &physical_table_route.region_routes {
1660 let expr_json = route.region.partition_expr();
1661 if !expr_json.is_empty() {
1662 match PartitionExpr::from_json_str(&expr_json) {
1663 Ok(Some(expr)) => existing_partition_exprs.push(expr),
1664 Ok(None) => {
1665 }
1667 Err(e) => {
1668 return Err(e).context(DeserializePartitionExprSnafu);
1669 }
1670 }
1671 }
1672 }
1673
1674 if matches!(&request.source, RepartitionSource::Partitions { .. }) {
1677 for from_expr in &from_partition_exprs {
1678 ensure!(
1679 existing_partition_exprs.contains(from_expr),
1680 InvalidPartitionRuleSnafu {
1681 reason: format!(
1682 "partition expression '{}' does not exist in table {}",
1683 from_expr, table_ref
1684 )
1685 }
1686 );
1687 }
1688 }
1689
1690 let new_partition_exprs: Vec<PartitionExpr> = match &request.source {
1693 RepartitionSource::Partitions { .. } => existing_partition_exprs
1694 .into_iter()
1695 .filter(|expr| !from_partition_exprs.contains(expr))
1696 .chain(into_partition_exprs.clone().into_iter())
1697 .collect(),
1698 RepartitionSource::Unpartitioned { .. } => into_partition_exprs.clone(),
1699 };
1700 ensure_partition_expr_columns_in_target(
1701 &new_partition_exprs,
1702 &target_partition_column_names.iter().collect(),
1703 )?;
1704 let new_partition_exprs_len = new_partition_exprs.len();
1705 let from_partition_exprs_len = from_partition_exprs.len();
1706
1707 let _ = MultiDimPartitionRule::try_new(
1709 target_partition_column_names,
1710 vec![],
1711 new_partition_exprs,
1712 true,
1713 )
1714 .context(InvalidPartitionSnafu)?;
1715
1716 let ddl_options = parse_ddl_options(&request.options)?;
1717 let serialize_exprs = |exprs: Vec<PartitionExpr>| -> Result<Vec<String>> {
1718 let mut json_exprs = Vec::with_capacity(exprs.len());
1719 for expr in exprs {
1720 json_exprs.push(expr.as_json_str().context(SerializePartitionExprSnafu)?);
1721 }
1722 Ok(json_exprs)
1723 };
1724 let from_partition_exprs_json = serialize_exprs(from_partition_exprs)?;
1725 let into_partition_exprs_json = serialize_exprs(into_partition_exprs)?;
1726 let source = match &request.source {
1727 RepartitionSource::Partitions {
1728 target_partition_columns,
1729 ..
1730 } => Source::PartitionExprs(PartitionedSource {
1731 exprs: from_partition_exprs_json,
1732 target_partition_columns: target_partition_columns
1733 .clone()
1734 .map(|columns| TargetPartitionColumns { columns }),
1735 }),
1736 RepartitionSource::Unpartitioned { partition_columns } => {
1737 Source::Unpartitioned(UnpartitionedSource {
1738 partition_columns: partition_columns.clone(),
1739 })
1740 }
1741 };
1742 let repartition = Repartition {
1743 into_partition_exprs: into_partition_exprs_json,
1744 source: Some(source),
1745 ..Default::default()
1746 };
1747 let mut req = SubmitDdlTaskRequest::new(
1748 to_meta_query_context(query_context.clone()),
1749 DdlTask::new_alter_table(AlterTableExpr {
1750 catalog_name: request.catalog_name.clone(),
1751 schema_name: request.schema_name.clone(),
1752 table_name: request.table_name.clone(),
1753 kind: Some(Kind::Repartition(repartition)),
1754 }),
1755 );
1756 req.wait = ddl_options.wait;
1757 req.timeout = ddl_options.timeout;
1758
1759 info!(
1760 "Submitting repartition task for table {} (table_id={}), from {} to {} partitions, timeout: {:?}, wait: {}",
1761 table_ref,
1762 table_id,
1763 from_partition_exprs_len,
1764 new_partition_exprs_len,
1765 ddl_options.timeout,
1766 ddl_options.wait
1767 );
1768
1769 let response = self
1770 .procedure_executor
1771 .submit_ddl_task(&ExecutorContext::default(), req)
1772 .await
1773 .context(error::ExecuteDdlSnafu)?;
1774
1775 if !ddl_options.wait {
1776 return build_procedure_id_output(response.key);
1777 }
1778
1779 let invalidate_keys = vec![
1781 CacheIdent::TableId(table_id),
1782 CacheIdent::TableName(TableName::new(
1783 request.catalog_name,
1784 request.schema_name,
1785 request.table_name,
1786 )),
1787 ];
1788
1789 self.cache_invalidator
1791 .invalidate(&Context::default(), &invalidate_keys)
1792 .await
1793 .context(error::InvalidateTableCacheSnafu)?;
1794
1795 Ok(Output::new_with_affected_rows(0))
1796 }
1797
1798 #[tracing::instrument(skip_all)]
1799 pub async fn alter_table_inner(
1800 &self,
1801 expr: AlterTableExpr,
1802 query_context: QueryContextRef,
1803 ) -> Result<Output> {
1804 ensure!(
1805 !is_readonly_schema(&expr.schema_name),
1806 SchemaReadOnlySnafu {
1807 name: expr.schema_name.clone()
1808 }
1809 );
1810
1811 let catalog_name = if expr.catalog_name.is_empty() {
1812 DEFAULT_CATALOG_NAME.to_string()
1813 } else {
1814 expr.catalog_name.clone()
1815 };
1816
1817 let schema_name = if expr.schema_name.is_empty() {
1818 DEFAULT_SCHEMA_NAME.to_string()
1819 } else {
1820 expr.schema_name.clone()
1821 };
1822
1823 let table_name = expr.table_name.clone();
1824
1825 let table = self
1826 .catalog_manager
1827 .table(
1828 &catalog_name,
1829 &schema_name,
1830 &table_name,
1831 Some(&query_context),
1832 )
1833 .await
1834 .context(CatalogSnafu)?
1835 .with_context(|| TableNotFoundSnafu {
1836 table_name: format_full_table_name(&catalog_name, &schema_name, &table_name),
1837 })?;
1838
1839 let table_id = table.table_info().ident.table_id;
1840 let need_alter = verify_alter(table_id, table.table_info(), expr.clone())?;
1841 if !need_alter {
1842 return Ok(Output::new_with_affected_rows(0));
1843 }
1844 info!(
1845 "Table info before alter is {:?}, expr: {:?}",
1846 table.table_info(),
1847 expr
1848 );
1849
1850 let physical_table_id = self
1851 .table_metadata_manager
1852 .table_route_manager()
1853 .get_physical_table_id(table_id)
1854 .await
1855 .context(TableMetadataManagerSnafu)?;
1856
1857 let (req, invalidate_keys) = if physical_table_id == table_id {
1858 let req = SubmitDdlTaskRequest::new(
1860 to_meta_query_context(query_context),
1861 DdlTask::new_alter_table(expr),
1862 );
1863
1864 let invalidate_keys = vec![
1865 CacheIdent::TableId(table_id),
1866 CacheIdent::TableName(TableName::new(catalog_name, schema_name, table_name)),
1867 ];
1868
1869 (req, invalidate_keys)
1870 } else {
1871 let req = SubmitDdlTaskRequest::new(
1873 to_meta_query_context(query_context),
1874 DdlTask::new_alter_logical_tables(vec![expr]),
1875 );
1876
1877 let mut invalidate_keys = vec![
1878 CacheIdent::TableId(physical_table_id),
1879 CacheIdent::TableId(table_id),
1880 CacheIdent::TableName(TableName::new(catalog_name, schema_name, table_name)),
1881 ];
1882
1883 let physical_table = self
1884 .table_metadata_manager
1885 .table_info_manager()
1886 .get(physical_table_id)
1887 .await
1888 .context(TableMetadataManagerSnafu)?
1889 .map(|x| x.into_inner());
1890 if let Some(physical_table) = physical_table {
1891 let physical_table_name = TableName::new(
1892 physical_table.table_info.catalog_name,
1893 physical_table.table_info.schema_name,
1894 physical_table.table_info.name,
1895 );
1896 invalidate_keys.push(CacheIdent::TableName(physical_table_name));
1897 }
1898
1899 (req, invalidate_keys)
1900 };
1901
1902 self.procedure_executor
1903 .submit_ddl_task(&ExecutorContext::default(), req)
1904 .await
1905 .context(error::ExecuteDdlSnafu)?;
1906
1907 self.cache_invalidator
1909 .invalidate(&Context::default(), &invalidate_keys)
1910 .await
1911 .context(error::InvalidateTableCacheSnafu)?;
1912
1913 Ok(Output::new_with_affected_rows(0))
1914 }
1915
1916 #[cfg(feature = "enterprise")]
1917 #[tracing::instrument(skip_all)]
1918 pub async fn alter_trigger(
1919 &self,
1920 _alter_expr: AlterTrigger,
1921 _query_context: QueryContextRef,
1922 ) -> Result<Output> {
1923 crate::error::NotSupportedSnafu {
1924 feat: "alter trigger",
1925 }
1926 .fail()
1927 }
1928
1929 #[tracing::instrument(skip_all)]
1930 pub async fn alter_database(
1931 &self,
1932 alter_expr: AlterDatabase,
1933 query_context: QueryContextRef,
1934 ) -> Result<Output> {
1935 let alter_expr = expr_helper::to_alter_database_expr(alter_expr, &query_context)?;
1936 self.alter_database_inner(alter_expr, query_context).await
1937 }
1938
1939 #[tracing::instrument(skip_all)]
1940 pub async fn alter_database_inner(
1941 &self,
1942 alter_expr: AlterDatabaseExpr,
1943 query_context: QueryContextRef,
1944 ) -> Result<Output> {
1945 ensure!(
1946 !is_readonly_schema(&alter_expr.schema_name),
1947 SchemaReadOnlySnafu {
1948 name: query_context.current_schema().clone()
1949 }
1950 );
1951
1952 let exists = self
1953 .catalog_manager
1954 .schema_exists(&alter_expr.catalog_name, &alter_expr.schema_name, None)
1955 .await
1956 .context(CatalogSnafu)?;
1957 ensure!(
1958 exists,
1959 SchemaNotFoundSnafu {
1960 schema_info: alter_expr.schema_name,
1961 }
1962 );
1963
1964 let cache_ident = [CacheIdent::SchemaName(SchemaName {
1965 catalog_name: alter_expr.catalog_name.clone(),
1966 schema_name: alter_expr.schema_name.clone(),
1967 })];
1968
1969 self.alter_database_procedure(alter_expr, query_context)
1970 .await?;
1971
1972 self.cache_invalidator
1974 .invalidate(&Context::default(), &cache_ident)
1975 .await
1976 .context(error::InvalidateTableCacheSnafu)?;
1977
1978 Ok(Output::new_with_affected_rows(0))
1979 }
1980
1981 async fn create_table_procedure(
1982 &self,
1983 create_table: CreateTableExpr,
1984 partitions: Vec<PartitionExpr>,
1985 table_info: TableInfo,
1986 query_context: QueryContextRef,
1987 ) -> Result<SubmitDdlTaskResponse> {
1988 let partitions = partitions
1989 .into_iter()
1990 .map(|expr| expr.as_pb_partition().context(PartitionExprToPbSnafu))
1991 .collect::<Result<Vec<_>>>()?;
1992
1993 let request = SubmitDdlTaskRequest::new(
1994 to_meta_query_context_with_origin_frontend(query_context, &self.origin_frontend_addr),
1995 DdlTask::new_create_table(create_table, partitions, table_info),
1996 );
1997
1998 self.procedure_executor
1999 .submit_ddl_task(&ExecutorContext::default(), request)
2000 .await
2001 .context(error::ExecuteDdlSnafu)
2002 }
2003
2004 async fn create_logical_tables_procedure(
2005 &self,
2006 tables_data: Vec<(CreateTableExpr, TableInfo)>,
2007 query_context: QueryContextRef,
2008 ) -> Result<SubmitDdlTaskResponse> {
2009 let request = SubmitDdlTaskRequest::new(
2010 to_meta_query_context_with_origin_frontend(query_context, &self.origin_frontend_addr),
2011 DdlTask::new_create_logical_tables(tables_data),
2012 );
2013
2014 self.procedure_executor
2015 .submit_ddl_task(&ExecutorContext::default(), request)
2016 .await
2017 .context(error::ExecuteDdlSnafu)
2018 }
2019
2020 async fn alter_logical_tables_procedure(
2021 &self,
2022 tables_data: Vec<AlterTableExpr>,
2023 query_context: QueryContextRef,
2024 ) -> Result<SubmitDdlTaskResponse> {
2025 let request = SubmitDdlTaskRequest::new(
2026 to_meta_query_context(query_context),
2027 DdlTask::new_alter_logical_tables(tables_data),
2028 );
2029
2030 self.procedure_executor
2031 .submit_ddl_task(&ExecutorContext::default(), request)
2032 .await
2033 .context(error::ExecuteDdlSnafu)
2034 }
2035
2036 async fn drop_table_procedure(
2037 &self,
2038 table_name: &TableName,
2039 table_id: TableId,
2040 drop_if_exists: bool,
2041 query_context: QueryContextRef,
2042 ) -> Result<SubmitDdlTaskResponse> {
2043 let request = SubmitDdlTaskRequest::new(
2044 to_meta_query_context(query_context),
2045 DdlTask::new_drop_table(
2046 table_name.catalog_name.clone(),
2047 table_name.schema_name.clone(),
2048 table_name.table_name.clone(),
2049 table_id,
2050 drop_if_exists,
2051 ),
2052 );
2053
2054 self.procedure_executor
2055 .submit_ddl_task(&ExecutorContext::default(), request)
2056 .await
2057 .context(error::ExecuteDdlSnafu)
2058 }
2059
2060 async fn drop_database_procedure(
2061 &self,
2062 catalog: String,
2063 schema: String,
2064 drop_if_exists: bool,
2065 query_context: QueryContextRef,
2066 ) -> Result<SubmitDdlTaskResponse> {
2067 let request = SubmitDdlTaskRequest::new(
2068 to_meta_query_context(query_context),
2069 DdlTask::new_drop_database(catalog, schema, drop_if_exists),
2070 );
2071
2072 self.procedure_executor
2073 .submit_ddl_task(&ExecutorContext::default(), request)
2074 .await
2075 .context(error::ExecuteDdlSnafu)
2076 }
2077
2078 async fn alter_database_procedure(
2079 &self,
2080 alter_expr: AlterDatabaseExpr,
2081 query_context: QueryContextRef,
2082 ) -> Result<SubmitDdlTaskResponse> {
2083 let request = SubmitDdlTaskRequest::new(
2084 to_meta_query_context(query_context),
2085 DdlTask::new_alter_database(alter_expr),
2086 );
2087
2088 self.procedure_executor
2089 .submit_ddl_task(&ExecutorContext::default(), request)
2090 .await
2091 .context(error::ExecuteDdlSnafu)
2092 }
2093
2094 async fn truncate_table_procedure(
2095 &self,
2096 table_name: &TableName,
2097 table_id: TableId,
2098 time_ranges: Vec<(Timestamp, Timestamp)>,
2099 query_context: QueryContextRef,
2100 ) -> Result<SubmitDdlTaskResponse> {
2101 let request = SubmitDdlTaskRequest::new(
2102 to_meta_query_context(query_context),
2103 DdlTask::new_truncate_table(
2104 table_name.catalog_name.clone(),
2105 table_name.schema_name.clone(),
2106 table_name.table_name.clone(),
2107 table_id,
2108 time_ranges,
2109 ),
2110 );
2111
2112 self.procedure_executor
2113 .submit_ddl_task(&ExecutorContext::default(), request)
2114 .await
2115 .context(error::ExecuteDdlSnafu)
2116 }
2117
2118 #[tracing::instrument(skip_all)]
2119 pub async fn create_database(
2120 &self,
2121 database: &str,
2122 create_if_not_exists: bool,
2123 options: HashMap<String, String>,
2124 query_context: QueryContextRef,
2125 ) -> Result<Output> {
2126 let catalog = query_context.current_catalog();
2127 ensure!(
2128 NAME_PATTERN_REG.is_match(catalog),
2129 error::UnexpectedSnafu {
2130 violated: format!("Invalid catalog name: {}", catalog)
2131 }
2132 );
2133
2134 ensure!(
2135 NAME_PATTERN_REG.is_match(database),
2136 error::UnexpectedSnafu {
2137 violated: format!("Invalid database name: {}", database)
2138 }
2139 );
2140
2141 #[cfg(feature = "enterprise")]
2142 let creator = match &self.create_database_handler {
2143 Some(handler) => handler.creator(&query_context).context(ExternalSnafu)?,
2144 None => None,
2145 };
2146 #[cfg(not(feature = "enterprise"))]
2147 let creator = None;
2148
2149 let output = if !self
2150 .catalog_manager
2151 .schema_exists(catalog, database, None)
2152 .await
2153 .context(CatalogSnafu)?
2154 && !self.catalog_manager.is_reserved_schema_name(database)
2155 {
2156 self.create_database_procedure(
2157 catalog.to_string(),
2158 database.to_string(),
2159 create_if_not_exists,
2160 options,
2161 query_context.clone(),
2162 creator,
2163 )
2164 .await?;
2165
2166 Output::new_with_affected_rows(1)
2167 } else if create_if_not_exists {
2168 Output::new_with_affected_rows(1)
2169 } else {
2170 return error::SchemaExistsSnafu { name: database }.fail();
2171 };
2172
2173 #[cfg(feature = "enterprise")]
2174 if let Some(handler) = &self.create_database_handler {
2175 handler
2176 .refresh_current_user(&query_context)
2177 .await
2178 .context(ExternalSnafu)?;
2179 }
2180
2181 Ok(output)
2182 }
2183
2184 async fn create_database_procedure(
2185 &self,
2186 catalog: String,
2187 database: String,
2188 create_if_not_exists: bool,
2189 options: HashMap<String, String>,
2190 query_context: QueryContextRef,
2191 creator: Option<CreatorGrantIntent>,
2192 ) -> Result<SubmitDdlTaskResponse> {
2193 let request = SubmitDdlTaskRequest::new(
2194 to_meta_query_context(query_context),
2195 DdlTask::new_create_database(catalog, database, create_if_not_exists, options, creator),
2196 );
2197
2198 self.procedure_executor
2199 .submit_ddl_task(&ExecutorContext::default(), request)
2200 .await
2201 .context(error::ExecuteDdlSnafu)
2202 }
2203}
2204
2205pub fn parse_partitions(
2207 create_table: &CreateTableExpr,
2208 partitions: Option<Partitions>,
2209 query_ctx: &QueryContextRef,
2210) -> Result<(Vec<PartitionExpr>, Vec<String>)> {
2211 let partition_columns = find_partition_columns(&partitions)?;
2214 let partition_exprs =
2215 find_partition_entries(create_table, &partitions, &partition_columns, query_ctx)?;
2216
2217 let exprs = partition_exprs.clone();
2219 MultiDimPartitionRule::try_new(partition_columns.clone(), vec![], exprs, true)
2220 .context(InvalidPartitionSnafu)?;
2221
2222 Ok((partition_exprs, partition_columns))
2223}
2224
2225fn parse_partitions_for_logical_validation(
2226 create_table: &CreateTableExpr,
2227 partitions: &Partitions,
2228 query_ctx: &QueryContextRef,
2229) -> Result<(Vec<String>, Vec<PartitionExpr>)> {
2230 let partition_columns = partitions
2231 .column_list
2232 .iter()
2233 .map(|ident| ident.value.clone())
2234 .collect::<Vec<_>>();
2235
2236 let column_name_and_type = partition_columns
2237 .iter()
2238 .map(|pc| {
2239 let column = create_table
2240 .column_defs
2241 .iter()
2242 .find(|c| &c.name == pc)
2243 .context(ColumnNotFoundSnafu { msg: pc.clone() })?;
2244 let column_name = &column.name;
2245 let data_type = ConcreteDataType::from(
2246 ColumnDataTypeWrapper::try_new(column.data_type, column.datatype_extension.clone())
2247 .context(ColumnDataTypeSnafu)?,
2248 );
2249 Ok((column_name, data_type))
2250 })
2251 .collect::<Result<HashMap<_, _>>>()?;
2252
2253 let mut partition_exprs = Vec::with_capacity(partitions.exprs.len());
2254 for expr in &partitions.exprs {
2255 let partition_expr = convert_one_expr(expr, &column_name_and_type, &query_ctx.timezone())?;
2256 partition_exprs.push(partition_expr);
2257 }
2258
2259 MultiDimPartitionRule::try_new(
2260 partition_columns.clone(),
2261 vec![],
2262 partition_exprs.clone(),
2263 true,
2264 )
2265 .context(InvalidPartitionSnafu)?;
2266
2267 Ok((partition_columns, partition_exprs))
2268}
2269
2270pub fn verify_alter(
2276 table_id: TableId,
2277 table_info: Arc<TableInfo>,
2278 expr: AlterTableExpr,
2279) -> Result<bool> {
2280 let request: AlterTableRequest =
2281 common_grpc_expr::alter_expr_to_request(table_id, expr, Some(&table_info.meta))
2282 .context(AlterExprToRequestSnafu)?;
2283
2284 let AlterTableRequest {
2285 table_name,
2286 alter_kind,
2287 ..
2288 } = &request;
2289
2290 if let AlterKind::RenameTable { new_table_name } = alter_kind {
2291 ensure!(
2292 NAME_PATTERN_REG.is_match(new_table_name),
2293 error::UnexpectedSnafu {
2294 violated: format!("Invalid table name: {}", new_table_name)
2295 }
2296 );
2297 } else if let AlterKind::AddColumns { columns } = alter_kind {
2298 let column_names: HashSet<_> = table_info
2301 .meta
2302 .schema
2303 .column_schemas()
2304 .iter()
2305 .map(|schema| &schema.name)
2306 .collect();
2307 if columns.iter().all(|column| {
2308 column_names.contains(&column.column_schema.name) && column.add_if_not_exists
2309 }) {
2310 return Ok(false);
2311 }
2312 }
2313
2314 let new_meta = table_info
2315 .meta
2316 .builder_with_alter_kind(table_name, &request.alter_kind)
2317 .context(error::TableSnafu)?
2318 .build()
2319 .context(error::BuildTableMetaSnafu { table_name })?;
2320
2321 validate_json2_columns_append_mode(&new_meta.schema, &new_meta.options)?;
2322
2323 Ok(true)
2324}
2325
2326pub fn create_table_info(
2327 create_table: &CreateTableExpr,
2328 partition_columns: Vec<String>,
2329) -> Result<TableInfo> {
2330 let mut column_schemas = Vec::with_capacity(create_table.column_defs.len());
2331 let mut column_name_to_index_map = HashMap::new();
2332
2333 for (idx, column) in create_table.column_defs.iter().enumerate() {
2334 let schema =
2335 column_def::try_as_column_schema(column).context(error::InvalidColumnDefSnafu {
2336 column: &column.name,
2337 })?;
2338 let schema = schema.with_time_index(column.name == create_table.time_index);
2339
2340 column_schemas.push(schema);
2341 let _ = column_name_to_index_map.insert(column.name.clone(), idx);
2342 }
2343
2344 let next_column_id = column_schemas.len() as u32;
2345 let schema = Arc::new(Schema::new(column_schemas));
2346
2347 let primary_key_indices = create_table
2348 .primary_keys
2349 .iter()
2350 .map(|name| {
2351 column_name_to_index_map
2352 .get(name)
2353 .cloned()
2354 .context(ColumnNotFoundSnafu { msg: name })
2355 })
2356 .collect::<Result<Vec<_>>>()?;
2357
2358 let partition_key_indices = partition_columns
2359 .into_iter()
2360 .map(|col_name| {
2361 column_name_to_index_map
2362 .get(&col_name)
2363 .cloned()
2364 .context(ColumnNotFoundSnafu { msg: col_name })
2365 })
2366 .collect::<Result<Vec<_>>>()?;
2367
2368 let mut table_options = TableOptions::try_from_iter(&create_table.table_options)
2369 .context(UnrecognizedTableOptionSnafu)?;
2370
2371 validate_json2_columns_append_mode(&schema, &table_options)?;
2372
2373 validate_repartition_column_hint(
2374 &mut table_options,
2375 &column_name_to_index_map,
2376 &partition_key_indices,
2377 &create_table.time_index,
2378 )?;
2379
2380 let meta = TableMeta {
2381 schema,
2382 primary_key_indices,
2383 value_indices: vec![],
2384 engine: create_table.engine.clone(),
2385 next_column_id,
2386 options: table_options,
2387 created_on: Utc::now(),
2388 updated_on: Utc::now(),
2389 partition_key_indices,
2390 column_ids: vec![],
2391 };
2392
2393 let desc = if create_table.desc.is_empty() {
2394 create_table.table_options.get(COMMENT_KEY).cloned()
2395 } else {
2396 Some(create_table.desc.clone())
2397 };
2398
2399 let table_info = TableInfo {
2400 ident: metadata::TableIdent {
2401 table_id: 0,
2403 version: 0,
2404 },
2405 name: create_table.table_name.clone(),
2406 desc,
2407 catalog_name: create_table.catalog_name.clone(),
2408 schema_name: create_table.schema_name.clone(),
2409 meta,
2410 table_type: TableType::Base,
2411 };
2412 Ok(table_info)
2413}
2414
2415fn validate_json2_columns_append_mode(schema: &Schema, table_options: &TableOptions) -> Result<()> {
2416 let append_mode = table_options
2417 .extra_options
2418 .get(APPEND_MODE_KEY)
2419 .is_some_and(|value| value == "true");
2420
2421 for column in schema.column_schemas() {
2422 if column
2423 .data_type
2424 .as_json()
2425 .is_some_and(|json_type| json_type.is_json2())
2426 {
2427 ensure!(
2428 append_mode,
2429 InvalidSqlSnafu {
2430 err_msg: format!(
2431 "JSON2 column `{}` requires {}='true'",
2432 column.name, APPEND_MODE_KEY
2433 ),
2434 }
2435 );
2436 }
2437 }
2438
2439 Ok(())
2440}
2441
2442fn validate_repartition_column_hint(
2443 table_options: &mut TableOptions,
2444 column_name_to_index_map: &HashMap<String, usize>,
2445 partition_key_indices: &[usize],
2446 time_index: &str,
2447) -> Result<()> {
2448 let Some(column_name) = table_options
2449 .extra_options
2450 .get(REPARTITION_COLUMN_HINT_KEY)
2451 .map(|value| value.trim().to_string())
2452 else {
2453 return Ok(());
2454 };
2455
2456 ensure!(
2457 !column_name.is_empty(),
2458 InvalidPartitionRuleSnafu {
2459 reason: format!("{REPARTITION_COLUMN_HINT_KEY} expects exactly one column name"),
2460 }
2461 );
2462
2463 ensure!(
2464 !column_name.contains(','),
2465 InvalidPartitionRuleSnafu {
2466 reason: format!("{REPARTITION_COLUMN_HINT_KEY} expects exactly one column name"),
2467 }
2468 );
2469
2470 ensure!(
2471 partition_key_indices.is_empty(),
2472 InvalidPartitionRuleSnafu {
2473 reason: format!(
2474 "cannot set {REPARTITION_COLUMN_HINT_KEY} on a table with partition metadata"
2475 ),
2476 }
2477 );
2478
2479 column_name_to_index_map
2480 .get(&column_name)
2481 .context(ColumnNotFoundSnafu { msg: &column_name })?;
2482
2483 ensure!(
2484 column_name != time_index,
2485 InvalidPartitionRuleSnafu {
2486 reason: format!("cannot set {REPARTITION_COLUMN_HINT_KEY} to the time index column"),
2487 }
2488 );
2489
2490 table_options
2491 .extra_options
2492 .insert(REPARTITION_COLUMN_HINT_KEY.to_string(), column_name);
2493
2494 Ok(())
2495}
2496
2497fn find_partition_columns(partitions: &Option<Partitions>) -> Result<Vec<String>> {
2498 let columns = if let Some(partitions) = partitions {
2499 partitions
2500 .column_list
2501 .iter()
2502 .map(|x| x.value.clone())
2503 .collect::<Vec<_>>()
2504 } else {
2505 vec![]
2506 };
2507 Ok(columns)
2508}
2509
2510fn find_partition_entries(
2514 create_table: &CreateTableExpr,
2515 partitions: &Option<Partitions>,
2516 partition_columns: &[String],
2517 query_ctx: &QueryContextRef,
2518) -> Result<Vec<PartitionExpr>> {
2519 let Some(partitions) = partitions else {
2520 return Ok(vec![]);
2521 };
2522
2523 let column_name_and_type = partition_columns
2525 .iter()
2526 .map(|pc| {
2527 let column = create_table
2528 .column_defs
2529 .iter()
2530 .find(|c| &c.name == pc)
2531 .unwrap();
2533 let column_name = &column.name;
2534 let data_type = ConcreteDataType::from(
2535 ColumnDataTypeWrapper::try_new(column.data_type, column.datatype_extension.clone())
2536 .context(ColumnDataTypeSnafu)?,
2537 );
2538 Ok((column_name, data_type))
2539 })
2540 .collect::<Result<HashMap<_, _>>>()?;
2541
2542 let mut partition_exprs = Vec::with_capacity(partitions.exprs.len());
2544 for partition in &partitions.exprs {
2545 let partition_expr =
2546 convert_one_expr(partition, &column_name_and_type, &query_ctx.timezone())?;
2547 partition_exprs.push(partition_expr);
2548 }
2549
2550 Ok(partition_exprs)
2551}
2552
2553fn column_name_and_type<'a>(
2554 partition_columns: &'a [&'a ColumnSchema],
2555) -> HashMap<&'a String, ConcreteDataType> {
2556 partition_columns
2557 .iter()
2558 .map(|column| (&column.name, column.data_type.clone()))
2559 .collect()
2560}
2561
2562fn validate_and_collect_partition_columns<'a>(
2563 column_names: &[String],
2564 column_schemas: &'a [ColumnSchema],
2565) -> Result<Vec<&'a ColumnSchema>> {
2566 let mut seen = HashSet::with_capacity(column_names.len());
2567 column_names
2568 .iter()
2569 .map(|column_name| {
2570 ensure!(
2571 seen.insert(column_name),
2572 InvalidPartitionRuleSnafu {
2573 reason: format!("duplicate partition column '{}'", column_name)
2574 }
2575 );
2576 column_schemas
2577 .iter()
2578 .find(|column| &column.name == column_name)
2579 .with_context(|| ColumnNotFoundSnafu { msg: column_name })
2580 })
2581 .collect()
2582}
2583
2584fn ensure_partition_expr_columns_in_target(
2585 partition_exprs: &[PartitionExpr],
2586 target_partition_columns: &HashSet<&String>,
2587) -> Result<()> {
2588 for expr in partition_exprs {
2589 ensure_partition_operand_columns_in_target(&expr.lhs, target_partition_columns)?;
2590 ensure_partition_operand_columns_in_target(&expr.rhs, target_partition_columns)?;
2591 }
2592
2593 Ok(())
2594}
2595
2596fn ensure_partition_operand_columns_in_target(
2597 operand: &Operand,
2598 target_partition_columns: &HashSet<&String>,
2599) -> Result<()> {
2600 match operand {
2601 Operand::Column(column) => ensure!(
2602 target_partition_columns.contains(column),
2603 InvalidPartitionRuleSnafu {
2604 reason: format!(
2605 "partition expression references column '{}' that is not in target partition columns",
2606 column
2607 )
2608 }
2609 ),
2610 Operand::Expr(expr) => {
2611 ensure_partition_operand_columns_in_target(&expr.lhs, target_partition_columns)?;
2612 ensure_partition_operand_columns_in_target(&expr.rhs, target_partition_columns)?;
2613 }
2614 Operand::Value(_) => {}
2615 }
2616
2617 Ok(())
2618}
2619
2620fn convert_one_expr(
2621 expr: &Expr,
2622 column_name_and_type: &HashMap<&String, ConcreteDataType>,
2623 timezone: &Timezone,
2624) -> Result<PartitionExpr> {
2625 let Expr::BinaryOp { left, op, right } = expr else {
2626 return InvalidPartitionRuleSnafu {
2627 reason: "partition rule must be a binary expression",
2628 }
2629 .fail();
2630 };
2631
2632 let op =
2633 RestrictedOp::try_from_parser(&op.clone()).with_context(|| InvalidPartitionRuleSnafu {
2634 reason: format!("unsupported operator in partition expr {op}"),
2635 })?;
2636
2637 let (lhs, op, rhs) = match (left.as_ref(), right.as_ref()) {
2639 (Expr::Identifier(ident), Expr::Value(value)) => {
2641 let (column_name, data_type) = convert_identifier(ident, column_name_and_type)?;
2642 let value = convert_value(&value.value, data_type, timezone, None)?;
2643 (Operand::Column(column_name), op, Operand::Value(value))
2644 }
2645 (Expr::Identifier(ident), Expr::UnaryOp { op: unary_op, expr })
2646 if let Expr::Value(v) = &**expr =>
2647 {
2648 let (column_name, data_type) = convert_identifier(ident, column_name_and_type)?;
2649 let value = convert_value(&v.value, data_type, timezone, Some(*unary_op))?;
2650 (Operand::Column(column_name), op, Operand::Value(value))
2651 }
2652 (Expr::Value(value), Expr::Identifier(ident)) => {
2654 let (column_name, data_type) = convert_identifier(ident, column_name_and_type)?;
2655 let value = convert_value(&value.value, data_type, timezone, None)?;
2656 (Operand::Value(value), op, Operand::Column(column_name))
2657 }
2658 (Expr::UnaryOp { op: unary_op, expr }, Expr::Identifier(ident))
2659 if let Expr::Value(v) = &**expr =>
2660 {
2661 let (column_name, data_type) = convert_identifier(ident, column_name_and_type)?;
2662 let value = convert_value(&v.value, data_type, timezone, Some(*unary_op))?;
2663 (Operand::Value(value), op, Operand::Column(column_name))
2664 }
2665 (Expr::BinaryOp { .. }, Expr::BinaryOp { .. }) => {
2666 let lhs = convert_one_expr(left, column_name_and_type, timezone)?;
2668 let rhs = convert_one_expr(right, column_name_and_type, timezone)?;
2669 (Operand::Expr(lhs), op, Operand::Expr(rhs))
2670 }
2671 _ => {
2672 return InvalidPartitionRuleSnafu {
2673 reason: format!("invalid partition expr {expr}"),
2674 }
2675 .fail();
2676 }
2677 };
2678
2679 Ok(PartitionExpr::new(lhs, op, rhs))
2680}
2681
2682fn convert_identifier(
2683 ident: &Ident,
2684 column_name_and_type: &HashMap<&String, ConcreteDataType>,
2685) -> Result<(String, ConcreteDataType)> {
2686 let column_name = ident.value.clone();
2687 let data_type = column_name_and_type
2688 .get(&column_name)
2689 .cloned()
2690 .with_context(|| ColumnNotFoundSnafu { msg: &column_name })?;
2691 Ok((column_name, data_type))
2692}
2693
2694fn convert_value(
2695 value: &ParserValue,
2696 data_type: ConcreteDataType,
2697 timezone: &Timezone,
2698 unary_op: Option<UnaryOperator>,
2699) -> Result<Value> {
2700 sql_value_to_value(
2701 &ColumnSchema::new("<NONAME>", data_type, true),
2702 value,
2703 Some(timezone),
2704 unary_op,
2705 false,
2706 )
2707 .context(error::SqlCommonSnafu)
2708}
2709
2710async fn execute_undrop_table(
2711 table_metadata_manager: &TableMetadataManagerRef,
2712 procedure_executor: &ProcedureExecutorRef,
2713 cache_invalidator: &CacheInvalidatorRef,
2714 table_name: TableName,
2715 query_context: QueryContextRef,
2716) -> Result<Output> {
2717 ensure!(
2718 !is_readonly_schema(&table_name.schema_name),
2719 SchemaReadOnlySnafu {
2720 name: table_name.schema_name.clone()
2721 }
2722 );
2723
2724 let dropped = table_metadata_manager
2725 .get_dropped_table(&table_name)
2726 .await
2727 .context(TableMetadataManagerSnafu)?
2728 .with_context(|| TableNotFoundSnafu {
2729 table_name: table_name.to_string(),
2730 })?;
2731
2732 let request = SubmitDdlTaskRequest::new(
2733 to_meta_query_context(query_context),
2734 DdlTask::new_undrop_table(dropped.table_id),
2735 );
2736 procedure_executor
2737 .submit_ddl_task(&ExecutorContext::default(), request)
2738 .await
2739 .context(error::ExecuteDdlSnafu)?;
2740
2741 if let Err(err) = cache_invalidator
2742 .invalidate(
2743 &Context::default(),
2744 &[
2745 CacheIdent::TableId(dropped.table_id),
2746 CacheIdent::TableName(table_name),
2747 ],
2748 )
2749 .await
2750 {
2751 warn!(
2752 "Failed to invalidate cache after restoring table '{}' (id={}): {}",
2753 dropped.table_name, dropped.table_id, err
2754 );
2755 }
2756
2757 Ok(Output::new_with_affected_rows(0))
2758}
2759
2760#[cfg(test)]
2761mod test {
2762 use std::sync::{Arc, Mutex};
2763 use std::time::Duration;
2764
2765 use api::v1::meta::{ProcedureDetailResponse, ReconcileRequest, ReconcileResponse};
2766 use common_meta::cache_invalidator::{CacheInvalidator, CacheInvalidatorRef};
2767 use common_meta::instruction::CacheIdent;
2768 use common_meta::key::table_route::TableRouteValue;
2769 use common_meta::key::test_utils::new_test_table_info_with_name;
2770 use common_meta::key::{TableMetadataManager, TableMetadataManagerRef};
2771 use common_meta::kv_backend::memory::MemoryKvBackend;
2772 use common_meta::procedure_executor::{
2773 ExecutorContext, ProcedureExecutor, ProcedureExecutorRef,
2774 };
2775 use common_meta::rpc::ddl::{DdlTask, SubmitDdlTaskRequest, SubmitDdlTaskResponse};
2776 use common_meta::rpc::procedure::{
2777 MigrateRegionRequest, MigrateRegionResponse, ProcedureStateResponse,
2778 };
2779 use session::context::{QueryContext, QueryContextBuilder};
2780 use sql::dialect::GreptimeDbDialect;
2781 use sql::parser::{ParseOptions, ParserContext};
2782 use sql::statements::statement::Statement;
2783 use sqlparser::parser::Parser;
2784
2785 use super::*;
2786 use crate::expr_helper;
2787
2788 #[derive(Default)]
2789 struct RecordingProcedureExecutor {
2790 requests: Mutex<Vec<SubmitDdlTaskRequest>>,
2791 fail: bool,
2792 }
2793
2794 #[async_trait::async_trait]
2795 impl ProcedureExecutor for RecordingProcedureExecutor {
2796 async fn submit_ddl_task(
2797 &self,
2798 _ctx: &ExecutorContext,
2799 request: SubmitDdlTaskRequest,
2800 ) -> common_meta::error::Result<SubmitDdlTaskResponse> {
2801 self.requests.lock().unwrap().push(request);
2802 if self.fail {
2803 return common_meta::error::UnsupportedSnafu {
2804 operation: "test submit failure",
2805 }
2806 .fail();
2807 }
2808 Ok(SubmitDdlTaskResponse::default())
2809 }
2810
2811 async fn migrate_region(
2812 &self,
2813 _: &ExecutorContext,
2814 _: MigrateRegionRequest,
2815 ) -> common_meta::error::Result<MigrateRegionResponse> {
2816 unimplemented!()
2817 }
2818 async fn reconcile(
2819 &self,
2820 _: &ExecutorContext,
2821 _: ReconcileRequest,
2822 ) -> common_meta::error::Result<ReconcileResponse> {
2823 unimplemented!()
2824 }
2825 async fn query_procedure_state(
2826 &self,
2827 _: &ExecutorContext,
2828 _: &str,
2829 ) -> common_meta::error::Result<ProcedureStateResponse> {
2830 unimplemented!()
2831 }
2832 async fn list_procedures(
2833 &self,
2834 _: &ExecutorContext,
2835 ) -> common_meta::error::Result<ProcedureDetailResponse> {
2836 unimplemented!()
2837 }
2838 }
2839
2840 #[derive(Default)]
2841 struct RecordingCacheInvalidator {
2842 invalidations: Mutex<Vec<Vec<CacheIdent>>>,
2843 fail: bool,
2844 }
2845
2846 #[async_trait::async_trait]
2847 impl CacheInvalidator for RecordingCacheInvalidator {
2848 async fn invalidate(
2849 &self,
2850 _: &Context,
2851 caches: &[CacheIdent],
2852 ) -> common_meta::error::Result<()> {
2853 self.invalidations.lock().unwrap().push(caches.to_vec());
2854 if self.fail {
2855 return common_meta::error::UnsupportedSnafu {
2856 operation: "test cache failure",
2857 }
2858 .fail();
2859 }
2860 Ok(())
2861 }
2862
2863 fn invalidate_all(&self) -> common_meta::error::Result<()> {
2864 Ok(())
2865 }
2866 }
2867
2868 async fn dropped_table_manager(table_id: TableId, name: &TableName) -> TableMetadataManagerRef {
2869 let backend = Arc::new(MemoryKvBackend::default());
2870 let manager = Arc::new(TableMetadataManager::new(backend));
2871 let mut info = new_test_table_info_with_name(table_id, &name.table_name);
2872 info.catalog_name = name.catalog_name.clone();
2873 info.schema_name = name.schema_name.clone();
2874 let route = TableRouteValue::physical(vec![]);
2875 manager
2876 .create_table_metadata(info, route.clone(), HashMap::new())
2877 .await
2878 .unwrap();
2879 manager
2880 .delete_table_metadata(table_id, name, &route, &HashMap::new(), None)
2881 .await
2882 .unwrap();
2883 manager
2884 }
2885
2886 #[tokio::test]
2887 async fn test_undrop_table_execution_path() {
2888 let name = TableName::new("greptime", "public", "metrics");
2889 let manager = dropped_table_manager(42, &name).await;
2890 let mut live = new_test_table_info_with_name(99, "metrics");
2892 live.catalog_name = "greptime".to_string();
2893 live.schema_name = "public".to_string();
2894 manager
2895 .create_table_metadata(live, TableRouteValue::physical(vec![]), HashMap::new())
2896 .await
2897 .unwrap();
2898 let procedure = Arc::new(RecordingProcedureExecutor::default());
2899 let cache = Arc::new(RecordingCacheInvalidator::default());
2900
2901 execute_undrop_table(
2902 &manager,
2903 &(procedure.clone() as ProcedureExecutorRef),
2904 &(cache.clone() as CacheInvalidatorRef),
2905 name.clone(),
2906 QueryContext::arc(),
2907 )
2908 .await
2909 .unwrap();
2910
2911 let requests = procedure.requests.lock().unwrap();
2912 assert!(matches!(&requests[0].task, DdlTask::UndropTable(task) if task.table_id == 42));
2913 assert_eq!(
2914 cache.invalidations.lock().unwrap()[0],
2915 vec![CacheIdent::TableId(42), CacheIdent::TableName(name)]
2916 );
2917 }
2918
2919 #[tokio::test]
2920 async fn test_undrop_table_missing_tombstone_submits_nothing() {
2921 let manager = Arc::new(TableMetadataManager::new(Arc::new(
2922 MemoryKvBackend::default(),
2923 )));
2924 let procedure = Arc::new(RecordingProcedureExecutor::default());
2925 let cache = Arc::new(RecordingCacheInvalidator::default());
2926 let err = execute_undrop_table(
2927 &manager,
2928 &(procedure.clone() as ProcedureExecutorRef),
2929 &(cache as CacheInvalidatorRef),
2930 TableName::new("greptime", "public", "missing"),
2931 QueryContext::arc(),
2932 )
2933 .await
2934 .unwrap_err();
2935 assert!(matches!(err, crate::error::Error::TableNotFound { .. }));
2936 assert!(procedure.requests.lock().unwrap().is_empty());
2937 }
2938
2939 #[tokio::test]
2940 async fn test_undrop_table_submit_failure_does_not_invalidate() {
2941 let name = TableName::new("greptime", "public", "metrics");
2942 let manager = dropped_table_manager(42, &name).await;
2943 let procedure = Arc::new(RecordingProcedureExecutor {
2944 fail: true,
2945 ..Default::default()
2946 });
2947 let cache = Arc::new(RecordingCacheInvalidator::default());
2948 execute_undrop_table(
2949 &manager,
2950 &(procedure as ProcedureExecutorRef),
2951 &(cache.clone() as CacheInvalidatorRef),
2952 name,
2953 QueryContext::arc(),
2954 )
2955 .await
2956 .unwrap_err();
2957 assert!(cache.invalidations.lock().unwrap().is_empty());
2958 }
2959
2960 #[tokio::test]
2961 async fn test_undrop_table_cache_failure_returns_success_after_submit() {
2962 let name = TableName::new("greptime", "public", "metrics");
2963 let manager = dropped_table_manager(42, &name).await;
2964 let procedure = Arc::new(RecordingProcedureExecutor::default());
2965 let cache = Arc::new(RecordingCacheInvalidator {
2966 fail: true,
2967 ..Default::default()
2968 });
2969 execute_undrop_table(
2970 &manager,
2971 &(procedure.clone() as ProcedureExecutorRef),
2972 &(cache.clone() as CacheInvalidatorRef),
2973 name.clone(),
2974 QueryContext::arc(),
2975 )
2976 .await
2977 .unwrap();
2978 assert_eq!(procedure.requests.lock().unwrap().len(), 1);
2979 assert_eq!(
2980 cache.invalidations.lock().unwrap()[0],
2981 vec![CacheIdent::TableId(42), CacheIdent::TableName(name)]
2982 );
2983 }
2984
2985 #[test]
2986 fn test_parse_ddl_options() {
2987 let options = OptionMap::from([
2988 ("timeout".to_string(), "5m".to_string()),
2989 ("wait".to_string(), "false".to_string()),
2990 ]);
2991 let ddl_options = parse_ddl_options(&options).unwrap();
2992 assert!(!ddl_options.wait);
2993 assert_eq!(Duration::from_secs(300), ddl_options.timeout);
2994 }
2995
2996 #[test]
2997 fn test_validate_and_normalize_flow_options_empty() {
2998 assert!(
2999 validate_and_normalize_flow_options(HashMap::new(), None)
3000 .unwrap()
3001 .is_empty()
3002 );
3003 }
3004
3005 #[test]
3006 fn test_validate_and_normalize_flow_options_valid() {
3007 let options = HashMap::from([
3008 (DEFER_ON_MISSING_SOURCE_KEY.to_string(), "TRUE".to_string()),
3009 (
3010 FLOW_EXPERIMENTAL_ENABLE_INCREMENTAL_READ_KEY.to_string(),
3011 "FALSE".to_string(),
3012 ),
3013 ]);
3014
3015 assert_eq!(
3016 validate_and_normalize_flow_options(options, None).unwrap(),
3017 HashMap::from([
3018 (DEFER_ON_MISSING_SOURCE_KEY.to_string(), "true".to_string(),),
3019 (
3020 FLOW_EXPERIMENTAL_ENABLE_INCREMENTAL_READ_KEY.to_string(),
3021 "false".to_string(),
3022 )
3023 ])
3024 );
3025 }
3026
3027 #[test]
3028 fn test_validate_and_normalize_flow_options_unknown_option() {
3029 let err = validate_and_normalize_flow_options(
3030 HashMap::from([("foo".to_string(), "bar".to_string())]),
3031 None,
3032 )
3033 .unwrap_err();
3034
3035 assert!(
3036 err.to_string()
3037 .contains("unknown flow option 'foo', supported options:")
3038 );
3039 }
3040
3041 #[test]
3042 fn test_validate_and_normalize_flow_options_reserved_option() {
3043 let err = validate_and_normalize_flow_options(
3044 HashMap::from([(
3045 FlowType::FLOW_TYPE_KEY.to_string(),
3046 FlowType::BATCHING.to_string(),
3047 )]),
3048 None,
3049 )
3050 .unwrap_err();
3051
3052 assert!(
3053 err.to_string()
3054 .contains("flow option 'flow_type' is reserved for internal use")
3055 );
3056 }
3057
3058 #[test]
3059 fn test_validate_and_normalize_flow_options_invalid_bool() {
3060 let err = validate_and_normalize_flow_options(
3061 HashMap::from([(
3062 DEFER_ON_MISSING_SOURCE_KEY.to_string(),
3063 "not-a-bool".to_string(),
3064 )]),
3065 None,
3066 )
3067 .unwrap_err();
3068
3069 assert!(
3070 err.to_string()
3071 .contains("invalid flow option 'defer_on_missing_source': 'not-a-bool'")
3072 );
3073 }
3074
3075 #[test]
3076 fn test_validate_and_normalize_flow_options_rejects_redacted_invalid_input() {
3077 let sql = r"
3078CREATE FLOW task_6
3079SINK TO schema_1.table_1
3080WITH (access_key_id = [true])
3081AS
3082SELECT max(c1), min(c2) FROM schema_2.table_2;";
3083 let stmt =
3084 ParserContext::create_with_dialect(sql, &GreptimeDbDialect {}, ParseOptions::default())
3085 .unwrap()
3086 .pop()
3087 .unwrap();
3088
3089 let Statement::CreateFlow(create_flow) = stmt else {
3090 unreachable!()
3091 };
3092 let expr =
3093 expr_helper::to_create_flow_task_expr(create_flow, &QueryContext::arc()).unwrap();
3094 let err = validate_and_normalize_flow_options(expr.flow_options, None).unwrap_err();
3095
3096 assert!(
3097 err.to_string()
3098 .contains("unknown flow option 'access_key_id'")
3099 );
3100 }
3101
3102 #[test]
3105 fn test_eval_interval_rejected_non_positive() {
3106 let err = validate_and_normalize_flow_options(HashMap::new(), Some(0)).unwrap_err();
3108 assert!(err.to_string().contains("EVAL INTERVAL must be positive"));
3109
3110 let err = validate_and_normalize_flow_options(HashMap::new(), Some(-5)).unwrap_err();
3112 assert!(err.to_string().contains("EVAL INTERVAL must be positive"));
3113
3114 let result = validate_and_normalize_flow_options(HashMap::new(), Some(300));
3116 assert!(result.is_ok());
3117 }
3118
3119 #[test]
3120 fn test_schedule_and_internal_keys_rejected_as_unknown_options() {
3121 for key in [
3122 "eval_interval_anchor",
3123 "eval_interval_start",
3124 "eval_interval_missed_tick_policy",
3125 "eval_interval_catchup_max_runs",
3126 "eval_interval_catchup_max_lag",
3127 "__greptime_internal_eval_schedule",
3128 ] {
3129 let err = validate_and_normalize_flow_options(
3130 HashMap::from([(key.to_string(), "value".to_string())]),
3131 Some(300),
3132 )
3133 .unwrap_err();
3134
3135 assert!(
3136 err.to_string()
3137 .contains(&format!("unknown flow option '{key}'")),
3138 "unexpected error for {key}: {err}"
3139 );
3140 }
3141 }
3142
3143 #[test]
3144 fn test_determine_flow_type_for_source_state_missing_sources_require_opt_in() {
3145 let err = determine_flow_type_for_source_state("my_flow", &HashMap::new(), true, false)
3146 .unwrap_err();
3147
3148 assert!(err.to_string().contains(
3149 "missing source tables for flow 'my_flow'; use WITH (defer_on_missing_source = true) to create a pending flow"
3150 ));
3151 }
3152
3153 #[test]
3154 fn test_determine_flow_type_for_source_state_missing_sources_prefer_batching() {
3155 let flow_options =
3156 HashMap::from([(DEFER_ON_MISSING_SOURCE_KEY.to_string(), "true".to_string())]);
3157
3158 assert_eq!(
3159 determine_flow_type_for_source_state("my_flow", &flow_options, true, true).unwrap(),
3160 Some(FlowType::Batching)
3161 );
3162 }
3163
3164 #[test]
3165 fn test_determine_flow_type_for_source_state_instant_ttl_without_missing_sources() {
3166 assert_eq!(
3167 determine_flow_type_for_source_state("my_flow", &HashMap::new(), false, true).unwrap(),
3168 Some(FlowType::Streaming)
3169 );
3170 }
3171
3172 #[test]
3173 fn test_name_is_match() {
3174 assert!(!NAME_PATTERN_REG.is_match("/adaf"));
3175 assert!(!NAME_PATTERN_REG.is_match("🈲"));
3176 assert!(NAME_PATTERN_REG.is_match("hello"));
3177 assert!(NAME_PATTERN_REG.is_match("test@"));
3178 assert!(!NAME_PATTERN_REG.is_match("@test"));
3179 assert!(NAME_PATTERN_REG.is_match("test#"));
3180 assert!(!NAME_PATTERN_REG.is_match("#test"));
3181 assert!(!NAME_PATTERN_REG.is_match("@"));
3182 assert!(!NAME_PATTERN_REG.is_match("#"));
3183 }
3184
3185 #[test]
3186 fn test_partition_expr_equivalence_with_swapped_operands() {
3187 let column_name = "device_id".to_string();
3188 let column_name_and_type =
3189 HashMap::from([(&column_name, ConcreteDataType::int32_datatype())]);
3190 let timezone = Timezone::from_tz_string("UTC").unwrap();
3191 let dialect = GreptimeDbDialect {};
3192
3193 let mut parser = Parser::new(&dialect)
3194 .try_with_sql("device_id < 100")
3195 .unwrap();
3196 let expr_left = parser.parse_expr().unwrap();
3197
3198 let mut parser = Parser::new(&dialect)
3199 .try_with_sql("100 > device_id")
3200 .unwrap();
3201 let expr_right = parser.parse_expr().unwrap();
3202
3203 let partition_left =
3204 convert_one_expr(&expr_left, &column_name_and_type, &timezone).unwrap();
3205 let partition_right =
3206 convert_one_expr(&expr_right, &column_name_and_type, &timezone).unwrap();
3207
3208 assert_eq!(partition_left, partition_right);
3209 assert!([partition_left.clone()].contains(&partition_right));
3210
3211 let mut physical_partition_exprs = vec![partition_left];
3212 let mut logical_partition_exprs = vec![partition_right];
3213 physical_partition_exprs.sort_unstable();
3214 logical_partition_exprs.sort_unstable();
3215 assert_eq!(physical_partition_exprs, logical_partition_exprs);
3216 }
3217
3218 #[test]
3219 fn test_repartition_target_partition_columns_are_overwrite_context() {
3220 let device_id = ColumnSchema::new("device_id", ConcreteDataType::int32_datatype(), true);
3221 let area = ColumnSchema::new("area", ConcreteDataType::string_datatype(), true);
3222 let existing_partition_columns = vec![&device_id];
3223 let target_partition_columns = vec![&device_id, &area];
3224 let existing_column_name_and_type = column_name_and_type(&existing_partition_columns);
3225 let target_column_name_and_type = column_name_and_type(&target_partition_columns);
3226 let timezone = Timezone::from_tz_string("UTC").unwrap();
3227 let dialect = GreptimeDbDialect {};
3228
3229 let mut parser = Parser::new(&dialect)
3230 .try_with_sql("device_id < 100 AND area < 'South'")
3231 .unwrap();
3232 let expr = parser.parse_expr().unwrap();
3233
3234 let err = convert_one_expr(&expr, &existing_column_name_and_type, &timezone).unwrap_err();
3235 assert!(err.to_string().contains("area"));
3236
3237 let partition_expr = convert_one_expr(&expr, &target_column_name_and_type, &timezone)
3238 .expect("target columns should overwrite the conversion context");
3239 let partition_expr = partition_expr.to_string();
3240 assert!(partition_expr.contains("device_id"));
3241 assert!(partition_expr.contains("area"));
3242 assert!(partition_expr.contains("South"));
3243 }
3244
3245 #[test]
3246 fn test_repartition_rejects_remaining_expr_outside_target_columns() {
3247 let device_id = "device_id".to_string();
3248 let area = "area".to_string();
3249 let timezone = Timezone::from_tz_string("UTC").unwrap();
3250 let column_name_and_type = HashMap::from([
3251 (&device_id, ConcreteDataType::int32_datatype()),
3252 (&area, ConcreteDataType::string_datatype()),
3253 ]);
3254 let dialect = GreptimeDbDialect {};
3255 let mut parser = Parser::new(&dialect)
3256 .try_with_sql("device_id >= 100")
3257 .unwrap();
3258 let remaining_old_expr = convert_one_expr(
3259 &parser.parse_expr().unwrap(),
3260 &column_name_and_type,
3261 &timezone,
3262 )
3263 .unwrap();
3264 let target_partition_columns = HashSet::from([&area]);
3265
3266 let err = ensure_partition_expr_columns_in_target(
3267 &[remaining_old_expr],
3268 &target_partition_columns,
3269 )
3270 .unwrap_err();
3271
3272 assert!(err.to_string().contains("device_id"));
3273 assert!(err.to_string().contains("target partition columns"));
3274 }
3275
3276 #[test]
3277 fn test_repartition_rejects_duplicate_target_partition_columns() {
3278 let device_id = ColumnSchema::new("device_id", ConcreteDataType::int32_datatype(), true);
3279 let column_schemas = vec![device_id];
3280 let target_partition_columns = vec!["device_id".to_string(), "device_id".to_string()];
3281
3282 let err =
3283 validate_and_collect_partition_columns(&target_partition_columns, &column_schemas)
3284 .unwrap_err();
3285
3286 assert!(err.to_string().contains("duplicate partition column"));
3287 assert!(err.to_string().contains("device_id"));
3288 }
3289
3290 fn create_expr_from_sql(sql: &str) -> CreateTableExpr {
3291 let result =
3292 ParserContext::create_with_dialect(sql, &GreptimeDbDialect {}, ParseOptions::default())
3293 .unwrap();
3294
3295 match &result[0] {
3296 Statement::CreateTable(create) => {
3297 expr_helper::create_to_expr(create, &QueryContext::arc()).unwrap()
3298 }
3299 _ => unreachable!(),
3300 }
3301 }
3302
3303 #[test]
3304 fn test_create_table_with_repartition_column_hint() {
3305 let expr = create_expr_from_sql(
3306 r"
3307CREATE TABLE metrics (
3308 host STRING,
3309 ts TIMESTAMP TIME INDEX,
3310 cpu DOUBLE,
3311 PRIMARY KEY(host)
3312)
3313WITH ('repartition.column.hint' = ' host ')",
3314 );
3315
3316 let table_info = create_table_info(&expr, vec![]).unwrap();
3317 assert_eq!(
3318 table_info
3319 .meta
3320 .options
3321 .extra_options
3322 .get(REPARTITION_COLUMN_HINT_KEY),
3323 Some(&"host".to_string())
3324 );
3325 }
3326
3327 #[test]
3328 fn test_json2_requires_append_mode() {
3329 let cases = [
3330 "CREATE TABLE monitor (payload JSON2, ts TIMESTAMP TIME INDEX);",
3331 "CREATE TABLE monitor (payload JSON2, ts TIMESTAMP TIME INDEX) WITH (append_mode='false');",
3332 ];
3333
3334 for sql in cases {
3335 let expr = create_expr_from_sql(sql);
3336 let err = create_table_info(&expr, vec![]).unwrap_err();
3337 assert!(
3338 err.to_string()
3339 .contains("JSON2 column `payload` requires append_mode='true'"),
3340 "{err}"
3341 );
3342 }
3343
3344 let expr = create_expr_from_sql(
3345 "CREATE TABLE monitor (payload JSON2, ts TIMESTAMP TIME INDEX) WITH (append_mode='true');",
3346 );
3347 create_table_info(&expr, vec![]).unwrap();
3348 }
3349
3350 #[test]
3351 fn test_create_table_with_empty_repartition_column_hint() {
3352 let expr = create_expr_from_sql(
3353 r"
3354CREATE TABLE metrics (
3355 host STRING,
3356 ts TIMESTAMP TIME INDEX,
3357 cpu DOUBLE,
3358 PRIMARY KEY(host)
3359)
3360WITH ('repartition.column.hint' = '')",
3361 );
3362
3363 let err = create_table_info(&expr, vec![]).unwrap_err();
3364 assert!(
3365 err.to_string()
3366 .contains("repartition.column.hint expects exactly one column name")
3367 );
3368 }
3369
3370 #[test]
3371 fn test_create_table_with_multiple_repartition_column_hints() {
3372 let expr = create_expr_from_sql(
3373 r"
3374CREATE TABLE metrics (
3375 host STRING,
3376 region_id STRING,
3377 ts TIMESTAMP TIME INDEX,
3378 cpu DOUBLE,
3379 PRIMARY KEY(host)
3380)
3381WITH ('repartition.column.hint' = 'host,region_id')",
3382 );
3383
3384 let err = create_table_info(&expr, vec![]).unwrap_err();
3385 assert!(
3386 err.to_string()
3387 .contains("repartition.column.hint expects exactly one column name")
3388 );
3389 }
3390
3391 #[test]
3392 fn test_create_table_with_missing_repartition_column_hint() {
3393 let expr = create_expr_from_sql(
3394 r"
3395CREATE TABLE metrics (
3396 host STRING,
3397 ts TIMESTAMP TIME INDEX,
3398 cpu DOUBLE,
3399 PRIMARY KEY(host)
3400)
3401WITH ('repartition.column.hint' = 'region_id')",
3402 );
3403
3404 let err = create_table_info(&expr, vec![]).unwrap_err();
3405 assert!(
3406 err.to_string()
3407 .contains("Cannot find column by name: region")
3408 );
3409 }
3410
3411 #[test]
3412 fn test_create_table_with_time_index_repartition_column_hint() {
3413 let expr = create_expr_from_sql(
3414 r"
3415CREATE TABLE metrics (
3416 host STRING,
3417 ts TIMESTAMP TIME INDEX,
3418 cpu DOUBLE,
3419 PRIMARY KEY(host)
3420)
3421WITH ('repartition.column.hint' = 'ts')",
3422 );
3423
3424 let err = create_table_info(&expr, vec![]).unwrap_err();
3425 assert!(
3426 err.to_string()
3427 .contains("cannot set repartition.column.hint to the time index column")
3428 );
3429 }
3430
3431 #[test]
3432 fn test_create_partitioned_table_with_repartition_column_hint() {
3433 let expr = create_expr_from_sql(
3434 r"
3435CREATE TABLE metrics (
3436 host STRING,
3437 ts TIMESTAMP TIME INDEX,
3438 cpu DOUBLE,
3439 PRIMARY KEY(host)
3440)
3441WITH ('repartition.column.hint' = 'host')",
3442 );
3443
3444 let err = create_table_info(&expr, vec!["host".to_string()]).unwrap_err();
3445 assert!(
3446 err.to_string()
3447 .contains("cannot set repartition.column.hint on a table with partition metadata")
3448 );
3449 }
3450
3451 #[tokio::test]
3452 #[ignore = "TODO(ruihang): WIP new partition rule"]
3453 async fn test_parse_partitions() {
3454 common_telemetry::init_default_ut_logging();
3455 let cases = [
3456 (
3457 r"
3458CREATE TABLE rcx ( a INT, b STRING, c TIMESTAMP, TIME INDEX (c) )
3459PARTITION ON COLUMNS (b) (
3460 b < 'hz',
3461 b >= 'hz' AND b < 'sh',
3462 b >= 'sh'
3463)
3464ENGINE=mito",
3465 r#"[{"column_list":["b"],"value_list":["{\"Value\":{\"String\":\"hz\"}}"]},{"column_list":["b"],"value_list":["{\"Value\":{\"String\":\"sh\"}}"]},{"column_list":["b"],"value_list":["\"MaxValue\""]}]"#,
3466 ),
3467 (
3468 r"
3469CREATE TABLE rcx ( a INT, b STRING, c TIMESTAMP, TIME INDEX (c) )
3470PARTITION BY RANGE COLUMNS (b, a) (
3471 PARTITION r0 VALUES LESS THAN ('hz', 10),
3472 b < 'hz' AND a < 10,
3473 b >= 'hz' AND b < 'sh' AND a >= 10 AND a < 20,
3474 b >= 'sh' AND a >= 20
3475)
3476ENGINE=mito",
3477 r#"[{"column_list":["b","a"],"value_list":["{\"Value\":{\"String\":\"hz\"}}","{\"Value\":{\"Int32\":10}}"]},{"column_list":["b","a"],"value_list":["{\"Value\":{\"String\":\"sh\"}}","{\"Value\":{\"Int32\":20}}"]},{"column_list":["b","a"],"value_list":["\"MaxValue\"","\"MaxValue\""]}]"#,
3478 ),
3479 ];
3480 let ctx = QueryContextBuilder::default().build().into();
3481 for (sql, expected) in cases {
3482 let result = ParserContext::create_with_dialect(
3483 sql,
3484 &GreptimeDbDialect {},
3485 ParseOptions::default(),
3486 )
3487 .unwrap();
3488 match &result[0] {
3489 Statement::CreateTable(c) => {
3490 let expr = expr_helper::create_to_expr(c, &QueryContext::arc()).unwrap();
3491 let (partitions, _) =
3492 parse_partitions(&expr, c.partitions.clone(), &ctx).unwrap();
3493 let json = serde_json::to_string(&partitions).unwrap();
3494 assert_eq!(json, expected);
3495 }
3496 _ => unreachable!(),
3497 }
3498 }
3499 }
3500}