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