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;
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                    Ok(SetDatabaseOption::Other(key_lower, value))
1227                } else {
1228                    InvalidSetDatabaseOptionSnafu { key, value }.fail()
1229                }
1230            }
1231        }
1232    }
1233}
1234
1235#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)]
1236pub enum SetDatabaseOption {
1237    Ttl(DatabaseTimeToLive),
1238    Other(String, String),
1239}
1240
1241#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)]
1242pub enum UnsetDatabaseOption {
1243    Ttl,
1244    Other(String),
1245}
1246
1247impl TryFrom<&str> for UnsetDatabaseOption {
1248    type Error = error::Error;
1249
1250    fn try_from(key: &str) -> Result<Self> {
1251        let key_lower = key.to_ascii_lowercase();
1252        match key_lower.as_str() {
1253            TTL_KEY => Ok(UnsetDatabaseOption::Ttl),
1254            _ => {
1255                if validate_database_option(&key_lower) {
1256                    Ok(UnsetDatabaseOption::Other(key_lower))
1257                } else {
1258                    InvalidUnsetDatabaseOptionSnafu { key }.fail()
1259                }
1260            }
1261        }
1262    }
1263}
1264
1265#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)]
1266pub struct SetDatabaseOptions(pub Vec<SetDatabaseOption>);
1267
1268#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)]
1269pub struct UnsetDatabaseOptions(pub Vec<UnsetDatabaseOption>);
1270
1271#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)]
1272pub enum AlterDatabaseKind {
1273    SetDatabaseOptions(SetDatabaseOptions),
1274    UnsetDatabaseOptions(UnsetDatabaseOptions),
1275}
1276
1277impl AlterDatabaseTask {
1278    pub fn catalog(&self) -> &str {
1279        &self.alter_expr.catalog_name
1280    }
1281
1282    pub fn schema(&self) -> &str {
1283        &self.alter_expr.catalog_name
1284    }
1285}
1286
1287/// Create flow
1288#[derive(Debug, Clone, Serialize, Deserialize)]
1289pub struct CreateFlowTask {
1290    pub catalog_name: String,
1291    pub flow_name: String,
1292    pub source_table_names: Vec<TableName>,
1293    pub sink_table_name: TableName,
1294    pub or_replace: bool,
1295    pub create_if_not_exists: bool,
1296    /// Duration in seconds. Data older than this duration will not be used.
1297    pub expire_after: Option<i64>,
1298    pub eval_interval_secs: Option<i64>,
1299    pub comment: String,
1300    pub sql: String,
1301    pub flow_options: HashMap<String, String>,
1302    /// Typed schedule configuration resolved during `on_prepare`.
1303    /// Not populated from proto; set by the procedure layer after
1304    /// defaults are resolved.
1305    #[serde(default)]
1306    pub eval_schedule: Option<crate::key::flow::flow_info::FlowScheduleConfig>,
1307}
1308
1309impl TryFrom<PbCreateFlowTask> for CreateFlowTask {
1310    type Error = error::Error;
1311
1312    fn try_from(pb: PbCreateFlowTask) -> Result<Self> {
1313        let CreateFlowExpr {
1314            catalog_name,
1315            flow_name,
1316            source_table_names,
1317            sink_table_name,
1318            or_replace,
1319            create_if_not_exists,
1320            expire_after,
1321            eval_interval,
1322            comment,
1323            sql,
1324            flow_options,
1325        } = pb.create_flow.context(error::InvalidProtoMsgSnafu {
1326            err_msg: "expected create_flow",
1327        })?;
1328
1329        Ok(CreateFlowTask {
1330            catalog_name,
1331            flow_name,
1332            source_table_names: source_table_names.into_iter().map(Into::into).collect(),
1333            sink_table_name: sink_table_name
1334                .context(error::InvalidProtoMsgSnafu {
1335                    err_msg: "expected sink_table_name",
1336                })?
1337                .into(),
1338            or_replace,
1339            create_if_not_exists,
1340            expire_after: expire_after.map(|e| e.value),
1341            eval_interval_secs: eval_interval.map(|e| e.seconds),
1342            comment,
1343            sql,
1344            flow_options,
1345            eval_schedule: None,
1346        })
1347    }
1348}
1349
1350impl From<CreateFlowTask> for PbCreateFlowTask {
1351    fn from(
1352        CreateFlowTask {
1353            catalog_name,
1354            flow_name,
1355            source_table_names,
1356            sink_table_name,
1357            or_replace,
1358            create_if_not_exists,
1359            expire_after,
1360            eval_interval_secs: eval_interval,
1361            comment,
1362            sql,
1363            flow_options,
1364            ..
1365        }: CreateFlowTask,
1366    ) -> Self {
1367        PbCreateFlowTask {
1368            create_flow: Some(CreateFlowExpr {
1369                catalog_name,
1370                flow_name,
1371                source_table_names: source_table_names.into_iter().map(Into::into).collect(),
1372                sink_table_name: Some(sink_table_name.into()),
1373                or_replace,
1374                create_if_not_exists,
1375                expire_after: expire_after.map(|value| ExpireAfter { value }),
1376                eval_interval: eval_interval.map(|seconds| EvalInterval { seconds }),
1377                comment,
1378                sql,
1379                flow_options,
1380            }),
1381        }
1382    }
1383}
1384
1385/// Drop flow
1386#[derive(Debug, Clone, Serialize, Deserialize)]
1387pub struct DropFlowTask {
1388    pub catalog_name: String,
1389    pub flow_name: String,
1390    pub flow_id: FlowId,
1391    pub drop_if_exists: bool,
1392}
1393
1394impl TryFrom<PbDropFlowTask> for DropFlowTask {
1395    type Error = error::Error;
1396
1397    fn try_from(pb: PbDropFlowTask) -> Result<Self> {
1398        let DropFlowExpr {
1399            catalog_name,
1400            flow_name,
1401            flow_id,
1402            drop_if_exists,
1403        } = pb.drop_flow.context(error::InvalidProtoMsgSnafu {
1404            err_msg: "expected drop_flow",
1405        })?;
1406        let flow_id = flow_id
1407            .context(error::InvalidProtoMsgSnafu {
1408                err_msg: "expected flow_id",
1409            })?
1410            .id;
1411        Ok(DropFlowTask {
1412            catalog_name,
1413            flow_name,
1414            flow_id,
1415            drop_if_exists,
1416        })
1417    }
1418}
1419
1420impl From<DropFlowTask> for PbDropFlowTask {
1421    fn from(
1422        DropFlowTask {
1423            catalog_name,
1424            flow_name,
1425            flow_id,
1426            drop_if_exists,
1427        }: DropFlowTask,
1428    ) -> Self {
1429        PbDropFlowTask {
1430            drop_flow: Some(DropFlowExpr {
1431                catalog_name,
1432                flow_name,
1433                flow_id: Some(api::v1::FlowId { id: flow_id }),
1434                drop_if_exists,
1435            }),
1436        }
1437    }
1438}
1439
1440/// Represents the ID of the object being commented on (Table or Flow).
1441#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1442pub enum CommentObjectId {
1443    Table(TableId),
1444    Flow(FlowId),
1445}
1446
1447/// Comment on table, column, or flow
1448#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1449pub struct CommentOnTask {
1450    pub catalog_name: String,
1451    pub schema_name: String,
1452    pub object_type: CommentObjectType,
1453    pub object_name: String,
1454    /// Column name (only for Column comments)
1455    pub column_name: Option<String>,
1456    /// Object ID (Table or Flow) for validation and cache invalidation
1457    pub object_id: Option<CommentObjectId>,
1458    pub comment: Option<String>,
1459}
1460
1461#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1462pub enum CommentObjectType {
1463    Table,
1464    Column,
1465    Flow,
1466}
1467
1468impl CommentOnTask {
1469    pub fn table_id(&self) -> Option<TableId> {
1470        match self.object_id.as_ref() {
1471            Some(CommentObjectId::Table(table_id)) => Some(*table_id),
1472            _ => None,
1473        }
1474    }
1475
1476    pub fn flow_id(&self) -> Option<FlowId> {
1477        match self.object_id.as_ref() {
1478            Some(CommentObjectId::Flow(flow_id)) => Some(*flow_id),
1479            _ => None,
1480        }
1481    }
1482
1483    fn set_table_id(&mut self, table_id: TableId) {
1484        self.object_id = Some(CommentObjectId::Table(table_id));
1485    }
1486
1487    fn set_flow_id(&mut self, flow_id: FlowId) {
1488        self.object_id = Some(CommentObjectId::Flow(flow_id));
1489    }
1490
1491    /// Returns the cache identifiers for the object being commented on.
1492    pub fn cache_idents(&self) -> Vec<CacheIdent> {
1493        match self.object_type {
1494            CommentObjectType::Table | CommentObjectType::Column => {
1495                let mut cache_idents = Vec::with_capacity(2);
1496                if let Some(CommentObjectId::Table(table_id)) = self.object_id.as_ref() {
1497                    cache_idents.push(CacheIdent::TableId(*table_id));
1498                }
1499                cache_idents.push(CacheIdent::TableName(TableName {
1500                    catalog_name: self.catalog_name.clone(),
1501                    schema_name: self.schema_name.clone(),
1502                    table_name: self.object_name.clone(),
1503                }));
1504                cache_idents
1505            }
1506            CommentObjectType::Flow => {
1507                let mut cache_idents = Vec::with_capacity(2);
1508                if let Some(CommentObjectId::Flow(flow_id)) = self.object_id.as_ref() {
1509                    cache_idents.push(CacheIdent::FlowId(*flow_id));
1510                }
1511                cache_idents.push(CacheIdent::FlowName(FlowName {
1512                    catalog_name: self.catalog_name.clone(),
1513                    flow_name: self.object_name.clone(),
1514                }));
1515                cache_idents
1516            }
1517        }
1518    }
1519
1520    /// Enriches the `object_id` field of the `CommentOnTask`
1521    /// by looking up the corresponding table or flow ID using the provided managers.
1522    pub async fn enrich_object_id(
1523        &mut self,
1524        table_name_manager: &TableNameManager,
1525        flow_name_manager: &FlowNameManager,
1526    ) -> Result<()> {
1527        match self.object_type {
1528            CommentObjectType::Table | CommentObjectType::Column => {
1529                let table_id = table_name_manager
1530                    .get(TableNameKey::new(
1531                        &self.catalog_name,
1532                        &self.schema_name,
1533                        &self.object_name,
1534                    ))
1535                    .await?
1536                    .with_context(|| error::TableNotFoundSnafu {
1537                        table_name: format_full_table_name(
1538                            &self.catalog_name,
1539                            &self.schema_name,
1540                            &self.object_name,
1541                        ),
1542                    })?
1543                    .table_id();
1544
1545                self.set_table_id(table_id);
1546            }
1547            CommentObjectType::Flow => {
1548                let flow_id = flow_name_manager
1549                    .get(&self.catalog_name, &self.object_name)
1550                    .await?
1551                    .with_context(|| error::FlowNotFoundSnafu {
1552                        flow_name: format_full_flow_name(&self.catalog_name, &self.object_name),
1553                    })?
1554                    .flow_id();
1555
1556                self.set_flow_id(flow_id);
1557            }
1558        }
1559
1560        Ok(())
1561    }
1562}
1563
1564// Proto conversions for CommentObjectType
1565impl From<CommentObjectType> for PbCommentObjectType {
1566    fn from(object_type: CommentObjectType) -> Self {
1567        match object_type {
1568            CommentObjectType::Table => PbCommentObjectType::Table,
1569            CommentObjectType::Column => PbCommentObjectType::Column,
1570            CommentObjectType::Flow => PbCommentObjectType::Flow,
1571        }
1572    }
1573}
1574
1575impl TryFrom<i32> for CommentObjectType {
1576    type Error = error::Error;
1577
1578    fn try_from(value: i32) -> Result<Self> {
1579        match value {
1580            0 => Ok(CommentObjectType::Table),
1581            1 => Ok(CommentObjectType::Column),
1582            2 => Ok(CommentObjectType::Flow),
1583            _ => error::InvalidProtoMsgSnafu {
1584                err_msg: format!(
1585                    "Invalid CommentObjectType value: {}. Valid values are: 0 (Table), 1 (Column), 2 (Flow)",
1586                    value
1587                ),
1588            }
1589            .fail(),
1590        }
1591    }
1592}
1593
1594// Proto conversions for CommentOnTask
1595impl TryFrom<PbCommentOnTask> for CommentOnTask {
1596    type Error = error::Error;
1597
1598    fn try_from(pb: PbCommentOnTask) -> Result<Self> {
1599        let comment_on = pb.comment_on.context(error::InvalidProtoMsgSnafu {
1600            err_msg: "expected comment_on",
1601        })?;
1602
1603        Ok(CommentOnTask {
1604            catalog_name: comment_on.catalog_name,
1605            schema_name: comment_on.schema_name,
1606            object_type: comment_on.object_type.try_into()?,
1607            object_name: comment_on.object_name,
1608            column_name: if comment_on.column_name.is_empty() {
1609                None
1610            } else {
1611                Some(comment_on.column_name)
1612            },
1613            comment: if comment_on.comment.is_empty() {
1614                None
1615            } else {
1616                Some(comment_on.comment)
1617            },
1618            object_id: None,
1619        })
1620    }
1621}
1622
1623impl From<CommentOnTask> for PbCommentOnTask {
1624    fn from(task: CommentOnTask) -> Self {
1625        let pb_object_type: PbCommentObjectType = task.object_type.into();
1626        PbCommentOnTask {
1627            comment_on: Some(CommentOnExpr {
1628                catalog_name: task.catalog_name,
1629                schema_name: task.schema_name,
1630                object_type: pb_object_type as i32,
1631                object_name: task.object_name,
1632                column_name: task.column_name.unwrap_or_default(),
1633                comment: task.comment.unwrap_or_default(),
1634            }),
1635        }
1636    }
1637}
1638
1639#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
1640pub struct QueryContext {
1641    pub current_catalog: String,
1642    pub current_schema: String,
1643    pub timezone: String,
1644    pub extensions: HashMap<String, String>,
1645    pub channel: u8,
1646    /// Maps region id -> snapshot upper bound sequence for that region.
1647    #[serde(default)]
1648    pub snapshot_seqs: HashMap<u64, u64>,
1649    /// Maps region id -> minimal SST sequence allowed for that region.
1650    #[serde(default)]
1651    pub sst_min_sequences: HashMap<u64, u64>,
1652}
1653
1654impl QueryContext {
1655    /// Get the current catalog
1656    pub fn current_catalog(&self) -> &str {
1657        &self.current_catalog
1658    }
1659
1660    /// Get the current schema
1661    pub fn current_schema(&self) -> &str {
1662        &self.current_schema
1663    }
1664
1665    /// Get the timezone
1666    pub fn timezone(&self) -> &str {
1667        &self.timezone
1668    }
1669
1670    /// Get the extensions
1671    pub fn extensions(&self) -> &HashMap<String, String> {
1672        &self.extensions
1673    }
1674
1675    /// Get the channel
1676    pub fn channel(&self) -> u8 {
1677        self.channel
1678    }
1679
1680    /// Returns the protocol derived from the typed query channel.
1681    pub fn protocol(&self) -> Option<String> {
1682        let channel = Channel::from(u32::from(self.channel));
1683        (channel != Channel::Unknown).then(|| channel.as_ref().to_string())
1684    }
1685
1686    pub fn snapshot_seqs(&self) -> &HashMap<u64, u64> {
1687        &self.snapshot_seqs
1688    }
1689
1690    pub fn sst_min_sequences(&self) -> &HashMap<u64, u64> {
1691        &self.sst_min_sequences
1692    }
1693}
1694
1695/// Lightweight query context for flow operations containing only essential fields.
1696/// This is a subset of QueryContext that includes only the fields actually needed
1697/// for flow creation and execution.
1698#[derive(Debug, Clone, Serialize, PartialEq)]
1699pub struct FlowQueryContext {
1700    /// Current catalog name used for flow metadata and execution.
1701    pub catalog: String,
1702    /// Current schema name used for table resolution during flow execution.
1703    pub schema: String,
1704    /// Timezone used for timestamp evaluation in the flow.
1705    pub timezone: String,
1706    /// Query extensions carried into flow execution.
1707    #[serde(default)]
1708    pub extensions: HashMap<String, String>,
1709    /// Request channel propagated from the original query context.
1710    #[serde(default)]
1711    pub channel: u8,
1712    /// Per-region snapshot upper bounds bound during query planning/execution.
1713    #[serde(default)]
1714    pub snapshot_seqs: HashMap<u64, u64>,
1715    /// Per-region lower SST scan bounds carried with the flow context.
1716    #[serde(default)]
1717    pub sst_min_sequences: HashMap<u64, u64>,
1718}
1719
1720impl<'de> Deserialize<'de> for FlowQueryContext {
1721    fn deserialize<D>(deserializer: D) -> result::Result<Self, D::Error>
1722    where
1723        D: serde::Deserializer<'de>,
1724    {
1725        // Support both QueryContext format and FlowQueryContext format
1726        #[derive(Deserialize)]
1727        #[serde(untagged)]
1728        enum ContextCompat {
1729            Flow(FlowQueryContextHelper),
1730            Full(QueryContext),
1731        }
1732
1733        #[derive(Deserialize)]
1734        struct FlowQueryContextHelper {
1735            catalog: String,
1736            schema: String,
1737            timezone: String,
1738            #[serde(default)]
1739            extensions: HashMap<String, String>,
1740            #[serde(default)]
1741            channel: u8,
1742            #[serde(default)]
1743            snapshot_seqs: HashMap<u64, u64>,
1744            #[serde(default)]
1745            sst_min_sequences: HashMap<u64, u64>,
1746        }
1747
1748        match ContextCompat::deserialize(deserializer)? {
1749            ContextCompat::Flow(helper) => Ok(FlowQueryContext {
1750                catalog: helper.catalog,
1751                schema: helper.schema,
1752                timezone: helper.timezone,
1753                extensions: helper.extensions,
1754                channel: helper.channel,
1755                snapshot_seqs: helper.snapshot_seqs,
1756                sst_min_sequences: helper.sst_min_sequences,
1757            }),
1758            ContextCompat::Full(full_ctx) => Ok(full_ctx.into()),
1759        }
1760    }
1761}
1762
1763impl From<PbQueryContext> for QueryContext {
1764    fn from(pb_ctx: PbQueryContext) -> Self {
1765        let (snapshot_seqs, sst_min_sequences) = pb_ctx
1766            .snapshot_seqs
1767            .map(|seqs| (seqs.snapshot_seqs, seqs.sst_min_sequences))
1768            .unwrap_or_default();
1769
1770        Self {
1771            current_catalog: pb_ctx.current_catalog,
1772            current_schema: pb_ctx.current_schema,
1773            timezone: pb_ctx.timezone,
1774            extensions: pb_ctx.extensions,
1775            channel: pb_ctx.channel as u8,
1776            snapshot_seqs,
1777            sst_min_sequences,
1778        }
1779    }
1780}
1781
1782impl From<QueryContext> for PbQueryContext {
1783    fn from(
1784        QueryContext {
1785            current_catalog,
1786            current_schema,
1787            timezone,
1788            extensions,
1789            channel,
1790            snapshot_seqs,
1791            sst_min_sequences,
1792        }: QueryContext,
1793    ) -> Self {
1794        PbQueryContext {
1795            current_catalog,
1796            current_schema,
1797            timezone,
1798            extensions,
1799            channel: channel as u32,
1800            snapshot_seqs: (!snapshot_seqs.is_empty() || !sst_min_sequences.is_empty()).then_some(
1801                api::v1::SnapshotSequences {
1802                    snapshot_seqs,
1803                    sst_min_sequences,
1804                },
1805            ),
1806            explain: None,
1807        }
1808    }
1809}
1810
1811impl From<QueryContext> for FlowQueryContext {
1812    fn from(ctx: QueryContext) -> Self {
1813        Self {
1814            catalog: ctx.current_catalog,
1815            schema: ctx.current_schema,
1816            timezone: ctx.timezone,
1817            extensions: ctx.extensions,
1818            channel: ctx.channel,
1819            snapshot_seqs: ctx.snapshot_seqs,
1820            sst_min_sequences: ctx.sst_min_sequences,
1821        }
1822    }
1823}
1824
1825impl From<FlowQueryContext> for QueryContext {
1826    fn from(flow_ctx: FlowQueryContext) -> Self {
1827        Self {
1828            current_catalog: flow_ctx.catalog,
1829            current_schema: flow_ctx.schema,
1830            timezone: flow_ctx.timezone,
1831            extensions: flow_ctx.extensions,
1832            channel: flow_ctx.channel,
1833            snapshot_seqs: flow_ctx.snapshot_seqs,
1834            sst_min_sequences: flow_ctx.sst_min_sequences,
1835        }
1836    }
1837}
1838
1839impl From<FlowQueryContext> for PbQueryContext {
1840    fn from(flow_ctx: FlowQueryContext) -> Self {
1841        let query_ctx: QueryContext = flow_ctx.into();
1842        query_ctx.into()
1843    }
1844}
1845
1846#[cfg(test)]
1847mod tests {
1848    use std::sync::Arc;
1849
1850    use api::v1::{AlterTableExpr, ColumnDef, CreateTableExpr, SemanticType};
1851    use datatypes::schema::{ColumnSchema, Schema, SchemaBuilder};
1852    use store_api::metric_engine_consts::METRIC_ENGINE_NAME;
1853    use store_api::storage::ConcreteDataType;
1854    use table::metadata::{TableInfo, TableMeta, TableType};
1855    use table::test_util::table_info::test_table_info;
1856
1857    use super::{AlterTableTask, CreateTableTask, *};
1858
1859    #[test]
1860    fn test_ddl_timeout_secs() {
1861        assert_eq!(ddl_timeout_secs(Duration::ZERO), 0);
1862        assert_eq!(ddl_timeout_secs(Duration::from_nanos(1)), 1);
1863        assert_eq!(ddl_timeout_secs(Duration::from_secs(1)), 1);
1864        assert_eq!(ddl_timeout_secs(Duration::from_millis(1500)), 2);
1865        assert_eq!(
1866            ddl_timeout_secs(Duration::from_secs(u32::MAX as u64 + 1)),
1867            u32::MAX
1868        );
1869    }
1870
1871    #[test]
1872    fn test_basic_ser_de_create_table_task() {
1873        let schema = SchemaBuilder::default().build().unwrap();
1874        let table_info = test_table_info(1025, "foo", "bar", "baz", Arc::new(schema));
1875        let task = CreateTableTask::new(CreateTableExpr::default(), Vec::new(), table_info);
1876
1877        let output = serde_json::to_vec(&task).unwrap();
1878
1879        let de = serde_json::from_slice(&output).unwrap();
1880        assert_eq!(task, de);
1881    }
1882
1883    #[test]
1884    fn test_basic_ser_de_alter_table_task() {
1885        let task = AlterTableTask {
1886            alter_table: AlterTableExpr::default(),
1887        };
1888
1889        let output = serde_json::to_vec(&task).unwrap();
1890
1891        let de = serde_json::from_slice(&output).unwrap();
1892        assert_eq!(task, de);
1893    }
1894
1895    #[test]
1896    fn test_undrop_table_task_pb_roundtrip() {
1897        let expected = UndropTableTask { table_id: 1024 };
1898        let request = SubmitDdlTaskRequest::new(DdlTask::UndropTable(expected.clone()));
1899
1900        let pb = PbDdlTaskRequest::try_from(request).unwrap();
1901        let pb_task = pb.task.unwrap();
1902        let de = DdlTask::try_from(pb_task).unwrap();
1903
1904        assert!(matches!(de, DdlTask::UndropTable(task) if task == expected));
1905    }
1906
1907    #[test]
1908    fn test_purge_dropped_table_task_pb_roundtrip() {
1909        let expected = PurgeDroppedTableTask { table_id: 1024 };
1910        let request = SubmitDdlTaskRequest::new(DdlTask::PurgeDroppedTable(expected.clone()));
1911
1912        let pb = PbDdlTaskRequest::try_from(request).unwrap();
1913        let pb_task = pb.task.unwrap();
1914        let de = DdlTask::try_from(pb_task).unwrap();
1915
1916        assert!(matches!(de, DdlTask::PurgeDroppedTable(task) if task == expected));
1917    }
1918
1919    #[test]
1920    fn test_undrop_table_task_json_roundtrip() {
1921        let task = UndropTableTask { table_id: 1024 };
1922
1923        let output = serde_json::to_vec(&task).unwrap();
1924
1925        let de = serde_json::from_slice(&output).unwrap();
1926        assert_eq!(task, de);
1927    }
1928
1929    #[test]
1930    fn test_purge_dropped_table_task_json_roundtrip() {
1931        let task = PurgeDroppedTableTask { table_id: 1024 };
1932
1933        let output = serde_json::to_vec(&task).unwrap();
1934
1935        let de = serde_json::from_slice(&output).unwrap();
1936        assert_eq!(task, de);
1937    }
1938
1939    #[test]
1940    fn test_sort_columns() {
1941        // construct RawSchema
1942        let schema = Arc::new(Schema::new(vec![
1943            ColumnSchema::new(
1944                "column3".to_string(),
1945                ConcreteDataType::string_datatype(),
1946                true,
1947            ),
1948            ColumnSchema::new(
1949                "column1".to_string(),
1950                ConcreteDataType::timestamp_millisecond_datatype(),
1951                false,
1952            )
1953            .with_time_index(true),
1954            ColumnSchema::new(
1955                "column2".to_string(),
1956                ConcreteDataType::float64_datatype(),
1957                true,
1958            ),
1959        ]));
1960
1961        // construct RawTableMeta
1962        let meta = TableMeta {
1963            schema,
1964            primary_key_indices: vec![0],
1965            value_indices: vec![2],
1966            engine: METRIC_ENGINE_NAME.to_string(),
1967            next_column_id: 0,
1968            options: Default::default(),
1969            created_on: Default::default(),
1970            updated_on: Default::default(),
1971            partition_key_indices: Default::default(),
1972            column_ids: Default::default(),
1973        };
1974
1975        // construct TableInfo
1976        let raw_table_info = TableInfo {
1977            ident: Default::default(),
1978            meta,
1979            name: Default::default(),
1980            desc: Default::default(),
1981            catalog_name: Default::default(),
1982            schema_name: Default::default(),
1983            table_type: TableType::Base,
1984        };
1985
1986        // construct create table expr
1987        let create_table_expr = CreateTableExpr {
1988            column_defs: vec![
1989                ColumnDef {
1990                    name: "column3".to_string(),
1991                    semantic_type: SemanticType::Tag as i32,
1992                    ..Default::default()
1993                },
1994                ColumnDef {
1995                    name: "column1".to_string(),
1996                    semantic_type: SemanticType::Timestamp as i32,
1997                    ..Default::default()
1998                },
1999                ColumnDef {
2000                    name: "column2".to_string(),
2001                    semantic_type: SemanticType::Field as i32,
2002                    ..Default::default()
2003                },
2004            ],
2005            primary_keys: vec!["column3".to_string()],
2006            ..Default::default()
2007        };
2008
2009        let mut create_table_task =
2010            CreateTableTask::new(create_table_expr, Vec::new(), raw_table_info);
2011
2012        // Call the sort_columns method
2013        create_table_task.sort_columns();
2014
2015        // Assert that the columns are sorted correctly
2016        assert_eq!(
2017            create_table_task.create_table.column_defs[0].name,
2018            "column1".to_string()
2019        );
2020        assert_eq!(
2021            create_table_task.create_table.column_defs[1].name,
2022            "column2".to_string()
2023        );
2024        assert_eq!(
2025            create_table_task.create_table.column_defs[2].name,
2026            "column3".to_string()
2027        );
2028
2029        // Assert that the table_info is updated correctly
2030        assert_eq!(
2031            create_table_task.table_info.meta.schema.timestamp_index(),
2032            Some(0)
2033        );
2034        assert_eq!(
2035            create_table_task.table_info.meta.primary_key_indices,
2036            vec![2]
2037        );
2038        assert_eq!(create_table_task.table_info.meta.value_indices, vec![0, 1]);
2039    }
2040
2041    #[test]
2042    fn test_flow_query_context_conversion_from_query_context() {
2043        use std::collections::HashMap;
2044        let mut extensions = HashMap::new();
2045        extensions.insert("key1".to_string(), "value1".to_string());
2046        extensions.insert("key2".to_string(), "value2".to_string());
2047
2048        let query_ctx = QueryContext {
2049            current_catalog: "test_catalog".to_string(),
2050            current_schema: "test_schema".to_string(),
2051            timezone: "UTC".to_string(),
2052            extensions,
2053            channel: 5,
2054            snapshot_seqs: HashMap::from([(10, 100)]),
2055            sst_min_sequences: HashMap::from([(10, 90)]),
2056        };
2057
2058        let flow_ctx: FlowQueryContext = query_ctx.into();
2059
2060        assert_eq!(flow_ctx.catalog, "test_catalog");
2061        assert_eq!(flow_ctx.schema, "test_schema");
2062        assert_eq!(flow_ctx.timezone, "UTC");
2063        assert_eq!(flow_ctx.channel, 5);
2064        assert_eq!(flow_ctx.snapshot_seqs, HashMap::from([(10, 100)]));
2065        assert_eq!(flow_ctx.sst_min_sequences, HashMap::from([(10, 90)]));
2066    }
2067
2068    #[test]
2069    fn test_flow_query_context_conversion_to_query_context() {
2070        let flow_ctx = FlowQueryContext {
2071            catalog: "prod_catalog".to_string(),
2072            schema: "public".to_string(),
2073            timezone: "America/New_York".to_string(),
2074            extensions: HashMap::from([("k".to_string(), "v".to_string())]),
2075            channel: 7,
2076            snapshot_seqs: HashMap::from([(11, 111)]),
2077            sst_min_sequences: HashMap::from([(11, 101)]),
2078        };
2079
2080        let query_ctx: QueryContext = flow_ctx.clone().into();
2081
2082        assert_eq!(query_ctx.current_catalog, "prod_catalog");
2083        assert_eq!(query_ctx.current_schema, "public");
2084        assert_eq!(query_ctx.timezone, "America/New_York");
2085        assert_eq!(
2086            query_ctx.extensions,
2087            HashMap::from([("k".to_string(), "v".to_string())])
2088        );
2089        assert_eq!(query_ctx.channel, 7);
2090        assert_eq!(query_ctx.snapshot_seqs, HashMap::from([(11, 111)]));
2091        assert_eq!(query_ctx.sst_min_sequences, HashMap::from([(11, 101)]));
2092
2093        // Test roundtrip conversion
2094        let flow_ctx_roundtrip: FlowQueryContext = query_ctx.into();
2095        assert_eq!(flow_ctx, flow_ctx_roundtrip);
2096    }
2097
2098    #[test]
2099    fn test_flow_query_context_serialization() {
2100        let flow_ctx = FlowQueryContext {
2101            catalog: "test_catalog".to_string(),
2102            schema: "test_schema".to_string(),
2103            timezone: "UTC".to_string(),
2104            extensions: HashMap::new(),
2105            channel: 0,
2106            snapshot_seqs: HashMap::new(),
2107            sst_min_sequences: HashMap::new(),
2108        };
2109
2110        let serialized = serde_json::to_string(&flow_ctx).unwrap();
2111        let deserialized: FlowQueryContext = serde_json::from_str(&serialized).unwrap();
2112
2113        assert_eq!(flow_ctx, deserialized);
2114
2115        // Verify JSON structure
2116        let json_value: serde_json::Value = serde_json::from_str(&serialized).unwrap();
2117        assert_eq!(json_value["catalog"], "test_catalog");
2118        assert_eq!(json_value["schema"], "test_schema");
2119        assert_eq!(json_value["timezone"], "UTC");
2120    }
2121
2122    #[test]
2123    fn test_flow_query_context_conversion_to_pb() {
2124        let flow_ctx = FlowQueryContext {
2125            catalog: "pb_catalog".to_string(),
2126            schema: "pb_schema".to_string(),
2127            timezone: "Asia/Tokyo".to_string(),
2128            extensions: HashMap::from([("x".to_string(), "y".to_string())]),
2129            channel: 6,
2130            snapshot_seqs: HashMap::from([(3, 30)]),
2131            sst_min_sequences: HashMap::from([(3, 21)]),
2132        };
2133
2134        let pb_ctx: PbQueryContext = flow_ctx.into();
2135
2136        assert_eq!(pb_ctx.current_catalog, "pb_catalog");
2137        assert_eq!(pb_ctx.current_schema, "pb_schema");
2138        assert_eq!(pb_ctx.timezone, "Asia/Tokyo");
2139        assert_eq!(
2140            pb_ctx.extensions,
2141            HashMap::from([("x".to_string(), "y".to_string())])
2142        );
2143        assert_eq!(pb_ctx.channel, 6);
2144        assert_eq!(
2145            pb_ctx.snapshot_seqs,
2146            Some(api::v1::SnapshotSequences {
2147                snapshot_seqs: HashMap::from([(3, 30)]),
2148                sst_min_sequences: HashMap::from([(3, 21)]),
2149            })
2150        );
2151        assert!(pb_ctx.explain.is_none());
2152    }
2153
2154    #[test]
2155    fn test_pb_query_context_roundtrip_with_snapshot_sequences() {
2156        let pb = PbQueryContext {
2157            current_catalog: "c1".to_string(),
2158            current_schema: "s1".to_string(),
2159            timezone: "UTC".to_string(),
2160            extensions: HashMap::from([("flow.return_region_seq".to_string(), "true".to_string())]),
2161            channel: 3,
2162            snapshot_seqs: Some(api::v1::SnapshotSequences {
2163                snapshot_seqs: HashMap::from([(1, 100)]),
2164                sst_min_sequences: HashMap::from([(1, 90)]),
2165            }),
2166            explain: None,
2167        };
2168
2169        let query_ctx: QueryContext = pb.clone().into();
2170        let pb_roundtrip: PbQueryContext = query_ctx.into();
2171
2172        assert_eq!(pb_roundtrip.current_catalog, pb.current_catalog);
2173        assert_eq!(pb_roundtrip.current_schema, pb.current_schema);
2174        assert_eq!(pb_roundtrip.timezone, pb.timezone);
2175        assert_eq!(pb_roundtrip.extensions, pb.extensions);
2176        assert_eq!(pb_roundtrip.channel, pb.channel);
2177        assert_eq!(pb_roundtrip.snapshot_seqs, pb.snapshot_seqs);
2178    }
2179
2180    #[test]
2181    fn test_trigger_reason_deserializes_unknown_value() {
2182        let reason: TriggerReason = serde_json::from_str("\"future_reason\"").unwrap();
2183        assert_eq!(TriggerReason::Unknown, reason);
2184    }
2185}