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