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