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