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