Skip to main content

common_meta/
ddl_manager.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::sync::Arc;
16use std::time::Duration;
17
18use api::v1::Repartition;
19use api::v1::alter_table_expr::Kind;
20use api::v1::repartition::Source as PbRepartitionSource;
21use common_error::ext::BoxedError;
22use common_event_recorder::PersistentEventContext;
23#[cfg(feature = "enterprise")]
24use common_event_recorder::TriggerReason;
25use common_procedure::{
26    BoxedProcedure, BoxedProcedureLoader, Output, ProcedureContext, ProcedureId,
27    ProcedureManagerRef, ProcedureWithId, watcher,
28};
29use common_telemetry::tracing_context::{FutureExt, TracingContext};
30use common_telemetry::{debug, info, tracing};
31use derive_builder::Builder;
32use snafu::{OptionExt, ResultExt, ensure};
33use store_api::storage::{RegionId, TableId};
34use table::table_name::TableName;
35
36use crate::affinity::PeerAllocContextProviderRef;
37use crate::ddl::alter_database::AlterDatabaseProcedure;
38use crate::ddl::alter_logical_tables::AlterLogicalTablesProcedure;
39use crate::ddl::alter_table::{AlterTableProcedure, RegionRouteChanged, only_sets_skip_wal};
40use crate::ddl::comment_on::CommentOnProcedure;
41use crate::ddl::create_database::{CreateDatabaseMetadataCommitterRef, CreateDatabaseProcedure};
42use crate::ddl::create_flow::{
43    CreateFlowProcedure, FLOW_EXPERIMENTAL_ENABLE_INCREMENTAL_READ_KEY,
44    FLOW_EXPERIMENTAL_ENABLE_INCREMENTAL_READ_SEQUENCE_RANGE,
45};
46use crate::ddl::create_logical_tables::CreateLogicalTablesProcedure;
47use crate::ddl::create_table::CreateTableProcedure;
48use crate::ddl::create_view::CreateViewProcedure;
49use crate::ddl::drop_database::DropDatabaseProcedure;
50use crate::ddl::drop_flow::DropFlowProcedure;
51use crate::ddl::drop_table::DropTableProcedure;
52use crate::ddl::drop_view::DropViewProcedure;
53#[cfg(feature = "enterprise")]
54use crate::ddl::purge_dropped_table::PurgeDroppedTableProcedure;
55use crate::ddl::truncate_table::TruncateTableProcedure;
56#[cfg(feature = "enterprise")]
57use crate::ddl::undrop_table::UndropTableProcedure;
58use crate::ddl::{DdlContext, utils};
59use crate::error::{
60    self, ConvertAlterTableRequestSnafu, CreateRepartitionProcedureSnafu, EmptyDdlTasksSnafu,
61    PersistRepartitionGcRequirementSnafu, ProcedureOutputSnafu, RegisterProcedureLoaderSnafu,
62    RegisterRepartitionProcedureLoaderSnafu, Result, SubmitProcedureSnafu, TableInfoNotFoundSnafu,
63    TableNotFoundSnafu, TableRouteNotFoundSnafu, UnexpectedLogicalRouteTableSnafu,
64    UnsupportedSnafu, WaitProcedureSnafu,
65};
66use crate::key::table_info::TableInfoValue;
67use crate::key::table_name::TableNameKey;
68use crate::key::{DeserializedValueWithBytes, TableMetadataManagerRef};
69use crate::procedure_executor::ExecutorContext;
70#[cfg(feature = "enterprise")]
71use crate::rpc::ddl::DdlTask::CreateTrigger;
72#[cfg(feature = "enterprise")]
73use crate::rpc::ddl::DdlTask::DropTrigger;
74use crate::rpc::ddl::DdlTask::{
75    AlterDatabase, AlterLogicalTables, AlterTable, CommentOn, CreateDatabase, CreateFlow,
76    CreateLogicalTables, CreateTable, CreateView, DropDatabase, DropFlow, DropLogicalTables,
77    DropTable, DropView, PurgeDroppedTable, TruncateTable, UndropTable,
78};
79#[cfg(feature = "enterprise")]
80use crate::rpc::ddl::trigger::CreateTriggerTask;
81#[cfg(feature = "enterprise")]
82use crate::rpc::ddl::trigger::DropTriggerTask;
83use crate::rpc::ddl::{
84    AlterDatabaseTask, AlterTableTask, CommentOnTask, CreateDatabaseTask, CreateFlowTask,
85    CreateTableTask, CreateViewTask, DropDatabaseTask, DropFlowTask, DropTableTask, DropViewTask,
86    PurgeDroppedTableTask, QueryContext, SubmitDdlTaskRequest, SubmitDdlTaskResponse,
87    TruncateTableTask, UndropTableTask,
88};
89
90const MAX_REGION_ROUTE_CHANGE_RETRIES: usize = 3;
91
92/// A configurator that customizes or enhances a [`DdlManager`].
93#[async_trait::async_trait]
94pub trait DdlManagerConfigurator<C>: Send + Sync {
95    /// Configures the given [`DdlManager`] using the provided [`DdlManagerConfigureContext`].
96    async fn configure(
97        &self,
98        ddl_manager: DdlManager,
99        ctx: C,
100    ) -> std::result::Result<DdlManager, BoxedError>;
101}
102
103pub type DdlManagerConfiguratorRef<C> = Arc<dyn DdlManagerConfigurator<C>>;
104
105pub type DdlManagerRef = Arc<DdlManager>;
106
107pub type BoxedProcedureLoaderFactory = dyn Fn(DdlContext) -> BoxedProcedureLoader;
108
109/// The [DdlManager] provides the ability to execute Ddl.
110#[derive(Builder)]
111pub struct DdlManager {
112    ddl_context: DdlContext,
113    procedure_manager: ProcedureManagerRef,
114    repartition_procedure_factory: RepartitionProcedureFactoryRef,
115    #[cfg(feature = "enterprise")]
116    trigger_ddl_manager: Option<TriggerDdlManagerRef>,
117    #[cfg(feature = "enterprise")]
118    create_flow_handler: Option<CreateFlowHandlerRef>,
119    #[cfg(feature = "enterprise")]
120    drop_flow_handler: Option<DropFlowHandlerRef>,
121}
122
123/// This trait is responsible for handling DDL tasks about triggers. e.g.,
124/// create trigger, drop trigger, etc.
125#[cfg(feature = "enterprise")]
126#[async_trait::async_trait]
127pub trait TriggerDdlManager: Send + Sync {
128    async fn create_trigger(
129        &self,
130        create_trigger_task: CreateTriggerTask,
131        procedure_manager: ProcedureManagerRef,
132        ddl_context: DdlContext,
133        query_context: QueryContext,
134        procedure_context: ProcedureContext,
135    ) -> Result<SubmitDdlTaskResponse>;
136
137    async fn drop_trigger(
138        &self,
139        drop_trigger_task: DropTriggerTask,
140        procedure_manager: ProcedureManagerRef,
141        ddl_context: DdlContext,
142        query_context: QueryContext,
143        procedure_context: ProcedureContext,
144    ) -> Result<SubmitDdlTaskResponse>;
145
146    fn as_any(&self) -> &dyn std::any::Any;
147}
148
149#[cfg(feature = "enterprise")]
150pub type TriggerDdlManagerRef = Arc<dyn TriggerDdlManager>;
151
152/// This trait is responsible for handling custom/extension CREATE FLOW tasks.
153#[cfg(feature = "enterprise")]
154#[async_trait::async_trait]
155pub trait CreateFlowHandler: Send + Sync {
156    async fn create_flow(
157        &self,
158        create_flow_task: CreateFlowTask,
159        procedure_manager: ProcedureManagerRef,
160        ddl_context: DdlContext,
161        query_context: QueryContext,
162        procedure_context: ProcedureContext,
163    ) -> Result<SubmitDdlTaskResponse>;
164}
165
166#[cfg(feature = "enterprise")]
167pub type CreateFlowHandlerRef = Arc<dyn CreateFlowHandler>;
168
169/// Hook for classifying and handling DROP FLOW requests.
170#[cfg(feature = "enterprise")]
171#[async_trait::async_trait]
172pub trait DropFlowHandler: Send + Sync {
173    async fn drop_flow(
174        &self,
175        drop_flow_task: DropFlowTask,
176        procedure_manager: ProcedureManagerRef,
177        ddl_context: DdlContext,
178        procedure_context: ProcedureContext,
179    ) -> Result<SubmitDdlTaskResponse>;
180}
181
182#[cfg(feature = "enterprise")]
183pub type DropFlowHandlerRef = Arc<dyn DropFlowHandler>;
184
185macro_rules! procedure_loader_entry {
186    ($procedure:ident) => {
187        (
188            $procedure::TYPE_NAME,
189            &|context: DdlContext| -> common_procedure::BoxedProcedureLoader {
190                Box::new(move |json: &str| {
191                    let context = context.clone();
192                    $procedure::from_json(json, context).map(|p| Box::new(p) as _)
193                })
194            },
195        )
196    };
197}
198
199macro_rules! procedure_loader {
200    ($($procedure:ident),*) => {
201        vec![
202            $(procedure_loader_entry!($procedure)),*
203        ]
204    };
205}
206
207pub type RepartitionProcedureFactoryRef = Arc<dyn RepartitionProcedureFactory>;
208
209pub enum RepartitionSource {
210    Partitioned {
211        exprs: Vec<String>,
212        /// Full target partition columns to overwrite table metadata.
213        ///
214        /// `None` means the repartition keeps using the current table
215        /// partition columns, so the procedure won't update
216        /// `partition_key_indices`.
217        target_partition_columns: Option<Vec<String>>,
218    },
219    Unpartitioned {
220        partition_columns: Vec<String>,
221    },
222}
223
224#[async_trait::async_trait]
225pub trait RepartitionProcedureFactory: Send + Sync {
226    #[allow(clippy::too_many_arguments)]
227    fn create(
228        &self,
229        ddl_ctx: &DdlContext,
230        table_name: TableName,
231        table_id: TableId,
232        source: RepartitionSource,
233        to_exprs: Vec<String>,
234        timeout: Option<Duration>,
235    ) -> std::result::Result<BoxedProcedure, BoxedError>;
236
237    fn register_loaders(
238        &self,
239        ddl_ctx: &DdlContext,
240        procedure_manager: &ProcedureManagerRef,
241    ) -> std::result::Result<(), BoxedError>;
242
243    /// Persists the cluster-level GC requirement before submitting a repartition.
244    async fn ensure_gc_requirement(&self) -> std::result::Result<(), BoxedError>;
245}
246
247/// The options for DDL tasks.
248///
249/// Note: These options may not be utilized by all procedures.
250/// At present, they are specifically applied in `RepartitionProcedure` and truncate tasks.
251#[derive(Debug, Clone, Copy)]
252pub struct DdlOptions {
253    /// The timeout will be passed to the procedure.
254    ///
255    /// Note: Each procedure may implement its own timeout handling mechanism.
256    /// For truncate tasks, the timeout bounds submission and waiting rather than procedure execution.
257    /// A timeout does not cancel a submitted procedure.
258    pub timeout: Duration,
259    /// The flag that controls whether to wait for the procedure to complete.
260    ///
261    /// If wait is `true`, the procedure will wait for completion(success or failure) and the result will be returned.
262    /// Otherwise, the procedure will be submitted and return the [ProcedureId](common_procedure::ProcedureId) immediately.
263    ///
264    /// Note: Except for truncate tasks, the value of `wait` is independent of the `timeout` option. If a procedure ignores the `timeout` and `wait` is set to true, the operation returns until the procedure completes.
265    pub wait: bool,
266}
267
268impl DdlManager {
269    /// Returns a new [DdlManager].
270    pub fn new(
271        ddl_context: DdlContext,
272        procedure_manager: ProcedureManagerRef,
273        repartition_procedure_factory: RepartitionProcedureFactoryRef,
274    ) -> Self {
275        Self {
276            ddl_context,
277            procedure_manager,
278            repartition_procedure_factory,
279            #[cfg(feature = "enterprise")]
280            trigger_ddl_manager: None,
281            #[cfg(feature = "enterprise")]
282            create_flow_handler: None,
283            #[cfg(feature = "enterprise")]
284            drop_flow_handler: None,
285        }
286    }
287
288    #[cfg(feature = "enterprise")]
289    pub fn with_drop_flow_handler(mut self, drop_flow_handler: DropFlowHandlerRef) -> Self {
290        self.drop_flow_handler = Some(drop_flow_handler);
291        self
292    }
293
294    #[cfg(feature = "enterprise")]
295    pub fn with_trigger_ddl_manager(mut self, trigger_ddl_manager: TriggerDdlManagerRef) -> Self {
296        self.trigger_ddl_manager = Some(trigger_ddl_manager);
297        self
298    }
299
300    #[cfg(feature = "enterprise")]
301    pub fn with_create_flow_handler(mut self, create_flow_handler: CreateFlowHandlerRef) -> Self {
302        self.create_flow_handler = Some(create_flow_handler);
303        self
304    }
305
306    /// Sets the provider used to build allocation inputs from queries.
307    pub fn with_peer_alloc_context_provider(
308        mut self,
309        provider: PeerAllocContextProviderRef,
310    ) -> Self {
311        self.ddl_context.peer_alloc_context_provider = Some(provider);
312        self
313    }
314
315    pub fn with_create_database_metadata_committer(
316        mut self,
317        committer: CreateDatabaseMetadataCommitterRef,
318    ) -> Self {
319        self.ddl_context.create_database_metadata_committer = Some(committer);
320        self
321    }
322
323    /// Returns the [TableMetadataManagerRef].
324    pub fn table_metadata_manager(&self) -> &TableMetadataManagerRef {
325        &self.ddl_context.table_metadata_manager
326    }
327
328    /// Returns the [DdlContext]
329    pub fn create_context(&self) -> DdlContext {
330        self.ddl_context.clone()
331    }
332
333    /// Registers all Ddl loaders.
334    pub fn register_loaders(&self) -> Result<()> {
335        let loaders: Vec<(&str, &BoxedProcedureLoaderFactory)> = procedure_loader!(
336            CreateTableProcedure,
337            CreateLogicalTablesProcedure,
338            CreateViewProcedure,
339            CreateFlowProcedure,
340            CreateDatabaseProcedure,
341            AlterTableProcedure,
342            AlterLogicalTablesProcedure,
343            AlterDatabaseProcedure,
344            DropTableProcedure,
345            DropFlowProcedure,
346            TruncateTableProcedure,
347            DropDatabaseProcedure,
348            DropViewProcedure,
349            CommentOnProcedure
350        );
351        #[cfg(feature = "enterprise")]
352        let loaders = {
353            let soft_drop_loaders: Vec<(&str, &BoxedProcedureLoaderFactory)> =
354                procedure_loader!(UndropTableProcedure, PurgeDroppedTableProcedure);
355            loaders
356                .into_iter()
357                .chain(soft_drop_loaders)
358                .collect::<Vec<_>>()
359        };
360
361        for (type_name, loader_factory) in loaders {
362            let context = self.create_context();
363            self.procedure_manager
364                .register_loader(type_name, loader_factory(context))
365                .context(RegisterProcedureLoaderSnafu { type_name })?;
366        }
367
368        #[cfg(feature = "enterprise")]
369        {
370            let type_name = PurgeDroppedTableProcedure::EXPIRED_TYPE_NAME;
371            let context = self.create_context();
372            self.procedure_manager
373                .register_loader(
374                    type_name,
375                    Box::new(move |json: &str| {
376                        PurgeDroppedTableProcedure::from_json(json, context.clone())
377                            .map(|procedure| Box::new(procedure) as _)
378                    }),
379                )
380                .context(RegisterProcedureLoaderSnafu { type_name })?;
381        }
382
383        self.repartition_procedure_factory
384            .register_loaders(&self.ddl_context, &self.procedure_manager)
385            .context(RegisterRepartitionProcedureLoaderSnafu)?;
386
387        Ok(())
388    }
389
390    /// Submits a repartition procedure for the specified table.
391    ///
392    /// This creates a repartition procedure using the provided `table_id`,
393    /// `table_name`, and `Repartition` configuration, and then either executes it
394    /// to completion or just submits it for asynchronous execution.
395    ///
396    /// The `Repartition` argument contains the original (`from_partition_exprs`)
397    /// and target (`into_partition_exprs`) partition expressions that define how
398    /// the table should be repartitioned.
399    ///
400    /// The `wait` flag controls whether this method waits for the repartition
401    /// procedure to finish:
402    /// - If `wait` is `true`, the procedure is executed and this method awaits
403    ///   its completion, returning both the generated `ProcedureId` and the
404    ///   final `Output` of the procedure.
405    /// - If `wait` is `false`, the procedure is only submitted to the procedure
406    ///   manager for asynchronous execution, and this method returns the
407    ///   `ProcedureId` along with `None` as the output.
408    async fn submit_repartition_task(
409        &self,
410        table_id: TableId,
411        table_name: TableName,
412        repartition: Repartition,
413        wait: bool,
414        timeout: Duration,
415        procedure_context: ProcedureContext,
416    ) -> Result<(ProcedureId, Option<Output>)> {
417        let context = self.create_context();
418
419        let into_partition_exprs = repartition.into_partition_exprs;
420        let source = repartition.source;
421
422        let source = match source {
423            Some(PbRepartitionSource::PartitionExprs(source)) => RepartitionSource::Partitioned {
424                exprs: source.exprs,
425                target_partition_columns: source
426                    .target_partition_columns
427                    .map(|columns| columns.columns),
428            },
429            Some(PbRepartitionSource::Unpartitioned(source)) => RepartitionSource::Unpartitioned {
430                partition_columns: source.partition_columns,
431            },
432            None => {
433                // Reads the deprecated field for backward compatibility with old persisted DDL tasks.
434                #[allow(deprecated)]
435                RepartitionSource::Partitioned {
436                    exprs: repartition.from_partition_exprs,
437                    target_partition_columns: None,
438                }
439            }
440        };
441
442        let procedure = self
443            .repartition_procedure_factory
444            .create(
445                &context,
446                table_name,
447                table_id,
448                source,
449                into_partition_exprs,
450                Some(timeout),
451            )
452            .context(CreateRepartitionProcedureSnafu)?;
453        self.repartition_procedure_factory
454            .ensure_gc_requirement()
455            .await
456            .context(PersistRepartitionGcRequirementSnafu)?;
457        let procedure_with_id =
458            ProcedureWithId::with_random_id(Box::new(procedure)).with_context(procedure_context);
459        self.execute_procedure_with_options(procedure_with_id, DdlOptions { wait, timeout })
460            .await
461    }
462
463    /// Submits and executes an alter table task.
464    #[tracing::instrument(skip_all)]
465    pub async fn submit_alter_table_task(
466        &self,
467        table_id: TableId,
468        alter_table_task: AlterTableTask,
469        procedure_context: ProcedureContext,
470        ddl_options: DdlOptions,
471    ) -> Result<(ProcedureId, Option<Output>)> {
472        // make alter_table_task mutable so we can call .take() on its field
473        let mut alter_table_task = alter_table_task;
474        if let Some(Kind::Repartition(_)) = alter_table_task.alter_table.kind.as_ref()
475            && let Kind::Repartition(repartition) =
476                alter_table_task.alter_table.kind.take().unwrap()
477        {
478            let table_name = TableName::new(
479                alter_table_task.alter_table.catalog_name,
480                alter_table_task.alter_table.schema_name,
481                alter_table_task.alter_table.table_name,
482            );
483            return self
484                .submit_repartition_task(
485                    table_id,
486                    table_name,
487                    repartition,
488                    ddl_options.wait,
489                    ddl_options.timeout,
490                    procedure_context,
491                )
492                .await;
493        }
494
495        let lock_regions = alter_table_task
496            .alter_table
497            .kind
498            .as_ref()
499            .is_some_and(only_sets_skip_wal);
500
501        let mut route_change_retries = 0;
502        loop {
503            // Hold the same physical region locks as migration while validating that
504            // this route snapshot is still the one the procedure will mutate.
505            let region_ids_to_lock = if lock_regions {
506                let (_, route) = self
507                    .table_metadata_manager()
508                    .table_route_manager()
509                    .get_physical_table_route(table_id)
510                    .await?;
511                route
512                    .region_routes
513                    .iter()
514                    .map(|route| route.region.id)
515                    .collect::<Vec<RegionId>>()
516            } else {
517                vec![]
518            };
519
520            let context = self.create_context();
521            let procedure = AlterTableProcedure::new_with_region_locks(
522                table_id,
523                alter_table_task.clone(),
524                region_ids_to_lock,
525                context,
526            )?;
527
528            let procedure_with_id = ProcedureWithId::with_random_id(Box::new(procedure))
529                .with_context(procedure_context.clone());
530            let result = self.execute_procedure_and_wait(procedure_with_id).await?;
531            if result
532                .1
533                .as_ref()
534                .is_some_and(|output| output.is::<RegionRouteChanged>())
535            {
536                if route_change_retries == MAX_REGION_ROUTE_CHANGE_RETRIES {
537                    let source = error::UnexpectedSnafu {
538                        err_msg: format!(
539                            "Region route kept changing while altering table {table_id}"
540                        ),
541                    }
542                    .build();
543                    return Err(error::Error::retry_later(source));
544                }
545                route_change_retries += 1;
546                continue;
547            }
548            return Ok(result);
549        }
550    }
551
552    /// Submits and executes a create table task.
553    #[tracing::instrument(skip_all)]
554    pub async fn submit_create_table_task(
555        &self,
556        create_table_task: CreateTableTask,
557        query_context: QueryContext,
558        procedure_context: ProcedureContext,
559    ) -> Result<(ProcedureId, Option<Output>)> {
560        let context = self.create_context();
561
562        let procedure = CreateTableProcedure::new_with_query_context(
563            create_table_task,
564            query_context,
565            context,
566        )?;
567
568        let procedure_with_id =
569            ProcedureWithId::with_random_id(Box::new(procedure)).with_context(procedure_context);
570
571        self.execute_procedure_and_wait(procedure_with_id).await
572    }
573
574    /// Submits and executes a `[CreateViewTask]`.
575    #[tracing::instrument(skip_all)]
576    pub async fn submit_create_view_task(
577        &self,
578        create_view_task: CreateViewTask,
579        procedure_context: ProcedureContext,
580    ) -> Result<(ProcedureId, Option<Output>)> {
581        let context = self.create_context();
582
583        let procedure = CreateViewProcedure::new(create_view_task, context);
584
585        let procedure_with_id =
586            ProcedureWithId::with_random_id(Box::new(procedure)).with_context(procedure_context);
587
588        self.execute_procedure_and_wait(procedure_with_id).await
589    }
590
591    /// Submits and executes a create multiple logical table tasks.
592    #[tracing::instrument(skip_all)]
593    pub async fn submit_create_logical_table_tasks(
594        &self,
595        create_table_tasks: Vec<CreateTableTask>,
596        physical_table_id: TableId,
597        procedure_context: ProcedureContext,
598    ) -> Result<(ProcedureId, Option<Output>)> {
599        let context = self.create_context();
600
601        let procedure =
602            CreateLogicalTablesProcedure::new(create_table_tasks, physical_table_id, context);
603
604        let procedure_with_id =
605            ProcedureWithId::with_random_id(Box::new(procedure)).with_context(procedure_context);
606
607        self.execute_procedure_and_wait(procedure_with_id).await
608    }
609
610    /// Submits and executes alter multiple table tasks.
611    #[tracing::instrument(skip_all)]
612    pub async fn submit_alter_logical_table_tasks(
613        &self,
614        alter_table_tasks: Vec<AlterTableTask>,
615        physical_table_id: TableId,
616        procedure_context: ProcedureContext,
617    ) -> Result<(ProcedureId, Option<Output>)> {
618        let context = self.create_context();
619
620        // Resolve the logical table ids up front: procedure locks are fixed
621        // at submission, so `lock_key` cannot derive them during `Prepare`.
622        let logical_table_ids = {
623            let table_refs = alter_table_tasks
624                .iter()
625                .map(|task| task.table_ref())
626                .collect::<Vec<_>>();
627            utils::table_id::get_all_table_ids_by_names(
628                self.table_metadata_manager().table_name_manager(),
629                &table_refs,
630            )
631            .await?
632        };
633
634        let procedure = AlterLogicalTablesProcedure::new(
635            alter_table_tasks,
636            physical_table_id,
637            logical_table_ids,
638            context,
639        );
640
641        let procedure_with_id =
642            ProcedureWithId::with_random_id(Box::new(procedure)).with_context(procedure_context);
643
644        self.execute_procedure_and_wait(procedure_with_id).await
645    }
646
647    /// Submits and executes a drop table task.
648    #[tracing::instrument(skip_all)]
649    pub async fn submit_drop_table_task(
650        &self,
651        drop_table_task: DropTableTask,
652        procedure_context: ProcedureContext,
653    ) -> Result<(ProcedureId, Option<Output>)> {
654        let context = self.create_context();
655
656        let procedure = DropTableProcedure::new(drop_table_task, context);
657
658        let procedure_with_id =
659            ProcedureWithId::with_random_id(Box::new(procedure)).with_context(procedure_context);
660
661        self.execute_procedure_and_wait(procedure_with_id).await
662    }
663
664    /// Submits and executes an undrop table task.
665    #[cfg_attr(not(feature = "enterprise"), allow(unused_variables))]
666    #[tracing::instrument(skip_all)]
667    pub async fn submit_undrop_table_task(
668        &self,
669        undrop_table_task: UndropTableTask,
670        procedure_context: ProcedureContext,
671    ) -> Result<(ProcedureId, Option<Output>)> {
672        #[cfg(not(feature = "enterprise"))]
673        {
674            use crate::error::UnsupportedSnafu;
675
676            return UnsupportedSnafu {
677                operation: "undrop table is only available in GreptimeDB Enterprise Edition",
678            }
679            .fail();
680        }
681        #[cfg(feature = "enterprise")]
682        {
683            let context = self.create_context();
684            let original_table_name = context
685                .table_metadata_manager
686                .get_dropped_table_by_id(undrop_table_task.table_id)
687                .await?
688                .with_context(|| TableNotFoundSnafu {
689                    table_name: undrop_table_task.table_id.to_string(),
690                })?
691                .table_name;
692            let procedure = UndropTableProcedure::new_with_original_table_name(
693                undrop_table_task,
694                context,
695                Some(original_table_name),
696            );
697            let procedure_with_id = ProcedureWithId::with_random_id(Box::new(procedure))
698                .with_context(procedure_context);
699
700            self.execute_procedure_and_wait(procedure_with_id).await
701        }
702    }
703
704    /// Submits and executes a purge dropped table task.
705    #[cfg_attr(not(feature = "enterprise"), allow(unused_variables))]
706    #[tracing::instrument(skip_all)]
707    pub async fn submit_purge_dropped_table_task(
708        &self,
709        purge_dropped_table_task: PurgeDroppedTableTask,
710        procedure_context: ProcedureContext,
711    ) -> Result<(ProcedureId, Option<Output>)> {
712        #[cfg(not(feature = "enterprise"))]
713        {
714            use crate::error::UnsupportedSnafu;
715
716            return UnsupportedSnafu {
717                operation: "purge dropped table is only available in GreptimeDB Enterprise Edition",
718            }
719            .fail();
720        }
721        #[cfg(feature = "enterprise")]
722        {
723            let context = self.create_context();
724            let procedure = PurgeDroppedTableProcedure::new(purge_dropped_table_task, context);
725            let procedure_with_id = ProcedureWithId::with_random_id(Box::new(procedure))
726                .with_context(procedure_context);
727
728            self.execute_procedure_and_wait(procedure_with_id).await
729        }
730    }
731
732    /// Submits and executes a purge task that first rechecks the tombstone deadline.
733    #[cfg_attr(not(feature = "enterprise"), allow(unused_variables))]
734    #[tracing::instrument(skip_all)]
735    pub async fn submit_expired_purge_dropped_table_task(
736        &self,
737        purge_dropped_table_task: PurgeDroppedTableTask,
738    ) -> Result<(ProcedureId, Option<Output>)> {
739        #[cfg(not(feature = "enterprise"))]
740        {
741            use crate::error::UnsupportedSnafu;
742
743            return UnsupportedSnafu {
744                operation:
745                    "purge expired dropped table is only available in GreptimeDB Enterprise Edition",
746            }
747            .fail();
748        }
749        #[cfg(feature = "enterprise")]
750        {
751            let context = self.create_context();
752            let procedure_context = ProcedureContext::from_event_context(
753                PersistentEventContext::new(TriggerReason::ScheduledGc),
754            );
755            let procedure =
756                PurgeDroppedTableProcedure::new_if_expired(purge_dropped_table_task, context);
757            let procedure_with_id = ProcedureWithId::with_random_id(Box::new(procedure))
758                .with_context(procedure_context);
759
760            self.execute_procedure_and_wait(procedure_with_id).await
761        }
762    }
763
764    /// Submits and executes a create database task.
765    #[tracing::instrument(skip_all)]
766    pub async fn submit_create_database(
767        &self,
768        CreateDatabaseTask {
769            catalog,
770            schema,
771            create_if_not_exists,
772            options,
773            creator,
774        }: CreateDatabaseTask,
775        procedure_context: ProcedureContext,
776    ) -> Result<(ProcedureId, Option<Output>)> {
777        let context = self.create_context();
778        let procedure = CreateDatabaseProcedure::new(
779            catalog,
780            schema,
781            create_if_not_exists,
782            options,
783            creator,
784            context,
785        );
786        let procedure_with_id =
787            ProcedureWithId::with_random_id(Box::new(procedure)).with_context(procedure_context);
788
789        self.execute_procedure_and_wait(procedure_with_id).await
790    }
791
792    /// Submits and executes a drop table task.
793    #[tracing::instrument(skip_all)]
794    pub async fn submit_drop_database(
795        &self,
796        DropDatabaseTask {
797            catalog,
798            schema,
799            drop_if_exists,
800        }: DropDatabaseTask,
801        procedure_context: ProcedureContext,
802    ) -> Result<(ProcedureId, Option<Output>)> {
803        let context = self.create_context();
804        let procedure = DropDatabaseProcedure::new(catalog, schema, drop_if_exists, context);
805        let procedure_with_id =
806            ProcedureWithId::with_random_id(Box::new(procedure)).with_context(procedure_context);
807
808        self.execute_procedure_and_wait(procedure_with_id).await
809    }
810
811    pub async fn submit_alter_database(
812        &self,
813        alter_database_task: AlterDatabaseTask,
814        procedure_context: ProcedureContext,
815    ) -> Result<(ProcedureId, Option<Output>)> {
816        let context = self.create_context();
817        let procedure = AlterDatabaseProcedure::new(alter_database_task, context)?;
818        let procedure_with_id =
819            ProcedureWithId::with_random_id(Box::new(procedure)).with_context(procedure_context);
820
821        self.execute_procedure_and_wait(procedure_with_id).await
822    }
823
824    /// Submits and executes a create flow task.
825    #[tracing::instrument(skip_all)]
826    pub async fn submit_create_flow_task(
827        &self,
828        create_flow: CreateFlowTask,
829        query_context: QueryContext,
830        procedure_context: ProcedureContext,
831    ) -> Result<(ProcedureId, Option<Output>)> {
832        let context = self.create_context();
833        let procedure = CreateFlowProcedure::new(create_flow, query_context, context);
834        let procedure_with_id =
835            ProcedureWithId::with_random_id(Box::new(procedure)).with_context(procedure_context);
836
837        self.execute_procedure_and_wait(procedure_with_id).await
838    }
839
840    /// Submits and executes a drop flow task.
841    #[tracing::instrument(skip_all)]
842    pub async fn submit_drop_flow_task(
843        &self,
844        drop_flow: DropFlowTask,
845        procedure_context: ProcedureContext,
846    ) -> Result<(ProcedureId, Option<Output>)> {
847        let context = self.create_context();
848        let procedure = DropFlowProcedure::new(drop_flow, context);
849        let procedure_with_id =
850            ProcedureWithId::with_random_id(Box::new(procedure)).with_context(procedure_context);
851
852        self.execute_procedure_and_wait(procedure_with_id).await
853    }
854
855    /// Submits and executes a drop view task.
856    #[tracing::instrument(skip_all)]
857    pub async fn submit_drop_view_task(
858        &self,
859        drop_view: DropViewTask,
860        procedure_context: ProcedureContext,
861    ) -> Result<(ProcedureId, Option<Output>)> {
862        let context = self.create_context();
863        let procedure = DropViewProcedure::new(drop_view, context);
864        let procedure_with_id =
865            ProcedureWithId::with_random_id(Box::new(procedure)).with_context(procedure_context);
866
867        self.execute_procedure_and_wait(procedure_with_id).await
868    }
869
870    /// Submits a truncate table task and optionally waits for completion.
871    #[tracing::instrument(skip_all)]
872    pub async fn submit_truncate_table_task(
873        &self,
874        truncate_table_task: TruncateTableTask,
875        table_info_value: DeserializedValueWithBytes<TableInfoValue>,
876        procedure_context: ProcedureContext,
877        ddl_options: DdlOptions,
878    ) -> Result<(ProcedureId, Option<Output>)> {
879        let context = self.create_context();
880        let procedure = TruncateTableProcedure::new(truncate_table_task, table_info_value, context);
881
882        let procedure_with_id =
883            ProcedureWithId::with_random_id(Box::new(procedure)).with_context(procedure_context);
884
885        self.execute_procedure_with_options(procedure_with_id, ddl_options)
886            .await
887    }
888
889    /// Submits and executes a comment on task.
890    #[tracing::instrument(skip_all)]
891    pub async fn submit_comment_on_task(
892        &self,
893        mut comment_on_task: CommentOnTask,
894        procedure_context: ProcedureContext,
895    ) -> Result<(ProcedureId, Option<Output>)> {
896        let context = self.create_context();
897        comment_on_task
898            .enrich_object_id(
899                context.table_metadata_manager.table_name_manager(),
900                context.flow_metadata_manager.flow_name_manager(),
901            )
902            .await?;
903        let procedure = CommentOnProcedure::new(comment_on_task, context);
904        let procedure_with_id =
905            ProcedureWithId::with_random_id(Box::new(procedure)).with_context(procedure_context);
906
907        self.execute_procedure_and_wait(procedure_with_id).await
908    }
909
910    /// Submits a procedure and optionally waits for completion.
911    ///
912    /// The truncate task handler bounds its whole request, including metadata reads.
913    /// The manager drives submitted procedures independently of this future.
914    async fn execute_procedure_with_options(
915        &self,
916        procedure_with_id: ProcedureWithId,
917        ddl_options: DdlOptions,
918    ) -> Result<(ProcedureId, Option<Output>)> {
919        if ddl_options.wait {
920            self.execute_procedure_and_wait(procedure_with_id).await
921        } else {
922            self.submit_procedure(procedure_with_id)
923                .await
924                .map(|id| (id, None))
925        }
926    }
927
928    /// Executes a procedure and waits for the result.
929    async fn execute_procedure_and_wait(
930        &self,
931        procedure_with_id: ProcedureWithId,
932    ) -> Result<(ProcedureId, Option<Output>)> {
933        let procedure_id = procedure_with_id.id;
934
935        let mut watcher = self
936            .procedure_manager
937            .submit(procedure_with_id)
938            .await
939            .context(SubmitProcedureSnafu)?;
940
941        let output = watcher::wait(&mut watcher)
942            .await
943            .context(WaitProcedureSnafu)?;
944
945        Ok((procedure_id, output))
946    }
947
948    /// Submits a procedure and returns the procedure id.
949    async fn submit_procedure(&self, procedure_with_id: ProcedureWithId) -> Result<ProcedureId> {
950        let procedure_id = procedure_with_id.id;
951        let _ = self
952            .procedure_manager
953            .submit(procedure_with_id)
954            .await
955            .context(SubmitProcedureSnafu)?;
956
957        Ok(procedure_id)
958    }
959
960    pub async fn submit_ddl_task(
961        &self,
962        context: ExecutorContext,
963        request: SubmitDdlTaskRequest,
964    ) -> Result<SubmitDdlTaskResponse> {
965        let ExecutorContext {
966            tracing_context,
967            query_context,
968            actor,
969            event_input,
970        } = context;
971        let query_context = query_context.context(UnsupportedSnafu {
972            operation: "submit_ddl_task without query context",
973        })?;
974        let procedure_context = ProcedureContext {
975            actor,
976            event_context: event_input
977                .map(|input| PersistentEventContext::from((input, query_context.protocol()))),
978        };
979        let span = tracing_context
980            .as_ref()
981            .map(TracingContext::from_w3c)
982            .unwrap_or_else(TracingContext::from_current_span)
983            .attach(tracing::info_span!("DdlManager::submit_ddl_task"));
984        let SubmitDdlTaskRequest {
985            wait,
986            timeout,
987            task,
988        } = request;
989        let ddl_options = DdlOptions { wait, timeout };
990        async move {
991            debug!("Submitting Ddl task: {:?}", task);
992            match task {
993                CreateTable(create_table_task) => {
994                    handle_create_table_task(
995                        self,
996                        create_table_task,
997                        query_context,
998                        procedure_context,
999                    )
1000                    .await
1001                }
1002                DropTable(drop_table_task) => {
1003                    handle_drop_table_task(self, drop_table_task, procedure_context).await
1004                }
1005                UndropTable(undrop_table_task) => {
1006                    handle_undrop_table_task(self, undrop_table_task, procedure_context).await
1007                }
1008                PurgeDroppedTable(purge_dropped_table_task) => {
1009                    handle_purge_dropped_table_task(
1010                        self,
1011                        purge_dropped_table_task,
1012                        procedure_context,
1013                    )
1014                    .await
1015                }
1016                AlterTable(alter_table_task) => {
1017                    handle_alter_table_task(self, alter_table_task, ddl_options, procedure_context)
1018                        .await
1019                }
1020                TruncateTable(truncate_table_task) => {
1021                    handle_truncate_table_task(
1022                        self,
1023                        truncate_table_task,
1024                        procedure_context,
1025                        ddl_options,
1026                    )
1027                    .await
1028                }
1029                CreateLogicalTables(create_table_tasks) => {
1030                    handle_create_logical_table_tasks(self, create_table_tasks, procedure_context)
1031                        .await
1032                }
1033                AlterLogicalTables(alter_table_tasks) => {
1034                    handle_alter_logical_table_tasks(self, alter_table_tasks, procedure_context)
1035                        .await
1036                }
1037                DropLogicalTables(_) => todo!(),
1038                CreateDatabase(create_database_task) => {
1039                    handle_create_database_task(self, create_database_task, procedure_context).await
1040                }
1041                DropDatabase(drop_database_task) => {
1042                    handle_drop_database_task(self, drop_database_task, procedure_context).await
1043                }
1044                AlterDatabase(alter_database_task) => {
1045                    handle_alter_database_task(self, alter_database_task, procedure_context).await
1046                }
1047                CreateFlow(create_flow_task) => {
1048                    handle_create_flow_task(
1049                        self,
1050                        create_flow_task,
1051                        query_context,
1052                        procedure_context,
1053                    )
1054                    .await
1055                }
1056                DropFlow(drop_flow_task) => {
1057                    #[cfg(feature = "enterprise")]
1058                    if let Some(handler) = self.drop_flow_handler.as_ref() {
1059                        return handler
1060                            .drop_flow(
1061                                drop_flow_task,
1062                                self.procedure_manager.clone(),
1063                                self.ddl_context.clone(),
1064                                procedure_context,
1065                            )
1066                            .await;
1067                    }
1068                    handle_drop_flow_task(self, drop_flow_task, procedure_context).await
1069                }
1070                CreateView(create_view_task) => {
1071                    handle_create_view_task(self, create_view_task, procedure_context).await
1072                }
1073                DropView(drop_view_task) => {
1074                    handle_drop_view_task(self, drop_view_task, procedure_context).await
1075                }
1076                CommentOn(comment_on_task) => {
1077                    handle_comment_on_task(self, comment_on_task, procedure_context).await
1078                }
1079                #[cfg(feature = "enterprise")]
1080                CreateTrigger(create_trigger_task) => {
1081                    handle_create_trigger_task(
1082                        self,
1083                        create_trigger_task,
1084                        query_context,
1085                        procedure_context,
1086                    )
1087                    .await
1088                }
1089                #[cfg(feature = "enterprise")]
1090                DropTrigger(drop_trigger_task) => {
1091                    handle_drop_trigger_task(
1092                        self,
1093                        drop_trigger_task,
1094                        query_context,
1095                        procedure_context,
1096                    )
1097                    .await
1098                }
1099            }
1100        }
1101        .trace(span)
1102        .await
1103    }
1104}
1105
1106async fn handle_truncate_table_task(
1107    ddl_manager: &DdlManager,
1108    truncate_table_task: TruncateTableTask,
1109    procedure_context: ProcedureContext,
1110    ddl_options: DdlOptions,
1111) -> Result<SubmitDdlTaskResponse> {
1112    // Share one timeout across metadata checks, submission and optional waiting.
1113    // Dropping this future does not cancel an already submitted procedure.
1114    tokio::time::timeout(
1115        ddl_options.timeout,
1116        handle_truncate_table_task_inner(
1117            ddl_manager,
1118            truncate_table_task,
1119            procedure_context,
1120            ddl_options,
1121        ),
1122    )
1123    .await
1124    .map_err(|_| {
1125        error::DdlTaskTimeoutSnafu {
1126            timeout: ddl_options.timeout,
1127        }
1128        .build()
1129    })?
1130}
1131
1132async fn handle_truncate_table_task_inner(
1133    ddl_manager: &DdlManager,
1134    truncate_table_task: TruncateTableTask,
1135    procedure_context: ProcedureContext,
1136    ddl_options: DdlOptions,
1137) -> Result<SubmitDdlTaskResponse> {
1138    let table_id = truncate_table_task.table_id;
1139    let table_metadata_manager = &ddl_manager.table_metadata_manager();
1140    let table_ref = truncate_table_task.table_ref();
1141
1142    let table_info_value = table_metadata_manager
1143        .table_info_manager()
1144        .get(table_id)
1145        .await?
1146        .with_context(|| TableInfoNotFoundSnafu {
1147            table: table_ref.to_string(),
1148        })?;
1149    let physical_table_id = table_metadata_manager
1150        .table_route_manager()
1151        .get_physical_table_id(table_id)
1152        .await?;
1153    ensure!(
1154        physical_table_id == table_id,
1155        error::UnexpectedSnafu {
1156            err_msg: "Truncate table is only supported for physical tables."
1157        }
1158    );
1159
1160    let (id, _) = ddl_manager
1161        .submit_truncate_table_task(
1162            truncate_table_task,
1163            table_info_value,
1164            procedure_context,
1165            ddl_options,
1166        )
1167        .await?;
1168
1169    info!(
1170        "Submitted truncate for table: {table_id} via procedure_id {id:?}, wait: {}",
1171        ddl_options.wait
1172    );
1173
1174    Ok(SubmitDdlTaskResponse {
1175        key: id.to_string().into(),
1176        ..Default::default()
1177    })
1178}
1179
1180async fn handle_alter_table_task(
1181    ddl_manager: &DdlManager,
1182    alter_table_task: AlterTableTask,
1183    ddl_options: DdlOptions,
1184    procedure_context: ProcedureContext,
1185) -> Result<SubmitDdlTaskResponse> {
1186    let table_ref = alter_table_task.table_ref();
1187
1188    let table_id = ddl_manager
1189        .table_metadata_manager()
1190        .table_name_manager()
1191        .get(TableNameKey::new(
1192            table_ref.catalog,
1193            table_ref.schema,
1194            table_ref.table,
1195        ))
1196        .await?
1197        .with_context(|| TableNotFoundSnafu {
1198            table_name: table_ref.to_string(),
1199        })?
1200        .table_id();
1201
1202    let table_route_value = ddl_manager
1203        .table_metadata_manager()
1204        .table_route_manager()
1205        .table_route_storage()
1206        .get(table_id)
1207        .await?
1208        .context(TableRouteNotFoundSnafu { table_id })?;
1209    // Classify before the route guard: a mixed annotation batch must surface
1210    // its own error here, not a misleading "non-physical route" one. Families
1211    // that only rewrite the logical table's metadata may target logical tables.
1212    let annotation_family = match alter_table_task.alter_table.kind.as_ref() {
1213        Some(kind) => common_grpc_expr::annotation_alter_family(kind)
1214            .context(ConvertAlterTableRequestSnafu)?,
1215        None => None,
1216    };
1217    ensure!(
1218        table_route_value.is_physical()
1219            || annotation_family.is_some_and(|family| family.allows_logical_tables()),
1220        UnexpectedLogicalRouteTableSnafu {
1221            err_msg: format!("{:?} is a non-physical TableRouteValue.", table_ref),
1222        }
1223    );
1224
1225    let (id, _) = ddl_manager
1226        .submit_alter_table_task(table_id, alter_table_task, procedure_context, ddl_options)
1227        .await?;
1228
1229    info!("Table: {table_id} is altered via procedure_id {id:?}");
1230
1231    Ok(SubmitDdlTaskResponse {
1232        key: id.to_string().into(),
1233        ..Default::default()
1234    })
1235}
1236
1237async fn handle_drop_table_task(
1238    ddl_manager: &DdlManager,
1239    drop_table_task: DropTableTask,
1240    procedure_context: ProcedureContext,
1241) -> Result<SubmitDdlTaskResponse> {
1242    let table_id = drop_table_task.table_id;
1243    let (id, _) = ddl_manager
1244        .submit_drop_table_task(drop_table_task, procedure_context)
1245        .await?;
1246
1247    info!("Table: {table_id} is dropped via procedure_id {id:?}");
1248
1249    Ok(SubmitDdlTaskResponse {
1250        key: id.to_string().into(),
1251        ..Default::default()
1252    })
1253}
1254
1255async fn handle_undrop_table_task(
1256    ddl_manager: &DdlManager,
1257    undrop_table_task: UndropTableTask,
1258    procedure_context: ProcedureContext,
1259) -> Result<SubmitDdlTaskResponse> {
1260    let table_id = undrop_table_task.table_id;
1261    let (id, _) = ddl_manager
1262        .submit_undrop_table_task(undrop_table_task, procedure_context)
1263        .await?;
1264
1265    info!("Table: {table_id} is undropped via procedure_id {id:?}");
1266
1267    Ok(SubmitDdlTaskResponse {
1268        key: id.to_string().into(),
1269        ..Default::default()
1270    })
1271}
1272
1273async fn handle_purge_dropped_table_task(
1274    ddl_manager: &DdlManager,
1275    purge_dropped_table_task: PurgeDroppedTableTask,
1276    procedure_context: ProcedureContext,
1277) -> Result<SubmitDdlTaskResponse> {
1278    let (id, _) = ddl_manager
1279        .submit_purge_dropped_table_task(purge_dropped_table_task, procedure_context)
1280        .await?;
1281
1282    info!("Dropped table is purged via procedure_id {id:?}");
1283
1284    Ok(SubmitDdlTaskResponse {
1285        key: id.to_string().into(),
1286        ..Default::default()
1287    })
1288}
1289
1290async fn handle_create_table_task(
1291    ddl_manager: &DdlManager,
1292    create_table_task: CreateTableTask,
1293    query_context: QueryContext,
1294    procedure_context: ProcedureContext,
1295) -> Result<SubmitDdlTaskResponse> {
1296    let (id, output) = ddl_manager
1297        .submit_create_table_task(create_table_task, query_context, procedure_context)
1298        .await?;
1299
1300    let procedure_id = id.to_string();
1301    let output = output.context(ProcedureOutputSnafu {
1302        procedure_id: &procedure_id,
1303        err_msg: "empty output",
1304    })?;
1305    let table_id = *(output.downcast_ref::<u32>().context(ProcedureOutputSnafu {
1306        procedure_id: &procedure_id,
1307        err_msg: "downcast to `u32`",
1308    })?);
1309    info!("Table: {table_id} is created via procedure_id {id:?}");
1310
1311    Ok(SubmitDdlTaskResponse {
1312        key: procedure_id.into(),
1313        table_ids: vec![table_id],
1314    })
1315}
1316
1317async fn handle_create_logical_table_tasks(
1318    ddl_manager: &DdlManager,
1319    create_table_tasks: Vec<CreateTableTask>,
1320    procedure_context: ProcedureContext,
1321) -> Result<SubmitDdlTaskResponse> {
1322    ensure!(
1323        !create_table_tasks.is_empty(),
1324        EmptyDdlTasksSnafu {
1325            name: "create logical tables"
1326        }
1327    );
1328    let physical_table_id = utils::check_and_get_physical_table_id(
1329        ddl_manager.table_metadata_manager(),
1330        &create_table_tasks,
1331    )
1332    .await?;
1333    let num_logical_tables = create_table_tasks.len();
1334
1335    let (id, output) = ddl_manager
1336        .submit_create_logical_table_tasks(create_table_tasks, physical_table_id, procedure_context)
1337        .await?;
1338
1339    info!(
1340        "{num_logical_tables} logical tables on physical table: {physical_table_id:?} is created via procedure_id {id:?}"
1341    );
1342
1343    let procedure_id = id.to_string();
1344    let output = output.context(ProcedureOutputSnafu {
1345        procedure_id: &procedure_id,
1346        err_msg: "empty output",
1347    })?;
1348    let table_ids = output
1349        .downcast_ref::<Vec<TableId>>()
1350        .context(ProcedureOutputSnafu {
1351            procedure_id: &procedure_id,
1352            err_msg: "downcast to `Vec<TableId>`",
1353        })?
1354        .clone();
1355
1356    Ok(SubmitDdlTaskResponse {
1357        key: procedure_id.into(),
1358        table_ids,
1359    })
1360}
1361
1362async fn handle_create_database_task(
1363    ddl_manager: &DdlManager,
1364    create_database_task: CreateDatabaseTask,
1365    procedure_context: ProcedureContext,
1366) -> Result<SubmitDdlTaskResponse> {
1367    let catalog = create_database_task.catalog.clone();
1368    let schema = create_database_task.schema.clone();
1369    let (id, _) = ddl_manager
1370        .submit_create_database(create_database_task, procedure_context)
1371        .await?;
1372
1373    let procedure_id = id.to_string();
1374    info!(
1375        "Database {}.{} is created via procedure_id {id:?}",
1376        catalog, schema
1377    );
1378
1379    Ok(SubmitDdlTaskResponse {
1380        key: procedure_id.into(),
1381        ..Default::default()
1382    })
1383}
1384
1385async fn handle_drop_database_task(
1386    ddl_manager: &DdlManager,
1387    drop_database_task: DropDatabaseTask,
1388    procedure_context: ProcedureContext,
1389) -> Result<SubmitDdlTaskResponse> {
1390    let (id, _) = ddl_manager
1391        .submit_drop_database(drop_database_task.clone(), procedure_context)
1392        .await?;
1393
1394    let procedure_id = id.to_string();
1395    info!(
1396        "Database {}.{} is dropped via procedure_id {id:?}",
1397        drop_database_task.catalog, drop_database_task.schema
1398    );
1399
1400    Ok(SubmitDdlTaskResponse {
1401        key: procedure_id.into(),
1402        ..Default::default()
1403    })
1404}
1405
1406async fn handle_alter_database_task(
1407    ddl_manager: &DdlManager,
1408    alter_database_task: AlterDatabaseTask,
1409    procedure_context: ProcedureContext,
1410) -> Result<SubmitDdlTaskResponse> {
1411    let (id, _) = ddl_manager
1412        .submit_alter_database(alter_database_task.clone(), procedure_context)
1413        .await?;
1414
1415    let procedure_id = id.to_string();
1416    info!(
1417        "Database {}.{} is altered via procedure_id {id:?}",
1418        alter_database_task.catalog(),
1419        alter_database_task.schema()
1420    );
1421
1422    Ok(SubmitDdlTaskResponse {
1423        key: procedure_id.into(),
1424        ..Default::default()
1425    })
1426}
1427
1428async fn handle_drop_flow_task(
1429    ddl_manager: &DdlManager,
1430    drop_flow_task: DropFlowTask,
1431    procedure_context: ProcedureContext,
1432) -> Result<SubmitDdlTaskResponse> {
1433    let (id, _) = ddl_manager
1434        .submit_drop_flow_task(drop_flow_task.clone(), procedure_context)
1435        .await?;
1436
1437    let procedure_id = id.to_string();
1438    info!(
1439        "Flow {}.{}({}) is dropped via procedure_id {id:?}",
1440        drop_flow_task.catalog_name, drop_flow_task.flow_name, drop_flow_task.flow_id,
1441    );
1442
1443    Ok(SubmitDdlTaskResponse {
1444        key: procedure_id.into(),
1445        ..Default::default()
1446    })
1447}
1448
1449#[cfg(feature = "enterprise")]
1450async fn handle_drop_trigger_task(
1451    ddl_manager: &DdlManager,
1452    drop_trigger_task: DropTriggerTask,
1453    query_context: QueryContext,
1454    procedure_context: ProcedureContext,
1455) -> Result<SubmitDdlTaskResponse> {
1456    let Some(m) = ddl_manager.trigger_ddl_manager.as_ref() else {
1457        use crate::error::UnsupportedSnafu;
1458
1459        return UnsupportedSnafu {
1460            operation: "drop trigger",
1461        }
1462        .fail();
1463    };
1464
1465    m.drop_trigger(
1466        drop_trigger_task,
1467        ddl_manager.procedure_manager.clone(),
1468        ddl_manager.ddl_context.clone(),
1469        query_context,
1470        procedure_context,
1471    )
1472    .await
1473}
1474
1475async fn handle_drop_view_task(
1476    ddl_manager: &DdlManager,
1477    drop_view_task: DropViewTask,
1478    procedure_context: ProcedureContext,
1479) -> Result<SubmitDdlTaskResponse> {
1480    let (id, _) = ddl_manager
1481        .submit_drop_view_task(drop_view_task.clone(), procedure_context)
1482        .await?;
1483
1484    let procedure_id = id.to_string();
1485    info!(
1486        "View {}({}) is dropped via procedure_id {id:?}",
1487        drop_view_task.table_ref(),
1488        drop_view_task.view_id,
1489    );
1490
1491    Ok(SubmitDdlTaskResponse {
1492        key: procedure_id.into(),
1493        ..Default::default()
1494    })
1495}
1496
1497async fn handle_create_flow_task(
1498    ddl_manager: &DdlManager,
1499    create_flow_task: CreateFlowTask,
1500    query_context: QueryContext,
1501    procedure_context: ProcedureContext,
1502) -> Result<SubmitDdlTaskResponse> {
1503    if create_flow_task
1504        .flow_options
1505        .get(FLOW_EXPERIMENTAL_ENABLE_INCREMENTAL_READ_KEY)
1506        .is_some_and(|value| value == FLOW_EXPERIMENTAL_ENABLE_INCREMENTAL_READ_SEQUENCE_RANGE)
1507    {
1508        return error::UnexpectedSnafu {
1509            err_msg: format!(
1510                "reserved flow option value for {FLOW_EXPERIMENTAL_ENABLE_INCREMENTAL_READ_KEY} is internal"
1511            ),
1512        }
1513        .fail();
1514    }
1515
1516    #[cfg(feature = "enterprise")]
1517    if let Some(handler) = ddl_manager.create_flow_handler.as_ref() {
1518        return handler
1519            .create_flow(
1520                create_flow_task,
1521                ddl_manager.procedure_manager.clone(),
1522                ddl_manager.ddl_context.clone(),
1523                query_context,
1524                procedure_context,
1525            )
1526            .await;
1527    }
1528
1529    let (id, output) = ddl_manager
1530        .submit_create_flow_task(create_flow_task.clone(), query_context, procedure_context)
1531        .await?;
1532
1533    let procedure_id = id.to_string();
1534    let output = output.context(ProcedureOutputSnafu {
1535        procedure_id: &procedure_id,
1536        err_msg: "empty output",
1537    })?;
1538    let flow_id = *(output.downcast_ref::<u32>().context(ProcedureOutputSnafu {
1539        procedure_id: &procedure_id,
1540        err_msg: "downcast to `u32`",
1541    })?);
1542    if !create_flow_task.or_replace {
1543        info!(
1544            "Flow {}.{}({flow_id}) is created via procedure_id {id:?}",
1545            create_flow_task.catalog_name, create_flow_task.flow_name,
1546        );
1547    } else {
1548        info!(
1549            "Flow {}.{}({flow_id}) is replaced via procedure_id {id:?}",
1550            create_flow_task.catalog_name, create_flow_task.flow_name,
1551        );
1552    }
1553
1554    Ok(SubmitDdlTaskResponse {
1555        key: procedure_id.into(),
1556        ..Default::default()
1557    })
1558}
1559
1560#[cfg(feature = "enterprise")]
1561async fn handle_create_trigger_task(
1562    ddl_manager: &DdlManager,
1563    create_trigger_task: CreateTriggerTask,
1564    query_context: QueryContext,
1565    procedure_context: ProcedureContext,
1566) -> Result<SubmitDdlTaskResponse> {
1567    let Some(m) = ddl_manager.trigger_ddl_manager.as_ref() else {
1568        use crate::error::UnsupportedSnafu;
1569
1570        return UnsupportedSnafu {
1571            operation: "create trigger",
1572        }
1573        .fail();
1574    };
1575
1576    m.create_trigger(
1577        create_trigger_task,
1578        ddl_manager.procedure_manager.clone(),
1579        ddl_manager.ddl_context.clone(),
1580        query_context,
1581        procedure_context,
1582    )
1583    .await
1584}
1585
1586async fn handle_alter_logical_table_tasks(
1587    ddl_manager: &DdlManager,
1588    alter_table_tasks: Vec<AlterTableTask>,
1589    procedure_context: ProcedureContext,
1590) -> Result<SubmitDdlTaskResponse> {
1591    ensure!(
1592        !alter_table_tasks.is_empty(),
1593        EmptyDdlTasksSnafu {
1594            name: "alter logical tables"
1595        }
1596    );
1597
1598    // Use the physical table id in the first logical table, then it will be checked in the procedure.
1599    let first_table = TableNameKey {
1600        catalog: &alter_table_tasks[0].alter_table.catalog_name,
1601        schema: &alter_table_tasks[0].alter_table.schema_name,
1602        table: &alter_table_tasks[0].alter_table.table_name,
1603    };
1604    let physical_table_id =
1605        utils::get_physical_table_id(ddl_manager.table_metadata_manager(), first_table).await?;
1606    let num_logical_tables = alter_table_tasks.len();
1607
1608    let (id, _) = ddl_manager
1609        .submit_alter_logical_table_tasks(alter_table_tasks, physical_table_id, procedure_context)
1610        .await?;
1611
1612    info!(
1613        "{num_logical_tables} logical tables on physical table: {physical_table_id:?} is altered via procedure_id {id:?}"
1614    );
1615
1616    let procedure_id = id.to_string();
1617
1618    Ok(SubmitDdlTaskResponse {
1619        key: procedure_id.into(),
1620        ..Default::default()
1621    })
1622}
1623
1624/// Handle the `[CreateViewTask]` and returns the DDL response when success.
1625async fn handle_create_view_task(
1626    ddl_manager: &DdlManager,
1627    create_view_task: CreateViewTask,
1628    procedure_context: ProcedureContext,
1629) -> Result<SubmitDdlTaskResponse> {
1630    let (id, output) = ddl_manager
1631        .submit_create_view_task(create_view_task, procedure_context)
1632        .await?;
1633
1634    let procedure_id = id.to_string();
1635    let output = output.context(ProcedureOutputSnafu {
1636        procedure_id: &procedure_id,
1637        err_msg: "empty output",
1638    })?;
1639    let view_id = *(output.downcast_ref::<u32>().context(ProcedureOutputSnafu {
1640        procedure_id: &procedure_id,
1641        err_msg: "downcast to `u32`",
1642    })?);
1643    info!("View: {view_id} is created via procedure_id {id:?}");
1644
1645    Ok(SubmitDdlTaskResponse {
1646        key: procedure_id.into(),
1647        table_ids: vec![view_id],
1648    })
1649}
1650
1651async fn handle_comment_on_task(
1652    ddl_manager: &DdlManager,
1653    comment_on_task: CommentOnTask,
1654    procedure_context: ProcedureContext,
1655) -> Result<SubmitDdlTaskResponse> {
1656    let (id, _) = ddl_manager
1657        .submit_comment_on_task(comment_on_task.clone(), procedure_context)
1658        .await?;
1659
1660    let procedure_id = id.to_string();
1661    info!(
1662        "Comment on {}.{}.{} is updated via procedure_id {id:?}",
1663        comment_on_task.catalog_name, comment_on_task.schema_name, comment_on_task.object_name
1664    );
1665
1666    Ok(SubmitDdlTaskResponse {
1667        key: procedure_id.into(),
1668        ..Default::default()
1669    })
1670}
1671
1672#[cfg(test)]
1673mod tests {
1674    use std::sync::Arc;
1675    #[cfg(feature = "enterprise")]
1676    use std::sync::Mutex;
1677    use std::time::Duration;
1678
1679    #[cfg(feature = "enterprise")]
1680    use common_base::protocol::Channel;
1681    use common_error::ext::BoxedError;
1682    use common_error::ext::ErrorExt;
1683    use common_error::status_code::StatusCode;
1684    #[cfg(feature = "enterprise")]
1685    use common_event_recorder::{PersistentEventContext, ProcedureEventInput, TriggerReason};
1686    use common_procedure::local::LocalManager;
1687    use common_procedure::test_util::InMemoryPoisonStore;
1688    use common_procedure::{
1689        BoxedProcedure, ProcedureContext, ProcedureManager, ProcedureManagerRef,
1690    };
1691    use store_api::storage::TableId;
1692    use table::table_name::TableName;
1693
1694    use super::DdlManager;
1695    use crate::cache_invalidator::DummyCacheInvalidator;
1696    use crate::ddl::alter_table::AlterTableProcedure;
1697    use crate::ddl::create_database::{
1698        AtomicCreateOutcome, CreateDatabaseMetadataCommitter, CreateDatabaseProcedure,
1699    };
1700    #[cfg(feature = "enterprise")]
1701    use crate::ddl::create_flow::{
1702        CreateFlowProcedure, FLOW_EXPERIMENTAL_ENABLE_INCREMENTAL_READ_KEY,
1703        FLOW_EXPERIMENTAL_ENABLE_INCREMENTAL_READ_SEQUENCE_RANGE,
1704    };
1705    use crate::ddl::create_table::CreateTableProcedure;
1706    use crate::ddl::drop_table::DropTableProcedure;
1707    use crate::ddl::flow_meta::FlowMetadataAllocator;
1708    use crate::ddl::table_meta::TableMetadataAllocator;
1709    use crate::ddl::truncate_table::TruncateTableProcedure;
1710    use crate::ddl::{DdlContext, NoopRegionFailureDetectorControl};
1711    use crate::ddl_manager::{RepartitionProcedureFactory, RepartitionSource};
1712    use crate::key::TableMetadataManager;
1713    use crate::key::flow::FlowMetadataManager;
1714    use crate::kv_backend::memory::MemoryKvBackend;
1715    use crate::kv_backend::test_util::MockKvBackendBuilder;
1716    use crate::node_manager::{DatanodeManager, DatanodeRef, FlownodeManager, FlownodeRef};
1717    use crate::peer::Peer;
1718    use crate::procedure_executor::ExecutorContext;
1719    use crate::region_keeper::MemoryRegionKeeper;
1720    use crate::region_registry::LeaderRegionRegistry;
1721    #[cfg(feature = "enterprise")]
1722    use crate::rpc::ddl::trigger::{CreateTriggerTask, DropTriggerTask};
1723    #[cfg(feature = "enterprise")]
1724    use crate::rpc::ddl::{
1725        CreateFlowTask, DdlTask, QueryContext, SubmitDdlTaskRequest, SubmitDdlTaskResponse,
1726    };
1727    use crate::rpc::ddl::{CreatorGrantIntent, UndropTableTask};
1728    #[cfg(not(feature = "enterprise"))]
1729    use crate::rpc::ddl::{DdlTask, PurgeDroppedTableTask, QueryContext, SubmitDdlTaskRequest};
1730    use crate::sequence::SequenceBuilder;
1731    use crate::state_store::KvStateStore;
1732    use crate::test_util::{MockDatanodeManager, new_ddl_context};
1733    use crate::wal_provider::WalProvider;
1734
1735    /// A dummy implemented [NodeManager].
1736    pub struct DummyDatanodeManager;
1737
1738    #[async_trait::async_trait]
1739    impl DatanodeManager for DummyDatanodeManager {
1740        async fn datanode(&self, _datanode: &Peer) -> DatanodeRef {
1741            unimplemented!()
1742        }
1743    }
1744
1745    #[async_trait::async_trait]
1746    impl FlownodeManager for DummyDatanodeManager {
1747        async fn flownode(&self, _node: &Peer) -> FlownodeRef {
1748            unimplemented!()
1749        }
1750    }
1751
1752    struct DummyRepartitionProcedureFactory;
1753
1754    #[async_trait::async_trait]
1755    impl RepartitionProcedureFactory for DummyRepartitionProcedureFactory {
1756        fn create(
1757            &self,
1758            _ddl_ctx: &DdlContext,
1759            _table_name: TableName,
1760            _table_id: TableId,
1761            _source: RepartitionSource,
1762            _to_exprs: Vec<String>,
1763            _timeout: Option<Duration>,
1764        ) -> std::result::Result<BoxedProcedure, BoxedError> {
1765            unimplemented!()
1766        }
1767
1768        fn register_loaders(
1769            &self,
1770            _ddl_ctx: &DdlContext,
1771            _procedure_manager: &ProcedureManagerRef,
1772        ) -> std::result::Result<(), BoxedError> {
1773            Ok(())
1774        }
1775
1776        async fn ensure_gc_requirement(&self) -> std::result::Result<(), BoxedError> {
1777            Ok(())
1778        }
1779    }
1780
1781    struct LoaderCommitter;
1782
1783    #[async_trait::async_trait]
1784    impl CreateDatabaseMetadataCommitter for LoaderCommitter {
1785        async fn commit(
1786            &self,
1787            _catalog: &str,
1788            _schema: &str,
1789            _value: &crate::key::schema_name::SchemaNameValue,
1790            _creator: &CreatorGrantIntent,
1791        ) -> std::result::Result<AtomicCreateOutcome, BoxedError> {
1792            unreachable!()
1793        }
1794    }
1795
1796    #[cfg(feature = "enterprise")]
1797    #[derive(Default)]
1798    struct RecordingCreateFlowHandler {
1799        tasks: Mutex<Vec<CreateFlowTask>>,
1800        contexts: Mutex<Vec<(QueryContext, ProcedureContext)>>,
1801        response: Mutex<Option<SubmitDdlTaskResponse>>,
1802        fail: bool,
1803    }
1804
1805    #[cfg(feature = "enterprise")]
1806    #[async_trait::async_trait]
1807    impl super::CreateFlowHandler for RecordingCreateFlowHandler {
1808        async fn create_flow(
1809            &self,
1810            create_flow_task: CreateFlowTask,
1811            _procedure_manager: ProcedureManagerRef,
1812            _ddl_context: DdlContext,
1813            query_context: QueryContext,
1814            procedure_context: ProcedureContext,
1815        ) -> crate::error::Result<SubmitDdlTaskResponse> {
1816            self.tasks.lock().unwrap().push(create_flow_task);
1817            self.contexts
1818                .lock()
1819                .unwrap()
1820                .push((query_context, procedure_context));
1821            if self.fail {
1822                return crate::error::UnsupportedSnafu {
1823                    operation: "test create flow handler",
1824                }
1825                .fail();
1826            }
1827            Ok(self.response.lock().unwrap().take().unwrap_or_default())
1828        }
1829    }
1830
1831    #[cfg(feature = "enterprise")]
1832    fn test_create_flow_task() -> CreateFlowTask {
1833        CreateFlowTask {
1834            catalog_name: "greptime".to_string(),
1835            flow_name: "test_flow".to_string(),
1836            source_table_names: vec![],
1837            sink_table_name: TableName::new("greptime", "public", "sink"),
1838            or_replace: false,
1839            create_if_not_exists: false,
1840            expire_after: None,
1841            eval_interval_secs: None,
1842            eval_offset_secs: None,
1843            comment: String::new(),
1844            sql: "select 1".to_string(),
1845            flow_options: Default::default(),
1846            eval_schedule: None,
1847        }
1848    }
1849
1850    #[cfg(feature = "enterprise")]
1851    #[derive(Default)]
1852    struct RecordingTriggerDdlManager {
1853        procedure_contexts: Mutex<Vec<ProcedureContext>>,
1854    }
1855
1856    #[cfg(feature = "enterprise")]
1857    #[async_trait::async_trait]
1858    impl super::TriggerDdlManager for RecordingTriggerDdlManager {
1859        async fn create_trigger(
1860            &self,
1861            _create_trigger_task: CreateTriggerTask,
1862            _procedure_manager: ProcedureManagerRef,
1863            _ddl_context: DdlContext,
1864            _query_context: QueryContext,
1865            procedure_context: ProcedureContext,
1866        ) -> crate::error::Result<crate::rpc::ddl::SubmitDdlTaskResponse> {
1867            self.procedure_contexts
1868                .lock()
1869                .unwrap()
1870                .push(procedure_context);
1871            Ok(Default::default())
1872        }
1873
1874        async fn drop_trigger(
1875            &self,
1876            _drop_trigger_task: DropTriggerTask,
1877            _procedure_manager: ProcedureManagerRef,
1878            _ddl_context: DdlContext,
1879            _query_context: QueryContext,
1880            procedure_context: ProcedureContext,
1881        ) -> crate::error::Result<crate::rpc::ddl::SubmitDdlTaskResponse> {
1882            self.procedure_contexts
1883                .lock()
1884                .unwrap()
1885                .push(procedure_context);
1886            Ok(Default::default())
1887        }
1888
1889        fn as_any(&self) -> &dyn std::any::Any {
1890            self
1891        }
1892    }
1893
1894    #[test]
1895    fn test_generic_loader_captures_configured_committer() {
1896        let mut context = new_ddl_context(Arc::new(MockDatanodeManager::new(())));
1897        let committer = Arc::new(LoaderCommitter);
1898        context.create_database_metadata_committer = Some(committer.clone());
1899        let loader = procedure_loader!(CreateDatabaseProcedure)[0].1(context);
1900        assert_eq!(Arc::strong_count(&committer), 2);
1901
1902        let loaded = loader(
1903            r#"{
1904                "state":"CreateMetadata",
1905                "catalog":"greptime",
1906                "schema":"metrics",
1907                "create_if_not_exists":false,
1908                "options":{},
1909                "creator":{"username":"alice","created_at_ns":42}
1910            }"#,
1911        )
1912        .unwrap();
1913
1914        assert_eq!(loaded.type_name(), "metasrv-procedure::CreateDatabase");
1915        assert_eq!(Arc::strong_count(&committer), 3);
1916        drop(loaded);
1917        assert_eq!(Arc::strong_count(&committer), 2);
1918    }
1919
1920    #[test]
1921    fn test_register_loaders() {
1922        let kv_backend = Arc::new(MemoryKvBackend::new());
1923        let table_metadata_manager = Arc::new(TableMetadataManager::new(kv_backend.clone()));
1924        let table_metadata_allocator = Arc::new(TableMetadataAllocator::new(
1925            Arc::new(SequenceBuilder::new("test", kv_backend.clone()).build()),
1926            Arc::new(WalProvider::default()),
1927        ));
1928        let flow_metadata_manager = Arc::new(FlowMetadataManager::new(kv_backend.clone()));
1929        let flow_metadata_allocator = Arc::new(FlowMetadataAllocator::with_noop_peer_allocator(
1930            Arc::new(SequenceBuilder::new("flow-test", kv_backend.clone()).build()),
1931        ));
1932
1933        let state_store = Arc::new(KvStateStore::new(kv_backend.clone()));
1934        let poison_manager = Arc::new(InMemoryPoisonStore::default());
1935        let procedure_manager = Arc::new(LocalManager::new(
1936            Default::default(),
1937            state_store,
1938            poison_manager,
1939            None,
1940            None,
1941        ));
1942
1943        let ddl_manager = DdlManager::new(
1944            DdlContext {
1945                affinity_resolver: None,
1946                affinity_fallback: Default::default(),
1947                peer_alloc_context_provider: None,
1948                node_manager: Arc::new(DummyDatanodeManager),
1949                cache_invalidator: Arc::new(DummyCacheInvalidator),
1950                table_metadata_manager,
1951                table_metadata_allocator,
1952                flow_metadata_manager,
1953                flow_metadata_allocator,
1954                memory_region_keeper: Arc::new(MemoryRegionKeeper::default()),
1955                leader_region_registry: Arc::new(LeaderRegionRegistry::default()),
1956                region_failure_detector_controller: Arc::new(NoopRegionFailureDetectorControl),
1957                soft_drop_enabled: false,
1958                soft_drop_retention: None,
1959                create_database_metadata_committer: None,
1960            },
1961            procedure_manager.clone(),
1962            Arc::new(DummyRepartitionProcedureFactory),
1963        );
1964        ddl_manager.register_loaders().unwrap();
1965
1966        let expected_loaders = vec![
1967            CreateTableProcedure::TYPE_NAME,
1968            AlterTableProcedure::TYPE_NAME,
1969            DropTableProcedure::TYPE_NAME,
1970            TruncateTableProcedure::TYPE_NAME,
1971        ];
1972
1973        for loader in expected_loaders {
1974            assert!(procedure_manager.contains_loader(loader));
1975        }
1976
1977        let soft_drop_loaders = [
1978            "metasrv-procedure::UndropTable",
1979            "metasrv-procedure::PurgeDroppedTable",
1980            "metasrv-procedure::PurgeExpiredDroppedTable",
1981        ];
1982        for loader in soft_drop_loaders {
1983            assert_eq!(
1984                cfg!(feature = "enterprise"),
1985                procedure_manager.contains_loader(loader)
1986            );
1987        }
1988    }
1989
1990    fn build_soft_drop_test_ddl_manager() -> DdlManager {
1991        let kv_backend = Arc::new(MemoryKvBackend::new());
1992        let table_metadata_manager = Arc::new(TableMetadataManager::new(kv_backend.clone()));
1993        let table_metadata_allocator = Arc::new(TableMetadataAllocator::new(
1994            Arc::new(SequenceBuilder::new("test", kv_backend.clone()).build()),
1995            Arc::new(WalProvider::default()),
1996        ));
1997        let flow_metadata_manager = Arc::new(FlowMetadataManager::new(kv_backend.clone()));
1998        let flow_metadata_allocator = Arc::new(FlowMetadataAllocator::with_noop_peer_allocator(
1999            Arc::new(SequenceBuilder::new("flow-test", kv_backend.clone()).build()),
2000        ));
2001
2002        let state_store = Arc::new(KvStateStore::new(kv_backend.clone()));
2003        let poison_manager = Arc::new(InMemoryPoisonStore::default());
2004        let procedure_manager = Arc::new(LocalManager::new(
2005            Default::default(),
2006            state_store,
2007            poison_manager,
2008            None,
2009            None,
2010        ));
2011
2012        DdlManager::new(
2013            DdlContext {
2014                affinity_resolver: None,
2015                affinity_fallback: Default::default(),
2016                peer_alloc_context_provider: None,
2017                node_manager: Arc::new(DummyDatanodeManager),
2018                cache_invalidator: Arc::new(DummyCacheInvalidator),
2019                table_metadata_manager,
2020                table_metadata_allocator,
2021                flow_metadata_manager,
2022                flow_metadata_allocator,
2023                memory_region_keeper: Arc::new(MemoryRegionKeeper::default()),
2024                leader_region_registry: Arc::new(LeaderRegionRegistry::default()),
2025                region_failure_detector_controller: Arc::new(NoopRegionFailureDetectorControl),
2026                soft_drop_enabled: true,
2027                soft_drop_retention: Some(std::time::Duration::from_secs(1)),
2028                create_database_metadata_committer: None,
2029            },
2030            procedure_manager,
2031            Arc::new(DummyRepartitionProcedureFactory),
2032        )
2033    }
2034
2035    #[cfg(feature = "enterprise")]
2036    #[tokio::test]
2037    async fn test_reserved_sequence_range_is_rejected_before_custom_handler() {
2038        let handler = Arc::new(RecordingCreateFlowHandler::default());
2039        let ddl_manager =
2040            build_soft_drop_test_ddl_manager().with_create_flow_handler(handler.clone());
2041        let mut task = test_create_flow_task();
2042        task.flow_options.insert(
2043            FLOW_EXPERIMENTAL_ENABLE_INCREMENTAL_READ_KEY.to_string(),
2044            FLOW_EXPERIMENTAL_ENABLE_INCREMENTAL_READ_SEQUENCE_RANGE.to_string(),
2045        );
2046
2047        let result = ddl_manager
2048            .submit_ddl_task(
2049                ExecutorContext {
2050                    query_context: Some(QueryContext::default()),
2051                    ..Default::default()
2052                },
2053                SubmitDdlTaskRequest::new(DdlTask::new_create_flow(task)),
2054            )
2055            .await;
2056
2057        assert!(result.is_err());
2058        assert!(handler.tasks.lock().unwrap().is_empty());
2059    }
2060
2061    #[cfg(feature = "enterprise")]
2062    #[tokio::test]
2063    async fn test_create_flow_handler_dispatches_without_procedure() {
2064        let response = SubmitDdlTaskResponse {
2065            key: b"custom-handler".to_vec(),
2066            ..Default::default()
2067        };
2068        let handler = Arc::new(RecordingCreateFlowHandler {
2069            response: Mutex::new(Some(response)),
2070            ..Default::default()
2071        });
2072        let ddl_manager =
2073            build_soft_drop_test_ddl_manager().with_create_flow_handler(handler.clone());
2074        let procedure_context = ProcedureContext {
2075            actor: Some("test-user".to_string()),
2076            event_context: Some(
2077                PersistentEventContext::new(TriggerReason::Manual).with_protocol("mysql"),
2078            ),
2079        };
2080        let query_context = QueryContext {
2081            channel: Channel::Mysql as u8,
2082            ..Default::default()
2083        };
2084        let actual = ddl_manager
2085            .submit_ddl_task(
2086                ExecutorContext {
2087                    query_context: Some(query_context.clone()),
2088                    actor: procedure_context.actor.clone(),
2089                    event_input: Some(ProcedureEventInput::new(TriggerReason::Manual)),
2090                    ..Default::default()
2091                },
2092                SubmitDdlTaskRequest::new(DdlTask::new_create_flow(test_create_flow_task())),
2093            )
2094            .await
2095            .unwrap();
2096        assert_eq!(actual.key, b"custom-handler");
2097        assert_eq!(handler.tasks.lock().unwrap().len(), 1);
2098        assert_eq!(
2099            handler.contexts.lock().unwrap().as_slice(),
2100            &[(query_context, procedure_context)]
2101        );
2102    }
2103
2104    #[cfg(feature = "enterprise")]
2105    #[tokio::test]
2106    async fn test_create_flow_handler_error_does_not_fallback() {
2107        let handler = Arc::new(RecordingCreateFlowHandler {
2108            fail: true,
2109            ..Default::default()
2110        });
2111        let ddl_manager =
2112            build_soft_drop_test_ddl_manager().with_create_flow_handler(handler.clone());
2113        let err = ddl_manager
2114            .submit_ddl_task(
2115                ExecutorContext {
2116                    query_context: Some(QueryContext::default()),
2117                    ..Default::default()
2118                },
2119                SubmitDdlTaskRequest::new(DdlTask::new_create_flow(test_create_flow_task())),
2120            )
2121            .await
2122            .unwrap_err();
2123        assert!(err.to_string().contains("test create flow handler"));
2124        assert_eq!(handler.tasks.lock().unwrap().len(), 1);
2125    }
2126
2127    #[cfg(feature = "enterprise")]
2128    #[tokio::test]
2129    async fn test_create_flow_without_handler_uses_ordinary_procedure() {
2130        let ddl_manager = build_soft_drop_test_ddl_manager();
2131        ddl_manager.procedure_manager.start().await.unwrap();
2132        let err = ddl_manager
2133            .submit_ddl_task(
2134                ExecutorContext {
2135                    query_context: Some(QueryContext::default()),
2136                    ..Default::default()
2137                },
2138                SubmitDdlTaskRequest::new(DdlTask::new_create_flow(test_create_flow_task())),
2139            )
2140            .await
2141            .unwrap_err();
2142        assert!(!err.to_string().contains("test create flow handler"));
2143        assert_eq!(
2144            ddl_manager
2145                .procedure_manager
2146                .list_procedures()
2147                .await
2148                .unwrap()
2149                .iter()
2150                .filter(|procedure| procedure.type_name == CreateFlowProcedure::TYPE_NAME)
2151                .count(),
2152            1
2153        );
2154    }
2155
2156    #[cfg(feature = "enterprise")]
2157    #[tokio::test]
2158    async fn test_trigger_ddl_forwards_procedure_context() {
2159        let trigger_ddl_manager = Arc::new(RecordingTriggerDdlManager::default());
2160        let ddl_manager = build_soft_drop_test_ddl_manager()
2161            .with_trigger_ddl_manager(trigger_ddl_manager.clone());
2162        let procedure_context = ProcedureContext {
2163            actor: Some("test-user".to_string()),
2164            event_context: Some(
2165                PersistentEventContext::new(TriggerReason::Manual).with_protocol("mysql"),
2166            ),
2167        };
2168        let executor_context = || ExecutorContext {
2169            query_context: Some(QueryContext {
2170                channel: Channel::Mysql as u8,
2171                ..Default::default()
2172            }),
2173            actor: Some("test-user".to_string()),
2174            event_input: Some(ProcedureEventInput::new(TriggerReason::Manual)),
2175            ..Default::default()
2176        };
2177
2178        ddl_manager
2179            .submit_ddl_task(
2180                executor_context(),
2181                SubmitDdlTaskRequest::new(DdlTask::CreateTrigger(CreateTriggerTask {
2182                    catalog_name: "greptime".to_string(),
2183                    trigger_name: "test_trigger".to_string(),
2184                    if_not_exists: false,
2185                    sql: "SELECT 1".to_string(),
2186                    channels: vec![],
2187                    labels: Default::default(),
2188                    annotations: Default::default(),
2189                    interval: Duration::from_secs(1),
2190                    raw_interval_expr: None,
2191                    r#for: None,
2192                    for_raw_expr: None,
2193                    keep_firing_for: None,
2194                    keep_firing_for_raw_expr: None,
2195                })),
2196            )
2197            .await
2198            .unwrap();
2199        ddl_manager
2200            .submit_ddl_task(
2201                executor_context(),
2202                SubmitDdlTaskRequest::new(DdlTask::DropTrigger(DropTriggerTask {
2203                    catalog_name: "greptime".to_string(),
2204                    trigger_name: "test_trigger".to_string(),
2205                    drop_if_exists: false,
2206                })),
2207            )
2208            .await
2209            .unwrap();
2210
2211        assert_eq!(
2212            *trigger_ddl_manager.procedure_contexts.lock().unwrap(),
2213            vec![procedure_context.clone(), procedure_context]
2214        );
2215    }
2216
2217    #[cfg(feature = "enterprise")]
2218    #[tokio::test]
2219    async fn test_submit_undrop_missing_tombstone_returns_table_not_found_directly() {
2220        let ddl_manager = build_soft_drop_test_ddl_manager();
2221
2222        let err = ddl_manager
2223            .submit_undrop_table_task(
2224                UndropTableTask { table_id: 1024 },
2225                ProcedureContext::default(),
2226            )
2227            .await
2228            .unwrap_err();
2229
2230        assert_eq!(err.status_code(), StatusCode::TableNotFound);
2231        assert!(matches!(err, crate::error::Error::TableNotFound { .. }));
2232    }
2233
2234    #[cfg(not(feature = "enterprise"))]
2235    #[tokio::test]
2236    async fn test_submit_undrop_and_purge_rejected_in_non_enterprise_build() {
2237        let ddl_manager = build_soft_drop_test_ddl_manager();
2238
2239        let err = ddl_manager
2240            .submit_undrop_table_task(
2241                UndropTableTask { table_id: 1024 },
2242                ProcedureContext::default(),
2243            )
2244            .await
2245            .unwrap_err();
2246        assert!(matches!(err, crate::error::Error::Unsupported { .. }));
2247
2248        let err = ddl_manager
2249            .submit_purge_dropped_table_task(
2250                PurgeDroppedTableTask { table_id: 1024 },
2251                ProcedureContext::default(),
2252            )
2253            .await
2254            .unwrap_err();
2255        assert!(matches!(err, crate::error::Error::Unsupported { .. }));
2256
2257        let err = ddl_manager
2258            .submit_expired_purge_dropped_table_task(PurgeDroppedTableTask { table_id: 1024 })
2259            .await
2260            .unwrap_err();
2261        assert!(matches!(err, crate::error::Error::Unsupported { .. }));
2262
2263        for task in [
2264            DdlTask::UndropTable(UndropTableTask { table_id: 1024 }),
2265            DdlTask::PurgeDroppedTable(PurgeDroppedTableTask { table_id: 1024 }),
2266        ] {
2267            let err = ddl_manager
2268                .submit_ddl_task(
2269                    ExecutorContext {
2270                        query_context: Some(QueryContext::default()),
2271                        ..Default::default()
2272                    },
2273                    SubmitDdlTaskRequest::new(task),
2274                )
2275                .await
2276                .unwrap_err();
2277            assert!(matches!(err, crate::error::Error::Unsupported { .. }));
2278        }
2279    }
2280
2281    #[tokio::test]
2282    async fn test_truncate_timeout_covers_metadata_checks() {
2283        let mut context = new_ddl_context(Arc::new(DummyDatanodeManager));
2284        let kv_backend = MockKvBackendBuilder::default()
2285            .range_async_fn(Arc::new(|_| Box::pin(std::future::pending())))
2286            .build()
2287            .unwrap();
2288        context.table_metadata_manager = Arc::new(TableMetadataManager::new(Arc::new(kv_backend)));
2289        let ddl_manager = ddl_manager_with_context(context).await;
2290
2291        for wait in [true, false] {
2292            let timeout = Duration::from_millis(20);
2293            let request = SubmitDdlTaskRequest {
2294                task: DdlTask::new_truncate_table(
2295                    "greptime".to_string(),
2296                    "public".to_string(),
2297                    "metrics".to_string(),
2298                    1024,
2299                    Vec::new(),
2300                ),
2301                wait,
2302                timeout,
2303            };
2304            let err = tokio::time::timeout(
2305                Duration::from_secs(1),
2306                ddl_manager.submit_ddl_task(
2307                    ExecutorContext {
2308                        query_context: Some(QueryContext::default()),
2309                        ..Default::default()
2310                    },
2311                    request,
2312                ),
2313            )
2314            .await
2315            .expect("metadata reads must be bounded by the request timeout")
2316            .unwrap_err();
2317            assert!(matches!(
2318                err,
2319                crate::error::Error::DdlTaskTimeout { timeout: actual, .. } if actual == timeout
2320            ));
2321            assert_eq!(err.status_code(), StatusCode::Cancelled);
2322            assert!(
2323                ddl_manager
2324                    .procedure_manager
2325                    .list_procedures()
2326                    .await
2327                    .unwrap()
2328                    .is_empty()
2329            );
2330        }
2331
2332        ddl_manager.procedure_manager.stop().await.unwrap();
2333    }
2334
2335    async fn ddl_manager_with_context(ddl_context: DdlContext) -> DdlManager {
2336        let kv_backend = Arc::new(MemoryKvBackend::new());
2337        let state_store = Arc::new(KvStateStore::new(kv_backend.clone()));
2338        let poison_manager = Arc::new(InMemoryPoisonStore::default());
2339        let procedure_manager = Arc::new(LocalManager::new(
2340            Default::default(),
2341            state_store,
2342            poison_manager,
2343            None,
2344            None,
2345        ));
2346        procedure_manager.start().await.unwrap();
2347        let ddl_manager = DdlManager::new(
2348            ddl_context,
2349            procedure_manager,
2350            Arc::new(DummyRepartitionProcedureFactory),
2351        );
2352        ddl_manager.register_loaders().unwrap();
2353        ddl_manager
2354    }
2355
2356    fn set_options_expr(table_name: &str, options: &[(&str, &str)]) -> api::v1::AlterTableExpr {
2357        api::v1::AlterTableExpr {
2358            catalog_name: common_catalog::consts::DEFAULT_CATALOG_NAME.to_string(),
2359            schema_name: common_catalog::consts::DEFAULT_SCHEMA_NAME.to_string(),
2360            table_name: table_name.to_string(),
2361            kind: Some(api::v1::alter_table_expr::Kind::SetTableOptions(
2362                api::v1::SetTableOptions {
2363                    table_options: options
2364                        .iter()
2365                        .map(|(key, value)| api::v1::Option {
2366                            key: key.to_string(),
2367                            value: value.to_string(),
2368                        })
2369                        .collect(),
2370                },
2371            )),
2372        }
2373    }
2374
2375    #[tokio::test]
2376    async fn test_logical_table_annotation_alter_routing() {
2377        let (tx, mut rx) = tokio::sync::mpsc::channel(8);
2378        let node_manager = Arc::new(crate::test_util::MockDatanodeManager::new(
2379            crate::ddl::test_util::datanode_handler::DatanodeWatcher::new(tx),
2380        ));
2381        let ddl_context = crate::test_util::new_ddl_context(node_manager);
2382        let phy_id = crate::ddl::test_util::create_physical_table(&ddl_context, "phy").await;
2383        let logical_id =
2384            crate::ddl::test_util::create_logical_table(ddl_context.clone(), phy_id, "logical")
2385                .await;
2386        let ddl_manager = ddl_manager_with_context(ddl_context.clone()).await;
2387
2388        // A semantic alter on a logical table passes the route guard, updates
2389        // only the logical table's metadata, and dispatches nothing.
2390        ddl_manager
2391            .submit_ddl_task(
2392                ExecutorContext {
2393                    query_context: Some(QueryContext::default()),
2394                    ..Default::default()
2395                },
2396                SubmitDdlTaskRequest::new(DdlTask::new_alter_table(set_options_expr(
2397                    "logical",
2398                    &[("greptime.semantic.signal_type", "metric")],
2399                ))),
2400            )
2401            .await
2402            .unwrap();
2403        rx.try_recv().unwrap_err();
2404        let table_info = ddl_manager
2405            .table_metadata_manager()
2406            .table_info_manager()
2407            .get(logical_id)
2408            .await
2409            .unwrap()
2410            .unwrap()
2411            .into_inner()
2412            .table_info;
2413        assert_eq!(
2414            table_info
2415                .meta
2416                .options
2417                .extra_options
2418                .get("greptime.semantic.signal_type"),
2419            Some(&"metric".to_string())
2420        );
2421
2422        // A mixed batch fails with its own error, not the route guard's.
2423        let err = ddl_manager
2424            .submit_ddl_task(
2425                ExecutorContext {
2426                    query_context: Some(QueryContext::default()),
2427                    ..Default::default()
2428                },
2429                SubmitDdlTaskRequest::new(DdlTask::new_alter_table(set_options_expr(
2430                    "logical",
2431                    &[("greptime.semantic.source", "prometheus"), ("ttl", "7d")],
2432                ))),
2433            )
2434            .await
2435            .unwrap_err();
2436        let msg = common_error::ext::ErrorExt::output_msg(&err);
2437        assert!(msg.contains("must be altered separately"), "{msg}");
2438
2439        // The repartition hint drives physical repartitioning; on a logical
2440        // route it stays rejected by the guard.
2441        for (key, value) in [
2442            (table::requests::REPARTITION_COLUMN_HINT_KEY, "host"),
2443            (table::requests::REPARTITION_PARTITION_NUM_HINT_KEY, "8"),
2444        ] {
2445            let err = ddl_manager
2446                .submit_ddl_task(
2447                    ExecutorContext {
2448                        query_context: Some(QueryContext::default()),
2449                        ..Default::default()
2450                    },
2451                    SubmitDdlTaskRequest::new(DdlTask::new_alter_table(set_options_expr(
2452                        "logical",
2453                        &[(key, value)],
2454                    ))),
2455                )
2456                .await
2457                .unwrap_err();
2458            let msg = common_error::ext::ErrorExt::output_msg(&err);
2459            assert!(msg.contains("non-physical TableRouteValue"), "{msg}");
2460        }
2461    }
2462}