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::{DEFAULT_CATALOG_NAME, DEFAULT_SCHEMA_NAME, is_readonly_schema};
35use common_catalog::{format_full_flow_name, format_full_table_name};
36use common_error::ext::BoxedError;
37use common_meta::cache_invalidator::Context;
38use common_meta::ddl::create_flow::{
39    DEFER_ON_MISSING_SOURCE_KEY, FLOW_EXPERIMENTAL_ENABLE_INCREMENTAL_READ_KEY, FlowType,
40};
41use common_meta::instruction::CacheIdent;
42use common_meta::key::schema_name::{SchemaName, SchemaNameKey};
43use common_meta::procedure_executor::ExecutorContext;
44#[cfg(feature = "enterprise")]
45use common_meta::rpc::ddl::trigger::CreateTriggerTask;
46#[cfg(feature = "enterprise")]
47use common_meta::rpc::ddl::trigger::DropTriggerTask;
48use common_meta::rpc::ddl::{
49    CreateFlowTask, DdlTask, DropFlowTask, DropViewTask, SubmitDdlTaskRequest,
50    SubmitDdlTaskResponse,
51};
52use common_query::Output;
53use common_recordbatch::{RecordBatch, RecordBatches};
54use common_sql::convert::sql_value_to_value;
55use common_telemetry::{debug, info, tracing, warn};
56use common_time::{Timestamp, Timezone};
57use datafusion_common::tree_node::TreeNodeVisitor;
58use datafusion_expr::LogicalPlan;
59use datatypes::prelude::ConcreteDataType;
60use datatypes::schema::{ColumnSchema, Schema};
61use datatypes::value::Value;
62use datatypes::vectors::{StringVector, VectorRef};
63use humantime::parse_duration;
64use partition::expr::{Operand, PartitionExpr, RestrictedOp};
65use partition::multi_dim::MultiDimPartitionRule;
66use query::parser::QueryStatement;
67use query::plan::extract_and_rewrite_full_table_names;
68use query::query_engine::DefaultSerializer;
69use query::sql::create_table_stmt;
70use session::context::QueryContextRef;
71use session::table_name::table_idents_to_full_name;
72use snafu::{OptionExt, ResultExt, ensure};
73use sql::parser::{ParseOptions, ParserContext};
74use sql::parsers::utils::is_tql;
75use sql::statements::OptionMap;
76#[cfg(feature = "enterprise")]
77use sql::statements::alter::trigger::AlterTrigger;
78use sql::statements::alter::{AlterDatabase, AlterTable, AlterTableOperation};
79#[cfg(feature = "enterprise")]
80use sql::statements::create::trigger::CreateTrigger;
81use sql::statements::create::{
82    CreateExternalTable, CreateFlow, CreateTable, CreateTableLike, CreateView, Partitions,
83};
84use sql::statements::statement::Statement;
85use sqlparser::ast::{Expr, Ident, UnaryOperator, Value as ParserValue};
86use store_api::metric_engine_consts::{LOGICAL_TABLE_METADATA_KEY, METRIC_ENGINE_NAME};
87use store_api::mito_engine_options::APPEND_MODE_KEY;
88use substrait::{DFLogicalSubstraitConvertor, SubstraitPlan};
89use table::TableRef;
90use table::dist_table::DistTable;
91use table::metadata::{self, TableId, TableInfo, TableMeta, TableType};
92use table::requests::{
93    AlterKind, AlterTableRequest, COMMENT_KEY, DDL_TIMEOUT, DDL_WAIT, REPARTITION_COLUMN_HINT_KEY,
94    TableOptions,
95};
96use table::table_name::TableName;
97use table::table_reference::TableReference;
98
99use crate::error::{
100    self, AlterExprToRequestSnafu, BuildDfLogicalPlanSnafu, CatalogSnafu, ColumnDataTypeSnafu,
101    ColumnNotFoundSnafu, ConvertSchemaSnafu, CreateLogicalTablesSnafu,
102    DeserializePartitionExprSnafu, EmptyDdlExprSnafu, ExternalSnafu, ExtractTableNamesSnafu,
103    FlowNotFoundSnafu, InvalidPartitionRuleSnafu, InvalidPartitionSnafu, InvalidSqlSnafu,
104    InvalidTableNameSnafu, InvalidViewNameSnafu, InvalidViewStmtSnafu, NotSupportedSnafu,
105    PartitionExprToPbSnafu, Result, SchemaInUseSnafu, SchemaNotFoundSnafu, SchemaReadOnlySnafu,
106    SerializePartitionExprSnafu, SubstraitCodecSnafu, TableAlreadyExistsSnafu,
107    TableMetadataManagerSnafu, TableNotFoundSnafu, UnrecognizedTableOptionSnafu,
108    ViewAlreadyExistsSnafu,
109};
110use crate::expr_helper::{self, RepartitionRequest, RepartitionSource};
111use crate::statement::StatementExecutor;
112use crate::statement::show::create_partitions_stmt;
113use crate::utils::{to_meta_query_context, to_meta_query_context_with_origin_frontend};
114
115#[derive(Debug, Clone, Copy)]
116struct DdlSubmitOptions {
117    wait: bool,
118    timeout: Duration,
119}
120
121const ALLOWED_FLOW_OPTIONS: [&str; 2] = [
122    DEFER_ON_MISSING_SOURCE_KEY,
123    FLOW_EXPERIMENTAL_ENABLE_INCREMENTAL_READ_KEY,
124];
125
126fn build_procedure_id_output(procedure_id: Vec<u8>) -> Result<Output> {
127    let procedure_id = String::from_utf8_lossy(&procedure_id).to_string();
128    let vector: VectorRef = Arc::new(StringVector::from(vec![procedure_id]));
129    let schema = Arc::new(Schema::new(vec![ColumnSchema::new(
130        "Procedure ID",
131        vector.data_type(),
132        false,
133    )]));
134    let batch =
135        RecordBatch::new(schema.clone(), vec![vector]).context(error::BuildRecordBatchSnafu)?;
136    let batches =
137        RecordBatches::try_new(schema, vec![batch]).context(error::BuildRecordBatchSnafu)?;
138    Ok(Output::new_with_record_batches(batches))
139}
140
141fn parse_ddl_options(options: &OptionMap) -> Result<DdlSubmitOptions> {
142    let wait = match options.get(DDL_WAIT) {
143        Some(value) => value.parse::<bool>().map_err(|_| {
144            InvalidSqlSnafu {
145                err_msg: format!("invalid DDL option '{DDL_WAIT}': '{value}'"),
146            }
147            .build()
148        })?,
149        None => SubmitDdlTaskRequest::default_wait(),
150    };
151
152    let timeout = match options.get(DDL_TIMEOUT) {
153        Some(value) => parse_duration(value).map_err(|err| {
154            InvalidSqlSnafu {
155                err_msg: format!("invalid DDL option '{DDL_TIMEOUT}': '{value}': {err}"),
156            }
157            .build()
158        })?,
159        None => SubmitDdlTaskRequest::default_timeout(),
160    };
161
162    Ok(DdlSubmitOptions { wait, timeout })
163}
164
165fn supported_flow_options() -> String {
166    ALLOWED_FLOW_OPTIONS.join(", ")
167}
168
169fn normalize_flow_bool_option(key: &str, value: &str) -> Result<String> {
170    value
171        .trim()
172        .to_ascii_lowercase()
173        .parse::<bool>()
174        .map(|value| value.to_string())
175        .map_err(|_| {
176            InvalidSqlSnafu {
177                err_msg: format!("invalid flow option '{key}': '{value}'"),
178            }
179            .build()
180        })
181}
182
183fn validate_and_normalize_flow_options(
184    options: HashMap<String, String>,
185    eval_interval: Option<i64>,
186) -> Result<HashMap<String, String>> {
187    // Reject non-positive eval_interval (zero or negative).
188    if let Some(secs) = eval_interval
189        && secs <= 0
190    {
191        return InvalidSqlSnafu {
192            err_msg: format!("EVAL INTERVAL must be positive, got {secs} seconds"),
193        }
194        .fail();
195    }
196
197    options
198        .into_iter()
199        .map(|(key, value)| {
200            if key == FlowType::FLOW_TYPE_KEY {
201                return InvalidSqlSnafu {
202                    err_msg: format!("flow option '{key}' is reserved for internal use"),
203                }
204                .fail();
205            }
206
207            let normalized_value = match key.as_str() {
208                DEFER_ON_MISSING_SOURCE_KEY | FLOW_EXPERIMENTAL_ENABLE_INCREMENTAL_READ_KEY => {
209                    normalize_flow_bool_option(&key, &value)?
210                }
211                _ => {
212                    return InvalidSqlSnafu {
213                        err_msg: format!(
214                            "unknown flow option '{key}', supported options: {}",
215                            supported_flow_options()
216                        ),
217                    }
218                    .fail();
219                }
220            };
221
222            Ok((key, normalized_value))
223        })
224        .collect()
225}
226
227fn determine_flow_type_for_source_state(
228    flow_name: &str,
229    flow_options: &HashMap<String, String>,
230    has_missing_source_table: bool,
231    has_instant_ttl_source_table: bool,
232) -> Result<Option<FlowType>> {
233    if has_missing_source_table {
234        let defer_on_missing_source = flow_options
235            .get(DEFER_ON_MISSING_SOURCE_KEY)
236            .is_some_and(|value| value == "true");
237        ensure!(
238            defer_on_missing_source,
239            InvalidSqlSnafu {
240                err_msg: format!(
241                    "missing source tables for flow '{}'; use WITH ({DEFER_ON_MISSING_SOURCE_KEY} = true) to create a pending flow",
242                    flow_name
243                )
244            }
245        );
246        info!(
247            "Flow `{}` is created as a pending batching flow because source tables are missing and defer_on_missing_source=true",
248            flow_name
249        );
250        return Ok(Some(FlowType::Batching));
251    }
252
253    if has_instant_ttl_source_table {
254        return Ok(Some(FlowType::Streaming));
255    }
256
257    Ok(None)
258}
259
260impl StatementExecutor {
261    pub fn catalog_manager(&self) -> CatalogManagerRef {
262        self.catalog_manager.clone()
263    }
264
265    #[tracing::instrument(skip_all)]
266    pub async fn create_table(&self, stmt: CreateTable, ctx: QueryContextRef) -> Result<TableRef> {
267        let (catalog, schema, _table) = table_idents_to_full_name(&stmt.name, &ctx)
268            .map_err(BoxedError::new)
269            .context(error::ExternalSnafu)?;
270
271        let schema_options = self
272            .table_metadata_manager
273            .schema_manager()
274            .get(SchemaNameKey {
275                catalog: &catalog,
276                schema: &schema,
277            })
278            .await
279            .context(TableMetadataManagerSnafu)?
280            .map(|v| v.into_inner());
281
282        let create_expr = &mut expr_helper::create_to_expr(&stmt, &ctx)?;
283        // Don't inherit schema-level TTL/compaction options into table options:
284        // TTL is applied during compaction, and `compaction.*` is handled separately.
285        if let Some(schema_options) = schema_options {
286            for (key, value) in schema_options.extra_options.iter() {
287                if key.starts_with("compaction.") {
288                    continue;
289                }
290                create_expr
291                    .table_options
292                    .entry(key.clone())
293                    .or_insert(value.clone());
294            }
295        }
296
297        self.create_table_inner(create_expr, stmt.partitions, ctx)
298            .await
299    }
300
301    #[tracing::instrument(skip_all)]
302    pub async fn create_table_like(
303        &self,
304        stmt: CreateTableLike,
305        ctx: QueryContextRef,
306    ) -> Result<TableRef> {
307        let (catalog, schema, table) = table_idents_to_full_name(&stmt.source_name, &ctx)
308            .map_err(BoxedError::new)
309            .context(error::ExternalSnafu)?;
310        let table_ref = self
311            .catalog_manager
312            .table(&catalog, &schema, &table, Some(&ctx))
313            .await
314            .context(CatalogSnafu)?
315            .context(TableNotFoundSnafu { table_name: &table })?;
316        let partition_info = self
317            .partition_manager
318            .find_physical_partition_info(table_ref.table_info().table_id())
319            .await
320            .context(error::FindTablePartitionRuleSnafu { table_name: table })?;
321
322        // CREATE TABLE LIKE also inherits database level options.
323        let schema_options = self
324            .table_metadata_manager
325            .schema_manager()
326            .get(SchemaNameKey {
327                catalog: &catalog,
328                schema: &schema,
329            })
330            .await
331            .context(TableMetadataManagerSnafu)?
332            .map(|v| v.into_inner());
333
334        let quote_style = ctx.quote_style();
335        let mut create_stmt =
336            create_table_stmt(&table_ref.table_info(), schema_options, quote_style)
337                .context(error::ParseQuerySnafu)?;
338        create_stmt.name = stmt.table_name;
339        create_stmt.if_not_exists = false;
340
341        let table_info = table_ref.table_info();
342        let partitions = create_partitions_stmt(&table_info, &partition_info.partitions)?.and_then(
343            |mut partitions| {
344                if !partitions.column_list.is_empty() {
345                    partitions.set_quote(quote_style);
346                    Some(partitions)
347                } else {
348                    None
349                }
350            },
351        );
352
353        let create_expr = &mut expr_helper::create_to_expr(&create_stmt, &ctx)?;
354        self.create_table_inner(create_expr, partitions, ctx).await
355    }
356
357    #[tracing::instrument(skip_all)]
358    pub async fn create_external_table(
359        &self,
360        create_expr: CreateExternalTable,
361        ctx: QueryContextRef,
362    ) -> Result<TableRef> {
363        let create_expr = &mut expr_helper::create_external_expr(create_expr, &ctx).await?;
364        self.create_table_inner(create_expr, None, ctx).await
365    }
366
367    #[tracing::instrument(skip_all)]
368    pub async fn create_table_inner(
369        &self,
370        create_table: &mut CreateTableExpr,
371        partitions: Option<Partitions>,
372        query_ctx: QueryContextRef,
373    ) -> Result<TableRef> {
374        ensure!(
375            !is_readonly_schema(&create_table.schema_name),
376            SchemaReadOnlySnafu {
377                name: create_table.schema_name.clone()
378            }
379        );
380
381        if create_table.engine == METRIC_ENGINE_NAME
382            && create_table
383                .table_options
384                .contains_key(LOGICAL_TABLE_METADATA_KEY)
385        {
386            if let Some(partitions) = partitions.as_ref()
387                && !partitions.exprs.is_empty()
388            {
389                self.validate_logical_table_partition_rule(create_table, partitions, &query_ctx)
390                    .await?;
391            }
392            // Create logical tables
393            self.create_logical_tables(std::slice::from_ref(create_table), query_ctx)
394                .await?
395                .into_iter()
396                .next()
397                .context(error::UnexpectedSnafu {
398                    violated: "expected to create logical tables",
399                })
400        } else {
401            // Create other normal table
402            self.create_non_logic_table(create_table, partitions, query_ctx)
403                .await
404        }
405    }
406
407    #[tracing::instrument(skip_all)]
408    pub async fn create_non_logic_table(
409        &self,
410        create_table: &mut CreateTableExpr,
411        partitions: Option<Partitions>,
412        query_ctx: QueryContextRef,
413    ) -> Result<TableRef> {
414        let _timer = crate::metrics::DIST_CREATE_TABLE.start_timer();
415
416        // Check if schema exists
417        let schema = self
418            .table_metadata_manager
419            .schema_manager()
420            .get(SchemaNameKey::new(
421                &create_table.catalog_name,
422                &create_table.schema_name,
423            ))
424            .await
425            .context(TableMetadataManagerSnafu)?;
426        ensure!(
427            schema.is_some(),
428            SchemaNotFoundSnafu {
429                schema_info: &create_table.schema_name,
430            }
431        );
432
433        // if table exists.
434        if let Some(table) = self
435            .catalog_manager
436            .table(
437                &create_table.catalog_name,
438                &create_table.schema_name,
439                &create_table.table_name,
440                Some(&query_ctx),
441            )
442            .await
443            .context(CatalogSnafu)?
444        {
445            return if create_table.create_if_not_exists {
446                Ok(table)
447            } else {
448                TableAlreadyExistsSnafu {
449                    table: format_full_table_name(
450                        &create_table.catalog_name,
451                        &create_table.schema_name,
452                        &create_table.table_name,
453                    ),
454                }
455                .fail()
456            };
457        }
458
459        ensure!(
460            NAME_PATTERN_REG.is_match(&create_table.table_name),
461            InvalidTableNameSnafu {
462                table_name: &create_table.table_name,
463            }
464        );
465
466        let table_name = TableName::new(
467            &create_table.catalog_name,
468            &create_table.schema_name,
469            &create_table.table_name,
470        );
471
472        let (partitions, partition_cols) = parse_partitions(create_table, partitions, &query_ctx)?;
473        let mut table_info = create_table_info(create_table, partition_cols)?;
474
475        let resp = self
476            .create_table_procedure(
477                create_table.clone(),
478                partitions,
479                table_info.clone(),
480                query_ctx,
481            )
482            .await?;
483
484        let table_id = resp
485            .table_ids
486            .into_iter()
487            .next()
488            .context(error::UnexpectedSnafu {
489                violated: "expected table_id",
490            })?;
491        info!("Successfully created table '{table_name}' with table id {table_id}");
492
493        table_info.ident.table_id = table_id;
494
495        let table_info = Arc::new(table_info);
496        create_table.table_id = Some(api::v1::TableId { id: table_id });
497
498        let table = DistTable::table(table_info);
499
500        Ok(table)
501    }
502
503    #[tracing::instrument(skip_all)]
504    pub async fn create_logical_tables(
505        &self,
506        create_table_exprs: &[CreateTableExpr],
507        query_context: QueryContextRef,
508    ) -> Result<Vec<TableRef>> {
509        let _timer = crate::metrics::DIST_CREATE_TABLES.start_timer();
510        ensure!(
511            !create_table_exprs.is_empty(),
512            EmptyDdlExprSnafu {
513                name: "create logic tables"
514            }
515        );
516
517        // Check table names
518        for create_table in create_table_exprs {
519            ensure!(
520                NAME_PATTERN_REG.is_match(&create_table.table_name),
521                InvalidTableNameSnafu {
522                    table_name: &create_table.table_name,
523                }
524            );
525        }
526
527        let raw_tables_info = create_table_exprs
528            .iter()
529            .map(|create| create_table_info(create, vec![]))
530            .collect::<Result<Vec<_>>>()?;
531        let tables_data = create_table_exprs
532            .iter()
533            .cloned()
534            .zip(raw_tables_info.iter().cloned())
535            .collect::<Vec<_>>();
536
537        let resp = self
538            .create_logical_tables_procedure(tables_data, query_context.clone())
539            .await?;
540
541        let table_ids = resp.table_ids;
542        ensure!(
543            table_ids.len() == raw_tables_info.len(),
544            CreateLogicalTablesSnafu {
545                reason: format!(
546                    "The number of tables is inconsistent with the expected number to be created, expected: {}, actual: {}",
547                    raw_tables_info.len(),
548                    table_ids.len()
549                )
550            }
551        );
552        info!("Successfully created logical tables: {:?}", table_ids);
553
554        // Reacquire table infos from catalog so logical tables inherit the latest partition
555        // metadata (e.g. partition_key_indices) from their physical tables.
556        // And the returned table info also included extra partition columns that are in physical table but not in logical table's create table expr
557        let mut tables_info = Vec::with_capacity(table_ids.len());
558        for (table_id, create_table) in table_ids.iter().zip(create_table_exprs.iter()) {
559            let table = self
560                .catalog_manager
561                .table(
562                    &create_table.catalog_name,
563                    &create_table.schema_name,
564                    &create_table.table_name,
565                    Some(&query_context),
566                )
567                .await
568                .context(CatalogSnafu)?
569                .with_context(|| TableNotFoundSnafu {
570                    table_name: format_full_table_name(
571                        &create_table.catalog_name,
572                        &create_table.schema_name,
573                        &create_table.table_name,
574                    ),
575                })?;
576
577            let table_info = table.table_info();
578            // Safety check: ensure we are returning the table info that matches the newly created table id.
579            ensure!(
580                table_info.table_id() == *table_id,
581                CreateLogicalTablesSnafu {
582                    reason: format!(
583                        "Table id mismatch after creation, expected {}, got {} for table {}",
584                        table_id,
585                        table_info.table_id(),
586                        format_full_table_name(
587                            &create_table.catalog_name,
588                            &create_table.schema_name,
589                            &create_table.table_name
590                        )
591                    )
592                }
593            );
594
595            tables_info.push(table_info);
596        }
597
598        Ok(tables_info.into_iter().map(DistTable::table).collect())
599    }
600
601    async fn validate_logical_table_partition_rule(
602        &self,
603        create_table: &CreateTableExpr,
604        partitions: &Partitions,
605        query_ctx: &QueryContextRef,
606    ) -> Result<()> {
607        let (_, mut logical_partition_exprs) =
608            parse_partitions_for_logical_validation(create_table, partitions, query_ctx)?;
609
610        let physical_table_name = create_table
611            .table_options
612            .get(LOGICAL_TABLE_METADATA_KEY)
613            .with_context(|| CreateLogicalTablesSnafu {
614                reason: format!(
615                    "expect `{LOGICAL_TABLE_METADATA_KEY}` option on creating logical table"
616                ),
617            })?;
618
619        let physical_table = self
620            .catalog_manager
621            .table(
622                &create_table.catalog_name,
623                &create_table.schema_name,
624                physical_table_name,
625                Some(query_ctx),
626            )
627            .await
628            .context(CatalogSnafu)?
629            .context(TableNotFoundSnafu {
630                table_name: physical_table_name.clone(),
631            })?;
632
633        let physical_table_info = physical_table.table_info();
634        let (partition_rule, _) = self
635            .partition_manager
636            .find_table_partition_rule(&physical_table_info)
637            .await
638            .context(error::FindTablePartitionRuleSnafu {
639                table_name: physical_table_name.clone(),
640            })?;
641
642        let multi_dim_rule = partition_rule
643            .as_ref()
644            .as_any()
645            .downcast_ref::<MultiDimPartitionRule>()
646            .context(InvalidPartitionRuleSnafu {
647                reason: "physical table partition rule is not range-based",
648            })?;
649
650        // TODO(ruihang): project physical partition exprs to logical partition column
651        let mut physical_partition_exprs = multi_dim_rule.exprs().to_vec();
652        logical_partition_exprs.sort_unstable();
653        physical_partition_exprs.sort_unstable();
654
655        ensure!(
656            physical_partition_exprs == logical_partition_exprs,
657            InvalidPartitionRuleSnafu {
658                reason: format!(
659                    "logical table partition rule must match the corresponding physical table's\n logical table partition exprs:\t\t {:?}\n physical table partition exprs:\t {:?}",
660                    logical_partition_exprs, physical_partition_exprs
661                ),
662            }
663        );
664
665        Ok(())
666    }
667
668    #[cfg(feature = "enterprise")]
669    #[tracing::instrument(skip_all)]
670    pub async fn create_trigger(
671        &self,
672        stmt: CreateTrigger,
673        query_context: QueryContextRef,
674    ) -> Result<Output> {
675        let expr = expr_helper::to_create_trigger_task_expr(stmt, &query_context)?;
676        self.create_trigger_inner(expr, query_context).await
677    }
678
679    #[cfg(feature = "enterprise")]
680    pub async fn create_trigger_inner(
681        &self,
682        expr: PbCreateTriggerExpr,
683        query_context: QueryContextRef,
684    ) -> Result<Output> {
685        self.create_trigger_procedure(expr, query_context).await?;
686        Ok(Output::new_with_affected_rows(0))
687    }
688
689    #[cfg(feature = "enterprise")]
690    async fn create_trigger_procedure(
691        &self,
692        expr: PbCreateTriggerExpr,
693        query_context: QueryContextRef,
694    ) -> Result<SubmitDdlTaskResponse> {
695        let task = CreateTriggerTask::try_from(PbCreateTriggerTask {
696            create_trigger: Some(expr),
697        })
698        .context(error::InvalidExprSnafu)?;
699
700        let request = SubmitDdlTaskRequest::new(
701            to_meta_query_context(query_context),
702            DdlTask::new_create_trigger(task),
703        );
704
705        self.procedure_executor
706            .submit_ddl_task(&ExecutorContext::default(), request)
707            .await
708            .context(error::ExecuteDdlSnafu)
709    }
710
711    #[tracing::instrument(skip_all)]
712    pub async fn create_flow(
713        &self,
714        stmt: CreateFlow,
715        query_context: QueryContextRef,
716    ) -> Result<Output> {
717        // TODO(ruihang): do some verification
718        let expr = expr_helper::to_create_flow_task_expr(stmt, &query_context)?;
719
720        self.create_flow_inner(expr, query_context).await
721    }
722
723    pub async fn create_flow_inner(
724        &self,
725        expr: CreateFlowExpr,
726        query_context: QueryContextRef,
727    ) -> Result<Output> {
728        self.create_flow_procedure(expr, query_context).await?;
729        Ok(Output::new_with_affected_rows(0))
730    }
731
732    async fn create_flow_procedure(
733        &self,
734        mut expr: CreateFlowExpr,
735        query_context: QueryContextRef,
736    ) -> Result<SubmitDdlTaskResponse> {
737        let eval_interval_secs = expr.eval_interval.as_ref().map(|e| e.seconds);
738
739        // Reject non-positive eval_interval (zero or negative).
740        if let Some(secs) = eval_interval_secs
741            && secs <= 0
742        {
743            return InvalidSqlSnafu {
744                err_msg: format!("EVAL INTERVAL must be positive, got {secs} seconds"),
745            }
746            .fail();
747        }
748
749        expr.flow_options =
750            validate_and_normalize_flow_options(expr.flow_options, eval_interval_secs)?;
751
752        let flow_type = self
753            .determine_flow_type(&expr, query_context.clone())
754            .await?;
755        info!("determined flow={} type: {:#?}", expr.flow_name, flow_type);
756
757        expr.flow_options
758            .insert(FlowType::FLOW_TYPE_KEY.to_string(), flow_type.to_string());
759
760        let task = CreateFlowTask::try_from(PbCreateFlowTask {
761            create_flow: Some(expr),
762        })
763        .context(error::InvalidExprSnafu)?;
764        let request = SubmitDdlTaskRequest::new(
765            to_meta_query_context(query_context),
766            DdlTask::new_create_flow(task),
767        );
768
769        self.procedure_executor
770            .submit_ddl_task(&ExecutorContext::default(), request)
771            .await
772            .context(error::ExecuteDdlSnafu)
773    }
774
775    /// Determine the flow type based on the SQL query
776    ///
777    /// If it contains aggregation or distinct, then it is a batch flow, otherwise it is a streaming flow
778    async fn determine_flow_type(
779        &self,
780        expr: &CreateFlowExpr,
781        query_ctx: QueryContextRef,
782    ) -> Result<FlowType> {
783        let mut has_missing_source_table = false;
784        let mut has_instant_ttl_source_table = false;
785
786        for src_table_name in &expr.source_table_names {
787            let table = self
788                .catalog_manager()
789                .table(
790                    &src_table_name.catalog_name,
791                    &src_table_name.schema_name,
792                    &src_table_name.table_name,
793                    Some(&query_ctx),
794                )
795                .await
796                .map_err(BoxedError::new)
797                .context(ExternalSnafu)?;
798
799            let Some(table) = table else {
800                has_missing_source_table = true;
801                continue;
802            };
803
804            if table.table_info().meta.options.ttl == Some(common_time::TimeToLive::Instant) {
805                warn!(
806                    "Source table `{}` for flow `{}`'s ttl=instant, fallback to streaming mode",
807                    format_full_table_name(
808                        &src_table_name.catalog_name,
809                        &src_table_name.schema_name,
810                        &src_table_name.table_name
811                    ),
812                    expr.flow_name
813                );
814                has_instant_ttl_source_table = true;
815            }
816        }
817
818        if let Some(flow_type) = determine_flow_type_for_source_state(
819            &expr.flow_name,
820            &expr.flow_options,
821            has_missing_source_table,
822            has_instant_ttl_source_table,
823        )? {
824            return Ok(flow_type);
825        }
826
827        let engine = &self.query_engine;
828        let stmts = ParserContext::create_with_dialect(
829            &expr.sql,
830            query_ctx.sql_dialect(),
831            ParseOptions::default(),
832        )
833        .map_err(BoxedError::new)
834        .context(ExternalSnafu)?;
835
836        ensure!(
837            stmts.len() == 1,
838            InvalidSqlSnafu {
839                err_msg: format!("Expect only one statement, found {}", stmts.len())
840            }
841        );
842        let stmt = &stmts[0];
843
844        if is_tql(query_ctx.sql_dialect(), &expr.sql)
845            .map_err(BoxedError::new)
846            .context(ExternalSnafu)?
847        {
848            return Ok(FlowType::Batching);
849        }
850
851        // support tql parse too
852        let plan = match stmt {
853            // prom ql is only supported in batching mode
854            Statement::Tql(_) => return Ok(FlowType::Batching),
855            _ => engine
856                .planner()
857                .plan(&QueryStatement::Sql(stmt.clone()), query_ctx)
858                .await
859                .map_err(BoxedError::new)
860                .context(ExternalSnafu)?,
861        };
862
863        /// Visitor to find aggregation or distinct
864        struct FindAggr {
865            is_aggr: bool,
866        }
867
868        impl TreeNodeVisitor<'_> for FindAggr {
869            type Node = LogicalPlan;
870            fn f_down(
871                &mut self,
872                node: &Self::Node,
873            ) -> datafusion_common::Result<datafusion_common::tree_node::TreeNodeRecursion>
874            {
875                match node {
876                    LogicalPlan::Aggregate(_) | LogicalPlan::Distinct(_) => {
877                        self.is_aggr = true;
878                        return Ok(datafusion_common::tree_node::TreeNodeRecursion::Stop);
879                    }
880                    _ => (),
881                }
882                Ok(datafusion_common::tree_node::TreeNodeRecursion::Continue)
883            }
884        }
885
886        let mut find_aggr = FindAggr { is_aggr: false };
887
888        plan.visit_with_subqueries(&mut find_aggr)
889            .context(BuildDfLogicalPlanSnafu)?;
890        if find_aggr.is_aggr {
891            Ok(FlowType::Batching)
892        } else {
893            Ok(FlowType::Streaming)
894        }
895    }
896
897    #[tracing::instrument(skip_all)]
898    pub async fn create_view(
899        &self,
900        create_view: CreateView,
901        ctx: QueryContextRef,
902    ) -> Result<TableRef> {
903        // convert input into logical plan
904        let logical_plan = match &*create_view.query {
905            Statement::Query(query) => {
906                self.plan(
907                    &QueryStatement::Sql(Statement::Query(query.clone())),
908                    ctx.clone(),
909                )
910                .await?
911            }
912            Statement::Tql(query) => self.plan_tql(query.clone(), &ctx).await?,
913            _ => {
914                return InvalidViewStmtSnafu {}.fail();
915            }
916        };
917        // Save the definition for `show create view`.
918        let definition = create_view.to_string();
919
920        // Save the columns in plan, it may changed when the schemas of tables in plan
921        // are altered.
922        let schema: Schema = logical_plan
923            .schema()
924            .clone()
925            .try_into()
926            .context(ConvertSchemaSnafu)?;
927        let plan_columns: Vec<_> = schema
928            .column_schemas()
929            .iter()
930            .map(|c| c.name.clone())
931            .collect();
932
933        let columns: Vec<_> = create_view
934            .columns
935            .iter()
936            .map(|ident| ident.to_string())
937            .collect();
938
939        // Validate columns
940        if !columns.is_empty() {
941            ensure!(
942                columns.len() == plan_columns.len(),
943                error::ViewColumnsMismatchSnafu {
944                    view_name: create_view.name.to_string(),
945                    expected: plan_columns.len(),
946                    actual: columns.len(),
947                }
948            );
949        }
950
951        // Extract the table names from the original plan
952        // and rewrite them as fully qualified names.
953        let (table_names, plan) = extract_and_rewrite_full_table_names(logical_plan, ctx.clone())
954            .context(ExtractTableNamesSnafu)?;
955
956        let table_names = table_names.into_iter().map(|t| t.into()).collect();
957
958        // TODO(dennis): we don't save the optimized plan yet,
959        // because there are some serialization issue with our own defined plan node (such as `MergeScanLogicalPlan`).
960        // When the issues are fixed, we can use the `optimized_plan` instead.
961        // let optimized_plan = self.optimize_logical_plan(logical_plan)?.unwrap_df_plan();
962
963        // encode logical plan
964        let encoded_plan = DFLogicalSubstraitConvertor
965            .encode(&plan, DefaultSerializer)
966            .context(SubstraitCodecSnafu)?;
967
968        let expr = expr_helper::to_create_view_expr(
969            create_view,
970            encoded_plan.to_vec(),
971            table_names,
972            columns,
973            plan_columns,
974            definition,
975            ctx.clone(),
976        )?;
977
978        // TODO(dennis): validate the logical plan
979        self.create_view_by_expr(expr, ctx).await
980    }
981
982    pub async fn create_view_by_expr(
983        &self,
984        expr: CreateViewExpr,
985        ctx: QueryContextRef,
986    ) -> Result<TableRef> {
987        ensure! {
988            !(expr.create_if_not_exists & expr.or_replace),
989            InvalidSqlSnafu {
990                err_msg: "syntax error Create Or Replace and If Not Exist cannot be used together",
991            }
992        };
993        let _timer = crate::metrics::DIST_CREATE_VIEW.start_timer();
994
995        let schema_exists = self
996            .table_metadata_manager
997            .schema_manager()
998            .exists(SchemaNameKey::new(&expr.catalog_name, &expr.schema_name))
999            .await
1000            .context(TableMetadataManagerSnafu)?;
1001
1002        ensure!(
1003            schema_exists,
1004            SchemaNotFoundSnafu {
1005                schema_info: &expr.schema_name,
1006            }
1007        );
1008
1009        // if view or table exists.
1010        if let Some(table) = self
1011            .catalog_manager
1012            .table(
1013                &expr.catalog_name,
1014                &expr.schema_name,
1015                &expr.view_name,
1016                Some(&ctx),
1017            )
1018            .await
1019            .context(CatalogSnafu)?
1020        {
1021            let table_type = table.table_info().table_type;
1022
1023            match (table_type, expr.create_if_not_exists, expr.or_replace) {
1024                (TableType::View, true, false) => {
1025                    return Ok(table);
1026                }
1027                (TableType::View, false, false) => {
1028                    return ViewAlreadyExistsSnafu {
1029                        name: format_full_table_name(
1030                            &expr.catalog_name,
1031                            &expr.schema_name,
1032                            &expr.view_name,
1033                        ),
1034                    }
1035                    .fail();
1036                }
1037                (TableType::View, _, true) => {
1038                    // Try to replace an exists view
1039                }
1040                _ => {
1041                    return TableAlreadyExistsSnafu {
1042                        table: format_full_table_name(
1043                            &expr.catalog_name,
1044                            &expr.schema_name,
1045                            &expr.view_name,
1046                        ),
1047                    }
1048                    .fail();
1049                }
1050            }
1051        }
1052
1053        ensure!(
1054            NAME_PATTERN_REG.is_match(&expr.view_name),
1055            InvalidViewNameSnafu {
1056                name: expr.view_name.clone(),
1057            }
1058        );
1059
1060        let view_name = TableName::new(&expr.catalog_name, &expr.schema_name, &expr.view_name);
1061
1062        let mut view_info = TableInfo {
1063            ident: metadata::TableIdent {
1064                // The view id of distributed table is assigned by Meta, set "0" here as a placeholder.
1065                table_id: 0,
1066                version: 0,
1067            },
1068            name: expr.view_name.clone(),
1069            desc: None,
1070            catalog_name: expr.catalog_name.clone(),
1071            schema_name: expr.schema_name.clone(),
1072            // The meta doesn't make sense for views, so using a default one.
1073            meta: TableMeta::empty(),
1074            table_type: TableType::View,
1075        };
1076
1077        let request = SubmitDdlTaskRequest::new(
1078            to_meta_query_context(ctx),
1079            DdlTask::new_create_view(expr, view_info.clone()),
1080        );
1081
1082        let resp = self
1083            .procedure_executor
1084            .submit_ddl_task(&ExecutorContext::default(), request)
1085            .await
1086            .context(error::ExecuteDdlSnafu)?;
1087
1088        debug!(
1089            "Submit creating view '{view_name}' task response: {:?}",
1090            resp
1091        );
1092
1093        let view_id = resp
1094            .table_ids
1095            .into_iter()
1096            .next()
1097            .context(error::UnexpectedSnafu {
1098                violated: "expected table_id",
1099            })?;
1100        info!("Successfully created view '{view_name}' with view id {view_id}");
1101
1102        view_info.ident.table_id = view_id;
1103
1104        let view_info = Arc::new(view_info);
1105
1106        let table = DistTable::table(view_info);
1107
1108        // Invalidates local cache ASAP.
1109        self.cache_invalidator
1110            .invalidate(
1111                &Context::default(),
1112                &[
1113                    CacheIdent::TableId(view_id),
1114                    CacheIdent::TableName(view_name.clone()),
1115                ],
1116            )
1117            .await
1118            .context(error::InvalidateTableCacheSnafu)?;
1119
1120        Ok(table)
1121    }
1122
1123    #[tracing::instrument(skip_all)]
1124    pub async fn drop_flow(
1125        &self,
1126        catalog_name: String,
1127        flow_name: String,
1128        drop_if_exists: bool,
1129        query_context: QueryContextRef,
1130    ) -> Result<Output> {
1131        if let Some(flow) = self
1132            .flow_metadata_manager
1133            .flow_name_manager()
1134            .get(&catalog_name, &flow_name)
1135            .await
1136            .context(error::TableMetadataManagerSnafu)?
1137        {
1138            let flow_id = flow.flow_id();
1139            let task = DropFlowTask {
1140                catalog_name,
1141                flow_name,
1142                flow_id,
1143                drop_if_exists,
1144            };
1145            self.drop_flow_procedure(task, query_context).await?;
1146
1147            Ok(Output::new_with_affected_rows(0))
1148        } else if drop_if_exists {
1149            Ok(Output::new_with_affected_rows(0))
1150        } else {
1151            FlowNotFoundSnafu {
1152                flow_name: format_full_flow_name(&catalog_name, &flow_name),
1153            }
1154            .fail()
1155        }
1156    }
1157
1158    async fn drop_flow_procedure(
1159        &self,
1160        expr: DropFlowTask,
1161        query_context: QueryContextRef,
1162    ) -> Result<SubmitDdlTaskResponse> {
1163        let request = SubmitDdlTaskRequest::new(
1164            to_meta_query_context(query_context),
1165            DdlTask::new_drop_flow(expr),
1166        );
1167
1168        self.procedure_executor
1169            .submit_ddl_task(&ExecutorContext::default(), request)
1170            .await
1171            .context(error::ExecuteDdlSnafu)
1172    }
1173
1174    #[cfg(feature = "enterprise")]
1175    #[tracing::instrument(skip_all)]
1176    pub(super) async fn drop_trigger(
1177        &self,
1178        catalog_name: String,
1179        trigger_name: String,
1180        drop_if_exists: bool,
1181        query_context: QueryContextRef,
1182    ) -> Result<Output> {
1183        let task = DropTriggerTask {
1184            catalog_name,
1185            trigger_name,
1186            drop_if_exists,
1187        };
1188        self.drop_trigger_procedure(task, query_context).await?;
1189        Ok(Output::new_with_affected_rows(0))
1190    }
1191
1192    #[cfg(feature = "enterprise")]
1193    async fn drop_trigger_procedure(
1194        &self,
1195        expr: DropTriggerTask,
1196        query_context: QueryContextRef,
1197    ) -> Result<SubmitDdlTaskResponse> {
1198        let request = SubmitDdlTaskRequest::new(
1199            to_meta_query_context(query_context),
1200            DdlTask::new_drop_trigger(expr),
1201        );
1202
1203        self.procedure_executor
1204            .submit_ddl_task(&ExecutorContext::default(), request)
1205            .await
1206            .context(error::ExecuteDdlSnafu)
1207    }
1208
1209    /// Drop a view
1210    #[tracing::instrument(skip_all)]
1211    pub(crate) async fn drop_view(
1212        &self,
1213        catalog: String,
1214        schema: String,
1215        view: String,
1216        drop_if_exists: bool,
1217        query_context: QueryContextRef,
1218    ) -> Result<Output> {
1219        let view_info = if let Some(view) = self
1220            .catalog_manager
1221            .table(&catalog, &schema, &view, None)
1222            .await
1223            .context(CatalogSnafu)?
1224        {
1225            view.table_info()
1226        } else if drop_if_exists {
1227            // DROP VIEW IF EXISTS meets view not found - ignored
1228            return Ok(Output::new_with_affected_rows(0));
1229        } else {
1230            return TableNotFoundSnafu {
1231                table_name: format_full_table_name(&catalog, &schema, &view),
1232            }
1233            .fail();
1234        };
1235
1236        // Ensure the exists one is view, we can't drop other table types
1237        ensure!(
1238            view_info.table_type == TableType::View,
1239            error::InvalidViewSnafu {
1240                msg: "not a view",
1241                view_name: format_full_table_name(&catalog, &schema, &view),
1242            }
1243        );
1244
1245        let view_id = view_info.table_id();
1246
1247        let task = DropViewTask {
1248            catalog,
1249            schema,
1250            view,
1251            view_id,
1252            drop_if_exists,
1253        };
1254
1255        self.drop_view_procedure(task, query_context).await?;
1256
1257        Ok(Output::new_with_affected_rows(0))
1258    }
1259
1260    /// Submit [DropViewTask] to procedure executor.
1261    async fn drop_view_procedure(
1262        &self,
1263        expr: DropViewTask,
1264        query_context: QueryContextRef,
1265    ) -> Result<SubmitDdlTaskResponse> {
1266        let request = SubmitDdlTaskRequest::new(
1267            to_meta_query_context(query_context),
1268            DdlTask::new_drop_view(expr),
1269        );
1270
1271        self.procedure_executor
1272            .submit_ddl_task(&ExecutorContext::default(), request)
1273            .await
1274            .context(error::ExecuteDdlSnafu)
1275    }
1276
1277    #[tracing::instrument(skip_all)]
1278    pub async fn alter_logical_tables(
1279        &self,
1280        alter_table_exprs: Vec<AlterTableExpr>,
1281        query_context: QueryContextRef,
1282    ) -> Result<Output> {
1283        let _timer = crate::metrics::DIST_ALTER_TABLES.start_timer();
1284        ensure!(
1285            !alter_table_exprs.is_empty(),
1286            EmptyDdlExprSnafu {
1287                name: "alter logical tables"
1288            }
1289        );
1290
1291        // group by physical table id
1292        let mut groups: HashMap<TableId, Vec<AlterTableExpr>> = HashMap::new();
1293        for expr in alter_table_exprs {
1294            // Get table_id from catalog_manager
1295            let catalog = if expr.catalog_name.is_empty() {
1296                query_context.current_catalog()
1297            } else {
1298                &expr.catalog_name
1299            };
1300            let schema = if expr.schema_name.is_empty() {
1301                query_context.current_schema()
1302            } else {
1303                expr.schema_name.clone()
1304            };
1305            let table_name = &expr.table_name;
1306            let table = self
1307                .catalog_manager
1308                .table(catalog, &schema, table_name, Some(&query_context))
1309                .await
1310                .context(CatalogSnafu)?
1311                .with_context(|| TableNotFoundSnafu {
1312                    table_name: format_full_table_name(catalog, &schema, table_name),
1313                })?;
1314            let table_id = table.table_info().ident.table_id;
1315            let physical_table_id = self
1316                .table_metadata_manager
1317                .table_route_manager()
1318                .get_physical_table_id(table_id)
1319                .await
1320                .context(TableMetadataManagerSnafu)?;
1321            groups.entry(physical_table_id).or_default().push(expr);
1322        }
1323
1324        // Submit procedure for each physical table
1325        let mut handles = Vec::with_capacity(groups.len());
1326        for (_physical_table_id, exprs) in groups {
1327            let fut = self.alter_logical_tables_procedure(exprs, query_context.clone());
1328            handles.push(fut);
1329        }
1330        let _results = futures::future::try_join_all(handles).await?;
1331
1332        Ok(Output::new_with_affected_rows(0))
1333    }
1334
1335    #[tracing::instrument(skip_all)]
1336    pub async fn drop_table(
1337        &self,
1338        table_name: TableName,
1339        drop_if_exists: bool,
1340        query_context: QueryContextRef,
1341    ) -> Result<Output> {
1342        // Reserved for grpc call
1343        self.drop_tables(&[table_name], drop_if_exists, query_context)
1344            .await
1345    }
1346
1347    #[tracing::instrument(skip_all)]
1348    pub async fn drop_tables(
1349        &self,
1350        table_names: &[TableName],
1351        drop_if_exists: bool,
1352        query_context: QueryContextRef,
1353    ) -> Result<Output> {
1354        let mut tables = Vec::with_capacity(table_names.len());
1355        for table_name in table_names {
1356            ensure!(
1357                !is_readonly_schema(&table_name.schema_name),
1358                SchemaReadOnlySnafu {
1359                    name: table_name.schema_name.clone()
1360                }
1361            );
1362
1363            if let Some(table) = self
1364                .catalog_manager
1365                .table(
1366                    &table_name.catalog_name,
1367                    &table_name.schema_name,
1368                    &table_name.table_name,
1369                    Some(&query_context),
1370                )
1371                .await
1372                .context(CatalogSnafu)?
1373            {
1374                tables.push(table.table_info().table_id());
1375            } else if drop_if_exists {
1376                // DROP TABLE IF EXISTS meets table not found - ignored
1377                continue;
1378            } else {
1379                return TableNotFoundSnafu {
1380                    table_name: table_name.to_string(),
1381                }
1382                .fail();
1383            }
1384        }
1385
1386        for (table_name, table_id) in table_names.iter().zip(tables.into_iter()) {
1387            self.drop_table_procedure(table_name, table_id, drop_if_exists, query_context.clone())
1388                .await?;
1389
1390            // Invalidates local cache ASAP.
1391            self.cache_invalidator
1392                .invalidate(
1393                    &Context::default(),
1394                    &[
1395                        CacheIdent::TableId(table_id),
1396                        CacheIdent::TableName(table_name.clone()),
1397                    ],
1398                )
1399                .await
1400                .context(error::InvalidateTableCacheSnafu)?;
1401        }
1402        Ok(Output::new_with_affected_rows(0))
1403    }
1404
1405    #[tracing::instrument(skip_all)]
1406    pub async fn drop_database(
1407        &self,
1408        catalog: String,
1409        schema: String,
1410        drop_if_exists: bool,
1411        query_context: QueryContextRef,
1412    ) -> Result<Output> {
1413        ensure!(
1414            !is_readonly_schema(&schema),
1415            SchemaReadOnlySnafu { name: schema }
1416        );
1417
1418        if self
1419            .catalog_manager
1420            .schema_exists(&catalog, &schema, None)
1421            .await
1422            .context(CatalogSnafu)?
1423        {
1424            if schema == query_context.current_schema() {
1425                SchemaInUseSnafu { name: schema }.fail()
1426            } else {
1427                self.drop_database_procedure(catalog, schema, drop_if_exists, query_context)
1428                    .await?;
1429
1430                Ok(Output::new_with_affected_rows(0))
1431            }
1432        } else if drop_if_exists {
1433            // DROP TABLE IF EXISTS meets table not found - ignored
1434            Ok(Output::new_with_affected_rows(0))
1435        } else {
1436            SchemaNotFoundSnafu {
1437                schema_info: schema,
1438            }
1439            .fail()
1440        }
1441    }
1442
1443    #[tracing::instrument(skip_all)]
1444    pub async fn truncate_table(
1445        &self,
1446        table_name: TableName,
1447        time_ranges: Vec<(Timestamp, Timestamp)>,
1448        query_context: QueryContextRef,
1449    ) -> Result<Output> {
1450        ensure!(
1451            !is_readonly_schema(&table_name.schema_name),
1452            SchemaReadOnlySnafu {
1453                name: table_name.schema_name.clone()
1454            }
1455        );
1456
1457        let table = self
1458            .catalog_manager
1459            .table(
1460                &table_name.catalog_name,
1461                &table_name.schema_name,
1462                &table_name.table_name,
1463                Some(&query_context),
1464            )
1465            .await
1466            .context(CatalogSnafu)?
1467            .with_context(|| TableNotFoundSnafu {
1468                table_name: table_name.to_string(),
1469            })?;
1470        let table_id = table.table_info().table_id();
1471        self.truncate_table_procedure(&table_name, table_id, time_ranges, query_context)
1472            .await?;
1473
1474        Ok(Output::new_with_affected_rows(0))
1475    }
1476
1477    #[tracing::instrument(skip_all)]
1478    pub async fn alter_table(
1479        &self,
1480        alter_table: AlterTable,
1481        query_context: QueryContextRef,
1482    ) -> Result<Output> {
1483        if matches!(
1484            alter_table.alter_operation(),
1485            AlterTableOperation::Repartition { .. } | AlterTableOperation::Partition { .. }
1486        ) {
1487            let request = expr_helper::to_repartition_request(alter_table, &query_context)?;
1488            return self.repartition_table(request, &query_context).await;
1489        }
1490
1491        let expr = expr_helper::to_alter_table_expr(alter_table, &query_context)?;
1492        self.alter_table_inner(expr, query_context).await
1493    }
1494
1495    #[tracing::instrument(skip_all)]
1496    pub async fn repartition_table(
1497        &self,
1498        request: RepartitionRequest,
1499        query_context: &QueryContextRef,
1500    ) -> Result<Output> {
1501        // Check if the schema is read-only.
1502        ensure!(
1503            !is_readonly_schema(&request.schema_name),
1504            SchemaReadOnlySnafu {
1505                name: request.schema_name.clone()
1506            }
1507        );
1508
1509        let table_ref = TableReference::full(
1510            &request.catalog_name,
1511            &request.schema_name,
1512            &request.table_name,
1513        );
1514        // Get the table from the catalog.
1515        let table = self
1516            .catalog_manager
1517            .table(
1518                &request.catalog_name,
1519                &request.schema_name,
1520                &request.table_name,
1521                Some(query_context),
1522            )
1523            .await
1524            .context(CatalogSnafu)?
1525            .with_context(|| TableNotFoundSnafu {
1526                table_name: table_ref.to_string(),
1527            })?;
1528        let table_id = table.table_info().ident.table_id;
1529        // Get existing partition expressions from the table route.
1530        let (physical_table_id, physical_table_route) = self
1531            .table_metadata_manager
1532            .table_route_manager()
1533            .get_physical_table_route(table_id)
1534            .await
1535            .context(TableMetadataManagerSnafu)?;
1536
1537        ensure!(
1538            physical_table_id == table_id,
1539            NotSupportedSnafu {
1540                feat: "REPARTITION on logical tables"
1541            }
1542        );
1543
1544        let table_info = table.table_info();
1545        let existing_partition_columns = table_info.meta.partition_columns().collect::<Vec<_>>();
1546        let column_schemas = table_info.meta.schema.column_schemas();
1547        // `REPARTITION ... ON COLUMNS` uses overwrite semantics: the provided
1548        // columns are the full target partition columns, not an extension of the
1549        // current ones. Therefore source expressions are converted with the
1550        // existing partition columns, while target expressions and the final
1551        // partition rule are validated against this effective target column set.
1552        let target_partition_columns = match &request.source {
1553            RepartitionSource::Partitions {
1554                target_partition_columns,
1555                ..
1556            } => {
1557                ensure!(
1558                    !existing_partition_columns.is_empty(),
1559                    InvalidPartitionRuleSnafu {
1560                        reason: format!(
1561                            "table {} does not have partition columns, cannot repartition",
1562                            table_ref
1563                        )
1564                    }
1565                );
1566
1567                if let Some(target_partition_columns) = target_partition_columns {
1568                    ensure!(
1569                        !target_partition_columns.is_empty(),
1570                        InvalidPartitionRuleSnafu {
1571                            reason: "ON COLUMNS requires at least one partition column"
1572                        }
1573                    );
1574                    validate_and_collect_partition_columns(
1575                        target_partition_columns,
1576                        column_schemas,
1577                    )?
1578                } else {
1579                    existing_partition_columns.clone()
1580                }
1581            }
1582            RepartitionSource::Unpartitioned { partition_columns } => {
1583                ensure!(
1584                    !partition_columns.is_empty(),
1585                    InvalidPartitionRuleSnafu {
1586                        reason: "PARTITION ON COLUMNS requires at least one partition column"
1587                    }
1588                );
1589                ensure!(
1590                    existing_partition_columns.is_empty(),
1591                    InvalidPartitionRuleSnafu {
1592                        reason: format!("table {} already has partition columns", table_ref)
1593                    }
1594                );
1595                partition_columns
1596                    .iter()
1597                    .map(|column_name| {
1598                        column_schemas
1599                            .iter()
1600                            .find(|column| &column.name == column_name)
1601                            .with_context(|| ColumnNotFoundSnafu { msg: column_name })
1602                    })
1603                    .collect::<Result<Vec<_>>>()?
1604            }
1605        };
1606
1607        let from_column_name_and_type = column_name_and_type(&existing_partition_columns);
1608        let target_column_name_and_type = column_name_and_type(&target_partition_columns);
1609        let target_partition_column_names = target_partition_columns
1610            .iter()
1611            .map(|column| column.name.clone())
1612            .collect::<Vec<_>>();
1613        let timezone = query_context.timezone();
1614        // Convert SQL Exprs to PartitionExprs.
1615        let from_partition_exprs = match &request.source {
1616            RepartitionSource::Partitions { from_exprs, .. } => from_exprs
1617                .iter()
1618                .map(|expr| convert_one_expr(expr, &from_column_name_and_type, &timezone))
1619                .collect::<Result<Vec<_>>>()?,
1620            RepartitionSource::Unpartitioned { .. } => vec![],
1621        };
1622
1623        let mut into_partition_exprs = request
1624            .into_exprs
1625            .iter()
1626            .map(|expr| convert_one_expr(expr, &target_column_name_and_type, &timezone))
1627            .collect::<Result<Vec<_>>>()?;
1628
1629        // `MERGE PARTITION` (and some `REPARTITION`) generates a single `OR` expression from
1630        // multiple source partitions; try to simplify it for better readability and stability.
1631        if matches!(&request.source, RepartitionSource::Partitions { .. })
1632            && from_partition_exprs.len() > 1
1633            && into_partition_exprs.len() == 1
1634            && let Some(expr) = into_partition_exprs.pop()
1635        {
1636            into_partition_exprs.push(partition::simplify::simplify_merged_partition_expr(expr));
1637        }
1638
1639        // Parse existing partition expressions from region routes.
1640        let mut existing_partition_exprs =
1641            Vec::with_capacity(physical_table_route.region_routes.len());
1642        for route in &physical_table_route.region_routes {
1643            let expr_json = route.region.partition_expr();
1644            if !expr_json.is_empty() {
1645                match PartitionExpr::from_json_str(&expr_json) {
1646                    Ok(Some(expr)) => existing_partition_exprs.push(expr),
1647                    Ok(None) => {
1648                        // Empty
1649                    }
1650                    Err(e) => {
1651                        return Err(e).context(DeserializePartitionExprSnafu);
1652                    }
1653                }
1654            }
1655        }
1656
1657        // Validate that from_partition_exprs are a subset of existing partition exprs.
1658        // We compare PartitionExpr directly since it implements Eq.
1659        if matches!(&request.source, RepartitionSource::Partitions { .. }) {
1660            for from_expr in &from_partition_exprs {
1661                ensure!(
1662                    existing_partition_exprs.contains(from_expr),
1663                    InvalidPartitionRuleSnafu {
1664                        reason: format!(
1665                            "partition expression '{}' does not exist in table {}",
1666                            from_expr, table_ref
1667                        )
1668                    }
1669                );
1670            }
1671        }
1672
1673        // Build the new partition expressions:
1674        // new_exprs = existing_exprs - from_exprs + into_exprs
1675        let new_partition_exprs: Vec<PartitionExpr> = match &request.source {
1676            RepartitionSource::Partitions { .. } => existing_partition_exprs
1677                .into_iter()
1678                .filter(|expr| !from_partition_exprs.contains(expr))
1679                .chain(into_partition_exprs.clone().into_iter())
1680                .collect(),
1681            RepartitionSource::Unpartitioned { .. } => into_partition_exprs.clone(),
1682        };
1683        ensure_partition_expr_columns_in_target(
1684            &new_partition_exprs,
1685            &target_partition_column_names.iter().collect(),
1686        )?;
1687        let new_partition_exprs_len = new_partition_exprs.len();
1688        let from_partition_exprs_len = from_partition_exprs.len();
1689
1690        // Validate the new partition expressions using MultiDimPartitionRule and PartitionChecker.
1691        let _ = MultiDimPartitionRule::try_new(
1692            target_partition_column_names,
1693            vec![],
1694            new_partition_exprs,
1695            true,
1696        )
1697        .context(InvalidPartitionSnafu)?;
1698
1699        let ddl_options = parse_ddl_options(&request.options)?;
1700        let serialize_exprs = |exprs: Vec<PartitionExpr>| -> Result<Vec<String>> {
1701            let mut json_exprs = Vec::with_capacity(exprs.len());
1702            for expr in exprs {
1703                json_exprs.push(expr.as_json_str().context(SerializePartitionExprSnafu)?);
1704            }
1705            Ok(json_exprs)
1706        };
1707        let from_partition_exprs_json = serialize_exprs(from_partition_exprs)?;
1708        let into_partition_exprs_json = serialize_exprs(into_partition_exprs)?;
1709        let source = match &request.source {
1710            RepartitionSource::Partitions {
1711                target_partition_columns,
1712                ..
1713            } => Source::PartitionExprs(PartitionedSource {
1714                exprs: from_partition_exprs_json,
1715                target_partition_columns: target_partition_columns
1716                    .clone()
1717                    .map(|columns| TargetPartitionColumns { columns }),
1718            }),
1719            RepartitionSource::Unpartitioned { partition_columns } => {
1720                Source::Unpartitioned(UnpartitionedSource {
1721                    partition_columns: partition_columns.clone(),
1722                })
1723            }
1724        };
1725        let repartition = Repartition {
1726            into_partition_exprs: into_partition_exprs_json,
1727            source: Some(source),
1728            ..Default::default()
1729        };
1730        let mut req = SubmitDdlTaskRequest::new(
1731            to_meta_query_context(query_context.clone()),
1732            DdlTask::new_alter_table(AlterTableExpr {
1733                catalog_name: request.catalog_name.clone(),
1734                schema_name: request.schema_name.clone(),
1735                table_name: request.table_name.clone(),
1736                kind: Some(Kind::Repartition(repartition)),
1737            }),
1738        );
1739        req.wait = ddl_options.wait;
1740        req.timeout = ddl_options.timeout;
1741
1742        info!(
1743            "Submitting repartition task for table {} (table_id={}), from {} to {} partitions, timeout: {:?}, wait: {}",
1744            table_ref,
1745            table_id,
1746            from_partition_exprs_len,
1747            new_partition_exprs_len,
1748            ddl_options.timeout,
1749            ddl_options.wait
1750        );
1751
1752        let response = self
1753            .procedure_executor
1754            .submit_ddl_task(&ExecutorContext::default(), req)
1755            .await
1756            .context(error::ExecuteDdlSnafu)?;
1757
1758        if !ddl_options.wait {
1759            return build_procedure_id_output(response.key);
1760        }
1761
1762        // Only invalidate cache if wait is true.
1763        let invalidate_keys = vec![
1764            CacheIdent::TableId(table_id),
1765            CacheIdent::TableName(TableName::new(
1766                request.catalog_name,
1767                request.schema_name,
1768                request.table_name,
1769            )),
1770        ];
1771
1772        // Invalidates local cache ASAP.
1773        self.cache_invalidator
1774            .invalidate(&Context::default(), &invalidate_keys)
1775            .await
1776            .context(error::InvalidateTableCacheSnafu)?;
1777
1778        Ok(Output::new_with_affected_rows(0))
1779    }
1780
1781    #[tracing::instrument(skip_all)]
1782    pub async fn alter_table_inner(
1783        &self,
1784        expr: AlterTableExpr,
1785        query_context: QueryContextRef,
1786    ) -> Result<Output> {
1787        ensure!(
1788            !is_readonly_schema(&expr.schema_name),
1789            SchemaReadOnlySnafu {
1790                name: expr.schema_name.clone()
1791            }
1792        );
1793
1794        let catalog_name = if expr.catalog_name.is_empty() {
1795            DEFAULT_CATALOG_NAME.to_string()
1796        } else {
1797            expr.catalog_name.clone()
1798        };
1799
1800        let schema_name = if expr.schema_name.is_empty() {
1801            DEFAULT_SCHEMA_NAME.to_string()
1802        } else {
1803            expr.schema_name.clone()
1804        };
1805
1806        let table_name = expr.table_name.clone();
1807
1808        let table = self
1809            .catalog_manager
1810            .table(
1811                &catalog_name,
1812                &schema_name,
1813                &table_name,
1814                Some(&query_context),
1815            )
1816            .await
1817            .context(CatalogSnafu)?
1818            .with_context(|| TableNotFoundSnafu {
1819                table_name: format_full_table_name(&catalog_name, &schema_name, &table_name),
1820            })?;
1821
1822        let table_id = table.table_info().ident.table_id;
1823        let need_alter = verify_alter(table_id, table.table_info(), expr.clone())?;
1824        if !need_alter {
1825            return Ok(Output::new_with_affected_rows(0));
1826        }
1827        info!(
1828            "Table info before alter is {:?}, expr: {:?}",
1829            table.table_info(),
1830            expr
1831        );
1832
1833        let physical_table_id = self
1834            .table_metadata_manager
1835            .table_route_manager()
1836            .get_physical_table_id(table_id)
1837            .await
1838            .context(TableMetadataManagerSnafu)?;
1839
1840        let (req, invalidate_keys) = if physical_table_id == table_id {
1841            // This is physical table
1842            let req = SubmitDdlTaskRequest::new(
1843                to_meta_query_context(query_context),
1844                DdlTask::new_alter_table(expr),
1845            );
1846
1847            let invalidate_keys = vec![
1848                CacheIdent::TableId(table_id),
1849                CacheIdent::TableName(TableName::new(catalog_name, schema_name, table_name)),
1850            ];
1851
1852            (req, invalidate_keys)
1853        } else {
1854            // This is logical table
1855            let req = SubmitDdlTaskRequest::new(
1856                to_meta_query_context(query_context),
1857                DdlTask::new_alter_logical_tables(vec![expr]),
1858            );
1859
1860            let mut invalidate_keys = vec![
1861                CacheIdent::TableId(physical_table_id),
1862                CacheIdent::TableId(table_id),
1863                CacheIdent::TableName(TableName::new(catalog_name, schema_name, table_name)),
1864            ];
1865
1866            let physical_table = self
1867                .table_metadata_manager
1868                .table_info_manager()
1869                .get(physical_table_id)
1870                .await
1871                .context(TableMetadataManagerSnafu)?
1872                .map(|x| x.into_inner());
1873            if let Some(physical_table) = physical_table {
1874                let physical_table_name = TableName::new(
1875                    physical_table.table_info.catalog_name,
1876                    physical_table.table_info.schema_name,
1877                    physical_table.table_info.name,
1878                );
1879                invalidate_keys.push(CacheIdent::TableName(physical_table_name));
1880            }
1881
1882            (req, invalidate_keys)
1883        };
1884
1885        self.procedure_executor
1886            .submit_ddl_task(&ExecutorContext::default(), req)
1887            .await
1888            .context(error::ExecuteDdlSnafu)?;
1889
1890        // Invalidates local cache ASAP.
1891        self.cache_invalidator
1892            .invalidate(&Context::default(), &invalidate_keys)
1893            .await
1894            .context(error::InvalidateTableCacheSnafu)?;
1895
1896        Ok(Output::new_with_affected_rows(0))
1897    }
1898
1899    #[cfg(feature = "enterprise")]
1900    #[tracing::instrument(skip_all)]
1901    pub async fn alter_trigger(
1902        &self,
1903        _alter_expr: AlterTrigger,
1904        _query_context: QueryContextRef,
1905    ) -> Result<Output> {
1906        crate::error::NotSupportedSnafu {
1907            feat: "alter trigger",
1908        }
1909        .fail()
1910    }
1911
1912    #[tracing::instrument(skip_all)]
1913    pub async fn alter_database(
1914        &self,
1915        alter_expr: AlterDatabase,
1916        query_context: QueryContextRef,
1917    ) -> Result<Output> {
1918        let alter_expr = expr_helper::to_alter_database_expr(alter_expr, &query_context)?;
1919        self.alter_database_inner(alter_expr, query_context).await
1920    }
1921
1922    #[tracing::instrument(skip_all)]
1923    pub async fn alter_database_inner(
1924        &self,
1925        alter_expr: AlterDatabaseExpr,
1926        query_context: QueryContextRef,
1927    ) -> Result<Output> {
1928        ensure!(
1929            !is_readonly_schema(&alter_expr.schema_name),
1930            SchemaReadOnlySnafu {
1931                name: query_context.current_schema().clone()
1932            }
1933        );
1934
1935        let exists = self
1936            .catalog_manager
1937            .schema_exists(&alter_expr.catalog_name, &alter_expr.schema_name, None)
1938            .await
1939            .context(CatalogSnafu)?;
1940        ensure!(
1941            exists,
1942            SchemaNotFoundSnafu {
1943                schema_info: alter_expr.schema_name,
1944            }
1945        );
1946
1947        let cache_ident = [CacheIdent::SchemaName(SchemaName {
1948            catalog_name: alter_expr.catalog_name.clone(),
1949            schema_name: alter_expr.schema_name.clone(),
1950        })];
1951
1952        self.alter_database_procedure(alter_expr, query_context)
1953            .await?;
1954
1955        // Invalidates local cache ASAP.
1956        self.cache_invalidator
1957            .invalidate(&Context::default(), &cache_ident)
1958            .await
1959            .context(error::InvalidateTableCacheSnafu)?;
1960
1961        Ok(Output::new_with_affected_rows(0))
1962    }
1963
1964    async fn create_table_procedure(
1965        &self,
1966        create_table: CreateTableExpr,
1967        partitions: Vec<PartitionExpr>,
1968        table_info: TableInfo,
1969        query_context: QueryContextRef,
1970    ) -> Result<SubmitDdlTaskResponse> {
1971        let partitions = partitions
1972            .into_iter()
1973            .map(|expr| expr.as_pb_partition().context(PartitionExprToPbSnafu))
1974            .collect::<Result<Vec<_>>>()?;
1975
1976        let request = SubmitDdlTaskRequest::new(
1977            to_meta_query_context_with_origin_frontend(query_context, &self.origin_frontend_addr),
1978            DdlTask::new_create_table(create_table, partitions, table_info),
1979        );
1980
1981        self.procedure_executor
1982            .submit_ddl_task(&ExecutorContext::default(), request)
1983            .await
1984            .context(error::ExecuteDdlSnafu)
1985    }
1986
1987    async fn create_logical_tables_procedure(
1988        &self,
1989        tables_data: Vec<(CreateTableExpr, TableInfo)>,
1990        query_context: QueryContextRef,
1991    ) -> Result<SubmitDdlTaskResponse> {
1992        let request = SubmitDdlTaskRequest::new(
1993            to_meta_query_context_with_origin_frontend(query_context, &self.origin_frontend_addr),
1994            DdlTask::new_create_logical_tables(tables_data),
1995        );
1996
1997        self.procedure_executor
1998            .submit_ddl_task(&ExecutorContext::default(), request)
1999            .await
2000            .context(error::ExecuteDdlSnafu)
2001    }
2002
2003    async fn alter_logical_tables_procedure(
2004        &self,
2005        tables_data: Vec<AlterTableExpr>,
2006        query_context: QueryContextRef,
2007    ) -> Result<SubmitDdlTaskResponse> {
2008        let request = SubmitDdlTaskRequest::new(
2009            to_meta_query_context(query_context),
2010            DdlTask::new_alter_logical_tables(tables_data),
2011        );
2012
2013        self.procedure_executor
2014            .submit_ddl_task(&ExecutorContext::default(), request)
2015            .await
2016            .context(error::ExecuteDdlSnafu)
2017    }
2018
2019    async fn drop_table_procedure(
2020        &self,
2021        table_name: &TableName,
2022        table_id: TableId,
2023        drop_if_exists: bool,
2024        query_context: QueryContextRef,
2025    ) -> Result<SubmitDdlTaskResponse> {
2026        let request = SubmitDdlTaskRequest::new(
2027            to_meta_query_context(query_context),
2028            DdlTask::new_drop_table(
2029                table_name.catalog_name.clone(),
2030                table_name.schema_name.clone(),
2031                table_name.table_name.clone(),
2032                table_id,
2033                drop_if_exists,
2034            ),
2035        );
2036
2037        self.procedure_executor
2038            .submit_ddl_task(&ExecutorContext::default(), request)
2039            .await
2040            .context(error::ExecuteDdlSnafu)
2041    }
2042
2043    async fn drop_database_procedure(
2044        &self,
2045        catalog: String,
2046        schema: String,
2047        drop_if_exists: bool,
2048        query_context: QueryContextRef,
2049    ) -> Result<SubmitDdlTaskResponse> {
2050        let request = SubmitDdlTaskRequest::new(
2051            to_meta_query_context(query_context),
2052            DdlTask::new_drop_database(catalog, schema, drop_if_exists),
2053        );
2054
2055        self.procedure_executor
2056            .submit_ddl_task(&ExecutorContext::default(), request)
2057            .await
2058            .context(error::ExecuteDdlSnafu)
2059    }
2060
2061    async fn alter_database_procedure(
2062        &self,
2063        alter_expr: AlterDatabaseExpr,
2064        query_context: QueryContextRef,
2065    ) -> Result<SubmitDdlTaskResponse> {
2066        let request = SubmitDdlTaskRequest::new(
2067            to_meta_query_context(query_context),
2068            DdlTask::new_alter_database(alter_expr),
2069        );
2070
2071        self.procedure_executor
2072            .submit_ddl_task(&ExecutorContext::default(), request)
2073            .await
2074            .context(error::ExecuteDdlSnafu)
2075    }
2076
2077    async fn truncate_table_procedure(
2078        &self,
2079        table_name: &TableName,
2080        table_id: TableId,
2081        time_ranges: Vec<(Timestamp, Timestamp)>,
2082        query_context: QueryContextRef,
2083    ) -> Result<SubmitDdlTaskResponse> {
2084        let request = SubmitDdlTaskRequest::new(
2085            to_meta_query_context(query_context),
2086            DdlTask::new_truncate_table(
2087                table_name.catalog_name.clone(),
2088                table_name.schema_name.clone(),
2089                table_name.table_name.clone(),
2090                table_id,
2091                time_ranges,
2092            ),
2093        );
2094
2095        self.procedure_executor
2096            .submit_ddl_task(&ExecutorContext::default(), request)
2097            .await
2098            .context(error::ExecuteDdlSnafu)
2099    }
2100
2101    #[tracing::instrument(skip_all)]
2102    pub async fn create_database(
2103        &self,
2104        database: &str,
2105        create_if_not_exists: bool,
2106        options: HashMap<String, String>,
2107        query_context: QueryContextRef,
2108    ) -> Result<Output> {
2109        let catalog = query_context.current_catalog();
2110        ensure!(
2111            NAME_PATTERN_REG.is_match(catalog),
2112            error::UnexpectedSnafu {
2113                violated: format!("Invalid catalog name: {}", catalog)
2114            }
2115        );
2116
2117        ensure!(
2118            NAME_PATTERN_REG.is_match(database),
2119            error::UnexpectedSnafu {
2120                violated: format!("Invalid database name: {}", database)
2121            }
2122        );
2123
2124        if !self
2125            .catalog_manager
2126            .schema_exists(catalog, database, None)
2127            .await
2128            .context(CatalogSnafu)?
2129            && !self.catalog_manager.is_reserved_schema_name(database)
2130        {
2131            self.create_database_procedure(
2132                catalog.to_string(),
2133                database.to_string(),
2134                create_if_not_exists,
2135                options,
2136                query_context,
2137            )
2138            .await?;
2139
2140            Ok(Output::new_with_affected_rows(1))
2141        } else if create_if_not_exists {
2142            Ok(Output::new_with_affected_rows(1))
2143        } else {
2144            error::SchemaExistsSnafu { name: database }.fail()
2145        }
2146    }
2147
2148    async fn create_database_procedure(
2149        &self,
2150        catalog: String,
2151        database: String,
2152        create_if_not_exists: bool,
2153        options: HashMap<String, String>,
2154        query_context: QueryContextRef,
2155    ) -> Result<SubmitDdlTaskResponse> {
2156        let request = SubmitDdlTaskRequest::new(
2157            to_meta_query_context(query_context),
2158            DdlTask::new_create_database(catalog, database, create_if_not_exists, options),
2159        );
2160
2161        self.procedure_executor
2162            .submit_ddl_task(&ExecutorContext::default(), request)
2163            .await
2164            .context(error::ExecuteDdlSnafu)
2165    }
2166}
2167
2168/// Parse partition statement [Partitions] into [PartitionExpr] and partition columns.
2169pub fn parse_partitions(
2170    create_table: &CreateTableExpr,
2171    partitions: Option<Partitions>,
2172    query_ctx: &QueryContextRef,
2173) -> Result<(Vec<PartitionExpr>, Vec<String>)> {
2174    // If partitions are not defined by user, use the timestamp column (which has to be existed) as
2175    // the partition column, and create only one partition.
2176    let partition_columns = find_partition_columns(&partitions)?;
2177    let partition_exprs =
2178        find_partition_entries(create_table, &partitions, &partition_columns, query_ctx)?;
2179
2180    // Validates partition
2181    let exprs = partition_exprs.clone();
2182    MultiDimPartitionRule::try_new(partition_columns.clone(), vec![], exprs, true)
2183        .context(InvalidPartitionSnafu)?;
2184
2185    Ok((partition_exprs, partition_columns))
2186}
2187
2188fn parse_partitions_for_logical_validation(
2189    create_table: &CreateTableExpr,
2190    partitions: &Partitions,
2191    query_ctx: &QueryContextRef,
2192) -> Result<(Vec<String>, Vec<PartitionExpr>)> {
2193    let partition_columns = partitions
2194        .column_list
2195        .iter()
2196        .map(|ident| ident.value.clone())
2197        .collect::<Vec<_>>();
2198
2199    let column_name_and_type = partition_columns
2200        .iter()
2201        .map(|pc| {
2202            let column = create_table
2203                .column_defs
2204                .iter()
2205                .find(|c| &c.name == pc)
2206                .context(ColumnNotFoundSnafu { msg: pc.clone() })?;
2207            let column_name = &column.name;
2208            let data_type = ConcreteDataType::from(
2209                ColumnDataTypeWrapper::try_new(column.data_type, column.datatype_extension.clone())
2210                    .context(ColumnDataTypeSnafu)?,
2211            );
2212            Ok((column_name, data_type))
2213        })
2214        .collect::<Result<HashMap<_, _>>>()?;
2215
2216    let mut partition_exprs = Vec::with_capacity(partitions.exprs.len());
2217    for expr in &partitions.exprs {
2218        let partition_expr = convert_one_expr(expr, &column_name_and_type, &query_ctx.timezone())?;
2219        partition_exprs.push(partition_expr);
2220    }
2221
2222    MultiDimPartitionRule::try_new(
2223        partition_columns.clone(),
2224        vec![],
2225        partition_exprs.clone(),
2226        true,
2227    )
2228    .context(InvalidPartitionSnafu)?;
2229
2230    Ok((partition_columns, partition_exprs))
2231}
2232
2233/// Verifies an alter and returns whether it is necessary to perform the alter.
2234///
2235/// # Returns
2236///
2237/// Returns true if the alter need to be porformed; otherwise, it returns false.
2238pub fn verify_alter(
2239    table_id: TableId,
2240    table_info: Arc<TableInfo>,
2241    expr: AlterTableExpr,
2242) -> Result<bool> {
2243    let request: AlterTableRequest =
2244        common_grpc_expr::alter_expr_to_request(table_id, expr, Some(&table_info.meta))
2245            .context(AlterExprToRequestSnafu)?;
2246
2247    let AlterTableRequest {
2248        table_name,
2249        alter_kind,
2250        ..
2251    } = &request;
2252
2253    if let AlterKind::RenameTable { new_table_name } = alter_kind {
2254        ensure!(
2255            NAME_PATTERN_REG.is_match(new_table_name),
2256            error::UnexpectedSnafu {
2257                violated: format!("Invalid table name: {}", new_table_name)
2258            }
2259        );
2260    } else if let AlterKind::AddColumns { columns } = alter_kind {
2261        // If all the columns are marked as add_if_not_exists and they already exist in the table,
2262        // there is no need to perform the alter.
2263        let column_names: HashSet<_> = table_info
2264            .meta
2265            .schema
2266            .column_schemas()
2267            .iter()
2268            .map(|schema| &schema.name)
2269            .collect();
2270        if columns.iter().all(|column| {
2271            column_names.contains(&column.column_schema.name) && column.add_if_not_exists
2272        }) {
2273            return Ok(false);
2274        }
2275    }
2276
2277    let new_meta = table_info
2278        .meta
2279        .builder_with_alter_kind(table_name, &request.alter_kind)
2280        .context(error::TableSnafu)?
2281        .build()
2282        .context(error::BuildTableMetaSnafu { table_name })?;
2283
2284    validate_json2_columns_append_mode(&new_meta.schema, &new_meta.options)?;
2285
2286    Ok(true)
2287}
2288
2289pub fn create_table_info(
2290    create_table: &CreateTableExpr,
2291    partition_columns: Vec<String>,
2292) -> Result<TableInfo> {
2293    let mut column_schemas = Vec::with_capacity(create_table.column_defs.len());
2294    let mut column_name_to_index_map = HashMap::new();
2295
2296    for (idx, column) in create_table.column_defs.iter().enumerate() {
2297        let schema =
2298            column_def::try_as_column_schema(column).context(error::InvalidColumnDefSnafu {
2299                column: &column.name,
2300            })?;
2301        let schema = schema.with_time_index(column.name == create_table.time_index);
2302
2303        column_schemas.push(schema);
2304        let _ = column_name_to_index_map.insert(column.name.clone(), idx);
2305    }
2306
2307    let next_column_id = column_schemas.len() as u32;
2308    let schema = Arc::new(Schema::new(column_schemas));
2309
2310    let primary_key_indices = create_table
2311        .primary_keys
2312        .iter()
2313        .map(|name| {
2314            column_name_to_index_map
2315                .get(name)
2316                .cloned()
2317                .context(ColumnNotFoundSnafu { msg: name })
2318        })
2319        .collect::<Result<Vec<_>>>()?;
2320
2321    let partition_key_indices = partition_columns
2322        .into_iter()
2323        .map(|col_name| {
2324            column_name_to_index_map
2325                .get(&col_name)
2326                .cloned()
2327                .context(ColumnNotFoundSnafu { msg: col_name })
2328        })
2329        .collect::<Result<Vec<_>>>()?;
2330
2331    let mut table_options = TableOptions::try_from_iter(&create_table.table_options)
2332        .context(UnrecognizedTableOptionSnafu)?;
2333
2334    validate_json2_columns_append_mode(&schema, &table_options)?;
2335
2336    validate_repartition_column_hint(
2337        &mut table_options,
2338        &column_name_to_index_map,
2339        &partition_key_indices,
2340        &create_table.time_index,
2341    )?;
2342
2343    let meta = TableMeta {
2344        schema,
2345        primary_key_indices,
2346        value_indices: vec![],
2347        engine: create_table.engine.clone(),
2348        next_column_id,
2349        options: table_options,
2350        created_on: Utc::now(),
2351        updated_on: Utc::now(),
2352        partition_key_indices,
2353        column_ids: vec![],
2354    };
2355
2356    let desc = if create_table.desc.is_empty() {
2357        create_table.table_options.get(COMMENT_KEY).cloned()
2358    } else {
2359        Some(create_table.desc.clone())
2360    };
2361
2362    let table_info = TableInfo {
2363        ident: metadata::TableIdent {
2364            // The table id of distributed table is assigned by Meta, set "0" here as a placeholder.
2365            table_id: 0,
2366            version: 0,
2367        },
2368        name: create_table.table_name.clone(),
2369        desc,
2370        catalog_name: create_table.catalog_name.clone(),
2371        schema_name: create_table.schema_name.clone(),
2372        meta,
2373        table_type: TableType::Base,
2374    };
2375    Ok(table_info)
2376}
2377
2378fn validate_json2_columns_append_mode(schema: &Schema, table_options: &TableOptions) -> Result<()> {
2379    let append_mode = table_options
2380        .extra_options
2381        .get(APPEND_MODE_KEY)
2382        .is_some_and(|value| value == "true");
2383
2384    for column in schema.column_schemas() {
2385        if column
2386            .data_type
2387            .as_json()
2388            .is_some_and(|json_type| json_type.is_json2())
2389        {
2390            ensure!(
2391                append_mode,
2392                InvalidSqlSnafu {
2393                    err_msg: format!(
2394                        "JSON2 column `{}` requires {}='true'",
2395                        column.name, APPEND_MODE_KEY
2396                    ),
2397                }
2398            );
2399        }
2400    }
2401
2402    Ok(())
2403}
2404
2405fn validate_repartition_column_hint(
2406    table_options: &mut TableOptions,
2407    column_name_to_index_map: &HashMap<String, usize>,
2408    partition_key_indices: &[usize],
2409    time_index: &str,
2410) -> Result<()> {
2411    let Some(column_name) = table_options
2412        .extra_options
2413        .get(REPARTITION_COLUMN_HINT_KEY)
2414        .map(|value| value.trim().to_string())
2415    else {
2416        return Ok(());
2417    };
2418
2419    ensure!(
2420        !column_name.is_empty(),
2421        InvalidPartitionRuleSnafu {
2422            reason: format!("{REPARTITION_COLUMN_HINT_KEY} expects exactly one column name"),
2423        }
2424    );
2425
2426    ensure!(
2427        !column_name.contains(','),
2428        InvalidPartitionRuleSnafu {
2429            reason: format!("{REPARTITION_COLUMN_HINT_KEY} expects exactly one column name"),
2430        }
2431    );
2432
2433    ensure!(
2434        partition_key_indices.is_empty(),
2435        InvalidPartitionRuleSnafu {
2436            reason: format!(
2437                "cannot set {REPARTITION_COLUMN_HINT_KEY} on a table with partition metadata"
2438            ),
2439        }
2440    );
2441
2442    column_name_to_index_map
2443        .get(&column_name)
2444        .context(ColumnNotFoundSnafu { msg: &column_name })?;
2445
2446    ensure!(
2447        column_name != time_index,
2448        InvalidPartitionRuleSnafu {
2449            reason: format!("cannot set {REPARTITION_COLUMN_HINT_KEY} to the time index column"),
2450        }
2451    );
2452
2453    table_options
2454        .extra_options
2455        .insert(REPARTITION_COLUMN_HINT_KEY.to_string(), column_name);
2456
2457    Ok(())
2458}
2459
2460fn find_partition_columns(partitions: &Option<Partitions>) -> Result<Vec<String>> {
2461    let columns = if let Some(partitions) = partitions {
2462        partitions
2463            .column_list
2464            .iter()
2465            .map(|x| x.value.clone())
2466            .collect::<Vec<_>>()
2467    } else {
2468        vec![]
2469    };
2470    Ok(columns)
2471}
2472
2473/// Parse [Partitions] into a group of partition entries.
2474///
2475/// Returns a list of [PartitionExpr], each of which defines a partition.
2476fn find_partition_entries(
2477    create_table: &CreateTableExpr,
2478    partitions: &Option<Partitions>,
2479    partition_columns: &[String],
2480    query_ctx: &QueryContextRef,
2481) -> Result<Vec<PartitionExpr>> {
2482    let Some(partitions) = partitions else {
2483        return Ok(vec![]);
2484    };
2485
2486    // extract concrete data type of partition columns
2487    let column_name_and_type = partition_columns
2488        .iter()
2489        .map(|pc| {
2490            let column = create_table
2491                .column_defs
2492                .iter()
2493                .find(|c| &c.name == pc)
2494                // unwrap is safe here because we have checked that partition columns are defined
2495                .unwrap();
2496            let column_name = &column.name;
2497            let data_type = ConcreteDataType::from(
2498                ColumnDataTypeWrapper::try_new(column.data_type, column.datatype_extension.clone())
2499                    .context(ColumnDataTypeSnafu)?,
2500            );
2501            Ok((column_name, data_type))
2502        })
2503        .collect::<Result<HashMap<_, _>>>()?;
2504
2505    // Transform parser expr to partition expr
2506    let mut partition_exprs = Vec::with_capacity(partitions.exprs.len());
2507    for partition in &partitions.exprs {
2508        let partition_expr =
2509            convert_one_expr(partition, &column_name_and_type, &query_ctx.timezone())?;
2510        partition_exprs.push(partition_expr);
2511    }
2512
2513    Ok(partition_exprs)
2514}
2515
2516fn column_name_and_type<'a>(
2517    partition_columns: &'a [&'a ColumnSchema],
2518) -> HashMap<&'a String, ConcreteDataType> {
2519    partition_columns
2520        .iter()
2521        .map(|column| (&column.name, column.data_type.clone()))
2522        .collect()
2523}
2524
2525fn validate_and_collect_partition_columns<'a>(
2526    column_names: &[String],
2527    column_schemas: &'a [ColumnSchema],
2528) -> Result<Vec<&'a ColumnSchema>> {
2529    let mut seen = HashSet::with_capacity(column_names.len());
2530    column_names
2531        .iter()
2532        .map(|column_name| {
2533            ensure!(
2534                seen.insert(column_name),
2535                InvalidPartitionRuleSnafu {
2536                    reason: format!("duplicate partition column '{}'", column_name)
2537                }
2538            );
2539            column_schemas
2540                .iter()
2541                .find(|column| &column.name == column_name)
2542                .with_context(|| ColumnNotFoundSnafu { msg: column_name })
2543        })
2544        .collect()
2545}
2546
2547fn ensure_partition_expr_columns_in_target(
2548    partition_exprs: &[PartitionExpr],
2549    target_partition_columns: &HashSet<&String>,
2550) -> Result<()> {
2551    for expr in partition_exprs {
2552        ensure_partition_operand_columns_in_target(&expr.lhs, target_partition_columns)?;
2553        ensure_partition_operand_columns_in_target(&expr.rhs, target_partition_columns)?;
2554    }
2555
2556    Ok(())
2557}
2558
2559fn ensure_partition_operand_columns_in_target(
2560    operand: &Operand,
2561    target_partition_columns: &HashSet<&String>,
2562) -> Result<()> {
2563    match operand {
2564        Operand::Column(column) => ensure!(
2565            target_partition_columns.contains(column),
2566            InvalidPartitionRuleSnafu {
2567                reason: format!(
2568                    "partition expression references column '{}' that is not in target partition columns",
2569                    column
2570                )
2571            }
2572        ),
2573        Operand::Expr(expr) => {
2574            ensure_partition_operand_columns_in_target(&expr.lhs, target_partition_columns)?;
2575            ensure_partition_operand_columns_in_target(&expr.rhs, target_partition_columns)?;
2576        }
2577        Operand::Value(_) => {}
2578    }
2579
2580    Ok(())
2581}
2582
2583fn convert_one_expr(
2584    expr: &Expr,
2585    column_name_and_type: &HashMap<&String, ConcreteDataType>,
2586    timezone: &Timezone,
2587) -> Result<PartitionExpr> {
2588    let Expr::BinaryOp { left, op, right } = expr else {
2589        return InvalidPartitionRuleSnafu {
2590            reason: "partition rule must be a binary expression",
2591        }
2592        .fail();
2593    };
2594
2595    let op =
2596        RestrictedOp::try_from_parser(&op.clone()).with_context(|| InvalidPartitionRuleSnafu {
2597            reason: format!("unsupported operator in partition expr {op}"),
2598        })?;
2599
2600    // convert leaf node.
2601    let (lhs, op, rhs) = match (left.as_ref(), right.as_ref()) {
2602        // col, val
2603        (Expr::Identifier(ident), Expr::Value(value)) => {
2604            let (column_name, data_type) = convert_identifier(ident, column_name_and_type)?;
2605            let value = convert_value(&value.value, data_type, timezone, None)?;
2606            (Operand::Column(column_name), op, Operand::Value(value))
2607        }
2608        (Expr::Identifier(ident), Expr::UnaryOp { op: unary_op, expr })
2609            if let Expr::Value(v) = &**expr =>
2610        {
2611            let (column_name, data_type) = convert_identifier(ident, column_name_and_type)?;
2612            let value = convert_value(&v.value, data_type, timezone, Some(*unary_op))?;
2613            (Operand::Column(column_name), op, Operand::Value(value))
2614        }
2615        // val, col
2616        (Expr::Value(value), Expr::Identifier(ident)) => {
2617            let (column_name, data_type) = convert_identifier(ident, column_name_and_type)?;
2618            let value = convert_value(&value.value, data_type, timezone, None)?;
2619            (Operand::Value(value), op, Operand::Column(column_name))
2620        }
2621        (Expr::UnaryOp { op: unary_op, expr }, Expr::Identifier(ident))
2622            if let Expr::Value(v) = &**expr =>
2623        {
2624            let (column_name, data_type) = convert_identifier(ident, column_name_and_type)?;
2625            let value = convert_value(&v.value, data_type, timezone, Some(*unary_op))?;
2626            (Operand::Value(value), op, Operand::Column(column_name))
2627        }
2628        (Expr::BinaryOp { .. }, Expr::BinaryOp { .. }) => {
2629            // sub-expr must against another sub-expr
2630            let lhs = convert_one_expr(left, column_name_and_type, timezone)?;
2631            let rhs = convert_one_expr(right, column_name_and_type, timezone)?;
2632            (Operand::Expr(lhs), op, Operand::Expr(rhs))
2633        }
2634        _ => {
2635            return InvalidPartitionRuleSnafu {
2636                reason: format!("invalid partition expr {expr}"),
2637            }
2638            .fail();
2639        }
2640    };
2641
2642    Ok(PartitionExpr::new(lhs, op, rhs))
2643}
2644
2645fn convert_identifier(
2646    ident: &Ident,
2647    column_name_and_type: &HashMap<&String, ConcreteDataType>,
2648) -> Result<(String, ConcreteDataType)> {
2649    let column_name = ident.value.clone();
2650    let data_type = column_name_and_type
2651        .get(&column_name)
2652        .cloned()
2653        .with_context(|| ColumnNotFoundSnafu { msg: &column_name })?;
2654    Ok((column_name, data_type))
2655}
2656
2657fn convert_value(
2658    value: &ParserValue,
2659    data_type: ConcreteDataType,
2660    timezone: &Timezone,
2661    unary_op: Option<UnaryOperator>,
2662) -> Result<Value> {
2663    sql_value_to_value(
2664        &ColumnSchema::new("<NONAME>", data_type, true),
2665        value,
2666        Some(timezone),
2667        unary_op,
2668        false,
2669    )
2670    .context(error::SqlCommonSnafu)
2671}
2672
2673#[cfg(test)]
2674mod test {
2675    use std::time::Duration;
2676
2677    use session::context::{QueryContext, QueryContextBuilder};
2678    use sql::dialect::GreptimeDbDialect;
2679    use sql::parser::{ParseOptions, ParserContext};
2680    use sql::statements::statement::Statement;
2681    use sqlparser::parser::Parser;
2682
2683    use super::*;
2684    use crate::expr_helper;
2685
2686    #[test]
2687    fn test_parse_ddl_options() {
2688        let options = OptionMap::from([
2689            ("timeout".to_string(), "5m".to_string()),
2690            ("wait".to_string(), "false".to_string()),
2691        ]);
2692        let ddl_options = parse_ddl_options(&options).unwrap();
2693        assert!(!ddl_options.wait);
2694        assert_eq!(Duration::from_secs(300), ddl_options.timeout);
2695    }
2696
2697    #[test]
2698    fn test_validate_and_normalize_flow_options_empty() {
2699        assert!(
2700            validate_and_normalize_flow_options(HashMap::new(), None)
2701                .unwrap()
2702                .is_empty()
2703        );
2704    }
2705
2706    #[test]
2707    fn test_validate_and_normalize_flow_options_valid() {
2708        let options = HashMap::from([
2709            (DEFER_ON_MISSING_SOURCE_KEY.to_string(), "TRUE".to_string()),
2710            (
2711                FLOW_EXPERIMENTAL_ENABLE_INCREMENTAL_READ_KEY.to_string(),
2712                "FALSE".to_string(),
2713            ),
2714        ]);
2715
2716        assert_eq!(
2717            validate_and_normalize_flow_options(options, None).unwrap(),
2718            HashMap::from([
2719                (DEFER_ON_MISSING_SOURCE_KEY.to_string(), "true".to_string(),),
2720                (
2721                    FLOW_EXPERIMENTAL_ENABLE_INCREMENTAL_READ_KEY.to_string(),
2722                    "false".to_string(),
2723                )
2724            ])
2725        );
2726    }
2727
2728    #[test]
2729    fn test_validate_and_normalize_flow_options_unknown_option() {
2730        let err = validate_and_normalize_flow_options(
2731            HashMap::from([("foo".to_string(), "bar".to_string())]),
2732            None,
2733        )
2734        .unwrap_err();
2735
2736        assert!(
2737            err.to_string()
2738                .contains("unknown flow option 'foo', supported options:")
2739        );
2740    }
2741
2742    #[test]
2743    fn test_validate_and_normalize_flow_options_reserved_option() {
2744        let err = validate_and_normalize_flow_options(
2745            HashMap::from([(
2746                FlowType::FLOW_TYPE_KEY.to_string(),
2747                FlowType::BATCHING.to_string(),
2748            )]),
2749            None,
2750        )
2751        .unwrap_err();
2752
2753        assert!(
2754            err.to_string()
2755                .contains("flow option 'flow_type' is reserved for internal use")
2756        );
2757    }
2758
2759    #[test]
2760    fn test_validate_and_normalize_flow_options_invalid_bool() {
2761        let err = validate_and_normalize_flow_options(
2762            HashMap::from([(
2763                DEFER_ON_MISSING_SOURCE_KEY.to_string(),
2764                "not-a-bool".to_string(),
2765            )]),
2766            None,
2767        )
2768        .unwrap_err();
2769
2770        assert!(
2771            err.to_string()
2772                .contains("invalid flow option 'defer_on_missing_source': 'not-a-bool'")
2773        );
2774    }
2775
2776    #[test]
2777    fn test_validate_and_normalize_flow_options_rejects_redacted_invalid_input() {
2778        let sql = r"
2779CREATE FLOW task_6
2780SINK TO schema_1.table_1
2781WITH (access_key_id = [true])
2782AS
2783SELECT max(c1), min(c2) FROM schema_2.table_2;";
2784        let stmt =
2785            ParserContext::create_with_dialect(sql, &GreptimeDbDialect {}, ParseOptions::default())
2786                .unwrap()
2787                .pop()
2788                .unwrap();
2789
2790        let Statement::CreateFlow(create_flow) = stmt else {
2791            unreachable!()
2792        };
2793        let expr =
2794            expr_helper::to_create_flow_task_expr(create_flow, &QueryContext::arc()).unwrap();
2795        let err = validate_and_normalize_flow_options(expr.flow_options, None).unwrap_err();
2796
2797        assert!(
2798            err.to_string()
2799                .contains("unknown flow option 'access_key_id'")
2800        );
2801    }
2802
2803    // --- Schedule option tests ---
2804
2805    #[test]
2806    fn test_eval_interval_rejected_non_positive() {
2807        // Zero eval_interval should be rejected.
2808        let err = validate_and_normalize_flow_options(HashMap::new(), Some(0)).unwrap_err();
2809        assert!(err.to_string().contains("EVAL INTERVAL must be positive"));
2810
2811        // Negative eval_interval should be rejected.
2812        let err = validate_and_normalize_flow_options(HashMap::new(), Some(-5)).unwrap_err();
2813        assert!(err.to_string().contains("EVAL INTERVAL must be positive"));
2814
2815        // Positive eval_interval should be accepted.
2816        let result = validate_and_normalize_flow_options(HashMap::new(), Some(300));
2817        assert!(result.is_ok());
2818    }
2819
2820    #[test]
2821    fn test_schedule_and_internal_keys_rejected_as_unknown_options() {
2822        for key in [
2823            "eval_interval_anchor",
2824            "eval_interval_start",
2825            "eval_interval_missed_tick_policy",
2826            "eval_interval_catchup_max_runs",
2827            "eval_interval_catchup_max_lag",
2828            "__greptime_internal_eval_schedule",
2829        ] {
2830            let err = validate_and_normalize_flow_options(
2831                HashMap::from([(key.to_string(), "value".to_string())]),
2832                Some(300),
2833            )
2834            .unwrap_err();
2835
2836            assert!(
2837                err.to_string()
2838                    .contains(&format!("unknown flow option '{key}'")),
2839                "unexpected error for {key}: {err}"
2840            );
2841        }
2842    }
2843
2844    #[test]
2845    fn test_determine_flow_type_for_source_state_missing_sources_require_opt_in() {
2846        let err = determine_flow_type_for_source_state("my_flow", &HashMap::new(), true, false)
2847            .unwrap_err();
2848
2849        assert!(err.to_string().contains(
2850            "missing source tables for flow 'my_flow'; use WITH (defer_on_missing_source = true) to create a pending flow"
2851        ));
2852    }
2853
2854    #[test]
2855    fn test_determine_flow_type_for_source_state_missing_sources_prefer_batching() {
2856        let flow_options =
2857            HashMap::from([(DEFER_ON_MISSING_SOURCE_KEY.to_string(), "true".to_string())]);
2858
2859        assert_eq!(
2860            determine_flow_type_for_source_state("my_flow", &flow_options, true, true).unwrap(),
2861            Some(FlowType::Batching)
2862        );
2863    }
2864
2865    #[test]
2866    fn test_determine_flow_type_for_source_state_instant_ttl_without_missing_sources() {
2867        assert_eq!(
2868            determine_flow_type_for_source_state("my_flow", &HashMap::new(), false, true).unwrap(),
2869            Some(FlowType::Streaming)
2870        );
2871    }
2872
2873    #[test]
2874    fn test_name_is_match() {
2875        assert!(!NAME_PATTERN_REG.is_match("/adaf"));
2876        assert!(!NAME_PATTERN_REG.is_match("🈲"));
2877        assert!(NAME_PATTERN_REG.is_match("hello"));
2878        assert!(NAME_PATTERN_REG.is_match("test@"));
2879        assert!(!NAME_PATTERN_REG.is_match("@test"));
2880        assert!(NAME_PATTERN_REG.is_match("test#"));
2881        assert!(!NAME_PATTERN_REG.is_match("#test"));
2882        assert!(!NAME_PATTERN_REG.is_match("@"));
2883        assert!(!NAME_PATTERN_REG.is_match("#"));
2884    }
2885
2886    #[test]
2887    fn test_partition_expr_equivalence_with_swapped_operands() {
2888        let column_name = "device_id".to_string();
2889        let column_name_and_type =
2890            HashMap::from([(&column_name, ConcreteDataType::int32_datatype())]);
2891        let timezone = Timezone::from_tz_string("UTC").unwrap();
2892        let dialect = GreptimeDbDialect {};
2893
2894        let mut parser = Parser::new(&dialect)
2895            .try_with_sql("device_id < 100")
2896            .unwrap();
2897        let expr_left = parser.parse_expr().unwrap();
2898
2899        let mut parser = Parser::new(&dialect)
2900            .try_with_sql("100 > device_id")
2901            .unwrap();
2902        let expr_right = parser.parse_expr().unwrap();
2903
2904        let partition_left =
2905            convert_one_expr(&expr_left, &column_name_and_type, &timezone).unwrap();
2906        let partition_right =
2907            convert_one_expr(&expr_right, &column_name_and_type, &timezone).unwrap();
2908
2909        assert_eq!(partition_left, partition_right);
2910        assert!([partition_left.clone()].contains(&partition_right));
2911
2912        let mut physical_partition_exprs = vec![partition_left];
2913        let mut logical_partition_exprs = vec![partition_right];
2914        physical_partition_exprs.sort_unstable();
2915        logical_partition_exprs.sort_unstable();
2916        assert_eq!(physical_partition_exprs, logical_partition_exprs);
2917    }
2918
2919    #[test]
2920    fn test_repartition_target_partition_columns_are_overwrite_context() {
2921        let device_id = ColumnSchema::new("device_id", ConcreteDataType::int32_datatype(), true);
2922        let area = ColumnSchema::new("area", ConcreteDataType::string_datatype(), true);
2923        let existing_partition_columns = vec![&device_id];
2924        let target_partition_columns = vec![&device_id, &area];
2925        let existing_column_name_and_type = column_name_and_type(&existing_partition_columns);
2926        let target_column_name_and_type = column_name_and_type(&target_partition_columns);
2927        let timezone = Timezone::from_tz_string("UTC").unwrap();
2928        let dialect = GreptimeDbDialect {};
2929
2930        let mut parser = Parser::new(&dialect)
2931            .try_with_sql("device_id < 100 AND area < 'South'")
2932            .unwrap();
2933        let expr = parser.parse_expr().unwrap();
2934
2935        let err = convert_one_expr(&expr, &existing_column_name_and_type, &timezone).unwrap_err();
2936        assert!(err.to_string().contains("area"));
2937
2938        let partition_expr = convert_one_expr(&expr, &target_column_name_and_type, &timezone)
2939            .expect("target columns should overwrite the conversion context");
2940        let partition_expr = partition_expr.to_string();
2941        assert!(partition_expr.contains("device_id"));
2942        assert!(partition_expr.contains("area"));
2943        assert!(partition_expr.contains("South"));
2944    }
2945
2946    #[test]
2947    fn test_repartition_rejects_remaining_expr_outside_target_columns() {
2948        let device_id = "device_id".to_string();
2949        let area = "area".to_string();
2950        let timezone = Timezone::from_tz_string("UTC").unwrap();
2951        let column_name_and_type = HashMap::from([
2952            (&device_id, ConcreteDataType::int32_datatype()),
2953            (&area, ConcreteDataType::string_datatype()),
2954        ]);
2955        let dialect = GreptimeDbDialect {};
2956        let mut parser = Parser::new(&dialect)
2957            .try_with_sql("device_id >= 100")
2958            .unwrap();
2959        let remaining_old_expr = convert_one_expr(
2960            &parser.parse_expr().unwrap(),
2961            &column_name_and_type,
2962            &timezone,
2963        )
2964        .unwrap();
2965        let target_partition_columns = HashSet::from([&area]);
2966
2967        let err = ensure_partition_expr_columns_in_target(
2968            &[remaining_old_expr],
2969            &target_partition_columns,
2970        )
2971        .unwrap_err();
2972
2973        assert!(err.to_string().contains("device_id"));
2974        assert!(err.to_string().contains("target partition columns"));
2975    }
2976
2977    #[test]
2978    fn test_repartition_rejects_duplicate_target_partition_columns() {
2979        let device_id = ColumnSchema::new("device_id", ConcreteDataType::int32_datatype(), true);
2980        let column_schemas = vec![device_id];
2981        let target_partition_columns = vec!["device_id".to_string(), "device_id".to_string()];
2982
2983        let err =
2984            validate_and_collect_partition_columns(&target_partition_columns, &column_schemas)
2985                .unwrap_err();
2986
2987        assert!(err.to_string().contains("duplicate partition column"));
2988        assert!(err.to_string().contains("device_id"));
2989    }
2990
2991    fn create_expr_from_sql(sql: &str) -> CreateTableExpr {
2992        let result =
2993            ParserContext::create_with_dialect(sql, &GreptimeDbDialect {}, ParseOptions::default())
2994                .unwrap();
2995
2996        match &result[0] {
2997            Statement::CreateTable(create) => {
2998                expr_helper::create_to_expr(create, &QueryContext::arc()).unwrap()
2999            }
3000            _ => unreachable!(),
3001        }
3002    }
3003
3004    #[test]
3005    fn test_create_table_with_repartition_column_hint() {
3006        let expr = create_expr_from_sql(
3007            r"
3008CREATE TABLE metrics (
3009  host STRING,
3010  ts TIMESTAMP TIME INDEX,
3011  cpu DOUBLE,
3012  PRIMARY KEY(host)
3013)
3014WITH ('repartition.column.hint' = ' host ')",
3015        );
3016
3017        let table_info = create_table_info(&expr, vec![]).unwrap();
3018        assert_eq!(
3019            table_info
3020                .meta
3021                .options
3022                .extra_options
3023                .get(REPARTITION_COLUMN_HINT_KEY),
3024            Some(&"host".to_string())
3025        );
3026    }
3027
3028    #[test]
3029    fn test_json2_requires_append_mode() {
3030        let cases = [
3031            "CREATE TABLE monitor (payload JSON2, ts TIMESTAMP TIME INDEX);",
3032            "CREATE TABLE monitor (payload JSON2, ts TIMESTAMP TIME INDEX) WITH (append_mode='false');",
3033        ];
3034
3035        for sql in cases {
3036            let expr = create_expr_from_sql(sql);
3037            let err = create_table_info(&expr, vec![]).unwrap_err();
3038            assert!(
3039                err.to_string()
3040                    .contains("JSON2 column `payload` requires append_mode='true'"),
3041                "{err}"
3042            );
3043        }
3044
3045        let expr = create_expr_from_sql(
3046            "CREATE TABLE monitor (payload JSON2, ts TIMESTAMP TIME INDEX) WITH (append_mode='true');",
3047        );
3048        create_table_info(&expr, vec![]).unwrap();
3049    }
3050
3051    #[test]
3052    fn test_create_table_with_empty_repartition_column_hint() {
3053        let expr = create_expr_from_sql(
3054            r"
3055CREATE TABLE metrics (
3056  host STRING,
3057  ts TIMESTAMP TIME INDEX,
3058  cpu DOUBLE,
3059  PRIMARY KEY(host)
3060)
3061WITH ('repartition.column.hint' = '')",
3062        );
3063
3064        let err = create_table_info(&expr, vec![]).unwrap_err();
3065        assert!(
3066            err.to_string()
3067                .contains("repartition.column.hint expects exactly one column name")
3068        );
3069    }
3070
3071    #[test]
3072    fn test_create_table_with_multiple_repartition_column_hints() {
3073        let expr = create_expr_from_sql(
3074            r"
3075CREATE TABLE metrics (
3076  host STRING,
3077  region_id STRING,
3078  ts TIMESTAMP TIME INDEX,
3079  cpu DOUBLE,
3080  PRIMARY KEY(host)
3081)
3082WITH ('repartition.column.hint' = 'host,region_id')",
3083        );
3084
3085        let err = create_table_info(&expr, vec![]).unwrap_err();
3086        assert!(
3087            err.to_string()
3088                .contains("repartition.column.hint expects exactly one column name")
3089        );
3090    }
3091
3092    #[test]
3093    fn test_create_table_with_missing_repartition_column_hint() {
3094        let expr = create_expr_from_sql(
3095            r"
3096CREATE TABLE metrics (
3097  host STRING,
3098  ts TIMESTAMP TIME INDEX,
3099  cpu DOUBLE,
3100  PRIMARY KEY(host)
3101)
3102WITH ('repartition.column.hint' = 'region_id')",
3103        );
3104
3105        let err = create_table_info(&expr, vec![]).unwrap_err();
3106        assert!(
3107            err.to_string()
3108                .contains("Cannot find column by name: region")
3109        );
3110    }
3111
3112    #[test]
3113    fn test_create_table_with_time_index_repartition_column_hint() {
3114        let expr = create_expr_from_sql(
3115            r"
3116CREATE TABLE metrics (
3117  host STRING,
3118  ts TIMESTAMP TIME INDEX,
3119  cpu DOUBLE,
3120  PRIMARY KEY(host)
3121)
3122WITH ('repartition.column.hint' = 'ts')",
3123        );
3124
3125        let err = create_table_info(&expr, vec![]).unwrap_err();
3126        assert!(
3127            err.to_string()
3128                .contains("cannot set repartition.column.hint to the time index column")
3129        );
3130    }
3131
3132    #[test]
3133    fn test_create_partitioned_table_with_repartition_column_hint() {
3134        let expr = create_expr_from_sql(
3135            r"
3136CREATE TABLE metrics (
3137  host STRING,
3138  ts TIMESTAMP TIME INDEX,
3139  cpu DOUBLE,
3140  PRIMARY KEY(host)
3141)
3142WITH ('repartition.column.hint' = 'host')",
3143        );
3144
3145        let err = create_table_info(&expr, vec!["host".to_string()]).unwrap_err();
3146        assert!(
3147            err.to_string()
3148                .contains("cannot set repartition.column.hint on a table with partition metadata")
3149        );
3150    }
3151
3152    #[tokio::test]
3153    #[ignore = "TODO(ruihang): WIP new partition rule"]
3154    async fn test_parse_partitions() {
3155        common_telemetry::init_default_ut_logging();
3156        let cases = [
3157            (
3158                r"
3159CREATE TABLE rcx ( a INT, b STRING, c TIMESTAMP, TIME INDEX (c) )
3160PARTITION ON COLUMNS (b) (
3161  b < 'hz',
3162  b >= 'hz' AND b < 'sh',
3163  b >= 'sh'
3164)
3165ENGINE=mito",
3166                r#"[{"column_list":["b"],"value_list":["{\"Value\":{\"String\":\"hz\"}}"]},{"column_list":["b"],"value_list":["{\"Value\":{\"String\":\"sh\"}}"]},{"column_list":["b"],"value_list":["\"MaxValue\""]}]"#,
3167            ),
3168            (
3169                r"
3170CREATE TABLE rcx ( a INT, b STRING, c TIMESTAMP, TIME INDEX (c) )
3171PARTITION BY RANGE COLUMNS (b, a) (
3172  PARTITION r0 VALUES LESS THAN ('hz', 10),
3173  b < 'hz' AND a < 10,
3174  b >= 'hz' AND b < 'sh' AND a >= 10 AND a < 20,
3175  b >= 'sh' AND a >= 20
3176)
3177ENGINE=mito",
3178                r#"[{"column_list":["b","a"],"value_list":["{\"Value\":{\"String\":\"hz\"}}","{\"Value\":{\"Int32\":10}}"]},{"column_list":["b","a"],"value_list":["{\"Value\":{\"String\":\"sh\"}}","{\"Value\":{\"Int32\":20}}"]},{"column_list":["b","a"],"value_list":["\"MaxValue\"","\"MaxValue\""]}]"#,
3179            ),
3180        ];
3181        let ctx = QueryContextBuilder::default().build().into();
3182        for (sql, expected) in cases {
3183            let result = ParserContext::create_with_dialect(
3184                sql,
3185                &GreptimeDbDialect {},
3186                ParseOptions::default(),
3187            )
3188            .unwrap();
3189            match &result[0] {
3190                Statement::CreateTable(c) => {
3191                    let expr = expr_helper::create_to_expr(c, &QueryContext::arc()).unwrap();
3192                    let (partitions, _) =
3193                        parse_partitions(&expr, c.partitions.clone(), &ctx).unwrap();
3194                    let json = serde_json::to_string(&partitions).unwrap();
3195                    assert_eq!(json, expected);
3196                }
3197                _ => unreachable!(),
3198            }
3199        }
3200    }
3201}