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