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