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