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