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