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