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, COMMENT_KEY, DDL_TIMEOUT, DDL_WAIT, REPARTITION_COLUMN_HINT_KEY,
102    TableOptions, parse_entity_columns, parse_entity_option_key,
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
1885            let req = SubmitDdlTaskRequest::new(DdlTask::new_alter_logical_tables(vec![expr]));
1886
1887            let mut invalidate_keys = vec![
1888                CacheIdent::TableId(physical_table_id),
1889                CacheIdent::TableId(table_id),
1890                CacheIdent::TableName(TableName::new(catalog_name, schema_name, table_name)),
1891            ];
1892
1893            let physical_table = self
1894                .table_metadata_manager
1895                .table_info_manager()
1896                .get(physical_table_id)
1897                .await
1898                .context(TableMetadataManagerSnafu)?
1899                .map(|x| x.into_inner());
1900            if let Some(physical_table) = physical_table {
1901                let physical_table_name = TableName::new(
1902                    physical_table.table_info.catalog_name,
1903                    physical_table.table_info.schema_name,
1904                    physical_table.table_info.name,
1905                );
1906                invalidate_keys.push(CacheIdent::TableName(physical_table_name));
1907            }
1908
1909            (req, invalidate_keys)
1910        };
1911
1912        self.procedure_executor
1913            .submit_ddl_task(executor_context, req)
1914            .await
1915            .context(error::ExecuteDdlSnafu)?;
1916
1917        // Invalidates local cache ASAP.
1918        self.cache_invalidator
1919            .invalidate(&Context::default(), &invalidate_keys)
1920            .await
1921            .context(error::InvalidateTableCacheSnafu)?;
1922
1923        Ok(Output::new_with_affected_rows(0))
1924    }
1925
1926    #[cfg(feature = "enterprise")]
1927    #[tracing::instrument(skip_all)]
1928    pub async fn alter_trigger(
1929        &self,
1930        _alter_expr: AlterTrigger,
1931        _query_context: QueryContextRef,
1932    ) -> Result<Output> {
1933        crate::error::NotSupportedSnafu {
1934            feat: "alter trigger",
1935        }
1936        .fail()
1937    }
1938
1939    #[tracing::instrument(skip_all)]
1940    pub async fn alter_database(
1941        &self,
1942        alter_expr: AlterDatabase,
1943        query_context: QueryContextRef,
1944    ) -> Result<Output> {
1945        let alter_expr = expr_helper::to_alter_database_expr(alter_expr, &query_context)?;
1946        self.alter_database_inner(alter_expr, query_context).await
1947    }
1948
1949    #[tracing::instrument(skip_all)]
1950    pub async fn alter_database_inner(
1951        &self,
1952        alter_expr: AlterDatabaseExpr,
1953        query_context: QueryContextRef,
1954    ) -> Result<Output> {
1955        ensure!(
1956            !is_readonly_schema(&alter_expr.schema_name),
1957            SchemaReadOnlySnafu {
1958                name: query_context.current_schema().clone()
1959            }
1960        );
1961
1962        let exists = self
1963            .catalog_manager
1964            .schema_exists(&alter_expr.catalog_name, &alter_expr.schema_name, None)
1965            .await
1966            .context(CatalogSnafu)?;
1967        ensure!(
1968            exists,
1969            SchemaNotFoundSnafu {
1970                schema_info: alter_expr.schema_name,
1971            }
1972        );
1973
1974        let cache_ident = [CacheIdent::SchemaName(SchemaName {
1975            catalog_name: alter_expr.catalog_name.clone(),
1976            schema_name: alter_expr.schema_name.clone(),
1977        })];
1978
1979        self.alter_database_procedure(alter_expr, query_context)
1980            .await?;
1981
1982        // Invalidates local cache ASAP.
1983        self.cache_invalidator
1984            .invalidate(&Context::default(), &cache_ident)
1985            .await
1986            .context(error::InvalidateTableCacheSnafu)?;
1987
1988        Ok(Output::new_with_affected_rows(0))
1989    }
1990
1991    async fn create_table_procedure(
1992        &self,
1993        create_table: CreateTableExpr,
1994        partitions: Vec<PartitionExpr>,
1995        table_info: TableInfo,
1996        query_context: QueryContextRef,
1997        trigger_reason: TriggerReason,
1998    ) -> Result<SubmitDdlTaskResponse> {
1999        let partitions = partitions
2000            .into_iter()
2001            .map(|expr| expr.as_pb_partition().context(PartitionExprToPbSnafu))
2002            .collect::<Result<Vec<_>>>()?;
2003
2004        let executor_context = to_executor_context_with_origin_frontend(
2005            query_context,
2006            &self.origin_frontend_addr,
2007            trigger_reason,
2008        );
2009        let request = SubmitDdlTaskRequest::new(DdlTask::new_create_table(
2010            create_table,
2011            partitions,
2012            table_info,
2013        ));
2014
2015        self.procedure_executor
2016            .submit_ddl_task(executor_context, request)
2017            .await
2018            .context(error::ExecuteDdlSnafu)
2019    }
2020
2021    async fn create_logical_tables_procedure(
2022        &self,
2023        tables_data: Vec<(CreateTableExpr, TableInfo)>,
2024        query_context: QueryContextRef,
2025        trigger_reason: TriggerReason,
2026    ) -> Result<SubmitDdlTaskResponse> {
2027        let executor_context = to_executor_context_with_origin_frontend(
2028            query_context,
2029            &self.origin_frontend_addr,
2030            trigger_reason,
2031        );
2032        let request = SubmitDdlTaskRequest::new(DdlTask::new_create_logical_tables(tables_data));
2033
2034        self.procedure_executor
2035            .submit_ddl_task(executor_context, request)
2036            .await
2037            .context(error::ExecuteDdlSnafu)
2038    }
2039
2040    async fn alter_logical_tables_procedure(
2041        &self,
2042        tables_data: Vec<AlterTableExpr>,
2043        query_context: QueryContextRef,
2044        trigger_reason: TriggerReason,
2045    ) -> Result<SubmitDdlTaskResponse> {
2046        let executor_context = to_executor_context(query_context, trigger_reason);
2047        let request = SubmitDdlTaskRequest::new(DdlTask::new_alter_logical_tables(tables_data));
2048
2049        self.procedure_executor
2050            .submit_ddl_task(executor_context, request)
2051            .await
2052            .context(error::ExecuteDdlSnafu)
2053    }
2054
2055    async fn drop_table_procedure(
2056        &self,
2057        table_name: &TableName,
2058        table_id: TableId,
2059        drop_if_exists: bool,
2060        query_context: QueryContextRef,
2061    ) -> Result<SubmitDdlTaskResponse> {
2062        let executor_context = to_executor_context(query_context, TriggerReason::Manual);
2063        let request = SubmitDdlTaskRequest::new(DdlTask::new_drop_table(
2064            table_name.catalog_name.clone(),
2065            table_name.schema_name.clone(),
2066            table_name.table_name.clone(),
2067            table_id,
2068            drop_if_exists,
2069        ));
2070
2071        self.procedure_executor
2072            .submit_ddl_task(executor_context, request)
2073            .await
2074            .context(error::ExecuteDdlSnafu)
2075    }
2076
2077    async fn drop_database_procedure(
2078        &self,
2079        catalog: String,
2080        schema: String,
2081        drop_if_exists: bool,
2082        query_context: QueryContextRef,
2083    ) -> Result<SubmitDdlTaskResponse> {
2084        let executor_context = to_executor_context(query_context, TriggerReason::Manual);
2085        let request =
2086            SubmitDdlTaskRequest::new(DdlTask::new_drop_database(catalog, schema, drop_if_exists));
2087
2088        self.procedure_executor
2089            .submit_ddl_task(executor_context, request)
2090            .await
2091            .context(error::ExecuteDdlSnafu)
2092    }
2093
2094    async fn alter_database_procedure(
2095        &self,
2096        alter_expr: AlterDatabaseExpr,
2097        query_context: QueryContextRef,
2098    ) -> Result<SubmitDdlTaskResponse> {
2099        let executor_context = to_executor_context(query_context, TriggerReason::Manual);
2100        let request = SubmitDdlTaskRequest::new(DdlTask::new_alter_database(alter_expr));
2101
2102        self.procedure_executor
2103            .submit_ddl_task(executor_context, request)
2104            .await
2105            .context(error::ExecuteDdlSnafu)
2106    }
2107
2108    async fn truncate_table_procedure(
2109        &self,
2110        table_name: &TableName,
2111        table_id: TableId,
2112        time_ranges: Vec<(Timestamp, Timestamp)>,
2113        query_context: QueryContextRef,
2114    ) -> Result<SubmitDdlTaskResponse> {
2115        let executor_context = to_executor_context(query_context, TriggerReason::Manual);
2116        let request = SubmitDdlTaskRequest::new(DdlTask::new_truncate_table(
2117            table_name.catalog_name.clone(),
2118            table_name.schema_name.clone(),
2119            table_name.table_name.clone(),
2120            table_id,
2121            time_ranges,
2122        ));
2123
2124        self.procedure_executor
2125            .submit_ddl_task(executor_context, request)
2126            .await
2127            .context(error::ExecuteDdlSnafu)
2128    }
2129
2130    #[tracing::instrument(skip_all)]
2131    pub async fn create_database(
2132        &self,
2133        database: &str,
2134        create_if_not_exists: bool,
2135        options: HashMap<String, String>,
2136        query_context: QueryContextRef,
2137    ) -> Result<Output> {
2138        let catalog = query_context.current_catalog();
2139        ensure!(
2140            NAME_PATTERN_REG.is_match(catalog),
2141            error::UnexpectedSnafu {
2142                violated: format!("Invalid catalog name: {}", catalog)
2143            }
2144        );
2145
2146        ensure!(
2147            NAME_PATTERN_REG.is_match(database),
2148            error::UnexpectedSnafu {
2149                violated: format!("Invalid database name: {}", database)
2150            }
2151        );
2152
2153        #[cfg(feature = "enterprise")]
2154        let creator = match &self.create_database_handler {
2155            Some(handler) => handler.creator(&query_context).context(ExternalSnafu)?,
2156            None => None,
2157        };
2158        #[cfg(not(feature = "enterprise"))]
2159        let creator = None;
2160
2161        let output = if !self
2162            .catalog_manager
2163            .schema_exists(catalog, database, None)
2164            .await
2165            .context(CatalogSnafu)?
2166            && !self.catalog_manager.is_reserved_schema_name(database)
2167        {
2168            self.create_database_procedure(
2169                catalog.to_string(),
2170                database.to_string(),
2171                create_if_not_exists,
2172                options,
2173                query_context.clone(),
2174                creator,
2175            )
2176            .await?;
2177
2178            Output::new_with_affected_rows(1)
2179        } else if create_if_not_exists {
2180            Output::new_with_affected_rows(1)
2181        } else {
2182            return error::SchemaExistsSnafu { name: database }.fail();
2183        };
2184
2185        #[cfg(feature = "enterprise")]
2186        if let Some(handler) = &self.create_database_handler {
2187            handler
2188                .refresh_current_user(&query_context)
2189                .await
2190                .context(ExternalSnafu)?;
2191        }
2192
2193        Ok(output)
2194    }
2195
2196    async fn create_database_procedure(
2197        &self,
2198        catalog: String,
2199        database: String,
2200        create_if_not_exists: bool,
2201        options: HashMap<String, String>,
2202        query_context: QueryContextRef,
2203        creator: Option<CreatorGrantIntent>,
2204    ) -> Result<SubmitDdlTaskResponse> {
2205        let executor_context = to_executor_context(query_context, TriggerReason::Manual);
2206        let request = SubmitDdlTaskRequest::new(DdlTask::new_create_database(
2207            catalog,
2208            database,
2209            create_if_not_exists,
2210            options,
2211            creator,
2212        ));
2213
2214        self.procedure_executor
2215            .submit_ddl_task(executor_context, request)
2216            .await
2217            .context(error::ExecuteDdlSnafu)
2218    }
2219}
2220
2221/// Parse partition statement [Partitions] into [PartitionExpr] and partition columns.
2222pub fn parse_partitions(
2223    create_table: &CreateTableExpr,
2224    partitions: Option<Partitions>,
2225    query_ctx: &QueryContextRef,
2226) -> Result<(Vec<PartitionExpr>, Vec<String>)> {
2227    // If partitions are not defined by user, use the timestamp column (which has to be existed) as
2228    // the partition column, and create only one partition.
2229    let partition_columns = find_partition_columns(&partitions)?;
2230    let partition_exprs =
2231        find_partition_entries(create_table, &partitions, &partition_columns, query_ctx)?;
2232
2233    // Validates partition
2234    let exprs = partition_exprs.clone();
2235    MultiDimPartitionRule::try_new(partition_columns.clone(), vec![], exprs, true)
2236        .context(InvalidPartitionSnafu)?;
2237
2238    Ok((partition_exprs, partition_columns))
2239}
2240
2241fn parse_partitions_for_logical_validation(
2242    create_table: &CreateTableExpr,
2243    partitions: &Partitions,
2244    query_ctx: &QueryContextRef,
2245) -> Result<(Vec<String>, Vec<PartitionExpr>)> {
2246    let partition_columns = partitions
2247        .column_list
2248        .iter()
2249        .map(|ident| ident.value.clone())
2250        .collect::<Vec<_>>();
2251
2252    let column_name_and_type = partition_columns
2253        .iter()
2254        .map(|pc| {
2255            let column = create_table
2256                .column_defs
2257                .iter()
2258                .find(|c| &c.name == pc)
2259                .context(ColumnNotFoundSnafu { msg: pc.clone() })?;
2260            let column_name = &column.name;
2261            let data_type = ConcreteDataType::from(
2262                ColumnDataTypeWrapper::try_new(column.data_type, column.datatype_extension.clone())
2263                    .context(ColumnDataTypeSnafu)?,
2264            );
2265            Ok((column_name, data_type))
2266        })
2267        .collect::<Result<HashMap<_, _>>>()?;
2268
2269    let mut partition_exprs = Vec::with_capacity(partitions.exprs.len());
2270    for expr in &partitions.exprs {
2271        let partition_expr = convert_one_expr(expr, &column_name_and_type, &query_ctx.timezone())?;
2272        partition_exprs.push(partition_expr);
2273    }
2274
2275    MultiDimPartitionRule::try_new(
2276        partition_columns.clone(),
2277        vec![],
2278        partition_exprs.clone(),
2279        true,
2280    )
2281    .context(InvalidPartitionSnafu)?;
2282
2283    Ok((partition_columns, partition_exprs))
2284}
2285
2286/// Verifies an alter and returns whether it is necessary to perform the alter.
2287///
2288/// # Returns
2289///
2290/// Returns true if the alter need to be porformed; otherwise, it returns false.
2291pub fn verify_alter(
2292    table_id: TableId,
2293    table_info: Arc<TableInfo>,
2294    expr: AlterTableExpr,
2295) -> Result<bool> {
2296    let request: AlterTableRequest =
2297        common_grpc_expr::alter_expr_to_request(table_id, expr, Some(&table_info.meta))
2298            .context(AlterExprToRequestSnafu)?;
2299
2300    let AlterTableRequest {
2301        table_name,
2302        alter_kind,
2303        ..
2304    } = &request;
2305
2306    if let AlterKind::RenameTable { new_table_name } = alter_kind {
2307        ensure!(
2308            NAME_PATTERN_REG.is_match(new_table_name),
2309            error::UnexpectedSnafu {
2310                violated: format!("Invalid table name: {}", new_table_name)
2311            }
2312        );
2313        // Renaming INTO a computed graph table's name would let the overlay
2314        // shadow the renamed physical table, orphaning its data.
2315        ensure_table_definition_writable(&table_info.schema_name, new_table_name)?;
2316    } else if let AlterKind::AddColumns { columns } = alter_kind {
2317        // If all the columns are marked as add_if_not_exists and they already exist in the table,
2318        // there is no need to perform the alter.
2319        let column_names: HashSet<_> = table_info
2320            .meta
2321            .schema
2322            .column_schemas()
2323            .iter()
2324            .map(|schema| &schema.name)
2325            .collect();
2326        if columns.iter().all(|column| {
2327            column_names.contains(&column.column_schema.name) && column.add_if_not_exists
2328        }) {
2329            return Ok(false);
2330        }
2331    }
2332
2333    let new_meta = table_info
2334        .meta
2335        .builder_with_alter_kind(table_name, &request.alter_kind)
2336        .context(error::TableSnafu)?
2337        .build()
2338        .context(error::BuildTableMetaSnafu { table_name })?;
2339
2340    validate_json2_columns_append_mode(&new_meta.schema, &new_meta.options)?;
2341    // A column referenced by an entity declaration may be dropped (the
2342    // read-time derivation skips the stale declaration), but must not change
2343    // to a type without a stable string form.
2344    for (key, value) in &new_meta.options.extra_options {
2345        if parse_entity_option_key(key).is_none() {
2346            continue;
2347        }
2348        for col in &parse_entity_columns(value) {
2349            if let Some(column) = new_meta.schema.column_schema_by_name(col) {
2350                ensure_entity_column_renders_as_string(key, col, &column.data_type)?;
2351            }
2352        }
2353    }
2354
2355    Ok(true)
2356}
2357
2358pub fn create_table_info(
2359    create_table: &CreateTableExpr,
2360    partition_columns: Vec<String>,
2361) -> Result<TableInfo> {
2362    let mut column_schemas = Vec::with_capacity(create_table.column_defs.len());
2363    let mut column_name_to_index_map = HashMap::new();
2364
2365    for (idx, column) in create_table.column_defs.iter().enumerate() {
2366        let schema =
2367            column_def::try_as_column_schema(column).context(error::InvalidColumnDefSnafu {
2368                column: &column.name,
2369            })?;
2370        let schema = schema.with_time_index(column.name == create_table.time_index);
2371
2372        column_schemas.push(schema);
2373        let _ = column_name_to_index_map.insert(column.name.clone(), idx);
2374    }
2375
2376    let next_column_id = column_schemas.len() as u32;
2377    let schema = Arc::new(Schema::try_new(column_schemas).context(ConvertSchemaSnafu)?);
2378
2379    let primary_key_indices = create_table
2380        .primary_keys
2381        .iter()
2382        .map(|name| {
2383            column_name_to_index_map
2384                .get(name)
2385                .cloned()
2386                .context(ColumnNotFoundSnafu { msg: name })
2387        })
2388        .collect::<Result<Vec<_>>>()?;
2389
2390    let partition_key_indices = partition_columns
2391        .into_iter()
2392        .map(|col_name| {
2393            column_name_to_index_map
2394                .get(&col_name)
2395                .cloned()
2396                .context(ColumnNotFoundSnafu { msg: col_name })
2397        })
2398        .collect::<Result<Vec<_>>>()?;
2399
2400    let mut table_options = TableOptions::try_from_iter(&create_table.table_options)
2401        .context(UnrecognizedTableOptionSnafu)?;
2402
2403    validate_json2_columns_append_mode(&schema, &table_options)?;
2404
2405    validate_repartition_column_hint(
2406        &mut table_options,
2407        &column_name_to_index_map,
2408        &partition_key_indices,
2409        &create_table.time_index,
2410    )?;
2411
2412    validate_entity_semantic_options(&table_options, &schema)?;
2413
2414    let meta = TableMeta {
2415        schema,
2416        primary_key_indices,
2417        value_indices: vec![],
2418        engine: create_table.engine.clone(),
2419        next_column_id,
2420        options: table_options,
2421        created_on: Utc::now(),
2422        updated_on: Utc::now(),
2423        partition_key_indices,
2424        column_ids: vec![],
2425    };
2426
2427    let desc = if create_table.desc.is_empty() {
2428        create_table.table_options.get(COMMENT_KEY).cloned()
2429    } else {
2430        Some(create_table.desc.clone())
2431    };
2432
2433    let table_info = TableInfo {
2434        ident: metadata::TableIdent {
2435            // The table id of distributed table is assigned by Meta, set "0" here as a placeholder.
2436            table_id: 0,
2437            version: 0,
2438        },
2439        name: create_table.table_name.clone(),
2440        desc,
2441        catalog_name: create_table.catalog_name.clone(),
2442        schema_name: create_table.schema_name.clone(),
2443        meta,
2444        table_type: TableType::Base,
2445    };
2446    Ok(table_info)
2447}
2448
2449fn validate_json2_columns_append_mode(schema: &Schema, table_options: &TableOptions) -> Result<()> {
2450    let append_mode = table_options
2451        .extra_options
2452        .get(APPEND_MODE_KEY)
2453        .is_some_and(|value| value == "true");
2454
2455    for column in schema.column_schemas() {
2456        if column.data_type.is_json2() {
2457            ensure!(
2458                append_mode,
2459                InvalidSqlSnafu {
2460                    err_msg: format!(
2461                        "JSON2 column `{}` requires {}='true'",
2462                        column.name, APPEND_MODE_KEY
2463                    ),
2464                }
2465            );
2466        }
2467    }
2468
2469    Ok(())
2470}
2471
2472fn validate_repartition_column_hint(
2473    table_options: &mut TableOptions,
2474    column_name_to_index_map: &HashMap<String, usize>,
2475    partition_key_indices: &[usize],
2476    time_index: &str,
2477) -> Result<()> {
2478    let Some(column_name) = table_options
2479        .extra_options
2480        .get(REPARTITION_COLUMN_HINT_KEY)
2481        .map(|value| value.trim().to_string())
2482    else {
2483        return Ok(());
2484    };
2485
2486    ensure!(
2487        !column_name.is_empty(),
2488        InvalidPartitionRuleSnafu {
2489            reason: format!("{REPARTITION_COLUMN_HINT_KEY} expects exactly one column name"),
2490        }
2491    );
2492
2493    ensure!(
2494        !column_name.contains(','),
2495        InvalidPartitionRuleSnafu {
2496            reason: format!("{REPARTITION_COLUMN_HINT_KEY} expects exactly one column name"),
2497        }
2498    );
2499
2500    ensure!(
2501        partition_key_indices.is_empty(),
2502        InvalidPartitionRuleSnafu {
2503            reason: format!(
2504                "cannot set {REPARTITION_COLUMN_HINT_KEY} on a table with partition metadata"
2505            ),
2506        }
2507    );
2508
2509    column_name_to_index_map
2510        .get(&column_name)
2511        .context(ColumnNotFoundSnafu { msg: &column_name })?;
2512
2513    ensure!(
2514        column_name != time_index,
2515        InvalidPartitionRuleSnafu {
2516            reason: format!("cannot set {REPARTITION_COLUMN_HINT_KEY} to the time index column"),
2517        }
2518    );
2519
2520    table_options
2521        .extra_options
2522        .insert(REPARTITION_COLUMN_HINT_KEY.to_string(), column_name);
2523
2524    Ok(())
2525}
2526
2527/// Rejects DDL against read-only schemas and computed entity-graph tables.
2528fn ensure_table_writable(schema: &str, table: &str) -> Result<()> {
2529    ensure!(
2530        !is_readonly_schema(schema),
2531        SchemaReadOnlySnafu {
2532            name: schema.to_string()
2533        }
2534    );
2535    ensure!(
2536        !is_readonly_table(schema, table),
2537        TableReadOnlySnafu {
2538            name: table.to_string()
2539        }
2540    );
2541    Ok(())
2542}
2543
2544/// [`ensure_table_writable`] plus the definition guard for system-defined
2545/// tables: user CREATE, ALTER and RENAME-into are rejected so the canonical
2546/// schema cannot be squatted or mutated. DROP and TRUNCATE stay allowed — the
2547/// next INSERT recreates the table canonically, which is also the recovery
2548/// path if the canonical definition changes across an upgrade.
2549fn ensure_table_definition_writable(schema: &str, table: &str) -> Result<()> {
2550    ensure_table_writable(schema, table)?;
2551    ensure!(
2552        !is_ddl_reserved_table(schema, table),
2553        TableDdlReservedSnafu {
2554            name: table.to_string()
2555        }
2556    );
2557    Ok(())
2558}
2559
2560/// Whether `CAST(column AS Utf8)` yields a stable entity-id string — the
2561/// binary-backed and nested types do not, and without the DDL check the
2562/// failure would surface only when the graph is scanned.
2563fn has_stable_string_form(data_type: &datatypes::prelude::ConcreteDataType) -> bool {
2564    use datatypes::prelude::ConcreteDataType;
2565    !matches!(
2566        data_type,
2567        ConcreteDataType::Binary(_)
2568            | ConcreteDataType::Json(_)
2569            | ConcreteDataType::Vector(_)
2570            | ConcreteDataType::List(_)
2571            | ConcreteDataType::Struct(_)
2572            | ConcreteDataType::Dictionary(_)
2573            | ConcreteDataType::Null(_)
2574    )
2575}
2576
2577fn ensure_entity_column_renders_as_string(
2578    key: &str,
2579    col: &str,
2580    data_type: &datatypes::prelude::ConcreteDataType,
2581) -> Result<()> {
2582    ensure!(
2583        has_stable_string_form(data_type),
2584        InvalidSqlSnafu {
2585            err_msg: format!(
2586                "entity column `{col}` (option `{key}`) has type `{data_type}`, which cannot \
2587                 render as a string",
2588            ),
2589        }
2590    );
2591    Ok(())
2592}
2593
2594/// Validates `greptime.semantic.entity.<type>.{id|descriptive|scope}` options
2595/// against the table schema: every named column must exist (tag or field) and
2596/// have a stable string form — the derivation renders id, scope and
2597/// descriptive values as strings.
2598fn validate_entity_semantic_options(table_options: &TableOptions, schema: &Schema) -> Result<()> {
2599    for (key, value) in &table_options.extra_options {
2600        if parse_entity_option_key(key).is_none() {
2601            continue;
2602        };
2603        for col in &parse_entity_columns(value) {
2604            let column = schema
2605                .column_schema_by_name(col)
2606                .context(ColumnNotFoundSnafu { msg: col })?;
2607            ensure_entity_column_renders_as_string(key, col, &column.data_type)?;
2608        }
2609    }
2610    Ok(())
2611}
2612
2613fn find_partition_columns(partitions: &Option<Partitions>) -> Result<Vec<String>> {
2614    let columns = if let Some(partitions) = partitions {
2615        partitions
2616            .column_list
2617            .iter()
2618            .map(|x| x.value.clone())
2619            .collect::<Vec<_>>()
2620    } else {
2621        vec![]
2622    };
2623    Ok(columns)
2624}
2625
2626/// Parse [Partitions] into a group of partition entries.
2627///
2628/// Returns a list of [PartitionExpr], each of which defines a partition.
2629fn find_partition_entries(
2630    create_table: &CreateTableExpr,
2631    partitions: &Option<Partitions>,
2632    partition_columns: &[String],
2633    query_ctx: &QueryContextRef,
2634) -> Result<Vec<PartitionExpr>> {
2635    let Some(partitions) = partitions else {
2636        return Ok(vec![]);
2637    };
2638
2639    // extract concrete data type of partition columns
2640    let column_name_and_type = partition_columns
2641        .iter()
2642        .map(|pc| {
2643            let column = create_table
2644                .column_defs
2645                .iter()
2646                .find(|c| &c.name == pc)
2647                // unwrap is safe here because we have checked that partition columns are defined
2648                .unwrap();
2649            let column_name = &column.name;
2650            let data_type = ConcreteDataType::from(
2651                ColumnDataTypeWrapper::try_new(column.data_type, column.datatype_extension.clone())
2652                    .context(ColumnDataTypeSnafu)?,
2653            );
2654            Ok((column_name, data_type))
2655        })
2656        .collect::<Result<HashMap<_, _>>>()?;
2657
2658    // Transform parser expr to partition expr
2659    let mut partition_exprs = Vec::with_capacity(partitions.exprs.len());
2660    for partition in &partitions.exprs {
2661        let partition_expr =
2662            convert_one_expr(partition, &column_name_and_type, &query_ctx.timezone())?;
2663        partition_exprs.push(partition_expr);
2664    }
2665
2666    Ok(partition_exprs)
2667}
2668
2669fn column_name_and_type<'a>(
2670    partition_columns: &'a [&'a ColumnSchema],
2671) -> HashMap<&'a String, ConcreteDataType> {
2672    partition_columns
2673        .iter()
2674        .map(|column| (&column.name, column.data_type.clone()))
2675        .collect()
2676}
2677
2678fn validate_and_collect_partition_columns<'a>(
2679    column_names: &[String],
2680    column_schemas: &'a [ColumnSchema],
2681) -> Result<Vec<&'a ColumnSchema>> {
2682    let mut seen = HashSet::with_capacity(column_names.len());
2683    column_names
2684        .iter()
2685        .map(|column_name| {
2686            ensure!(
2687                seen.insert(column_name),
2688                InvalidPartitionRuleSnafu {
2689                    reason: format!("duplicate partition column '{}'", column_name)
2690                }
2691            );
2692            column_schemas
2693                .iter()
2694                .find(|column| &column.name == column_name)
2695                .with_context(|| ColumnNotFoundSnafu { msg: column_name })
2696        })
2697        .collect()
2698}
2699
2700fn ensure_partition_expr_columns_in_target(
2701    partition_exprs: &[PartitionExpr],
2702    target_partition_columns: &HashSet<&String>,
2703) -> Result<()> {
2704    for expr in partition_exprs {
2705        ensure_partition_operand_columns_in_target(&expr.lhs, target_partition_columns)?;
2706        ensure_partition_operand_columns_in_target(&expr.rhs, target_partition_columns)?;
2707    }
2708
2709    Ok(())
2710}
2711
2712fn ensure_partition_operand_columns_in_target(
2713    operand: &Operand,
2714    target_partition_columns: &HashSet<&String>,
2715) -> Result<()> {
2716    match operand {
2717        Operand::Column(column) => ensure!(
2718            target_partition_columns.contains(column),
2719            InvalidPartitionRuleSnafu {
2720                reason: format!(
2721                    "partition expression references column '{}' that is not in target partition columns",
2722                    column
2723                )
2724            }
2725        ),
2726        Operand::Expr(expr) => {
2727            ensure_partition_operand_columns_in_target(&expr.lhs, target_partition_columns)?;
2728            ensure_partition_operand_columns_in_target(&expr.rhs, target_partition_columns)?;
2729        }
2730        Operand::Value(_) => {}
2731    }
2732
2733    Ok(())
2734}
2735
2736fn convert_one_expr(
2737    expr: &Expr,
2738    column_name_and_type: &HashMap<&String, ConcreteDataType>,
2739    timezone: &Timezone,
2740) -> Result<PartitionExpr> {
2741    let Expr::BinaryOp { left, op, right } = expr else {
2742        return InvalidPartitionRuleSnafu {
2743            reason: "partition rule must be a binary expression",
2744        }
2745        .fail();
2746    };
2747
2748    let op =
2749        RestrictedOp::try_from_parser(&op.clone()).with_context(|| InvalidPartitionRuleSnafu {
2750            reason: format!("unsupported operator in partition expr {op}"),
2751        })?;
2752
2753    // convert leaf node.
2754    let (lhs, op, rhs) = match (left.as_ref(), right.as_ref()) {
2755        // col, val
2756        (Expr::Identifier(ident), Expr::Value(value)) => {
2757            let (column_name, data_type) = convert_identifier(ident, column_name_and_type)?;
2758            let value = convert_value(&value.value, data_type, timezone, None)?;
2759            (Operand::Column(column_name), op, Operand::Value(value))
2760        }
2761        (Expr::Identifier(ident), Expr::UnaryOp { op: unary_op, expr })
2762            if let Expr::Value(v) = &**expr =>
2763        {
2764            let (column_name, data_type) = convert_identifier(ident, column_name_and_type)?;
2765            let value = convert_value(&v.value, data_type, timezone, Some(*unary_op))?;
2766            (Operand::Column(column_name), op, Operand::Value(value))
2767        }
2768        // val, col
2769        (Expr::Value(value), Expr::Identifier(ident)) => {
2770            let (column_name, data_type) = convert_identifier(ident, column_name_and_type)?;
2771            let value = convert_value(&value.value, data_type, timezone, None)?;
2772            (Operand::Value(value), op, Operand::Column(column_name))
2773        }
2774        (Expr::UnaryOp { op: unary_op, expr }, Expr::Identifier(ident))
2775            if let Expr::Value(v) = &**expr =>
2776        {
2777            let (column_name, data_type) = convert_identifier(ident, column_name_and_type)?;
2778            let value = convert_value(&v.value, data_type, timezone, Some(*unary_op))?;
2779            (Operand::Value(value), op, Operand::Column(column_name))
2780        }
2781        (Expr::BinaryOp { .. }, Expr::BinaryOp { .. }) => {
2782            // sub-expr must against another sub-expr
2783            let lhs = convert_one_expr(left, column_name_and_type, timezone)?;
2784            let rhs = convert_one_expr(right, column_name_and_type, timezone)?;
2785            (Operand::Expr(lhs), op, Operand::Expr(rhs))
2786        }
2787        _ => {
2788            return InvalidPartitionRuleSnafu {
2789                reason: format!("invalid partition expr {expr}"),
2790            }
2791            .fail();
2792        }
2793    };
2794
2795    Ok(PartitionExpr::new(lhs, op, rhs))
2796}
2797
2798fn convert_identifier(
2799    ident: &Ident,
2800    column_name_and_type: &HashMap<&String, ConcreteDataType>,
2801) -> Result<(String, ConcreteDataType)> {
2802    let column_name = ident.value.clone();
2803    let data_type = column_name_and_type
2804        .get(&column_name)
2805        .cloned()
2806        .with_context(|| ColumnNotFoundSnafu { msg: &column_name })?;
2807    Ok((column_name, data_type))
2808}
2809
2810fn convert_value(
2811    value: &ParserValue,
2812    data_type: ConcreteDataType,
2813    timezone: &Timezone,
2814    unary_op: Option<UnaryOperator>,
2815) -> Result<Value> {
2816    sql_value_to_value(
2817        &ColumnSchema::new("<NONAME>", data_type, true),
2818        value,
2819        Some(timezone),
2820        unary_op,
2821        false,
2822    )
2823    .context(error::SqlCommonSnafu)
2824}
2825
2826#[cfg(feature = "enterprise")]
2827async fn execute_undrop_table(
2828    table_metadata_manager: &TableMetadataManagerRef,
2829    procedure_executor: &ProcedureExecutorRef,
2830    cache_invalidator: &CacheInvalidatorRef,
2831    table_name: TableName,
2832    query_context: QueryContextRef,
2833) -> Result<Output> {
2834    // Undropping restores a table definition and could resurrect a
2835    // pre-canonical shape of a DDL-reserved table; rejected like CREATE.
2836    ensure_table_definition_writable(&table_name.schema_name, &table_name.table_name)?;
2837
2838    let dropped = table_metadata_manager
2839        .get_dropped_table(&table_name)
2840        .await
2841        .context(TableMetadataManagerSnafu)?
2842        .with_context(|| TableNotFoundSnafu {
2843            table_name: table_name.to_string(),
2844        })?;
2845
2846    let executor_context = to_executor_context(query_context, TriggerReason::Manual);
2847    let request = SubmitDdlTaskRequest::new(DdlTask::new_undrop_table(dropped.table_id));
2848    procedure_executor
2849        .submit_ddl_task(executor_context, request)
2850        .await
2851        .context(error::ExecuteDdlSnafu)?;
2852
2853    if let Err(err) = cache_invalidator
2854        .invalidate(
2855            &Context::default(),
2856            &[
2857                CacheIdent::TableId(dropped.table_id),
2858                CacheIdent::TableName(table_name),
2859            ],
2860        )
2861        .await
2862    {
2863        warn!(
2864            "Failed to invalidate cache after restoring table '{}' (id={}): {}",
2865            dropped.table_name, dropped.table_id, err
2866        );
2867    }
2868
2869    Ok(Output::new_with_affected_rows(0))
2870}
2871
2872#[cfg(test)]
2873mod test {
2874    #[cfg(feature = "enterprise")]
2875    use std::sync::{Arc, Mutex};
2876    use std::time::Duration;
2877
2878    #[cfg(feature = "enterprise")]
2879    use api::v1::meta::{ProcedureDetailResponse, ReconcileRequest, ReconcileResponse};
2880    #[cfg(feature = "enterprise")]
2881    use common_meta::cache_invalidator::{CacheInvalidator, CacheInvalidatorRef};
2882    #[cfg(feature = "enterprise")]
2883    use common_meta::instruction::CacheIdent;
2884    #[cfg(feature = "enterprise")]
2885    use common_meta::key::table_route::TableRouteValue;
2886    #[cfg(feature = "enterprise")]
2887    use common_meta::key::test_utils::new_test_table_info_with_name;
2888    #[cfg(feature = "enterprise")]
2889    use common_meta::key::{TableMetadataManager, TableMetadataManagerRef};
2890    #[cfg(feature = "enterprise")]
2891    use common_meta::kv_backend::memory::MemoryKvBackend;
2892    #[cfg(feature = "enterprise")]
2893    use common_meta::procedure_executor::{
2894        ExecutorContext, ProcedureExecutor, ProcedureExecutorRef,
2895    };
2896    #[cfg(feature = "enterprise")]
2897    use common_meta::rpc::ddl::{DdlTask, SubmitDdlTaskRequest, SubmitDdlTaskResponse};
2898    #[cfg(feature = "enterprise")]
2899    use common_meta::rpc::procedure::{
2900        MigrateRegionRequest, MigrateRegionResponse, ProcedureStateResponse,
2901    };
2902    use session::context::{QueryContext, QueryContextBuilder};
2903    use sql::dialect::GreptimeDbDialect;
2904    use sql::parser::{ParseOptions, ParserContext};
2905    use sql::statements::statement::Statement;
2906    use sqlparser::parser::Parser;
2907
2908    use super::*;
2909    use crate::expr_helper;
2910
2911    #[cfg(feature = "enterprise")]
2912    #[derive(Default)]
2913    struct RecordingProcedureExecutor {
2914        requests: Mutex<Vec<SubmitDdlTaskRequest>>,
2915        contexts: Mutex<Vec<ExecutorContext>>,
2916        fail: bool,
2917    }
2918
2919    #[cfg(feature = "enterprise")]
2920    #[async_trait::async_trait]
2921    impl ProcedureExecutor for RecordingProcedureExecutor {
2922        async fn submit_ddl_task(
2923            &self,
2924            ctx: ExecutorContext,
2925            request: SubmitDdlTaskRequest,
2926        ) -> common_meta::error::Result<SubmitDdlTaskResponse> {
2927            self.contexts.lock().unwrap().push(ctx);
2928            self.requests.lock().unwrap().push(request);
2929            if self.fail {
2930                return common_meta::error::UnsupportedSnafu {
2931                    operation: "test submit failure",
2932                }
2933                .fail();
2934            }
2935            Ok(SubmitDdlTaskResponse::default())
2936        }
2937
2938        async fn migrate_region(
2939            &self,
2940            _: &ExecutorContext,
2941            _: MigrateRegionRequest,
2942        ) -> common_meta::error::Result<MigrateRegionResponse> {
2943            unimplemented!()
2944        }
2945        async fn reconcile(
2946            &self,
2947            _: &ExecutorContext,
2948            _: ReconcileRequest,
2949        ) -> common_meta::error::Result<ReconcileResponse> {
2950            unimplemented!()
2951        }
2952        async fn query_procedure_state(
2953            &self,
2954            _: &ExecutorContext,
2955            _: &str,
2956        ) -> common_meta::error::Result<ProcedureStateResponse> {
2957            unimplemented!()
2958        }
2959        async fn list_procedures(
2960            &self,
2961            _: &ExecutorContext,
2962        ) -> common_meta::error::Result<ProcedureDetailResponse> {
2963            unimplemented!()
2964        }
2965    }
2966
2967    #[cfg(feature = "enterprise")]
2968    #[derive(Default)]
2969    struct RecordingCacheInvalidator {
2970        invalidations: Mutex<Vec<Vec<CacheIdent>>>,
2971        fail: bool,
2972    }
2973
2974    #[cfg(feature = "enterprise")]
2975    #[async_trait::async_trait]
2976    impl CacheInvalidator for RecordingCacheInvalidator {
2977        async fn invalidate(
2978            &self,
2979            _: &Context,
2980            caches: &[CacheIdent],
2981        ) -> common_meta::error::Result<()> {
2982            self.invalidations.lock().unwrap().push(caches.to_vec());
2983            if self.fail {
2984                return common_meta::error::UnsupportedSnafu {
2985                    operation: "test cache failure",
2986                }
2987                .fail();
2988            }
2989            Ok(())
2990        }
2991
2992        fn invalidate_all(&self) -> common_meta::error::Result<()> {
2993            Ok(())
2994        }
2995    }
2996
2997    #[cfg(feature = "enterprise")]
2998    async fn dropped_table_manager(table_id: TableId, name: &TableName) -> TableMetadataManagerRef {
2999        let backend = Arc::new(MemoryKvBackend::default());
3000        let manager = Arc::new(TableMetadataManager::new(backend));
3001        let mut info = new_test_table_info_with_name(table_id, &name.table_name);
3002        info.catalog_name = name.catalog_name.clone();
3003        info.schema_name = name.schema_name.clone();
3004        let route = TableRouteValue::physical(vec![]);
3005        manager
3006            .create_table_metadata(info, route.clone(), HashMap::new())
3007            .await
3008            .unwrap();
3009        manager
3010            .delete_table_metadata(table_id, name, &route, &HashMap::new(), None)
3011            .await
3012            .unwrap();
3013        manager
3014    }
3015
3016    #[cfg(feature = "enterprise")]
3017    #[tokio::test]
3018    async fn test_undrop_table_execution_path() {
3019        let name = TableName::new("greptime", "public", "metrics");
3020        let manager = dropped_table_manager(42, &name).await;
3021        // A same-name live table must not redirect restoration to its ID.
3022        let mut live = new_test_table_info_with_name(99, "metrics");
3023        live.catalog_name = "greptime".to_string();
3024        live.schema_name = "public".to_string();
3025        manager
3026            .create_table_metadata(live, TableRouteValue::physical(vec![]), HashMap::new())
3027            .await
3028            .unwrap();
3029        let procedure = Arc::new(RecordingProcedureExecutor::default());
3030        let cache = Arc::new(RecordingCacheInvalidator::default());
3031
3032        execute_undrop_table(
3033            &manager,
3034            &(procedure.clone() as ProcedureExecutorRef),
3035            &(cache.clone() as CacheInvalidatorRef),
3036            name.clone(),
3037            QueryContext::arc(),
3038        )
3039        .await
3040        .unwrap();
3041
3042        let requests = procedure.requests.lock().unwrap();
3043        assert!(matches!(&requests[0].task, DdlTask::UndropTable(task) if task.table_id == 42));
3044        let contexts = procedure.contexts.lock().unwrap();
3045        assert_eq!(
3046            contexts[0].event_input.as_ref().map(|input| input.reason),
3047            Some(TriggerReason::Manual)
3048        );
3049        assert_eq!(
3050            cache.invalidations.lock().unwrap()[0],
3051            vec![CacheIdent::TableId(42), CacheIdent::TableName(name)]
3052        );
3053    }
3054
3055    #[cfg(feature = "enterprise")]
3056    #[tokio::test]
3057    async fn test_undrop_table_missing_tombstone_submits_nothing() {
3058        let manager = Arc::new(TableMetadataManager::new(Arc::new(
3059            MemoryKvBackend::default(),
3060        )));
3061        let procedure = Arc::new(RecordingProcedureExecutor::default());
3062        let cache = Arc::new(RecordingCacheInvalidator::default());
3063        let err = execute_undrop_table(
3064            &manager,
3065            &(procedure.clone() as ProcedureExecutorRef),
3066            &(cache as CacheInvalidatorRef),
3067            TableName::new("greptime", "public", "missing"),
3068            QueryContext::arc(),
3069        )
3070        .await
3071        .unwrap_err();
3072        assert!(matches!(err, crate::error::Error::TableNotFound { .. }));
3073        assert!(procedure.requests.lock().unwrap().is_empty());
3074    }
3075
3076    #[cfg(feature = "enterprise")]
3077    #[tokio::test]
3078    async fn test_undrop_table_submit_failure_does_not_invalidate() {
3079        let name = TableName::new("greptime", "public", "metrics");
3080        let manager = dropped_table_manager(42, &name).await;
3081        let procedure = Arc::new(RecordingProcedureExecutor {
3082            fail: true,
3083            ..Default::default()
3084        });
3085        let cache = Arc::new(RecordingCacheInvalidator::default());
3086        execute_undrop_table(
3087            &manager,
3088            &(procedure as ProcedureExecutorRef),
3089            &(cache.clone() as CacheInvalidatorRef),
3090            name,
3091            QueryContext::arc(),
3092        )
3093        .await
3094        .unwrap_err();
3095        assert!(cache.invalidations.lock().unwrap().is_empty());
3096    }
3097
3098    #[cfg(feature = "enterprise")]
3099    #[tokio::test]
3100    async fn test_undrop_table_cache_failure_returns_success_after_submit() {
3101        let name = TableName::new("greptime", "public", "metrics");
3102        let manager = dropped_table_manager(42, &name).await;
3103        let procedure = Arc::new(RecordingProcedureExecutor::default());
3104        let cache = Arc::new(RecordingCacheInvalidator {
3105            fail: true,
3106            ..Default::default()
3107        });
3108        execute_undrop_table(
3109            &manager,
3110            &(procedure.clone() as ProcedureExecutorRef),
3111            &(cache.clone() as CacheInvalidatorRef),
3112            name.clone(),
3113            QueryContext::arc(),
3114        )
3115        .await
3116        .unwrap();
3117        assert_eq!(procedure.requests.lock().unwrap().len(), 1);
3118        assert_eq!(
3119            cache.invalidations.lock().unwrap()[0],
3120            vec![CacheIdent::TableId(42), CacheIdent::TableName(name)]
3121        );
3122    }
3123
3124    #[test]
3125    fn test_parse_ddl_options() {
3126        let options = OptionMap::from([
3127            ("timeout".to_string(), "5m".to_string()),
3128            ("wait".to_string(), "false".to_string()),
3129        ]);
3130        let ddl_options = parse_ddl_options(&options).unwrap();
3131        assert!(!ddl_options.wait);
3132        assert_eq!(Duration::from_secs(300), ddl_options.timeout);
3133    }
3134
3135    #[test]
3136    fn test_validate_entity_semantic_options() {
3137        let schema = Schema::new(vec![
3138            ColumnSchema::new("service_name", ConcreteDataType::string_datatype(), true),
3139            ColumnSchema::new("host_id", ConcreteDataType::string_datatype(), true),
3140            ColumnSchema::new("value", ConcreteDataType::float64_datatype(), true),
3141            ColumnSchema::new("payload", ConcreteDataType::binary_datatype(), true),
3142        ]);
3143        let opts = |pairs: &[(&str, &str)]| TableOptions {
3144            extra_options: pairs
3145                .iter()
3146                .map(|(k, v)| (k.to_string(), v.to_string()))
3147                .collect(),
3148            ..Default::default()
3149        };
3150
3151        // Any existing column with a string form may be an id, tag or field.
3152        for pairs in [
3153            &[(
3154                "greptime.semantic.entity.process.id",
3155                "service_name,host_id",
3156            )][..],
3157            &[("greptime.semantic.entity.service.id", "value")][..],
3158        ] {
3159            assert!(validate_entity_semantic_options(&opts(pairs), &schema).is_ok());
3160        }
3161
3162        let missing = validate_entity_semantic_options(
3163            &opts(&[("greptime.semantic.entity.service.id", "nope")]),
3164            &schema,
3165        )
3166        .unwrap_err();
3167        assert!(matches!(missing, error::Error::ColumnNotFound { .. }));
3168
3169        let binary_id = validate_entity_semantic_options(
3170            &opts(&[("greptime.semantic.entity.service.id", "payload")]),
3171            &schema,
3172        )
3173        .unwrap_err();
3174        assert!(matches!(binary_id, error::Error::InvalidSql { .. }));
3175    }
3176
3177    #[test]
3178    fn test_validate_and_normalize_flow_options_empty() {
3179        assert!(
3180            validate_and_normalize_flow_options(HashMap::new(), None)
3181                .unwrap()
3182                .is_empty()
3183        );
3184    }
3185
3186    #[test]
3187    fn test_validate_and_normalize_flow_options_valid() {
3188        let options = HashMap::from([
3189            (DEFER_ON_MISSING_SOURCE_KEY.to_string(), "TRUE".to_string()),
3190            (
3191                FLOW_EXPERIMENTAL_ENABLE_INCREMENTAL_READ_KEY.to_string(),
3192                "FALSE".to_string(),
3193            ),
3194        ]);
3195
3196        assert_eq!(
3197            validate_and_normalize_flow_options(options, None).unwrap(),
3198            HashMap::from([
3199                (DEFER_ON_MISSING_SOURCE_KEY.to_string(), "true".to_string(),),
3200                (
3201                    FLOW_EXPERIMENTAL_ENABLE_INCREMENTAL_READ_KEY.to_string(),
3202                    "false".to_string(),
3203                )
3204            ])
3205        );
3206    }
3207
3208    #[test]
3209    fn test_validate_and_normalize_flow_options_unknown_option() {
3210        let err = validate_and_normalize_flow_options(
3211            HashMap::from([("foo".to_string(), "bar".to_string())]),
3212            None,
3213        )
3214        .unwrap_err();
3215
3216        assert!(
3217            err.to_string()
3218                .contains("unknown flow option 'foo', supported options:")
3219        );
3220    }
3221
3222    #[test]
3223    fn test_validate_and_normalize_flow_options_reserved_option() {
3224        let err = validate_and_normalize_flow_options(
3225            HashMap::from([(
3226                FlowType::FLOW_TYPE_KEY.to_string(),
3227                FlowType::BATCHING.to_string(),
3228            )]),
3229            None,
3230        )
3231        .unwrap_err();
3232
3233        assert!(
3234            err.to_string()
3235                .contains("flow option 'flow_type' is reserved for internal use")
3236        );
3237    }
3238
3239    #[test]
3240    fn test_validate_and_normalize_flow_options_invalid_bool() {
3241        let err = validate_and_normalize_flow_options(
3242            HashMap::from([(
3243                DEFER_ON_MISSING_SOURCE_KEY.to_string(),
3244                "not-a-bool".to_string(),
3245            )]),
3246            None,
3247        )
3248        .unwrap_err();
3249
3250        assert!(
3251            err.to_string()
3252                .contains("invalid flow option 'defer_on_missing_source': 'not-a-bool'")
3253        );
3254    }
3255
3256    #[test]
3257    fn test_validate_and_normalize_flow_options_rejects_redacted_invalid_input() {
3258        let sql = r"
3259CREATE FLOW task_6
3260SINK TO schema_1.table_1
3261WITH (access_key_id = [true])
3262AS
3263SELECT max(c1), min(c2) FROM schema_2.table_2;";
3264        let stmt =
3265            ParserContext::create_with_dialect(sql, &GreptimeDbDialect {}, ParseOptions::default())
3266                .unwrap()
3267                .pop()
3268                .unwrap();
3269
3270        let Statement::CreateFlow(create_flow) = stmt else {
3271            unreachable!()
3272        };
3273        let expr =
3274            expr_helper::to_create_flow_task_expr(create_flow, &QueryContext::arc()).unwrap();
3275        let err = validate_and_normalize_flow_options(expr.flow_options, None).unwrap_err();
3276
3277        assert!(
3278            err.to_string()
3279                .contains("unknown flow option 'access_key_id'")
3280        );
3281    }
3282
3283    // --- Schedule option tests ---
3284
3285    #[test]
3286    fn test_eval_interval_rejected_non_positive() {
3287        // Zero eval_interval should be rejected.
3288        let err = validate_and_normalize_flow_options(HashMap::new(), Some(0)).unwrap_err();
3289        assert!(err.to_string().contains("EVAL INTERVAL must be positive"));
3290
3291        // Negative eval_interval should be rejected.
3292        let err = validate_and_normalize_flow_options(HashMap::new(), Some(-5)).unwrap_err();
3293        assert!(err.to_string().contains("EVAL INTERVAL must be positive"));
3294
3295        // Positive eval_interval should be accepted.
3296        let result = validate_and_normalize_flow_options(HashMap::new(), Some(300));
3297        assert!(result.is_ok());
3298    }
3299
3300    #[test]
3301    fn test_schedule_and_internal_keys_rejected_as_unknown_options() {
3302        for key in [
3303            "eval_interval_anchor",
3304            "eval_interval_start",
3305            "eval_interval_missed_tick_policy",
3306            "eval_interval_catchup_max_runs",
3307            "eval_interval_catchup_max_lag",
3308            "__greptime_internal_eval_schedule",
3309        ] {
3310            let err = validate_and_normalize_flow_options(
3311                HashMap::from([(key.to_string(), "value".to_string())]),
3312                Some(300),
3313            )
3314            .unwrap_err();
3315
3316            assert!(
3317                err.to_string()
3318                    .contains(&format!("unknown flow option '{key}'")),
3319                "unexpected error for {key}: {err}"
3320            );
3321        }
3322    }
3323
3324    #[test]
3325    fn test_determine_flow_type_for_source_state_missing_sources_require_opt_in() {
3326        let err = determine_flow_type_for_source_state("my_flow", &HashMap::new(), true, false)
3327            .unwrap_err();
3328
3329        assert!(err.to_string().contains(
3330            "missing source tables for flow 'my_flow'; use WITH (defer_on_missing_source = true) to create a pending flow"
3331        ));
3332    }
3333
3334    #[test]
3335    fn test_determine_flow_type_for_source_state_missing_sources_prefer_batching() {
3336        let flow_options =
3337            HashMap::from([(DEFER_ON_MISSING_SOURCE_KEY.to_string(), "true".to_string())]);
3338
3339        assert_eq!(
3340            determine_flow_type_for_source_state("my_flow", &flow_options, true, true).unwrap(),
3341            Some(FlowType::Batching)
3342        );
3343    }
3344
3345    #[test]
3346    fn test_determine_flow_type_for_source_state_instant_ttl_without_missing_sources() {
3347        assert_eq!(
3348            determine_flow_type_for_source_state("my_flow", &HashMap::new(), false, true).unwrap(),
3349            Some(FlowType::Streaming)
3350        );
3351    }
3352
3353    #[test]
3354    fn test_name_is_match() {
3355        assert!(!NAME_PATTERN_REG.is_match("/adaf"));
3356        assert!(!NAME_PATTERN_REG.is_match("🈲"));
3357        assert!(NAME_PATTERN_REG.is_match("hello"));
3358        assert!(NAME_PATTERN_REG.is_match("test@"));
3359        assert!(!NAME_PATTERN_REG.is_match("@test"));
3360        assert!(NAME_PATTERN_REG.is_match("test#"));
3361        assert!(!NAME_PATTERN_REG.is_match("#test"));
3362        assert!(!NAME_PATTERN_REG.is_match("@"));
3363        assert!(!NAME_PATTERN_REG.is_match("#"));
3364    }
3365
3366    #[test]
3367    fn test_partition_expr_equivalence_with_swapped_operands() {
3368        let column_name = "device_id".to_string();
3369        let column_name_and_type =
3370            HashMap::from([(&column_name, ConcreteDataType::int32_datatype())]);
3371        let timezone = Timezone::from_tz_string("UTC").unwrap();
3372        let dialect = GreptimeDbDialect {};
3373
3374        let mut parser = Parser::new(&dialect)
3375            .try_with_sql("device_id < 100")
3376            .unwrap();
3377        let expr_left = parser.parse_expr().unwrap();
3378
3379        let mut parser = Parser::new(&dialect)
3380            .try_with_sql("100 > device_id")
3381            .unwrap();
3382        let expr_right = parser.parse_expr().unwrap();
3383
3384        let partition_left =
3385            convert_one_expr(&expr_left, &column_name_and_type, &timezone).unwrap();
3386        let partition_right =
3387            convert_one_expr(&expr_right, &column_name_and_type, &timezone).unwrap();
3388
3389        assert_eq!(partition_left, partition_right);
3390        assert!([partition_left.clone()].contains(&partition_right));
3391
3392        let mut physical_partition_exprs = vec![partition_left];
3393        let mut logical_partition_exprs = vec![partition_right];
3394        physical_partition_exprs.sort_unstable();
3395        logical_partition_exprs.sort_unstable();
3396        assert_eq!(physical_partition_exprs, logical_partition_exprs);
3397    }
3398
3399    #[test]
3400    fn test_repartition_target_partition_columns_are_overwrite_context() {
3401        let device_id = ColumnSchema::new("device_id", ConcreteDataType::int32_datatype(), true);
3402        let area = ColumnSchema::new("area", ConcreteDataType::string_datatype(), true);
3403        let existing_partition_columns = vec![&device_id];
3404        let target_partition_columns = vec![&device_id, &area];
3405        let existing_column_name_and_type = column_name_and_type(&existing_partition_columns);
3406        let target_column_name_and_type = column_name_and_type(&target_partition_columns);
3407        let timezone = Timezone::from_tz_string("UTC").unwrap();
3408        let dialect = GreptimeDbDialect {};
3409
3410        let mut parser = Parser::new(&dialect)
3411            .try_with_sql("device_id < 100 AND area < 'South'")
3412            .unwrap();
3413        let expr = parser.parse_expr().unwrap();
3414
3415        let err = convert_one_expr(&expr, &existing_column_name_and_type, &timezone).unwrap_err();
3416        assert!(err.to_string().contains("area"));
3417
3418        let partition_expr = convert_one_expr(&expr, &target_column_name_and_type, &timezone)
3419            .expect("target columns should overwrite the conversion context");
3420        let partition_expr = partition_expr.to_string();
3421        assert!(partition_expr.contains("device_id"));
3422        assert!(partition_expr.contains("area"));
3423        assert!(partition_expr.contains("South"));
3424    }
3425
3426    #[test]
3427    fn test_repartition_rejects_remaining_expr_outside_target_columns() {
3428        let device_id = "device_id".to_string();
3429        let area = "area".to_string();
3430        let timezone = Timezone::from_tz_string("UTC").unwrap();
3431        let column_name_and_type = HashMap::from([
3432            (&device_id, ConcreteDataType::int32_datatype()),
3433            (&area, ConcreteDataType::string_datatype()),
3434        ]);
3435        let dialect = GreptimeDbDialect {};
3436        let mut parser = Parser::new(&dialect)
3437            .try_with_sql("device_id >= 100")
3438            .unwrap();
3439        let remaining_old_expr = convert_one_expr(
3440            &parser.parse_expr().unwrap(),
3441            &column_name_and_type,
3442            &timezone,
3443        )
3444        .unwrap();
3445        let target_partition_columns = HashSet::from([&area]);
3446
3447        let err = ensure_partition_expr_columns_in_target(
3448            &[remaining_old_expr],
3449            &target_partition_columns,
3450        )
3451        .unwrap_err();
3452
3453        assert!(err.to_string().contains("device_id"));
3454        assert!(err.to_string().contains("target partition columns"));
3455    }
3456
3457    #[test]
3458    fn test_repartition_rejects_duplicate_target_partition_columns() {
3459        let device_id = ColumnSchema::new("device_id", ConcreteDataType::int32_datatype(), true);
3460        let column_schemas = vec![device_id];
3461        let target_partition_columns = vec!["device_id".to_string(), "device_id".to_string()];
3462
3463        let err =
3464            validate_and_collect_partition_columns(&target_partition_columns, &column_schemas)
3465                .unwrap_err();
3466
3467        assert!(err.to_string().contains("duplicate partition column"));
3468        assert!(err.to_string().contains("device_id"));
3469    }
3470
3471    fn create_expr_from_sql(sql: &str) -> CreateTableExpr {
3472        let result =
3473            ParserContext::create_with_dialect(sql, &GreptimeDbDialect {}, ParseOptions::default())
3474                .unwrap();
3475
3476        match &result[0] {
3477            Statement::CreateTable(create) => {
3478                expr_helper::create_to_expr(create, &QueryContext::arc()).unwrap()
3479            }
3480            _ => unreachable!(),
3481        }
3482    }
3483
3484    #[test]
3485    fn test_create_table_with_repartition_column_hint() {
3486        let expr = create_expr_from_sql(
3487            r"
3488CREATE TABLE metrics (
3489  host STRING,
3490  ts TIMESTAMP TIME INDEX,
3491  cpu DOUBLE,
3492  PRIMARY KEY(host)
3493)
3494WITH ('repartition.column.hint' = ' host ')",
3495        );
3496
3497        let table_info = create_table_info(&expr, vec![]).unwrap();
3498        assert_eq!(
3499            table_info
3500                .meta
3501                .options
3502                .extra_options
3503                .get(REPARTITION_COLUMN_HINT_KEY),
3504            Some(&"host".to_string())
3505        );
3506    }
3507
3508    #[test]
3509    fn test_create_table_info_rejects_non_timestamp_time_index() {
3510        let expr = CreateTableExpr {
3511            catalog_name: "greptime".to_string(),
3512            schema_name: "public".to_string(),
3513            table_name: "demo".to_string(),
3514            desc: String::new(),
3515            column_defs: vec![api::v1::ColumnDef {
3516                name: "host".to_string(),
3517                data_type: api::v1::ColumnDataType::String as i32,
3518                is_nullable: true,
3519                default_constraint: vec![],
3520                semantic_type: 0,
3521                comment: String::new(),
3522                datatype_extension: None,
3523                options: None,
3524            }],
3525            time_index: "host".to_string(),
3526            primary_keys: vec![],
3527            create_if_not_exists: false,
3528            table_options: HashMap::new(),
3529            table_id: None,
3530            engine: "mito".to_string(),
3531        };
3532
3533        let err = create_table_info(&expr, vec![]).unwrap_err();
3534        assert_eq!(
3535            common_error::ext::ErrorExt::status_code(&err),
3536            common_error::status_code::StatusCode::InvalidArguments
3537        );
3538    }
3539
3540    #[test]
3541    fn test_verify_alter_guards_entity_column_types() {
3542        let expr = create_expr_from_sql(
3543            "CREATE TABLE t (svc STRING, ts TIMESTAMP TIME INDEX) \
3544             WITH ('greptime.semantic.entity.service.id'='svc');",
3545        );
3546        let table_info = Arc::new(create_table_info(&expr, vec![]).unwrap());
3547        let alter = |kind| AlterTableExpr {
3548            catalog_name: "greptime".to_string(),
3549            schema_name: "public".to_string(),
3550            table_name: "t".to_string(),
3551            kind: Some(kind),
3552        };
3553
3554        let modify = alter(Kind::ModifyColumnTypes(api::v1::ModifyColumnTypes {
3555            modify_column_types: vec![api::v1::ModifyColumnType {
3556                column_name: "svc".to_string(),
3557                target_type: api::v1::ColumnDataType::Binary as i32,
3558                target_type_extension: None,
3559            }],
3560        }));
3561        let err = verify_alter(1, table_info.clone(), modify).unwrap_err();
3562        assert!(
3563            err.to_string().contains("cannot render as a string"),
3564            "{err}"
3565        );
3566
3567        // Dropping a declared column stays allowed: the derivation skips the
3568        // stale declaration.
3569        let drop = alter(Kind::DropColumns(api::v1::DropColumns {
3570            drop_columns: vec![api::v1::DropColumn {
3571                name: "svc".to_string(),
3572            }],
3573        }));
3574        assert!(verify_alter(1, table_info, drop).unwrap());
3575    }
3576
3577    #[test]
3578    fn test_json2_requires_append_mode() {
3579        let cases = [
3580            "CREATE TABLE monitor (payload JSON2, ts TIMESTAMP TIME INDEX);",
3581            "CREATE TABLE monitor (payload JSON2, ts TIMESTAMP TIME INDEX) WITH (append_mode='false');",
3582        ];
3583
3584        for sql in cases {
3585            let expr = create_expr_from_sql(sql);
3586            let err = create_table_info(&expr, vec![]).unwrap_err();
3587            assert!(
3588                err.to_string()
3589                    .contains("JSON2 column `payload` requires append_mode='true'"),
3590                "{err}"
3591            );
3592        }
3593
3594        let expr = create_expr_from_sql(
3595            "CREATE TABLE monitor (payload JSON2, ts TIMESTAMP TIME INDEX) WITH (append_mode='true');",
3596        );
3597        create_table_info(&expr, vec![]).unwrap();
3598    }
3599
3600    #[test]
3601    fn test_create_table_with_empty_repartition_column_hint() {
3602        let expr = create_expr_from_sql(
3603            r"
3604CREATE TABLE metrics (
3605  host STRING,
3606  ts TIMESTAMP TIME INDEX,
3607  cpu DOUBLE,
3608  PRIMARY KEY(host)
3609)
3610WITH ('repartition.column.hint' = '')",
3611        );
3612
3613        let err = create_table_info(&expr, vec![]).unwrap_err();
3614        assert!(
3615            err.to_string()
3616                .contains("repartition.column.hint expects exactly one column name")
3617        );
3618    }
3619
3620    #[test]
3621    fn test_create_table_with_multiple_repartition_column_hints() {
3622        let expr = create_expr_from_sql(
3623            r"
3624CREATE TABLE metrics (
3625  host STRING,
3626  region_id STRING,
3627  ts TIMESTAMP TIME INDEX,
3628  cpu DOUBLE,
3629  PRIMARY KEY(host)
3630)
3631WITH ('repartition.column.hint' = 'host,region_id')",
3632        );
3633
3634        let err = create_table_info(&expr, vec![]).unwrap_err();
3635        assert!(
3636            err.to_string()
3637                .contains("repartition.column.hint expects exactly one column name")
3638        );
3639    }
3640
3641    #[test]
3642    fn test_create_table_with_missing_repartition_column_hint() {
3643        let expr = create_expr_from_sql(
3644            r"
3645CREATE TABLE metrics (
3646  host STRING,
3647  ts TIMESTAMP TIME INDEX,
3648  cpu DOUBLE,
3649  PRIMARY KEY(host)
3650)
3651WITH ('repartition.column.hint' = 'region_id')",
3652        );
3653
3654        let err = create_table_info(&expr, vec![]).unwrap_err();
3655        assert!(
3656            err.to_string()
3657                .contains("Cannot find column by name: region")
3658        );
3659    }
3660
3661    #[test]
3662    fn test_create_table_with_time_index_repartition_column_hint() {
3663        let expr = create_expr_from_sql(
3664            r"
3665CREATE TABLE metrics (
3666  host STRING,
3667  ts TIMESTAMP TIME INDEX,
3668  cpu DOUBLE,
3669  PRIMARY KEY(host)
3670)
3671WITH ('repartition.column.hint' = 'ts')",
3672        );
3673
3674        let err = create_table_info(&expr, vec![]).unwrap_err();
3675        assert!(
3676            err.to_string()
3677                .contains("cannot set repartition.column.hint to the time index column")
3678        );
3679    }
3680
3681    #[test]
3682    fn test_create_partitioned_table_with_repartition_column_hint() {
3683        let expr = create_expr_from_sql(
3684            r"
3685CREATE TABLE metrics (
3686  host STRING,
3687  ts TIMESTAMP TIME INDEX,
3688  cpu DOUBLE,
3689  PRIMARY KEY(host)
3690)
3691WITH ('repartition.column.hint' = 'host')",
3692        );
3693
3694        let err = create_table_info(&expr, vec!["host".to_string()]).unwrap_err();
3695        assert!(
3696            err.to_string()
3697                .contains("cannot set repartition.column.hint on a table with partition metadata")
3698        );
3699    }
3700
3701    #[tokio::test]
3702    #[ignore = "TODO(ruihang): WIP new partition rule"]
3703    async fn test_parse_partitions() {
3704        common_telemetry::init_default_ut_logging();
3705        let cases = [
3706            (
3707                r"
3708CREATE TABLE rcx ( a INT, b STRING, c TIMESTAMP, TIME INDEX (c) )
3709PARTITION ON COLUMNS (b) (
3710  b < 'hz',
3711  b >= 'hz' AND b < 'sh',
3712  b >= 'sh'
3713)
3714ENGINE=mito",
3715                r#"[{"column_list":["b"],"value_list":["{\"Value\":{\"String\":\"hz\"}}"]},{"column_list":["b"],"value_list":["{\"Value\":{\"String\":\"sh\"}}"]},{"column_list":["b"],"value_list":["\"MaxValue\""]}]"#,
3716            ),
3717            (
3718                r"
3719CREATE TABLE rcx ( a INT, b STRING, c TIMESTAMP, TIME INDEX (c) )
3720PARTITION BY RANGE COLUMNS (b, a) (
3721  PARTITION r0 VALUES LESS THAN ('hz', 10),
3722  b < 'hz' AND a < 10,
3723  b >= 'hz' AND b < 'sh' AND a >= 10 AND a < 20,
3724  b >= 'sh' AND a >= 20
3725)
3726ENGINE=mito",
3727                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\""]}]"#,
3728            ),
3729        ];
3730        let ctx = QueryContextBuilder::default().build().into();
3731        for (sql, expected) in cases {
3732            let result = ParserContext::create_with_dialect(
3733                sql,
3734                &GreptimeDbDialect {},
3735                ParseOptions::default(),
3736            )
3737            .unwrap();
3738            match &result[0] {
3739                Statement::CreateTable(c) => {
3740                    let expr = expr_helper::create_to_expr(c, &QueryContext::arc()).unwrap();
3741                    let (partitions, _) =
3742                        parse_partitions(&expr, c.partitions.clone(), &ctx).unwrap();
3743                    let json = serde_json::to_string(&partitions).unwrap();
3744                    assert_eq!(json, expected);
3745                }
3746                _ => unreachable!(),
3747            }
3748        }
3749    }
3750}