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