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