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