Skip to main content

operator/statement/
ddl.rs

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