Skip to main content

common_meta/rpc/
ddl.rs

1// Copyright 2023 Greptime Team
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15#[cfg(feature = "enterprise")]
16pub mod trigger;
17
18use std::collections::{HashMap, HashSet};
19use std::result;
20use std::time::Duration;
21
22use api::helper::{from_pb_time_ranges, to_pb_time_ranges};
23use api::v1::alter_database_expr::Kind as PbAlterDatabaseKind;
24use api::v1::meta::ddl_task_request::Task;
25use api::v1::meta::{
26    AlterDatabaseTask as PbAlterDatabaseTask, AlterTableTask as PbAlterTableTask,
27    AlterTableTasks as PbAlterTableTasks, CommentOnTask as PbCommentOnTask,
28    CreateDatabaseTask as PbCreateDatabaseTask, CreateFlowTask as PbCreateFlowTask,
29    CreateTableTask as PbCreateTableTask, CreateTableTasks as PbCreateTableTasks,
30    CreateViewTask as PbCreateViewTask, DdlTaskRequest as PbDdlTaskRequest,
31    DdlTaskResponse as PbDdlTaskResponse, DropDatabaseTask as PbDropDatabaseTask,
32    DropFlowTask as PbDropFlowTask, DropTableTask as PbDropTableTask,
33    DropTableTasks as PbDropTableTasks, DropViewTask as PbDropViewTask, Partition, ProcedureId,
34    PurgeDroppedTableTask as PbPurgeDroppedTableTask, TruncateTableTask as PbTruncateTableTask,
35    UndropTableTask as PbUndropTableTask,
36};
37use api::v1::{
38    AlterDatabaseExpr, AlterTableExpr, CommentObjectType as PbCommentObjectType, CommentOnExpr,
39    CreateDatabaseExpr, CreateFlowExpr, CreateTableExpr, CreateViewExpr, DropDatabaseExpr,
40    DropFlowExpr, DropTableExpr, DropViewExpr, EvalInterval, ExpireAfter, Option as PbOption,
41    QueryContext as PbQueryContext, TruncateTableExpr,
42};
43use base64::Engine as _;
44use base64::engine::general_purpose;
45use common_base::protocol::Channel;
46use common_catalog::{format_full_flow_name, format_full_table_name};
47use common_error::ext::BoxedError;
48pub use common_event_recorder::TriggerReason;
49use common_time::{DatabaseTimeToLive, Timestamp};
50use prost::Message;
51use serde::{Deserialize, Serialize};
52use serde_with::{DefaultOnNull, serde_as};
53use snafu::{OptionExt, ResultExt};
54use table::metadata::{TableId, TableInfo};
55use table::requests::{validate_database_option, validate_database_option_value};
56use table::table_name::TableName;
57use table::table_reference::TableReference;
58
59use crate::error::{
60    self, ConvertTimeRangesSnafu, ExternalSnafu, InvalidSetDatabaseOptionSnafu,
61    InvalidUnsetDatabaseOptionSnafu, Result,
62};
63use crate::flow_name::FlowName;
64use crate::instruction::CacheIdent;
65use crate::key::FlowId;
66use crate::key::flow::flow_name::FlowNameManager;
67use crate::key::table_name::{TableNameKey, TableNameManager};
68
69/// Reserved query-context extension key for the frontend peer address that submitted a DDL request.
70pub const ORIGIN_FRONTEND_ADDR_EXTENSION_KEY: &str = "__greptime_origin_frontend.addr";
71/// Reserved query-context extension key for the authenticated database creator.
72pub const CREATE_DATABASE_CREATOR_EXTENSION_KEY: &str = "__greptime_create_database.creator";
73/// Internal gRPC metadata key for the authenticated database creator.
74pub const CREATE_DATABASE_CREATOR_METADATA_KEY: &str =
75    "x-greptime-internal-create-database-creator-bin";
76
77#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
78pub struct CreatorGrantIntent {
79    pub username: String,
80    pub created_at_ns: i64,
81}
82
83/// DDL tasks
84#[derive(Debug, Clone)]
85pub enum DdlTask {
86    CreateTable(CreateTableTask),
87    DropTable(DropTableTask),
88    UndropTable(UndropTableTask),
89    PurgeDroppedTable(PurgeDroppedTableTask),
90    AlterTable(AlterTableTask),
91    TruncateTable(TruncateTableTask),
92    CreateLogicalTables(Vec<CreateTableTask>),
93    DropLogicalTables(Vec<DropTableTask>),
94    AlterLogicalTables(Vec<AlterTableTask>),
95    CreateDatabase(CreateDatabaseTask),
96    DropDatabase(DropDatabaseTask),
97    AlterDatabase(AlterDatabaseTask),
98    CreateFlow(CreateFlowTask),
99    DropFlow(DropFlowTask),
100    #[cfg(feature = "enterprise")]
101    DropTrigger(trigger::DropTriggerTask),
102    CreateView(CreateViewTask),
103    DropView(DropViewTask),
104    #[cfg(feature = "enterprise")]
105    CreateTrigger(trigger::CreateTriggerTask),
106    CommentOn(CommentOnTask),
107}
108
109impl DdlTask {
110    /// Creates a [`DdlTask`] to create a flow.
111    pub fn new_create_flow(expr: CreateFlowTask) -> Self {
112        DdlTask::CreateFlow(expr)
113    }
114
115    /// Creates a [`DdlTask`] to drop a flow.
116    pub fn new_drop_flow(expr: DropFlowTask) -> Self {
117        DdlTask::DropFlow(expr)
118    }
119
120    /// Creates a [`DdlTask`] to drop a view.
121    pub fn new_drop_view(expr: DropViewTask) -> Self {
122        DdlTask::DropView(expr)
123    }
124
125    /// Creates a [`DdlTask`] to create a table.
126    pub fn new_create_table(
127        expr: CreateTableExpr,
128        partitions: Vec<Partition>,
129        table_info: TableInfo,
130    ) -> Self {
131        DdlTask::CreateTable(CreateTableTask::new(expr, partitions, table_info))
132    }
133
134    /// Creates a [`DdlTask`] to create several logical tables.
135    pub fn new_create_logical_tables(table_data: Vec<(CreateTableExpr, TableInfo)>) -> Self {
136        DdlTask::CreateLogicalTables(
137            table_data
138                .into_iter()
139                .map(|(expr, table_info)| CreateTableTask::new(expr, Vec::new(), table_info))
140                .collect(),
141        )
142    }
143
144    /// Creates a [`DdlTask`] to alter several logical tables.
145    pub fn new_alter_logical_tables(table_data: Vec<AlterTableExpr>) -> Self {
146        DdlTask::AlterLogicalTables(
147            table_data
148                .into_iter()
149                .map(|alter_table| AlterTableTask { alter_table })
150                .collect(),
151        )
152    }
153
154    /// Creates a [`DdlTask`] to drop a table.
155    pub fn new_drop_table(
156        catalog: String,
157        schema: String,
158        table: String,
159        table_id: TableId,
160        drop_if_exists: bool,
161    ) -> Self {
162        DdlTask::DropTable(DropTableTask {
163            catalog,
164            schema,
165            table,
166            table_id,
167            drop_if_exists,
168        })
169    }
170
171    /// Creates a [`DdlTask`] to undrop a table.
172    pub fn new_undrop_table(table_id: TableId) -> Self {
173        DdlTask::UndropTable(UndropTableTask { table_id })
174    }
175
176    /// Creates a [`DdlTask`] to purge a dropped table.
177    pub fn new_purge_dropped_table(table_id: TableId) -> Self {
178        DdlTask::PurgeDroppedTable(PurgeDroppedTableTask { table_id })
179    }
180
181    /// Creates a [`DdlTask`] to create a database.
182    pub fn new_create_database(
183        catalog: String,
184        schema: String,
185        create_if_not_exists: bool,
186        options: HashMap<String, String>,
187        creator: Option<CreatorGrantIntent>,
188    ) -> Self {
189        DdlTask::CreateDatabase(CreateDatabaseTask {
190            catalog,
191            schema,
192            create_if_not_exists,
193            options,
194            creator,
195        })
196    }
197
198    /// Creates a [`DdlTask`] to drop a database.
199    pub fn new_drop_database(catalog: String, schema: String, drop_if_exists: bool) -> Self {
200        DdlTask::DropDatabase(DropDatabaseTask {
201            catalog,
202            schema,
203            drop_if_exists,
204        })
205    }
206
207    /// Creates a [`DdlTask`] to alter a database.
208    pub fn new_alter_database(alter_expr: AlterDatabaseExpr) -> Self {
209        DdlTask::AlterDatabase(AlterDatabaseTask { alter_expr })
210    }
211
212    /// Creates a [`DdlTask`] to alter a table.
213    pub fn new_alter_table(alter_table: AlterTableExpr) -> Self {
214        DdlTask::AlterTable(AlterTableTask { alter_table })
215    }
216
217    /// Creates a [`DdlTask`] to truncate a table.
218    pub fn new_truncate_table(
219        catalog: String,
220        schema: String,
221        table: String,
222        table_id: TableId,
223        time_ranges: Vec<(Timestamp, Timestamp)>,
224    ) -> Self {
225        DdlTask::TruncateTable(TruncateTableTask {
226            catalog,
227            schema,
228            table,
229            table_id,
230            time_ranges,
231        })
232    }
233
234    /// Creates a [`DdlTask`] to create a view.
235    pub fn new_create_view(create_view: CreateViewExpr, view_info: TableInfo) -> Self {
236        DdlTask::CreateView(CreateViewTask {
237            create_view,
238            view_info,
239        })
240    }
241
242    /// Creates a [`DdlTask`] to comment on a table, column, or flow.
243    pub fn new_comment_on(task: CommentOnTask) -> Self {
244        DdlTask::CommentOn(task)
245    }
246}
247
248impl TryFrom<Task> for DdlTask {
249    type Error = error::Error;
250    fn try_from(task: Task) -> Result<Self> {
251        match task {
252            Task::CreateTableTask(create_table) => {
253                Ok(DdlTask::CreateTable(create_table.try_into()?))
254            }
255            Task::DropTableTask(drop_table) => Ok(DdlTask::DropTable(drop_table.try_into()?)),
256            Task::UndropTableTask(undrop_table) => {
257                Ok(DdlTask::UndropTable(undrop_table.try_into()?))
258            }
259            Task::PurgeDroppedTableTask(purge_dropped_table) => {
260                Ok(DdlTask::PurgeDroppedTable(purge_dropped_table.try_into()?))
261            }
262            Task::AlterTableTask(alter_table) => Ok(DdlTask::AlterTable(alter_table.try_into()?)),
263            Task::TruncateTableTask(truncate_table) => {
264                Ok(DdlTask::TruncateTable(truncate_table.try_into()?))
265            }
266            Task::CreateTableTasks(create_tables) => {
267                let tasks = create_tables
268                    .tasks
269                    .into_iter()
270                    .map(|task| task.try_into())
271                    .collect::<Result<Vec<_>>>()?;
272
273                Ok(DdlTask::CreateLogicalTables(tasks))
274            }
275            Task::DropTableTasks(drop_tables) => {
276                let tasks = drop_tables
277                    .tasks
278                    .into_iter()
279                    .map(|task| task.try_into())
280                    .collect::<Result<Vec<_>>>()?;
281
282                Ok(DdlTask::DropLogicalTables(tasks))
283            }
284            Task::AlterTableTasks(alter_tables) => {
285                let tasks = alter_tables
286                    .tasks
287                    .into_iter()
288                    .map(|task| task.try_into())
289                    .collect::<Result<Vec<_>>>()?;
290
291                Ok(DdlTask::AlterLogicalTables(tasks))
292            }
293            Task::CreateDatabaseTask(create_database) => {
294                Ok(DdlTask::CreateDatabase(create_database.try_into()?))
295            }
296            Task::DropDatabaseTask(drop_database) => {
297                Ok(DdlTask::DropDatabase(drop_database.try_into()?))
298            }
299            Task::AlterDatabaseTask(alter_database) => {
300                Ok(DdlTask::AlterDatabase(alter_database.try_into()?))
301            }
302            Task::CreateFlowTask(create_flow) => Ok(DdlTask::CreateFlow(create_flow.try_into()?)),
303            Task::DropFlowTask(drop_flow) => Ok(DdlTask::DropFlow(drop_flow.try_into()?)),
304            Task::CreateViewTask(create_view) => Ok(DdlTask::CreateView(create_view.try_into()?)),
305            Task::DropViewTask(drop_view) => Ok(DdlTask::DropView(drop_view.try_into()?)),
306            Task::CreateTriggerTask(create_trigger) => {
307                #[cfg(feature = "enterprise")]
308                return Ok(DdlTask::CreateTrigger(create_trigger.try_into()?));
309                #[cfg(not(feature = "enterprise"))]
310                {
311                    let _ = create_trigger;
312                    crate::error::UnsupportedSnafu {
313                        operation: "create trigger",
314                    }
315                    .fail()
316                }
317            }
318            Task::DropTriggerTask(drop_trigger) => {
319                #[cfg(feature = "enterprise")]
320                return Ok(DdlTask::DropTrigger(drop_trigger.try_into()?));
321                #[cfg(not(feature = "enterprise"))]
322                {
323                    let _ = drop_trigger;
324                    crate::error::UnsupportedSnafu {
325                        operation: "drop trigger",
326                    }
327                    .fail()
328                }
329            }
330            Task::CommentOnTask(comment_on) => Ok(DdlTask::CommentOn(comment_on.try_into()?)),
331        }
332    }
333}
334
335#[derive(Clone)]
336pub struct SubmitDdlTaskRequest {
337    pub wait: bool,
338    pub timeout: Duration,
339    pub task: DdlTask,
340}
341
342impl SubmitDdlTaskRequest {
343    /// The default constructor for [`SubmitDdlTaskRequest`].
344    pub fn new(task: DdlTask) -> Self {
345        Self {
346            wait: Self::default_wait(),
347            timeout: Self::default_timeout(),
348            task,
349        }
350    }
351
352    /// The default timeout for a DDL task.
353    pub fn default_timeout() -> Duration {
354        Duration::from_secs(60)
355    }
356
357    /// The default wait for a DDL task.
358    pub fn default_wait() -> bool {
359        true
360    }
361}
362
363fn ddl_timeout_secs(timeout: Duration) -> u32 {
364    timeout
365        .as_nanos()
366        .div_ceil(Duration::from_secs(1).as_nanos())
367        .try_into()
368        .unwrap_or(u32::MAX)
369}
370
371impl TryFrom<SubmitDdlTaskRequest> for PbDdlTaskRequest {
372    type Error = error::Error;
373
374    fn try_from(request: SubmitDdlTaskRequest) -> Result<Self> {
375        let SubmitDdlTaskRequest {
376            wait,
377            timeout,
378            task,
379        } = request;
380
381        let task = match task {
382            DdlTask::CreateTable(task) => Task::CreateTableTask(task.try_into()?),
383            DdlTask::DropTable(task) => Task::DropTableTask(task.into()),
384            DdlTask::UndropTable(task) => Task::UndropTableTask(task.into()),
385            DdlTask::PurgeDroppedTable(task) => Task::PurgeDroppedTableTask(task.into()),
386            DdlTask::AlterTable(task) => Task::AlterTableTask(task.try_into()?),
387            DdlTask::TruncateTable(task) => Task::TruncateTableTask(task.try_into()?),
388            DdlTask::CreateLogicalTables(tasks) => {
389                let tasks = tasks
390                    .into_iter()
391                    .map(|task| task.try_into())
392                    .collect::<Result<Vec<_>>>()?;
393
394                Task::CreateTableTasks(PbCreateTableTasks { tasks })
395            }
396            DdlTask::DropLogicalTables(tasks) => {
397                let tasks = tasks
398                    .into_iter()
399                    .map(|task| task.into())
400                    .collect::<Vec<_>>();
401
402                Task::DropTableTasks(PbDropTableTasks { tasks })
403            }
404            DdlTask::AlterLogicalTables(tasks) => {
405                let tasks = tasks
406                    .into_iter()
407                    .map(|task| task.try_into())
408                    .collect::<Result<Vec<_>>>()?;
409
410                Task::AlterTableTasks(PbAlterTableTasks { tasks })
411            }
412            DdlTask::CreateDatabase(task) => Task::CreateDatabaseTask(task.try_into()?),
413            DdlTask::DropDatabase(task) => Task::DropDatabaseTask(task.try_into()?),
414            DdlTask::AlterDatabase(task) => Task::AlterDatabaseTask(task.try_into()?),
415            DdlTask::CreateFlow(task) => Task::CreateFlowTask(task.into()),
416            DdlTask::DropFlow(task) => Task::DropFlowTask(task.into()),
417            DdlTask::CreateView(task) => Task::CreateViewTask(task.try_into()?),
418            DdlTask::DropView(task) => Task::DropViewTask(task.into()),
419            #[cfg(feature = "enterprise")]
420            DdlTask::CreateTrigger(task) => Task::CreateTriggerTask(task.try_into()?),
421            #[cfg(feature = "enterprise")]
422            DdlTask::DropTrigger(task) => Task::DropTriggerTask(task.into()),
423            DdlTask::CommentOn(task) => Task::CommentOnTask(task.into()),
424        };
425
426        Ok(Self {
427            header: None,
428            query_context: None,
429            timeout_secs: ddl_timeout_secs(timeout),
430            wait,
431            task: Some(task),
432            event_context: None,
433            actor: None,
434        })
435    }
436}
437
438#[derive(Debug, Default)]
439pub struct SubmitDdlTaskResponse {
440    pub key: Vec<u8>,
441    // `table_id`s for `CREATE TABLE` or `CREATE LOGICAL TABLES` task.
442    pub table_ids: Vec<TableId>,
443}
444
445impl TryFrom<PbDdlTaskResponse> for SubmitDdlTaskResponse {
446    type Error = error::Error;
447
448    fn try_from(resp: PbDdlTaskResponse) -> Result<Self> {
449        let table_ids = resp.table_ids.into_iter().map(|t| t.id).collect();
450        Ok(Self {
451            key: resp.pid.map(|pid| pid.key).unwrap_or_default(),
452            table_ids,
453        })
454    }
455}
456
457impl From<SubmitDdlTaskResponse> for PbDdlTaskResponse {
458    fn from(val: SubmitDdlTaskResponse) -> Self {
459        Self {
460            pid: Some(ProcedureId { key: val.key }),
461            table_ids: val
462                .table_ids
463                .into_iter()
464                .map(|id| api::v1::TableId { id })
465                .collect(),
466            ..Default::default()
467        }
468    }
469}
470
471/// A `CREATE VIEW` task.
472#[derive(Debug, PartialEq, Clone)]
473pub struct CreateViewTask {
474    pub create_view: CreateViewExpr,
475    pub view_info: TableInfo,
476}
477
478impl CreateViewTask {
479    /// Returns the [`TableReference`] of view.
480    pub fn table_ref(&self) -> TableReference<'_> {
481        TableReference {
482            catalog: &self.create_view.catalog_name,
483            schema: &self.create_view.schema_name,
484            table: &self.create_view.view_name,
485        }
486    }
487
488    /// Returns the encoded logical plan
489    pub fn raw_logical_plan(&self) -> &Vec<u8> {
490        &self.create_view.logical_plan
491    }
492
493    /// Returns the view definition in SQL
494    pub fn view_definition(&self) -> &str {
495        &self.create_view.definition
496    }
497
498    /// Returns the resolved table names in view's logical plan
499    pub fn table_names(&self) -> HashSet<TableName> {
500        self.create_view
501            .table_names
502            .iter()
503            .map(|t| t.clone().into())
504            .collect()
505    }
506
507    /// Returns the view's columns
508    pub fn columns(&self) -> &Vec<String> {
509        &self.create_view.columns
510    }
511
512    /// Returns the original logical plan's columns
513    pub fn plan_columns(&self) -> &Vec<String> {
514        &self.create_view.plan_columns
515    }
516}
517
518impl TryFrom<PbCreateViewTask> for CreateViewTask {
519    type Error = error::Error;
520
521    fn try_from(pb: PbCreateViewTask) -> Result<Self> {
522        let view_info = serde_json::from_slice(&pb.view_info).context(error::SerdeJsonSnafu)?;
523
524        Ok(CreateViewTask {
525            create_view: pb.create_view.context(error::InvalidProtoMsgSnafu {
526                err_msg: "expected create view",
527            })?,
528            view_info,
529        })
530    }
531}
532
533impl TryFrom<CreateViewTask> for PbCreateViewTask {
534    type Error = error::Error;
535
536    fn try_from(task: CreateViewTask) -> Result<PbCreateViewTask> {
537        Ok(PbCreateViewTask {
538            create_view: Some(task.create_view),
539            view_info: serde_json::to_vec(&task.view_info).context(error::SerdeJsonSnafu)?,
540        })
541    }
542}
543
544impl Serialize for CreateViewTask {
545    fn serialize<S>(&self, serializer: S) -> result::Result<S::Ok, S::Error>
546    where
547        S: serde::Serializer,
548    {
549        let view_info = serde_json::to_vec(&self.view_info)
550            .map_err(|err| serde::ser::Error::custom(err.to_string()))?;
551
552        let pb = PbCreateViewTask {
553            create_view: Some(self.create_view.clone()),
554            view_info,
555        };
556        let buf = pb.encode_to_vec();
557        let encoded = general_purpose::STANDARD_NO_PAD.encode(buf);
558        serializer.serialize_str(&encoded)
559    }
560}
561
562impl<'de> Deserialize<'de> for CreateViewTask {
563    fn deserialize<D>(deserializer: D) -> result::Result<Self, D::Error>
564    where
565        D: serde::Deserializer<'de>,
566    {
567        let encoded = String::deserialize(deserializer)?;
568        let buf = general_purpose::STANDARD_NO_PAD
569            .decode(encoded)
570            .map_err(|err| serde::de::Error::custom(err.to_string()))?;
571        let expr: PbCreateViewTask = PbCreateViewTask::decode(&*buf)
572            .map_err(|err| serde::de::Error::custom(err.to_string()))?;
573
574        let expr = CreateViewTask::try_from(expr)
575            .map_err(|err| serde::de::Error::custom(err.to_string()))?;
576
577        Ok(expr)
578    }
579}
580
581/// A `DROP VIEW` task.
582#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)]
583pub struct DropViewTask {
584    pub catalog: String,
585    pub schema: String,
586    pub view: String,
587    pub view_id: TableId,
588    pub drop_if_exists: bool,
589}
590
591impl DropViewTask {
592    /// Returns the [`TableReference`] of view.
593    pub fn table_ref(&self) -> TableReference<'_> {
594        TableReference {
595            catalog: &self.catalog,
596            schema: &self.schema,
597            table: &self.view,
598        }
599    }
600}
601
602impl TryFrom<PbDropViewTask> for DropViewTask {
603    type Error = error::Error;
604
605    fn try_from(pb: PbDropViewTask) -> Result<Self> {
606        let expr = pb.drop_view.context(error::InvalidProtoMsgSnafu {
607            err_msg: "expected drop view",
608        })?;
609
610        Ok(DropViewTask {
611            catalog: expr.catalog_name,
612            schema: expr.schema_name,
613            view: expr.view_name,
614            view_id: expr
615                .view_id
616                .context(error::InvalidProtoMsgSnafu {
617                    err_msg: "expected view_id",
618                })?
619                .id,
620            drop_if_exists: expr.drop_if_exists,
621        })
622    }
623}
624
625impl From<DropViewTask> for PbDropViewTask {
626    fn from(task: DropViewTask) -> Self {
627        PbDropViewTask {
628            drop_view: Some(DropViewExpr {
629                catalog_name: task.catalog,
630                schema_name: task.schema,
631                view_name: task.view,
632                view_id: Some(api::v1::TableId { id: task.view_id }),
633                drop_if_exists: task.drop_if_exists,
634            }),
635        }
636    }
637}
638
639#[derive(Debug, PartialEq, Serialize, Deserialize, Clone)]
640pub struct DropTableTask {
641    pub catalog: String,
642    pub schema: String,
643    pub table: String,
644    pub table_id: TableId,
645    #[serde(default)]
646    pub drop_if_exists: bool,
647}
648
649#[derive(Debug, PartialEq, Serialize, Deserialize, Clone)]
650pub struct UndropTableTask {
651    pub table_id: TableId,
652}
653
654#[derive(Debug, PartialEq, Serialize, Deserialize, Clone)]
655pub struct PurgeDroppedTableTask {
656    pub table_id: TableId,
657}
658
659impl TryFrom<PbUndropTableTask> for UndropTableTask {
660    type Error = error::Error;
661
662    fn try_from(pb: PbUndropTableTask) -> Result<Self> {
663        Ok(Self {
664            table_id: pb
665                .table_id
666                .context(error::InvalidProtoMsgSnafu {
667                    err_msg: "expected table_id",
668                })?
669                .id,
670        })
671    }
672}
673
674impl From<UndropTableTask> for PbUndropTableTask {
675    fn from(task: UndropTableTask) -> Self {
676        Self {
677            table_id: Some(api::v1::TableId { id: task.table_id }),
678        }
679    }
680}
681
682impl TryFrom<PbPurgeDroppedTableTask> for PurgeDroppedTableTask {
683    type Error = error::Error;
684
685    fn try_from(pb: PbPurgeDroppedTableTask) -> Result<Self> {
686        Ok(Self {
687            table_id: pb
688                .table_id
689                .context(error::InvalidProtoMsgSnafu {
690                    err_msg: "expected table_id",
691                })?
692                .id,
693        })
694    }
695}
696
697impl From<PurgeDroppedTableTask> for PbPurgeDroppedTableTask {
698    fn from(task: PurgeDroppedTableTask) -> Self {
699        Self {
700            table_id: Some(api::v1::TableId { id: task.table_id }),
701        }
702    }
703}
704
705impl DropTableTask {
706    pub fn table_ref(&self) -> TableReference<'_> {
707        TableReference {
708            catalog: &self.catalog,
709            schema: &self.schema,
710            table: &self.table,
711        }
712    }
713
714    pub fn table_name(&self) -> TableName {
715        TableName {
716            catalog_name: self.catalog.clone(),
717            schema_name: self.schema.clone(),
718            table_name: self.table.clone(),
719        }
720    }
721}
722
723impl TryFrom<PbDropTableTask> for DropTableTask {
724    type Error = error::Error;
725
726    fn try_from(pb: PbDropTableTask) -> Result<Self> {
727        let drop_table = pb.drop_table.context(error::InvalidProtoMsgSnafu {
728            err_msg: "expected drop table",
729        })?;
730
731        Ok(Self {
732            catalog: drop_table.catalog_name,
733            schema: drop_table.schema_name,
734            table: drop_table.table_name,
735            table_id: drop_table
736                .table_id
737                .context(error::InvalidProtoMsgSnafu {
738                    err_msg: "expected table_id",
739                })?
740                .id,
741            drop_if_exists: drop_table.drop_if_exists,
742        })
743    }
744}
745
746impl From<DropTableTask> for PbDropTableTask {
747    fn from(task: DropTableTask) -> Self {
748        PbDropTableTask {
749            drop_table: Some(DropTableExpr {
750                catalog_name: task.catalog,
751                schema_name: task.schema,
752                table_name: task.table,
753                table_id: Some(api::v1::TableId { id: task.table_id }),
754                drop_if_exists: task.drop_if_exists,
755            }),
756        }
757    }
758}
759
760#[derive(Debug, PartialEq, Clone)]
761pub struct CreateTableTask {
762    pub create_table: CreateTableExpr,
763    pub partitions: Vec<Partition>,
764    pub table_info: TableInfo,
765}
766
767impl TryFrom<PbCreateTableTask> for CreateTableTask {
768    type Error = error::Error;
769
770    fn try_from(pb: PbCreateTableTask) -> Result<Self> {
771        let table_info = serde_json::from_slice(&pb.table_info).context(error::SerdeJsonSnafu)?;
772
773        Ok(CreateTableTask::new(
774            pb.create_table.context(error::InvalidProtoMsgSnafu {
775                err_msg: "expected create table",
776            })?,
777            pb.partitions,
778            table_info,
779        ))
780    }
781}
782
783impl TryFrom<CreateTableTask> for PbCreateTableTask {
784    type Error = error::Error;
785
786    fn try_from(task: CreateTableTask) -> Result<Self> {
787        Ok(PbCreateTableTask {
788            table_info: serde_json::to_vec(&task.table_info).context(error::SerdeJsonSnafu)?,
789            create_table: Some(task.create_table),
790            partitions: task.partitions,
791        })
792    }
793}
794
795impl CreateTableTask {
796    pub fn new(
797        expr: CreateTableExpr,
798        partitions: Vec<Partition>,
799        table_info: TableInfo,
800    ) -> CreateTableTask {
801        CreateTableTask {
802            create_table: expr,
803            partitions,
804            table_info,
805        }
806    }
807
808    pub fn table_name(&self) -> TableName {
809        let table = &self.create_table;
810
811        TableName {
812            catalog_name: table.catalog_name.clone(),
813            schema_name: table.schema_name.clone(),
814            table_name: table.table_name.clone(),
815        }
816    }
817
818    pub fn table_ref(&self) -> TableReference<'_> {
819        let table = &self.create_table;
820
821        TableReference {
822            catalog: &table.catalog_name,
823            schema: &table.schema_name,
824            table: &table.table_name,
825        }
826    }
827
828    /// Sets the `table_info`'s table_id.
829    pub fn set_table_id(&mut self, table_id: TableId) {
830        self.table_info.ident.table_id = table_id;
831    }
832
833    /// Sort the columns in [CreateTableExpr] and [TableInfo].
834    ///
835    /// This function won't do any check or verification. Caller should
836    /// ensure this task is valid.
837    pub fn sort_columns(&mut self) {
838        // sort create table expr
839        // sort column_defs by name
840        self.create_table
841            .column_defs
842            .sort_unstable_by(|a, b| a.name.cmp(&b.name));
843
844        self.table_info.sort_columns();
845    }
846}
847
848impl Serialize for CreateTableTask {
849    fn serialize<S>(&self, serializer: S) -> result::Result<S::Ok, S::Error>
850    where
851        S: serde::Serializer,
852    {
853        let table_info = serde_json::to_vec(&self.table_info)
854            .map_err(|err| serde::ser::Error::custom(err.to_string()))?;
855
856        let pb = PbCreateTableTask {
857            create_table: Some(self.create_table.clone()),
858            partitions: self.partitions.clone(),
859            table_info,
860        };
861        let buf = pb.encode_to_vec();
862        let encoded = general_purpose::STANDARD_NO_PAD.encode(buf);
863        serializer.serialize_str(&encoded)
864    }
865}
866
867impl<'de> Deserialize<'de> for CreateTableTask {
868    fn deserialize<D>(deserializer: D) -> result::Result<Self, D::Error>
869    where
870        D: serde::Deserializer<'de>,
871    {
872        let encoded = String::deserialize(deserializer)?;
873        let buf = general_purpose::STANDARD_NO_PAD
874            .decode(encoded)
875            .map_err(|err| serde::de::Error::custom(err.to_string()))?;
876        let expr: PbCreateTableTask = PbCreateTableTask::decode(&*buf)
877            .map_err(|err| serde::de::Error::custom(err.to_string()))?;
878
879        let expr = CreateTableTask::try_from(expr)
880            .map_err(|err| serde::de::Error::custom(err.to_string()))?;
881
882        Ok(expr)
883    }
884}
885
886#[derive(Debug, PartialEq, Clone)]
887pub struct AlterTableTask {
888    // TODO(CookiePieWw): Replace proto struct with user-defined struct
889    pub alter_table: AlterTableExpr,
890}
891
892impl AlterTableTask {
893    pub fn validate(&self) -> Result<()> {
894        self.alter_table
895            .kind
896            .as_ref()
897            .context(error::UnexpectedSnafu {
898                err_msg: "'kind' is absent",
899            })?;
900        Ok(())
901    }
902
903    pub fn table_ref(&self) -> TableReference<'_> {
904        TableReference {
905            catalog: &self.alter_table.catalog_name,
906            schema: &self.alter_table.schema_name,
907            table: &self.alter_table.table_name,
908        }
909    }
910
911    pub fn table_name(&self) -> TableName {
912        let table = &self.alter_table;
913
914        TableName {
915            catalog_name: table.catalog_name.clone(),
916            schema_name: table.schema_name.clone(),
917            table_name: table.table_name.clone(),
918        }
919    }
920}
921
922impl TryFrom<PbAlterTableTask> for AlterTableTask {
923    type Error = error::Error;
924
925    fn try_from(pb: PbAlterTableTask) -> Result<Self> {
926        let alter_table = pb.alter_table.context(error::InvalidProtoMsgSnafu {
927            err_msg: "expected alter_table",
928        })?;
929
930        Ok(AlterTableTask { alter_table })
931    }
932}
933
934impl TryFrom<AlterTableTask> for PbAlterTableTask {
935    type Error = error::Error;
936
937    fn try_from(task: AlterTableTask) -> Result<Self> {
938        Ok(PbAlterTableTask {
939            alter_table: Some(task.alter_table),
940        })
941    }
942}
943
944impl Serialize for AlterTableTask {
945    fn serialize<S>(&self, serializer: S) -> result::Result<S::Ok, S::Error>
946    where
947        S: serde::Serializer,
948    {
949        let pb = PbAlterTableTask {
950            alter_table: Some(self.alter_table.clone()),
951        };
952        let buf = pb.encode_to_vec();
953        let encoded = general_purpose::STANDARD_NO_PAD.encode(buf);
954        serializer.serialize_str(&encoded)
955    }
956}
957
958impl<'de> Deserialize<'de> for AlterTableTask {
959    fn deserialize<D>(deserializer: D) -> result::Result<Self, D::Error>
960    where
961        D: serde::Deserializer<'de>,
962    {
963        let encoded = String::deserialize(deserializer)?;
964        let buf = general_purpose::STANDARD_NO_PAD
965            .decode(encoded)
966            .map_err(|err| serde::de::Error::custom(err.to_string()))?;
967        let expr: PbAlterTableTask = PbAlterTableTask::decode(&*buf)
968            .map_err(|err| serde::de::Error::custom(err.to_string()))?;
969
970        let expr = AlterTableTask::try_from(expr)
971            .map_err(|err| serde::de::Error::custom(err.to_string()))?;
972
973        Ok(expr)
974    }
975}
976
977#[derive(Debug, PartialEq, Serialize, Deserialize, Clone)]
978pub struct TruncateTableTask {
979    pub catalog: String,
980    pub schema: String,
981    pub table: String,
982    pub table_id: TableId,
983    pub time_ranges: Vec<(Timestamp, Timestamp)>,
984}
985
986impl TruncateTableTask {
987    pub fn table_ref(&self) -> TableReference<'_> {
988        TableReference {
989            catalog: &self.catalog,
990            schema: &self.schema,
991            table: &self.table,
992        }
993    }
994
995    pub fn table_name(&self) -> TableName {
996        TableName {
997            catalog_name: self.catalog.clone(),
998            schema_name: self.schema.clone(),
999            table_name: self.table.clone(),
1000        }
1001    }
1002}
1003
1004impl TryFrom<PbTruncateTableTask> for TruncateTableTask {
1005    type Error = error::Error;
1006
1007    fn try_from(pb: PbTruncateTableTask) -> Result<Self> {
1008        let truncate_table = pb.truncate_table.context(error::InvalidProtoMsgSnafu {
1009            err_msg: "expected truncate table",
1010        })?;
1011
1012        Ok(Self {
1013            catalog: truncate_table.catalog_name,
1014            schema: truncate_table.schema_name,
1015            table: truncate_table.table_name,
1016            table_id: truncate_table
1017                .table_id
1018                .context(error::InvalidProtoMsgSnafu {
1019                    err_msg: "expected table_id",
1020                })?
1021                .id,
1022            time_ranges: truncate_table
1023                .time_ranges
1024                .map(from_pb_time_ranges)
1025                .transpose()
1026                .map_err(BoxedError::new)
1027                .context(ExternalSnafu)?
1028                .unwrap_or_default(),
1029        })
1030    }
1031}
1032
1033impl TryFrom<TruncateTableTask> for PbTruncateTableTask {
1034    type Error = error::Error;
1035
1036    fn try_from(task: TruncateTableTask) -> Result<Self> {
1037        Ok(PbTruncateTableTask {
1038            truncate_table: Some(TruncateTableExpr {
1039                catalog_name: task.catalog,
1040                schema_name: task.schema,
1041                table_name: task.table,
1042                table_id: Some(api::v1::TableId { id: task.table_id }),
1043                time_ranges: Some(
1044                    to_pb_time_ranges(&task.time_ranges).context(ConvertTimeRangesSnafu)?,
1045                ),
1046            }),
1047        })
1048    }
1049}
1050
1051#[serde_as]
1052#[derive(Debug, PartialEq, Serialize, Deserialize, Clone)]
1053pub struct CreateDatabaseTask {
1054    pub catalog: String,
1055    pub schema: String,
1056    pub create_if_not_exists: bool,
1057    #[serde_as(deserialize_as = "DefaultOnNull")]
1058    pub options: HashMap<String, String>,
1059    #[serde(default)]
1060    pub creator: Option<CreatorGrantIntent>,
1061}
1062
1063impl TryFrom<PbCreateDatabaseTask> for CreateDatabaseTask {
1064    type Error = error::Error;
1065
1066    fn try_from(pb: PbCreateDatabaseTask) -> Result<Self> {
1067        let CreateDatabaseExpr {
1068            catalog_name,
1069            schema_name,
1070            create_if_not_exists,
1071            options,
1072        } = pb.create_database.context(error::InvalidProtoMsgSnafu {
1073            err_msg: "expected create database",
1074        })?;
1075
1076        Ok(CreateDatabaseTask {
1077            catalog: catalog_name,
1078            schema: schema_name,
1079            create_if_not_exists,
1080            options,
1081            creator: None,
1082        })
1083    }
1084}
1085
1086impl TryFrom<CreateDatabaseTask> for PbCreateDatabaseTask {
1087    type Error = error::Error;
1088
1089    fn try_from(
1090        CreateDatabaseTask {
1091            catalog,
1092            schema,
1093            create_if_not_exists,
1094            options,
1095            creator: _,
1096        }: CreateDatabaseTask,
1097    ) -> Result<Self> {
1098        Ok(PbCreateDatabaseTask {
1099            create_database: Some(CreateDatabaseExpr {
1100                catalog_name: catalog,
1101                schema_name: schema,
1102                create_if_not_exists,
1103                options,
1104            }),
1105        })
1106    }
1107}
1108
1109#[derive(Debug, PartialEq, Serialize, Deserialize, Clone)]
1110pub struct DropDatabaseTask {
1111    pub catalog: String,
1112    pub schema: String,
1113    pub drop_if_exists: bool,
1114}
1115
1116impl TryFrom<PbDropDatabaseTask> for DropDatabaseTask {
1117    type Error = error::Error;
1118
1119    fn try_from(pb: PbDropDatabaseTask) -> Result<Self> {
1120        let DropDatabaseExpr {
1121            catalog_name,
1122            schema_name,
1123            drop_if_exists,
1124        } = pb.drop_database.context(error::InvalidProtoMsgSnafu {
1125            err_msg: "expected drop database",
1126        })?;
1127
1128        Ok(DropDatabaseTask {
1129            catalog: catalog_name,
1130            schema: schema_name,
1131            drop_if_exists,
1132        })
1133    }
1134}
1135
1136impl TryFrom<DropDatabaseTask> for PbDropDatabaseTask {
1137    type Error = error::Error;
1138
1139    fn try_from(
1140        DropDatabaseTask {
1141            catalog,
1142            schema,
1143            drop_if_exists,
1144        }: DropDatabaseTask,
1145    ) -> Result<Self> {
1146        Ok(PbDropDatabaseTask {
1147            drop_database: Some(DropDatabaseExpr {
1148                catalog_name: catalog,
1149                schema_name: schema,
1150                drop_if_exists,
1151            }),
1152        })
1153    }
1154}
1155
1156#[derive(Debug, PartialEq, Clone)]
1157pub struct AlterDatabaseTask {
1158    pub alter_expr: AlterDatabaseExpr,
1159}
1160
1161impl TryFrom<AlterDatabaseTask> for PbAlterDatabaseTask {
1162    type Error = error::Error;
1163
1164    fn try_from(task: AlterDatabaseTask) -> Result<Self> {
1165        Ok(PbAlterDatabaseTask {
1166            task: Some(task.alter_expr),
1167        })
1168    }
1169}
1170
1171impl TryFrom<PbAlterDatabaseTask> for AlterDatabaseTask {
1172    type Error = error::Error;
1173
1174    fn try_from(pb: PbAlterDatabaseTask) -> Result<Self> {
1175        let alter_expr = pb.task.context(error::InvalidProtoMsgSnafu {
1176            err_msg: "expected alter database",
1177        })?;
1178
1179        Ok(AlterDatabaseTask { alter_expr })
1180    }
1181}
1182
1183impl TryFrom<PbAlterDatabaseKind> for AlterDatabaseKind {
1184    type Error = error::Error;
1185
1186    fn try_from(pb: PbAlterDatabaseKind) -> Result<Self> {
1187        match pb {
1188            PbAlterDatabaseKind::SetDatabaseOptions(options) => {
1189                Ok(AlterDatabaseKind::SetDatabaseOptions(SetDatabaseOptions(
1190                    options
1191                        .set_database_options
1192                        .into_iter()
1193                        .map(SetDatabaseOption::try_from)
1194                        .collect::<Result<Vec<_>>>()?,
1195                )))
1196            }
1197            PbAlterDatabaseKind::UnsetDatabaseOptions(options) => Ok(
1198                AlterDatabaseKind::UnsetDatabaseOptions(UnsetDatabaseOptions(
1199                    options
1200                        .keys
1201                        .iter()
1202                        .map(|key| UnsetDatabaseOption::try_from(key.as_str()))
1203                        .collect::<Result<Vec<_>>>()?,
1204                )),
1205            ),
1206        }
1207    }
1208}
1209
1210const TTL_KEY: &str = "ttl";
1211
1212impl TryFrom<PbOption> for SetDatabaseOption {
1213    type Error = error::Error;
1214
1215    fn try_from(PbOption { key, value }: PbOption) -> Result<Self> {
1216        let key_lower = key.to_ascii_lowercase();
1217        match key_lower.as_str() {
1218            TTL_KEY => {
1219                let ttl = DatabaseTimeToLive::from_humantime_or_str(&value)
1220                    .map_err(|_| InvalidSetDatabaseOptionSnafu { key, value }.build())?;
1221
1222                Ok(SetDatabaseOption::Ttl(ttl))
1223            }
1224            _ => {
1225                if validate_database_option(&key_lower)
1226                    && validate_database_option_value(&key_lower, Some(&value)).is_ok()
1227                {
1228                    Ok(SetDatabaseOption::Other(key_lower, value))
1229                } else {
1230                    InvalidSetDatabaseOptionSnafu { key, value }.fail()
1231                }
1232            }
1233        }
1234    }
1235}
1236
1237#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)]
1238pub enum SetDatabaseOption {
1239    Ttl(DatabaseTimeToLive),
1240    Other(String, String),
1241}
1242
1243#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)]
1244pub enum UnsetDatabaseOption {
1245    Ttl,
1246    Other(String),
1247}
1248
1249impl TryFrom<&str> for UnsetDatabaseOption {
1250    type Error = error::Error;
1251
1252    fn try_from(key: &str) -> Result<Self> {
1253        let key_lower = key.to_ascii_lowercase();
1254        match key_lower.as_str() {
1255            TTL_KEY => Ok(UnsetDatabaseOption::Ttl),
1256            _ => {
1257                if validate_database_option(&key_lower) {
1258                    Ok(UnsetDatabaseOption::Other(key_lower))
1259                } else {
1260                    InvalidUnsetDatabaseOptionSnafu { key }.fail()
1261                }
1262            }
1263        }
1264    }
1265}
1266
1267#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)]
1268pub struct SetDatabaseOptions(pub Vec<SetDatabaseOption>);
1269
1270#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)]
1271pub struct UnsetDatabaseOptions(pub Vec<UnsetDatabaseOption>);
1272
1273#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)]
1274pub enum AlterDatabaseKind {
1275    SetDatabaseOptions(SetDatabaseOptions),
1276    UnsetDatabaseOptions(UnsetDatabaseOptions),
1277}
1278
1279impl AlterDatabaseTask {
1280    pub fn catalog(&self) -> &str {
1281        &self.alter_expr.catalog_name
1282    }
1283
1284    pub fn schema(&self) -> &str {
1285        &self.alter_expr.catalog_name
1286    }
1287}
1288
1289/// Create flow
1290#[derive(Debug, Clone, Serialize, Deserialize)]
1291pub struct CreateFlowTask {
1292    pub catalog_name: String,
1293    pub flow_name: String,
1294    pub source_table_names: Vec<TableName>,
1295    pub sink_table_name: TableName,
1296    pub or_replace: bool,
1297    pub create_if_not_exists: bool,
1298    /// Duration in seconds. Data older than this duration will not be used.
1299    pub expire_after: Option<i64>,
1300    pub eval_interval_secs: Option<i64>,
1301    /// Phase offset of the evaluation schedule within `eval_interval_secs`,
1302    /// in seconds. Must be in `[0, eval_interval_secs)`. `None` means a zero
1303    /// offset (epoch-anchored schedule).
1304    /// Transported through the transient option map (no proto field), see
1305    /// `INTERNAL_EVAL_OFFSET_KEY`.
1306    #[serde(default)]
1307    pub eval_offset_secs: Option<i64>,
1308    pub comment: String,
1309    pub sql: String,
1310    pub flow_options: HashMap<String, String>,
1311    /// Typed schedule configuration resolved during `on_prepare`.
1312    /// Not populated from proto; set by the procedure layer after
1313    /// defaults are resolved.
1314    #[serde(default)]
1315    pub eval_schedule: Option<crate::key::flow::flow_info::FlowScheduleConfig>,
1316}
1317
1318impl TryFrom<PbCreateFlowTask> for CreateFlowTask {
1319    type Error = error::Error;
1320
1321    fn try_from(pb: PbCreateFlowTask) -> Result<Self> {
1322        let CreateFlowExpr {
1323            catalog_name,
1324            flow_name,
1325            source_table_names,
1326            sink_table_name,
1327            or_replace,
1328            create_if_not_exists,
1329            expire_after,
1330            eval_interval,
1331            comment,
1332            sql,
1333            mut flow_options,
1334        } = pb.create_flow.context(error::InvalidProtoMsgSnafu {
1335            err_msg: "expected create_flow",
1336        })?;
1337
1338        // Parse and strip the trusted transient offset key inserted by the
1339        // operator after user option validation. It must never persist in
1340        // user-visible options.
1341        let eval_offset_secs =
1342            match flow_options.remove(crate::ddl::create_flow::INTERNAL_EVAL_OFFSET_KEY) {
1343                Some(value) => Some(value.parse::<i64>().map_err(|_| {
1344                    error::UnexpectedSnafu {
1345                        err_msg: format!(
1346                            "Invalid internal eval offset payload '{value}': expected whole seconds"
1347                        ),
1348                    }
1349                    .build()
1350                })?),
1351                None => None,
1352            };
1353
1354        Ok(CreateFlowTask {
1355            catalog_name,
1356            flow_name,
1357            source_table_names: source_table_names.into_iter().map(Into::into).collect(),
1358            sink_table_name: sink_table_name
1359                .context(error::InvalidProtoMsgSnafu {
1360                    err_msg: "expected sink_table_name",
1361                })?
1362                .into(),
1363            or_replace,
1364            create_if_not_exists,
1365            expire_after: expire_after.map(|e| e.value),
1366            eval_interval_secs: eval_interval.map(|e| e.seconds),
1367            eval_offset_secs,
1368            comment,
1369            sql,
1370            flow_options,
1371            eval_schedule: None,
1372        })
1373    }
1374}
1375
1376impl From<CreateFlowTask> for PbCreateFlowTask {
1377    fn from(
1378        CreateFlowTask {
1379            catalog_name,
1380            flow_name,
1381            source_table_names,
1382            sink_table_name,
1383            or_replace,
1384            create_if_not_exists,
1385            expire_after,
1386            eval_interval_secs: eval_interval,
1387            eval_offset_secs,
1388            comment,
1389            sql,
1390            mut flow_options,
1391            ..
1392        }: CreateFlowTask,
1393    ) -> Self {
1394        // Re-insert the transient offset key so the proto round-trip (e.g. DDL
1395        // task submission between frontend and metasrv) preserves the offset.
1396        if let Some(offset_secs) = eval_offset_secs {
1397            flow_options.insert(
1398                crate::ddl::create_flow::INTERNAL_EVAL_OFFSET_KEY.to_string(),
1399                offset_secs.to_string(),
1400            );
1401        }
1402        PbCreateFlowTask {
1403            create_flow: Some(CreateFlowExpr {
1404                catalog_name,
1405                flow_name,
1406                source_table_names: source_table_names.into_iter().map(Into::into).collect(),
1407                sink_table_name: Some(sink_table_name.into()),
1408                or_replace,
1409                create_if_not_exists,
1410                expire_after: expire_after.map(|value| ExpireAfter { value }),
1411                eval_interval: eval_interval.map(|seconds| EvalInterval { seconds }),
1412                comment,
1413                sql,
1414                flow_options,
1415            }),
1416        }
1417    }
1418}
1419
1420/// Drop flow
1421#[derive(Debug, Clone, Serialize, Deserialize)]
1422pub struct DropFlowTask {
1423    pub catalog_name: String,
1424    pub flow_name: String,
1425    pub flow_id: FlowId,
1426    pub drop_if_exists: bool,
1427}
1428
1429impl TryFrom<PbDropFlowTask> for DropFlowTask {
1430    type Error = error::Error;
1431
1432    fn try_from(pb: PbDropFlowTask) -> Result<Self> {
1433        let DropFlowExpr {
1434            catalog_name,
1435            flow_name,
1436            flow_id,
1437            drop_if_exists,
1438        } = pb.drop_flow.context(error::InvalidProtoMsgSnafu {
1439            err_msg: "expected drop_flow",
1440        })?;
1441        let flow_id = flow_id
1442            .context(error::InvalidProtoMsgSnafu {
1443                err_msg: "expected flow_id",
1444            })?
1445            .id;
1446        Ok(DropFlowTask {
1447            catalog_name,
1448            flow_name,
1449            flow_id,
1450            drop_if_exists,
1451        })
1452    }
1453}
1454
1455impl From<DropFlowTask> for PbDropFlowTask {
1456    fn from(
1457        DropFlowTask {
1458            catalog_name,
1459            flow_name,
1460            flow_id,
1461            drop_if_exists,
1462        }: DropFlowTask,
1463    ) -> Self {
1464        PbDropFlowTask {
1465            drop_flow: Some(DropFlowExpr {
1466                catalog_name,
1467                flow_name,
1468                flow_id: Some(api::v1::FlowId { id: flow_id }),
1469                drop_if_exists,
1470            }),
1471        }
1472    }
1473}
1474
1475/// Represents the ID of the object being commented on (Table or Flow).
1476#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1477pub enum CommentObjectId {
1478    Table(TableId),
1479    Flow(FlowId),
1480}
1481
1482/// Comment on table, column, or flow
1483#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1484pub struct CommentOnTask {
1485    pub catalog_name: String,
1486    pub schema_name: String,
1487    pub object_type: CommentObjectType,
1488    pub object_name: String,
1489    /// Column name (only for Column comments)
1490    pub column_name: Option<String>,
1491    /// Object ID (Table or Flow) for validation and cache invalidation
1492    pub object_id: Option<CommentObjectId>,
1493    pub comment: Option<String>,
1494}
1495
1496#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1497pub enum CommentObjectType {
1498    Table,
1499    Column,
1500    Flow,
1501}
1502
1503impl CommentOnTask {
1504    pub fn table_id(&self) -> Option<TableId> {
1505        match self.object_id.as_ref() {
1506            Some(CommentObjectId::Table(table_id)) => Some(*table_id),
1507            _ => None,
1508        }
1509    }
1510
1511    pub fn flow_id(&self) -> Option<FlowId> {
1512        match self.object_id.as_ref() {
1513            Some(CommentObjectId::Flow(flow_id)) => Some(*flow_id),
1514            _ => None,
1515        }
1516    }
1517
1518    fn set_table_id(&mut self, table_id: TableId) {
1519        self.object_id = Some(CommentObjectId::Table(table_id));
1520    }
1521
1522    fn set_flow_id(&mut self, flow_id: FlowId) {
1523        self.object_id = Some(CommentObjectId::Flow(flow_id));
1524    }
1525
1526    /// Returns the cache identifiers for the object being commented on.
1527    pub fn cache_idents(&self) -> Vec<CacheIdent> {
1528        match self.object_type {
1529            CommentObjectType::Table | CommentObjectType::Column => {
1530                let mut cache_idents = Vec::with_capacity(2);
1531                if let Some(CommentObjectId::Table(table_id)) = self.object_id.as_ref() {
1532                    cache_idents.push(CacheIdent::TableId(*table_id));
1533                }
1534                cache_idents.push(CacheIdent::TableName(TableName {
1535                    catalog_name: self.catalog_name.clone(),
1536                    schema_name: self.schema_name.clone(),
1537                    table_name: self.object_name.clone(),
1538                }));
1539                cache_idents
1540            }
1541            CommentObjectType::Flow => {
1542                let mut cache_idents = Vec::with_capacity(2);
1543                if let Some(CommentObjectId::Flow(flow_id)) = self.object_id.as_ref() {
1544                    cache_idents.push(CacheIdent::FlowId(*flow_id));
1545                }
1546                cache_idents.push(CacheIdent::FlowName(FlowName {
1547                    catalog_name: self.catalog_name.clone(),
1548                    flow_name: self.object_name.clone(),
1549                }));
1550                cache_idents
1551            }
1552        }
1553    }
1554
1555    /// Enriches the `object_id` field of the `CommentOnTask`
1556    /// by looking up the corresponding table or flow ID using the provided managers.
1557    pub async fn enrich_object_id(
1558        &mut self,
1559        table_name_manager: &TableNameManager,
1560        flow_name_manager: &FlowNameManager,
1561    ) -> Result<()> {
1562        match self.object_type {
1563            CommentObjectType::Table | CommentObjectType::Column => {
1564                let table_id = table_name_manager
1565                    .get(TableNameKey::new(
1566                        &self.catalog_name,
1567                        &self.schema_name,
1568                        &self.object_name,
1569                    ))
1570                    .await?
1571                    .with_context(|| error::TableNotFoundSnafu {
1572                        table_name: format_full_table_name(
1573                            &self.catalog_name,
1574                            &self.schema_name,
1575                            &self.object_name,
1576                        ),
1577                    })?
1578                    .table_id();
1579
1580                self.set_table_id(table_id);
1581            }
1582            CommentObjectType::Flow => {
1583                let flow_id = flow_name_manager
1584                    .get(&self.catalog_name, &self.object_name)
1585                    .await?
1586                    .with_context(|| error::FlowNotFoundSnafu {
1587                        flow_name: format_full_flow_name(&self.catalog_name, &self.object_name),
1588                    })?
1589                    .flow_id();
1590
1591                self.set_flow_id(flow_id);
1592            }
1593        }
1594
1595        Ok(())
1596    }
1597}
1598
1599// Proto conversions for CommentObjectType
1600impl From<CommentObjectType> for PbCommentObjectType {
1601    fn from(object_type: CommentObjectType) -> Self {
1602        match object_type {
1603            CommentObjectType::Table => PbCommentObjectType::Table,
1604            CommentObjectType::Column => PbCommentObjectType::Column,
1605            CommentObjectType::Flow => PbCommentObjectType::Flow,
1606        }
1607    }
1608}
1609
1610impl TryFrom<i32> for CommentObjectType {
1611    type Error = error::Error;
1612
1613    fn try_from(value: i32) -> Result<Self> {
1614        match value {
1615            0 => Ok(CommentObjectType::Table),
1616            1 => Ok(CommentObjectType::Column),
1617            2 => Ok(CommentObjectType::Flow),
1618            _ => error::InvalidProtoMsgSnafu {
1619                err_msg: format!(
1620                    "Invalid CommentObjectType value: {}. Valid values are: 0 (Table), 1 (Column), 2 (Flow)",
1621                    value
1622                ),
1623            }
1624            .fail(),
1625        }
1626    }
1627}
1628
1629// Proto conversions for CommentOnTask
1630impl TryFrom<PbCommentOnTask> for CommentOnTask {
1631    type Error = error::Error;
1632
1633    fn try_from(pb: PbCommentOnTask) -> Result<Self> {
1634        let comment_on = pb.comment_on.context(error::InvalidProtoMsgSnafu {
1635            err_msg: "expected comment_on",
1636        })?;
1637
1638        Ok(CommentOnTask {
1639            catalog_name: comment_on.catalog_name,
1640            schema_name: comment_on.schema_name,
1641            object_type: comment_on.object_type.try_into()?,
1642            object_name: comment_on.object_name,
1643            column_name: if comment_on.column_name.is_empty() {
1644                None
1645            } else {
1646                Some(comment_on.column_name)
1647            },
1648            comment: if comment_on.comment.is_empty() {
1649                None
1650            } else {
1651                Some(comment_on.comment)
1652            },
1653            object_id: None,
1654        })
1655    }
1656}
1657
1658impl From<CommentOnTask> for PbCommentOnTask {
1659    fn from(task: CommentOnTask) -> Self {
1660        let pb_object_type: PbCommentObjectType = task.object_type.into();
1661        PbCommentOnTask {
1662            comment_on: Some(CommentOnExpr {
1663                catalog_name: task.catalog_name,
1664                schema_name: task.schema_name,
1665                object_type: pb_object_type as i32,
1666                object_name: task.object_name,
1667                column_name: task.column_name.unwrap_or_default(),
1668                comment: task.comment.unwrap_or_default(),
1669            }),
1670        }
1671    }
1672}
1673
1674#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
1675pub struct QueryContext {
1676    pub current_catalog: String,
1677    pub current_schema: String,
1678    pub timezone: String,
1679    pub extensions: HashMap<String, String>,
1680    pub channel: u8,
1681    /// Maps region id -> snapshot upper bound sequence for that region.
1682    #[serde(default)]
1683    pub snapshot_seqs: HashMap<u64, u64>,
1684    /// Maps region id -> minimal SST sequence allowed for that region.
1685    #[serde(default)]
1686    pub sst_min_sequences: HashMap<u64, u64>,
1687}
1688
1689impl QueryContext {
1690    /// Get the current catalog
1691    pub fn current_catalog(&self) -> &str {
1692        &self.current_catalog
1693    }
1694
1695    /// Get the current schema
1696    pub fn current_schema(&self) -> &str {
1697        &self.current_schema
1698    }
1699
1700    /// Get the timezone
1701    pub fn timezone(&self) -> &str {
1702        &self.timezone
1703    }
1704
1705    /// Get the extensions
1706    pub fn extensions(&self) -> &HashMap<String, String> {
1707        &self.extensions
1708    }
1709
1710    /// Get the channel
1711    pub fn channel(&self) -> u8 {
1712        self.channel
1713    }
1714
1715    /// Returns the protocol derived from the typed query channel.
1716    pub fn protocol(&self) -> Option<String> {
1717        let channel = Channel::from(u32::from(self.channel));
1718        (channel != Channel::Unknown).then(|| channel.as_ref().to_string())
1719    }
1720
1721    pub fn snapshot_seqs(&self) -> &HashMap<u64, u64> {
1722        &self.snapshot_seqs
1723    }
1724
1725    pub fn sst_min_sequences(&self) -> &HashMap<u64, u64> {
1726        &self.sst_min_sequences
1727    }
1728}
1729
1730/// Lightweight query context for flow operations containing only essential fields.
1731/// This is a subset of QueryContext that includes only the fields actually needed
1732/// for flow creation and execution.
1733#[derive(Debug, Clone, Serialize, PartialEq)]
1734pub struct FlowQueryContext {
1735    /// Current catalog name used for flow metadata and execution.
1736    pub catalog: String,
1737    /// Current schema name used for table resolution during flow execution.
1738    pub schema: String,
1739    /// Timezone used for timestamp evaluation in the flow.
1740    pub timezone: String,
1741    /// Query extensions carried into flow execution.
1742    #[serde(default)]
1743    pub extensions: HashMap<String, String>,
1744    /// Request channel propagated from the original query context.
1745    #[serde(default)]
1746    pub channel: u8,
1747    /// Per-region snapshot upper bounds bound during query planning/execution.
1748    #[serde(default)]
1749    pub snapshot_seqs: HashMap<u64, u64>,
1750    /// Per-region lower SST scan bounds carried with the flow context.
1751    #[serde(default)]
1752    pub sst_min_sequences: HashMap<u64, u64>,
1753}
1754
1755impl<'de> Deserialize<'de> for FlowQueryContext {
1756    fn deserialize<D>(deserializer: D) -> result::Result<Self, D::Error>
1757    where
1758        D: serde::Deserializer<'de>,
1759    {
1760        // Support both QueryContext format and FlowQueryContext format
1761        #[derive(Deserialize)]
1762        #[serde(untagged)]
1763        enum ContextCompat {
1764            Flow(FlowQueryContextHelper),
1765            Full(QueryContext),
1766        }
1767
1768        #[derive(Deserialize)]
1769        struct FlowQueryContextHelper {
1770            catalog: String,
1771            schema: String,
1772            timezone: String,
1773            #[serde(default)]
1774            extensions: HashMap<String, String>,
1775            #[serde(default)]
1776            channel: u8,
1777            #[serde(default)]
1778            snapshot_seqs: HashMap<u64, u64>,
1779            #[serde(default)]
1780            sst_min_sequences: HashMap<u64, u64>,
1781        }
1782
1783        match ContextCompat::deserialize(deserializer)? {
1784            ContextCompat::Flow(helper) => Ok(FlowQueryContext {
1785                catalog: helper.catalog,
1786                schema: helper.schema,
1787                timezone: helper.timezone,
1788                extensions: helper.extensions,
1789                channel: helper.channel,
1790                snapshot_seqs: helper.snapshot_seqs,
1791                sst_min_sequences: helper.sst_min_sequences,
1792            }),
1793            ContextCompat::Full(full_ctx) => Ok(full_ctx.into()),
1794        }
1795    }
1796}
1797
1798impl From<PbQueryContext> for QueryContext {
1799    fn from(pb_ctx: PbQueryContext) -> Self {
1800        let (snapshot_seqs, sst_min_sequences) = pb_ctx
1801            .snapshot_seqs
1802            .map(|seqs| (seqs.snapshot_seqs, seqs.sst_min_sequences))
1803            .unwrap_or_default();
1804
1805        Self {
1806            current_catalog: pb_ctx.current_catalog,
1807            current_schema: pb_ctx.current_schema,
1808            timezone: pb_ctx.timezone,
1809            extensions: pb_ctx.extensions,
1810            channel: pb_ctx.channel as u8,
1811            snapshot_seqs,
1812            sst_min_sequences,
1813        }
1814    }
1815}
1816
1817impl From<QueryContext> for PbQueryContext {
1818    fn from(
1819        QueryContext {
1820            current_catalog,
1821            current_schema,
1822            timezone,
1823            extensions,
1824            channel,
1825            snapshot_seqs,
1826            sst_min_sequences,
1827        }: QueryContext,
1828    ) -> Self {
1829        PbQueryContext {
1830            current_catalog,
1831            current_schema,
1832            timezone,
1833            extensions,
1834            channel: channel as u32,
1835            snapshot_seqs: (!snapshot_seqs.is_empty() || !sst_min_sequences.is_empty()).then_some(
1836                api::v1::SnapshotSequences {
1837                    snapshot_seqs,
1838                    sst_min_sequences,
1839                },
1840            ),
1841            explain: None,
1842        }
1843    }
1844}
1845
1846impl From<QueryContext> for FlowQueryContext {
1847    fn from(ctx: QueryContext) -> Self {
1848        Self {
1849            catalog: ctx.current_catalog,
1850            schema: ctx.current_schema,
1851            timezone: ctx.timezone,
1852            extensions: ctx.extensions,
1853            channel: ctx.channel,
1854            snapshot_seqs: ctx.snapshot_seqs,
1855            sst_min_sequences: ctx.sst_min_sequences,
1856        }
1857    }
1858}
1859
1860impl From<FlowQueryContext> for QueryContext {
1861    fn from(flow_ctx: FlowQueryContext) -> Self {
1862        Self {
1863            current_catalog: flow_ctx.catalog,
1864            current_schema: flow_ctx.schema,
1865            timezone: flow_ctx.timezone,
1866            extensions: flow_ctx.extensions,
1867            channel: flow_ctx.channel,
1868            snapshot_seqs: flow_ctx.snapshot_seqs,
1869            sst_min_sequences: flow_ctx.sst_min_sequences,
1870        }
1871    }
1872}
1873
1874impl From<FlowQueryContext> for PbQueryContext {
1875    fn from(flow_ctx: FlowQueryContext) -> Self {
1876        let query_ctx: QueryContext = flow_ctx.into();
1877        query_ctx.into()
1878    }
1879}
1880
1881#[cfg(test)]
1882mod tests {
1883    use std::sync::Arc;
1884
1885    use api::v1::{AlterTableExpr, ColumnDef, CreateTableExpr, SemanticType};
1886    use datatypes::schema::{ColumnSchema, Schema, SchemaBuilder};
1887    use store_api::metric_engine_consts::METRIC_ENGINE_NAME;
1888    use store_api::storage::ConcreteDataType;
1889    use table::metadata::{TableInfo, TableMeta, TableType};
1890    use table::test_util::table_info::test_table_info;
1891
1892    use super::{AlterTableTask, CreateTableTask, *};
1893
1894    #[test]
1895    fn test_ddl_timeout_secs() {
1896        assert_eq!(ddl_timeout_secs(Duration::ZERO), 0);
1897        assert_eq!(ddl_timeout_secs(Duration::from_nanos(1)), 1);
1898        assert_eq!(ddl_timeout_secs(Duration::from_secs(1)), 1);
1899        assert_eq!(ddl_timeout_secs(Duration::from_millis(1500)), 2);
1900        assert_eq!(
1901            ddl_timeout_secs(Duration::from_secs(u32::MAX as u64 + 1)),
1902            u32::MAX
1903        );
1904    }
1905
1906    #[test]
1907    fn test_alter_database_rejects_invalid_trigger_values() {
1908        let overflow = format!("{}0", usize::MAX);
1909        for key in [
1910            "compaction.twcs.trigger_file_num",
1911            "compaction.twcs.active_window.trigger_file_num",
1912            "compaction.twcs.inactive_window.trigger_file_num",
1913            "compaction.twcs.active_window.l1_merge_trigger",
1914            "compaction.twcs.inactive_window.l1_merge_trigger",
1915        ] {
1916            for invalid in ["invalid", "-1", overflow.as_str()] {
1917                let kind = PbAlterDatabaseKind::SetDatabaseOptions(api::v1::SetDatabaseOptions {
1918                    set_database_options: vec![PbOption {
1919                        key: key.to_string(),
1920                        value: invalid.to_string(),
1921                    }],
1922                });
1923                let err = AlterDatabaseKind::try_from(kind).unwrap_err();
1924                assert!(
1925                    matches!(err, error::Error::InvalidSetDatabaseOption { .. }),
1926                    "{key}: {invalid}"
1927                );
1928            }
1929            for boundary in ["0", "1", "2"] {
1930                let option = PbOption {
1931                    key: key.to_string(),
1932                    value: boundary.to_string(),
1933                };
1934                assert_eq!(
1935                    SetDatabaseOption::try_from(option).is_ok(),
1936                    !key.ends_with("l1_merge_trigger") || boundary == "2",
1937                    "{key}: {boundary}"
1938                );
1939            }
1940        }
1941    }
1942
1943    #[test]
1944    fn test_basic_ser_de_create_table_task() {
1945        let schema = SchemaBuilder::default().build().unwrap();
1946        let table_info = test_table_info(1025, "foo", "bar", "baz", Arc::new(schema));
1947        let task = CreateTableTask::new(CreateTableExpr::default(), Vec::new(), table_info);
1948
1949        let output = serde_json::to_vec(&task).unwrap();
1950
1951        let de = serde_json::from_slice(&output).unwrap();
1952        assert_eq!(task, de);
1953    }
1954
1955    #[test]
1956    fn test_basic_ser_de_alter_table_task() {
1957        let task = AlterTableTask {
1958            alter_table: AlterTableExpr::default(),
1959        };
1960
1961        let output = serde_json::to_vec(&task).unwrap();
1962
1963        let de = serde_json::from_slice(&output).unwrap();
1964        assert_eq!(task, de);
1965    }
1966
1967    #[test]
1968    fn test_undrop_table_task_pb_roundtrip() {
1969        let expected = UndropTableTask { table_id: 1024 };
1970        let request = SubmitDdlTaskRequest::new(DdlTask::UndropTable(expected.clone()));
1971
1972        let pb = PbDdlTaskRequest::try_from(request).unwrap();
1973        let pb_task = pb.task.unwrap();
1974        let de = DdlTask::try_from(pb_task).unwrap();
1975
1976        assert!(matches!(de, DdlTask::UndropTable(task) if task == expected));
1977    }
1978
1979    #[test]
1980    fn test_purge_dropped_table_task_pb_roundtrip() {
1981        let expected = PurgeDroppedTableTask { table_id: 1024 };
1982        let request = SubmitDdlTaskRequest::new(DdlTask::PurgeDroppedTable(expected.clone()));
1983
1984        let pb = PbDdlTaskRequest::try_from(request).unwrap();
1985        let pb_task = pb.task.unwrap();
1986        let de = DdlTask::try_from(pb_task).unwrap();
1987
1988        assert!(matches!(de, DdlTask::PurgeDroppedTable(task) if task == expected));
1989    }
1990
1991    #[test]
1992    fn test_undrop_table_task_json_roundtrip() {
1993        let task = UndropTableTask { table_id: 1024 };
1994
1995        let output = serde_json::to_vec(&task).unwrap();
1996
1997        let de = serde_json::from_slice(&output).unwrap();
1998        assert_eq!(task, de);
1999    }
2000
2001    #[test]
2002    fn test_purge_dropped_table_task_json_roundtrip() {
2003        let task = PurgeDroppedTableTask { table_id: 1024 };
2004
2005        let output = serde_json::to_vec(&task).unwrap();
2006
2007        let de = serde_json::from_slice(&output).unwrap();
2008        assert_eq!(task, de);
2009    }
2010
2011    #[test]
2012    fn test_sort_columns() {
2013        // construct RawSchema
2014        let schema = Arc::new(Schema::new(vec![
2015            ColumnSchema::new(
2016                "column3".to_string(),
2017                ConcreteDataType::string_datatype(),
2018                true,
2019            ),
2020            ColumnSchema::new(
2021                "column1".to_string(),
2022                ConcreteDataType::timestamp_millisecond_datatype(),
2023                false,
2024            )
2025            .with_time_index(true),
2026            ColumnSchema::new(
2027                "column2".to_string(),
2028                ConcreteDataType::float64_datatype(),
2029                true,
2030            ),
2031        ]));
2032
2033        // construct RawTableMeta
2034        let meta = TableMeta {
2035            schema,
2036            primary_key_indices: vec![0],
2037            value_indices: vec![2],
2038            engine: METRIC_ENGINE_NAME.to_string(),
2039            next_column_id: 0,
2040            options: Default::default(),
2041            created_on: Default::default(),
2042            updated_on: Default::default(),
2043            partition_key_indices: Default::default(),
2044            column_ids: Default::default(),
2045        };
2046
2047        // construct TableInfo
2048        let raw_table_info = TableInfo {
2049            ident: Default::default(),
2050            meta,
2051            name: Default::default(),
2052            desc: Default::default(),
2053            catalog_name: Default::default(),
2054            schema_name: Default::default(),
2055            table_type: TableType::Base,
2056        };
2057
2058        // construct create table expr
2059        let create_table_expr = CreateTableExpr {
2060            column_defs: vec![
2061                ColumnDef {
2062                    name: "column3".to_string(),
2063                    semantic_type: SemanticType::Tag as i32,
2064                    ..Default::default()
2065                },
2066                ColumnDef {
2067                    name: "column1".to_string(),
2068                    semantic_type: SemanticType::Timestamp as i32,
2069                    ..Default::default()
2070                },
2071                ColumnDef {
2072                    name: "column2".to_string(),
2073                    semantic_type: SemanticType::Field as i32,
2074                    ..Default::default()
2075                },
2076            ],
2077            primary_keys: vec!["column3".to_string()],
2078            ..Default::default()
2079        };
2080
2081        let mut create_table_task =
2082            CreateTableTask::new(create_table_expr, Vec::new(), raw_table_info);
2083
2084        // Call the sort_columns method
2085        create_table_task.sort_columns();
2086
2087        // Assert that the columns are sorted correctly
2088        assert_eq!(
2089            create_table_task.create_table.column_defs[0].name,
2090            "column1".to_string()
2091        );
2092        assert_eq!(
2093            create_table_task.create_table.column_defs[1].name,
2094            "column2".to_string()
2095        );
2096        assert_eq!(
2097            create_table_task.create_table.column_defs[2].name,
2098            "column3".to_string()
2099        );
2100
2101        // Assert that the table_info is updated correctly
2102        assert_eq!(
2103            create_table_task.table_info.meta.schema.timestamp_index(),
2104            Some(0)
2105        );
2106        assert_eq!(
2107            create_table_task.table_info.meta.primary_key_indices,
2108            vec![2]
2109        );
2110        assert_eq!(create_table_task.table_info.meta.value_indices, vec![0, 1]);
2111    }
2112
2113    #[test]
2114    fn test_flow_query_context_conversion_from_query_context() {
2115        use std::collections::HashMap;
2116        let mut extensions = HashMap::new();
2117        extensions.insert("key1".to_string(), "value1".to_string());
2118        extensions.insert("key2".to_string(), "value2".to_string());
2119
2120        let query_ctx = QueryContext {
2121            current_catalog: "test_catalog".to_string(),
2122            current_schema: "test_schema".to_string(),
2123            timezone: "UTC".to_string(),
2124            extensions,
2125            channel: 5,
2126            snapshot_seqs: HashMap::from([(10, 100)]),
2127            sst_min_sequences: HashMap::from([(10, 90)]),
2128        };
2129
2130        let flow_ctx: FlowQueryContext = query_ctx.into();
2131
2132        assert_eq!(flow_ctx.catalog, "test_catalog");
2133        assert_eq!(flow_ctx.schema, "test_schema");
2134        assert_eq!(flow_ctx.timezone, "UTC");
2135        assert_eq!(flow_ctx.channel, 5);
2136        assert_eq!(flow_ctx.snapshot_seqs, HashMap::from([(10, 100)]));
2137        assert_eq!(flow_ctx.sst_min_sequences, HashMap::from([(10, 90)]));
2138    }
2139
2140    #[test]
2141    fn test_flow_query_context_conversion_to_query_context() {
2142        let flow_ctx = FlowQueryContext {
2143            catalog: "prod_catalog".to_string(),
2144            schema: "public".to_string(),
2145            timezone: "America/New_York".to_string(),
2146            extensions: HashMap::from([("k".to_string(), "v".to_string())]),
2147            channel: 7,
2148            snapshot_seqs: HashMap::from([(11, 111)]),
2149            sst_min_sequences: HashMap::from([(11, 101)]),
2150        };
2151
2152        let query_ctx: QueryContext = flow_ctx.clone().into();
2153
2154        assert_eq!(query_ctx.current_catalog, "prod_catalog");
2155        assert_eq!(query_ctx.current_schema, "public");
2156        assert_eq!(query_ctx.timezone, "America/New_York");
2157        assert_eq!(
2158            query_ctx.extensions,
2159            HashMap::from([("k".to_string(), "v".to_string())])
2160        );
2161        assert_eq!(query_ctx.channel, 7);
2162        assert_eq!(query_ctx.snapshot_seqs, HashMap::from([(11, 111)]));
2163        assert_eq!(query_ctx.sst_min_sequences, HashMap::from([(11, 101)]));
2164
2165        // Test roundtrip conversion
2166        let flow_ctx_roundtrip: FlowQueryContext = query_ctx.into();
2167        assert_eq!(flow_ctx, flow_ctx_roundtrip);
2168    }
2169
2170    #[test]
2171    fn test_flow_query_context_serialization() {
2172        let flow_ctx = FlowQueryContext {
2173            catalog: "test_catalog".to_string(),
2174            schema: "test_schema".to_string(),
2175            timezone: "UTC".to_string(),
2176            extensions: HashMap::new(),
2177            channel: 0,
2178            snapshot_seqs: HashMap::new(),
2179            sst_min_sequences: HashMap::new(),
2180        };
2181
2182        let serialized = serde_json::to_string(&flow_ctx).unwrap();
2183        let deserialized: FlowQueryContext = serde_json::from_str(&serialized).unwrap();
2184
2185        assert_eq!(flow_ctx, deserialized);
2186
2187        // Verify JSON structure
2188        let json_value: serde_json::Value = serde_json::from_str(&serialized).unwrap();
2189        assert_eq!(json_value["catalog"], "test_catalog");
2190        assert_eq!(json_value["schema"], "test_schema");
2191        assert_eq!(json_value["timezone"], "UTC");
2192    }
2193
2194    #[test]
2195    fn test_flow_query_context_conversion_to_pb() {
2196        let flow_ctx = FlowQueryContext {
2197            catalog: "pb_catalog".to_string(),
2198            schema: "pb_schema".to_string(),
2199            timezone: "Asia/Tokyo".to_string(),
2200            extensions: HashMap::from([("x".to_string(), "y".to_string())]),
2201            channel: 6,
2202            snapshot_seqs: HashMap::from([(3, 30)]),
2203            sst_min_sequences: HashMap::from([(3, 21)]),
2204        };
2205
2206        let pb_ctx: PbQueryContext = flow_ctx.into();
2207
2208        assert_eq!(pb_ctx.current_catalog, "pb_catalog");
2209        assert_eq!(pb_ctx.current_schema, "pb_schema");
2210        assert_eq!(pb_ctx.timezone, "Asia/Tokyo");
2211        assert_eq!(
2212            pb_ctx.extensions,
2213            HashMap::from([("x".to_string(), "y".to_string())])
2214        );
2215        assert_eq!(pb_ctx.channel, 6);
2216        assert_eq!(
2217            pb_ctx.snapshot_seqs,
2218            Some(api::v1::SnapshotSequences {
2219                snapshot_seqs: HashMap::from([(3, 30)]),
2220                sst_min_sequences: HashMap::from([(3, 21)]),
2221            })
2222        );
2223        assert!(pb_ctx.explain.is_none());
2224    }
2225
2226    #[test]
2227    fn test_pb_query_context_roundtrip_with_snapshot_sequences() {
2228        let pb = PbQueryContext {
2229            current_catalog: "c1".to_string(),
2230            current_schema: "s1".to_string(),
2231            timezone: "UTC".to_string(),
2232            extensions: HashMap::from([("flow.return_region_seq".to_string(), "true".to_string())]),
2233            channel: 3,
2234            snapshot_seqs: Some(api::v1::SnapshotSequences {
2235                snapshot_seqs: HashMap::from([(1, 100)]),
2236                sst_min_sequences: HashMap::from([(1, 90)]),
2237            }),
2238            explain: None,
2239        };
2240
2241        let query_ctx: QueryContext = pb.clone().into();
2242        let pb_roundtrip: PbQueryContext = query_ctx.into();
2243
2244        assert_eq!(pb_roundtrip.current_catalog, pb.current_catalog);
2245        assert_eq!(pb_roundtrip.current_schema, pb.current_schema);
2246        assert_eq!(pb_roundtrip.timezone, pb.timezone);
2247        assert_eq!(pb_roundtrip.extensions, pb.extensions);
2248        assert_eq!(pb_roundtrip.channel, pb.channel);
2249        assert_eq!(pb_roundtrip.snapshot_seqs, pb.snapshot_seqs);
2250    }
2251
2252    #[test]
2253    fn test_trigger_reason_deserializes_unknown_value() {
2254        let reason: TriggerReason = serde_json::from_str("\"future_reason\"").unwrap();
2255        assert_eq!(TriggerReason::Unknown, reason);
2256    }
2257}