Skip to main content

common_meta/ddl/
create_flow.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
15mod metadata;
16
17use std::collections::{BTreeMap, HashMap};
18use std::fmt;
19
20use api::v1::ExpireAfter;
21use api::v1::flow::flow_request::Body as PbFlowRequest;
22use api::v1::flow::{CreateRequest, FlowRequest, FlowRequestHeader};
23use async_trait::async_trait;
24use common_catalog::format_full_flow_name;
25use common_procedure::error::{FromJsonSnafu, ToJsonSnafu};
26use common_procedure::{
27    Context as ProcedureContext, EventContext, EventTrigger, LockKey, Procedure, ProcedureState,
28    Result as ProcedureResult, Status,
29};
30use common_telemetry::info;
31use common_telemetry::tracing_context::TracingContext;
32use futures::future::join_all;
33use itertools::Itertools;
34use serde::{Deserialize, Serialize};
35use snafu::{ResultExt, ensure};
36use strum::AsRefStr;
37use table::metadata::TableId;
38use table::table_name::TableName;
39
40use crate::cache_invalidator::Context;
41use crate::ddl::DdlContext;
42use crate::ddl::event::flow::{CREATE_FLOW_EVENT_TYPE, CreateFlowEventIntent, FlowDdlEvent};
43use crate::ddl::utils::{add_peer_context_if_needed, map_to_procedure_error};
44use crate::error::{self, Result, UnexpectedSnafu};
45use crate::instruction::{CacheIdent, CreateFlow, DropFlow};
46use crate::key::flow::flow_info::{FlowInfoValue, FlowScheduleConfig, FlowStatus};
47use crate::key::flow::flow_route::FlowRouteValue;
48use crate::key::table_name::TableNameKey;
49use crate::key::{DeserializedValueWithBytes, FlowId, FlowPartitionId};
50use crate::lock_key::{CatalogLock, FlowNameLock};
51use crate::metrics;
52use crate::peer::Peer;
53use crate::rpc::ddl::{CreateFlowTask, FlowQueryContext, QueryContext};
54
55/// The procedure of flow creation.
56pub struct CreateFlowProcedure {
57    pub context: DdlContext,
58    pub data: CreateFlowData,
59}
60
61impl CreateFlowProcedure {
62    pub const TYPE_NAME: &'static str = "metasrv-procedure::CreateFlow";
63
64    /// Returns a new [CreateFlowProcedure].
65    pub fn new(task: CreateFlowTask, query_context: QueryContext, context: DdlContext) -> Self {
66        Self {
67            context,
68            data: CreateFlowData {
69                task,
70                flow_id: None,
71                peers: vec![],
72                source_table_ids: vec![],
73                unresolved_source_table_names: vec![],
74                flow_context: without_scheduled_time_extension(query_context).into(),
75                state: CreateFlowState::Prepare,
76                prev_flow_info_value: None,
77                did_replace: false,
78                flow_type: None,
79            },
80        }
81    }
82
83    /// Deserializes from `json`.
84    pub fn from_json(json: &str, context: DdlContext) -> ProcedureResult<Self> {
85        let data = serde_json::from_str(json).context(FromJsonSnafu)?;
86        Ok(CreateFlowProcedure { context, data })
87    }
88
89    pub(crate) async fn on_prepare(&mut self) -> Result<Status> {
90        let catalog_name = &self.data.task.catalog_name;
91        let flow_name = &self.data.task.flow_name;
92        let sink_table_name = &self.data.task.sink_table_name;
93        let create_if_not_exists = self.data.task.create_if_not_exists;
94        let or_replace = self.data.task.or_replace;
95
96        validate_flow_options(&self.data.task)?;
97
98        let flow_name_value = self
99            .context
100            .flow_metadata_manager
101            .flow_name_manager()
102            .get(catalog_name, flow_name)
103            .await?;
104
105        if create_if_not_exists && or_replace {
106            // this is forbidden because not clear what does that mean exactly
107            return error::UnsupportedSnafu {
108                operation: "Create flow with both `IF NOT EXISTS` and `OR REPLACE`",
109            }
110            .fail();
111        }
112
113        if let Some(value) = flow_name_value {
114            ensure!(
115                create_if_not_exists || or_replace,
116                error::FlowAlreadyExistsSnafu {
117                    flow_name: format_full_flow_name(catalog_name, flow_name),
118                }
119            );
120
121            let flow_id = value.flow_id();
122            if create_if_not_exists {
123                info!("Flow already exists, flow_id: {}", flow_id);
124                return Ok(Status::done_with_output(flow_id));
125            }
126
127            let flow_id = value.flow_id();
128            let peers = self
129                .context
130                .flow_metadata_manager
131                .flow_route_manager()
132                .routes(flow_id)
133                .await?
134                .into_iter()
135                .map(|(_, value)| value.peer)
136                .collect::<Vec<_>>();
137            self.data.flow_id = Some(flow_id);
138            self.data.peers = peers;
139            info!("Replacing flow, flow_id: {}", flow_id);
140
141            let flow_info_value = self
142                .context
143                .flow_metadata_manager
144                .flow_info_manager()
145                .get_raw(flow_id)
146                .await?;
147
148            ensure!(
149                flow_info_value.is_some(),
150                error::FlowNotFoundSnafu {
151                    flow_name: format_full_flow_name(catalog_name, flow_name),
152                }
153            );
154
155            self.data.prev_flow_info_value = flow_info_value;
156        }
157
158        //  Ensures sink table doesn't exist.
159        let exists = self
160            .context
161            .table_metadata_manager
162            .table_name_manager()
163            .exists(TableNameKey::new(
164                &sink_table_name.catalog_name,
165                &sink_table_name.schema_name,
166                &sink_table_name.table_name,
167            ))
168            .await?;
169        // TODO(discord9): due to undefined behavior in flow's plan in how to transform types in mfp, sometime flow can't deduce correct schema
170        // and require manually create sink table
171        if exists {
172            common_telemetry::warn!("Table already exists, table: {}", sink_table_name);
173        }
174
175        self.collect_source_tables().await?;
176        ensure!(
177            self.data.unresolved_source_table_names.is_empty()
178                || defer_on_missing_source(&self.data.task)?,
179            error::UnsupportedSnafu {
180                operation: format!(
181                    "Create flow with missing source tables requires WITH ('{DEFER_ON_MISSING_SOURCE_KEY}'='true'): {}",
182                    self.data
183                        .unresolved_source_table_names
184                        .iter()
185                        .map(ToString::to_string)
186                        .join(", ")
187                )
188            }
189        );
190        self.ensure_supported_replace_transition()?;
191
192        // Validate that source and sink tables are not the same
193        let sink_table_name = &self.data.task.sink_table_name;
194        if self
195            .data
196            .task
197            .source_table_names
198            .iter()
199            .any(|source| source == sink_table_name)
200        {
201            return error::UnsupportedSnafu {
202                operation: format!(
203                    "Creating flow with source and sink table being the same: {}",
204                    sink_table_name
205                ),
206            }
207            .fail();
208        }
209
210        if self.data.flow_id.is_none() {
211            self.allocate_flow_id().await?;
212        }
213        self.data.flow_type = Some(get_flow_type_from_options(&self.data.task)?);
214
215        // Resolve schedule defaults into task.eval_schedule once, before
216        // CreateRequest / FlowInfoValue are constructed. This ensures the same
217        // typed schedule config is sent to flownodes and persisted in metadata.
218        // Idempotent: if eval_schedule is already present we do not recompute.
219        resolve_schedule_defaults_into_task(
220            &mut self.data.task,
221            self.data
222                .prev_flow_info_value
223                .as_ref()
224                .map(|v| v.get_inner_ref()),
225        );
226
227        self.data.state = if self.data.is_pending() {
228            self.data.peers.clear();
229            CreateFlowState::CreateMetadata
230        } else {
231            CreateFlowState::CreateFlows
232        };
233
234        Ok(Status::executing(true))
235    }
236
237    fn ensure_supported_replace_transition(&self) -> Result<()> {
238        if !self.data.task.or_replace {
239            return Ok(());
240        }
241
242        let Some(prev_flow_info) = self.data.prev_flow_info_value.as_ref() else {
243            return Ok(());
244        };
245        let prev_pending = prev_flow_info.get_inner_ref().is_pending();
246        let new_pending = self.data.is_pending();
247        ensure!(
248            prev_pending == new_pending,
249            error::UnsupportedSnafu {
250                operation: "Replacing between pending and active flow states is not supported yet"
251            }
252        );
253
254        Ok(())
255    }
256
257    async fn on_flownode_create_flows(&mut self) -> Result<Status> {
258        // Safety: must be allocated.
259        let mut create_flow = Vec::with_capacity(self.data.peers.len());
260        for peer in &self.data.peers {
261            let requester = self.context.node_manager.flownode(peer).await;
262            let request = FlowRequest {
263                header: Some(FlowRequestHeader {
264                    tracing_context: TracingContext::from_current_span().to_w3c(),
265                    // Convert FlowQueryContext to QueryContext
266                    query_context: Some(
267                        without_scheduled_time_extension(QueryContext::from(
268                            self.data.flow_context.clone(),
269                        ))
270                        .into(),
271                    ),
272                }),
273                body: Some(PbFlowRequest::Create((&self.data).into())),
274            };
275            create_flow.push(async move {
276                requester
277                    .handle(request)
278                    .await
279                    .map_err(add_peer_context_if_needed(peer.clone()))
280            });
281        }
282        info!(
283            "Creating flow({:?}, type={:?}) on flownodes with peers={:?}",
284            self.data.flow_id, self.data.flow_type, self.data.peers
285        );
286        join_all(create_flow)
287            .await
288            .into_iter()
289            .collect::<Result<Vec<_>>>()?;
290
291        self.data.state = CreateFlowState::CreateMetadata;
292        Ok(Status::executing(true))
293    }
294
295    /// Creates flow metadata.
296    ///
297    /// Abort(not-retry):
298    /// - Failed to create table metadata.
299    async fn on_create_metadata(&mut self) -> Result<Status> {
300        // Safety: The flow id must be allocated.
301        let flow_id = self.data.flow_id.unwrap();
302        let (flow_info, flow_routes) = (&self.data).into();
303        if let Some(prev_flow_value) = self.data.prev_flow_info_value.as_ref()
304            && self.data.task.or_replace
305        {
306            self.context
307                .flow_metadata_manager
308                .update_flow_metadata(flow_id, prev_flow_value, &flow_info, flow_routes)
309                .await?;
310            info!("Replaced flow metadata for flow {flow_id}");
311            self.data.did_replace = true;
312        } else {
313            self.context
314                .flow_metadata_manager
315                .create_flow_metadata(flow_id, flow_info, flow_routes)
316                .await?;
317            info!("Created flow metadata for flow {flow_id}");
318        }
319
320        self.data.state = CreateFlowState::InvalidateFlowCache;
321        Ok(Status::executing(true))
322    }
323
324    async fn on_broadcast(&mut self) -> Result<Status> {
325        debug_assert!(self.data.state == CreateFlowState::InvalidateFlowCache);
326        // Safety: The flow id must be allocated.
327        let flow_id = self.data.flow_id.unwrap();
328        let did_replace = self.data.did_replace;
329        let ctx = Context {
330            subject: Some("Invalidate flow cache by creating flow".to_string()),
331        };
332
333        let mut caches = vec![];
334
335        // if did replaced, invalidate the flow cache with drop the old flow
336        if did_replace {
337            let old_flow_info = self.data.prev_flow_info_value.as_ref().unwrap();
338
339            // only drop flow is needed, since flow name haven't changed, and flow id already invalidated below
340            caches.extend([CacheIdent::DropFlow(DropFlow {
341                flow_id,
342                source_table_ids: old_flow_info.source_table_ids.clone(),
343                flow_part2node_id: old_flow_info.flownode_ids().clone().into_iter().collect(),
344            })]);
345        }
346
347        let (_flow_info, flow_routes) = (&self.data).into();
348        let flow_part2peers = flow_routes
349            .into_iter()
350            .map(|(part_id, route)| (part_id, route.peer))
351            .collect();
352
353        caches.extend([
354            CacheIdent::CreateFlow(CreateFlow {
355                flow_id,
356                source_table_ids: self.data.source_table_ids.clone(),
357                partition_to_peer_mapping: flow_part2peers,
358            }),
359            CacheIdent::FlowId(flow_id),
360        ]);
361
362        self.context
363            .cache_invalidator
364            .invalidate(&ctx, &caches)
365            .await?;
366
367        Ok(Status::done_with_output(flow_id))
368    }
369}
370
371#[async_trait]
372impl Procedure for CreateFlowProcedure {
373    fn type_name(&self) -> &str {
374        Self::TYPE_NAME
375    }
376
377    async fn execute(&mut self, _ctx: &ProcedureContext) -> ProcedureResult<Status> {
378        let state = &self.data.state;
379
380        let _timer = metrics::METRIC_META_PROCEDURE_CREATE_FLOW
381            .with_label_values(&[state.as_ref()])
382            .start_timer();
383
384        match state {
385            CreateFlowState::Prepare => self.on_prepare().await,
386            CreateFlowState::CreateFlows => self.on_flownode_create_flows().await,
387            CreateFlowState::CreateMetadata => self.on_create_metadata().await,
388            CreateFlowState::InvalidateFlowCache => self.on_broadcast().await,
389        }
390        .map_err(map_to_procedure_error)
391    }
392
393    fn dump(&self) -> ProcedureResult<String> {
394        serde_json::to_string(&self.data).context(ToJsonSnafu)
395    }
396
397    fn lock_key(&self) -> LockKey {
398        let catalog_name = &self.data.task.catalog_name;
399        let flow_name = &self.data.task.flow_name;
400
401        LockKey::new(vec![
402            CatalogLock::Read(catalog_name).into(),
403            FlowNameLock::new(catalog_name, flow_name).into(),
404        ])
405    }
406
407    fn event(&self, ctx: &EventContext<'_>) -> Option<Box<dyn common_event_recorder::Event>> {
408        if !ctx.event_type_filter.allows(CREATE_FLOW_EVENT_TYPE) {
409            return None;
410        }
411
412        let event = match &ctx.trigger {
413            EventTrigger::Submitted => FlowDdlEvent::create_submitted(
414                &self.data.task.catalog_name,
415                &self.data.task.flow_name,
416                CreateFlowEventIntent {
417                    or_replace: self.data.task.or_replace,
418                    create_if_not_exists: self.data.task.create_if_not_exists,
419                    expire_after: self.data.task.expire_after,
420                    eval_interval_secs: self.data.task.eval_interval_secs,
421                },
422            ),
423            EventTrigger::Succeeded => {
424                let flow_id = match ctx.lifecycle_state {
425                    ProcedureState::Done {
426                        output: Some(output),
427                    } => output
428                        .downcast_ref::<FlowId>()
429                        .copied()
430                        .or(self.data.flow_id),
431                    _ => self.data.flow_id,
432                };
433                FlowDdlEvent::create_succeeded(
434                    &self.data.task.catalog_name,
435                    &self.data.task.flow_name,
436                    flow_id,
437                )
438            }
439            _ => FlowDdlEvent::create_lifecycle(
440                &self.data.task.catalog_name,
441                &self.data.task.flow_name,
442            ),
443        };
444
445        Some(Box::new(event))
446    }
447}
448
449pub fn get_flow_type_from_options(flow_task: &CreateFlowTask) -> Result<FlowType> {
450    let flow_type = flow_task
451        .flow_options
452        .get(FlowType::FLOW_TYPE_KEY)
453        .map(|s| s.as_str());
454    match flow_type {
455        Some(FlowType::BATCHING) => Ok(FlowType::Batching),
456        Some(FlowType::STREAMING) => Ok(FlowType::Streaming),
457        Some(unknown) => UnexpectedSnafu {
458            err_msg: format!("Unknown flow type: {}", unknown),
459        }
460        .fail(),
461        None => Ok(FlowType::Batching),
462    }
463}
464
465/// The flow option key for creating pending flow metadata when source tables do not exist.
466pub const DEFER_ON_MISSING_SOURCE_KEY: &str = "defer_on_missing_source";
467
468/// Internal transient key used to pass the serialized `FlowScheduleConfig` from
469/// meta to flownode through `CreateRequest.flow_options`. This key must never
470/// be accepted as a user-provided option and must never be persisted into
471/// `FlowInfoValue.options`.
472/// TODO(discord9): Replace this transient flow_options transport with a typed
473/// field in the flow create request.
474pub const INTERNAL_EVAL_SCHEDULE_KEY: &str = "__greptime_internal_eval_schedule";
475
476const FLOW_SCHEDULED_TIME_MILLIS_EXTENSION_KEY: &str = "flow.scheduled_time_millis";
477
478fn without_scheduled_time_extension(mut query_context: QueryContext) -> QueryContext {
479    query_context
480        .extensions
481        .remove(FLOW_SCHEDULED_TIME_MILLIS_EXTENSION_KEY);
482    query_context
483}
484
485pub fn defer_on_missing_source(flow_task: &CreateFlowTask) -> Result<bool> {
486    flow_task
487        .flow_options
488        .get(DEFER_ON_MISSING_SOURCE_KEY)
489        .map(|value| {
490            value
491                .trim()
492                .to_ascii_lowercase()
493                .parse::<bool>()
494                .map_err(|_| {
495                    error::UnexpectedSnafu {
496                        err_msg: format!(
497                            "Invalid flow option '{DEFER_ON_MISSING_SOURCE_KEY}': {value}"
498                        ),
499                    }
500                    .build()
501                })
502        })
503        .transpose()
504        .map(|value| value.unwrap_or(false))
505}
506
507pub fn validate_flow_options(flow_task: &CreateFlowTask) -> Result<()> {
508    // Reject non-positive eval_interval_secs (zero or negative).
509    if let Some(secs) = flow_task.eval_interval_secs
510        && secs <= 0
511    {
512        return UnexpectedSnafu {
513            err_msg: format!("EVAL INTERVAL must be positive, got {secs} seconds"),
514        }
515        .fail();
516    }
517
518    for key in flow_task.flow_options.keys() {
519        match key.as_str() {
520            DEFER_ON_MISSING_SOURCE_KEY
521            | FLOW_EXPERIMENTAL_ENABLE_INCREMENTAL_READ_KEY
522            | FlowType::FLOW_TYPE_KEY => {}
523            unknown => {
524                return UnexpectedSnafu {
525                    err_msg: format!(
526                        "Unknown flow option '{unknown}', supported user options: {DEFER_ON_MISSING_SOURCE_KEY}, {FLOW_EXPERIMENTAL_ENABLE_INCREMENTAL_READ_KEY}"
527                    ),
528                }
529                .fail();
530            }
531        }
532    }
533
534    defer_on_missing_source(flow_task)?;
535    get_flow_type_from_options(flow_task)?;
536    Ok(())
537}
538
539/// Computes the ceiling of `time` to the next schedule boundary aligned with `anchor + k * interval`.
540/// All values are Unix timestamps in seconds.
541fn ceil_to_boundary(time: i64, anchor: i64, interval: i64) -> i64 {
542    if interval <= 0 {
543        return time;
544    }
545    if time <= anchor {
546        return anchor;
547    }
548
549    let diff = i128::from(time) - i128::from(anchor);
550    let interval = i128::from(interval);
551    let k = (diff + interval - 1) / interval;
552    let boundary = i128::from(anchor) + k * interval;
553
554    boundary.clamp(i128::from(i64::MIN), i128::from(i64::MAX)) as i64
555}
556
557/// Returns the effective typed schedule config for flow metadata.
558///
559/// New metadata should carry `FlowInfoValue.eval_schedule`. For older metadata
560/// that lacks the typed field, derive a deterministic config from `created_time`
561/// and defaults. This avoids recovery-time wall-clock drift.
562pub fn effective_eval_schedule_from_flow_info(
563    flow_info: &FlowInfoValue,
564) -> Option<FlowScheduleConfig> {
565    if let Some(schedule) = &flow_info.eval_schedule {
566        return Some(schedule.clone());
567    }
568
569    let eval_interval_secs = flow_info.eval_interval_secs?;
570    if eval_interval_secs <= 0 {
571        return None;
572    }
573
574    let start_secs = ceil_to_boundary(
575        flow_info.created_time.timestamp(),
576        FlowScheduleConfig::DEFAULT_ANCHOR_SECS,
577        eval_interval_secs,
578    );
579
580    Some(FlowScheduleConfig::default_with_start(
581        start_secs,
582        eval_interval_secs,
583    ))
584}
585
586/// Resolve `FlowScheduleConfig` into `task.eval_schedule`.
587///
588/// This must be called in `on_prepare` after `prev_flow_info` is loaded so that
589/// `CreateRequest` and `FlowInfoValue` both see the same resolved defaults.
590///
591/// The function is idempotent: if `task.eval_schedule` is already `Some`,
592/// it returns immediately (important for procedure retry / dump-restore).
593///
594/// Schedule configuration is NOT read from `task.flow_options` (those keys are
595/// no longer user-facing options). For new flows, pure defaults are computed.
596/// For OR REPLACE, the previous typed config is preserved when interval+anchor
597/// are unchanged; otherwise defaults are recomputed.
598pub(crate) fn resolve_schedule_defaults_into_task(
599    task: &mut CreateFlowTask,
600    prev_flow_info: Option<&FlowInfoValue>,
601) {
602    // Idempotent: if already computed, skip recomputation.
603    if task.eval_schedule.is_some() {
604        return;
605    }
606
607    let Some(eval_interval_secs) = task.eval_interval_secs else {
608        return;
609    };
610    if eval_interval_secs <= 0 {
611        return;
612    }
613
614    let anchor_secs = FlowScheduleConfig::DEFAULT_ANCHOR_SECS;
615
616    // For OR REPLACE: if interval+anchor unchanged, preserve the entire
617    // existing typed config so start / policy / limits are stable.
618    if task.or_replace
619        && let Some(prev) = prev_flow_info
620        && let Some(old_sched) = effective_eval_schedule_from_flow_info(prev)
621    {
622        let old_interval = prev.eval_interval_secs.unwrap_or(0);
623        if old_interval == eval_interval_secs && old_sched.anchor_secs == anchor_secs {
624            task.eval_schedule = Some(old_sched);
625            return;
626        }
627    }
628
629    // --- Compute start ---
630    let start_secs =
631        // New flow, or OR REPLACE with changed interval/anchor: start at the next
632        // aligned boundary after prepare time. The value is written into
633        // task.eval_schedule once so procedure retry does not recompute it.
634        ceil_to_boundary(chrono::Utc::now().timestamp(), anchor_secs, eval_interval_secs);
635
636    task.eval_schedule = Some(FlowScheduleConfig::default_with_start(
637        start_secs,
638        eval_interval_secs,
639    ));
640}
641
642fn user_runtime_flow_options(options: &HashMap<String, String>) -> HashMap<String, String> {
643    let mut options = options.clone();
644    options.remove(DEFER_ON_MISSING_SOURCE_KEY);
645    options.remove(INTERNAL_EVAL_SCHEDULE_KEY);
646    options
647}
648
649/// The state of [CreateFlowProcedure].
650#[derive(Debug, Clone, Serialize, Deserialize, AsRefStr, PartialEq)]
651pub enum CreateFlowState {
652    /// Prepares to create the flow.
653    Prepare,
654    /// Creates flows on the flownode.
655    CreateFlows,
656    /// Invalidate flow cache.
657    InvalidateFlowCache,
658    /// Create metadata.
659    CreateMetadata,
660}
661
662/// The type of flow.
663#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
664pub enum FlowType {
665    /// The flow is a batching task.
666    #[default]
667    Batching,
668    /// The flow is a streaming task.
669    Streaming,
670}
671
672pub const FLOW_EXPERIMENTAL_ENABLE_INCREMENTAL_READ_KEY: &str =
673    "experimental_enable_incremental_read";
674
675impl FlowType {
676    pub const BATCHING: &str = "batching";
677    pub const STREAMING: &str = "streaming";
678    pub const FLOW_TYPE_KEY: &str = "flow_type";
679}
680
681impl fmt::Display for FlowType {
682    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
683        match self {
684            FlowType::Batching => write!(f, "{}", FlowType::BATCHING),
685            FlowType::Streaming => write!(f, "{}", FlowType::STREAMING),
686        }
687    }
688}
689
690/// The serializable data.
691#[derive(Debug, Serialize, Deserialize)]
692pub struct CreateFlowData {
693    pub(crate) state: CreateFlowState,
694    pub(crate) task: CreateFlowTask,
695    pub(crate) flow_id: Option<FlowId>,
696    pub(crate) peers: Vec<Peer>,
697    pub(crate) source_table_ids: Vec<TableId>,
698    #[serde(default)]
699    pub(crate) unresolved_source_table_names: Vec<TableName>,
700    /// Use alias for backward compatibility with QueryContext serialized data
701    #[serde(alias = "query_context")]
702    pub(crate) flow_context: FlowQueryContext,
703    /// For verify if prev value is consistent when need to update flow metadata.
704    /// only set when `or_replace` is true.
705    pub(crate) prev_flow_info_value: Option<DeserializedValueWithBytes<FlowInfoValue>>,
706    /// Only set to true when replace actually happened.
707    /// This is used to determine whether to invalidate the cache.
708    #[serde(default)]
709    pub(crate) did_replace: bool,
710    pub(crate) flow_type: Option<FlowType>,
711}
712
713impl CreateFlowData {
714    pub(crate) fn is_pending(&self) -> bool {
715        !self.unresolved_source_table_names.is_empty()
716    }
717
718    pub(crate) fn is_active(&self) -> bool {
719        !self.is_pending()
720    }
721}
722
723impl From<&CreateFlowData> for CreateRequest {
724    fn from(value: &CreateFlowData) -> Self {
725        let flow_id = value.flow_id.unwrap();
726        let source_table_ids = &value.source_table_ids;
727
728        let mut req = CreateRequest {
729            flow_id: Some(api::v1::FlowId { id: flow_id }),
730            source_table_ids: source_table_ids
731                .iter()
732                .map(|table_id| api::v1::TableId { id: *table_id })
733                .collect_vec(),
734            sink_table_name: Some(value.task.sink_table_name.clone().into()),
735            // Always be true to ensure idempotent in case of retry
736            create_if_not_exists: true,
737            or_replace: value.task.or_replace,
738            expire_after: value.task.expire_after.map(|value| ExpireAfter { value }),
739            eval_interval: value
740                .task
741                .eval_interval_secs
742                .map(|seconds| api::v1::EvalInterval { seconds }),
743            comment: value.task.comment.clone(),
744            sql: value.task.sql.clone(),
745            flow_options: user_runtime_flow_options(&value.task.flow_options),
746        };
747
748        let flow_type = value.flow_type.unwrap_or_default().to_string();
749        req.flow_options
750            .insert(FlowType::FLOW_TYPE_KEY.to_string(), flow_type);
751
752        // Pass typed schedule config via internal transient key in flow_options.
753        if let Some(ref sched) = value.task.eval_schedule {
754            let json = serde_json::to_string(sched)
755                .expect("FlowScheduleConfig serialization should not fail");
756            req.flow_options
757                .insert(INTERNAL_EVAL_SCHEDULE_KEY.to_string(), json);
758        }
759
760        req
761    }
762}
763
764impl From<&CreateFlowData> for (FlowInfoValue, Vec<(FlowPartitionId, FlowRouteValue)>) {
765    fn from(value: &CreateFlowData) -> Self {
766        let catalog_name = value.task.catalog_name.clone();
767        let flow_name = value.task.flow_name.clone();
768        let sink_table_name = value.task.sink_table_name.clone();
769        let expire_after = value.task.expire_after;
770        let eval_interval = value.task.eval_interval_secs;
771        let comment = value.task.comment.clone();
772        let sql = value.task.sql.clone();
773        let eval_schedule = value.task.eval_schedule.clone();
774
775        // Start with a clean options map. The transient schedule payload is
776        // only for the meta→flownode CreateRequest boundary and must not be
777        // persisted in FlowInfoValue.options.
778        let mut options: HashMap<String, String> = value
779            .task
780            .flow_options
781            .iter()
782            .filter(|(k, _)| k.as_str() != INTERNAL_EVAL_SCHEDULE_KEY)
783            .map(|(k, v)| (k.clone(), v.clone()))
784            .collect();
785
786        let flownode_ids = value
787            .peers
788            .iter()
789            .enumerate()
790            .map(|(idx, peer)| (idx as u32, peer.id))
791            .collect::<BTreeMap<_, _>>();
792        let flow_routes = value
793            .peers
794            .iter()
795            .enumerate()
796            .map(|(idx, peer)| (idx as u32, FlowRouteValue { peer: peer.clone() }))
797            .collect::<Vec<_>>();
798
799        let flow_type = value.flow_type.unwrap_or_default().to_string();
800        options.insert(FlowType::FLOW_TYPE_KEY.to_string(), flow_type);
801
802        let mut create_time = chrono::Utc::now();
803        if let Some(prev_flow_value) = value.prev_flow_info_value.as_ref()
804            && value.task.or_replace
805        {
806            create_time = prev_flow_value.get_inner_ref().created_time;
807        }
808
809        // This conversion borrows the procedure data: the procedure keeps using
810        // `self.data` after metadata creation (for cache invalidation, retry and
811        // procedure dump/restore), while `FlowInfoValue` owns the persisted
812        // metadata. Cloning these owned fields is therefore intentional.
813        let flow_info: FlowInfoValue = FlowInfoValue {
814            source_table_ids: value.source_table_ids.clone(),
815            all_source_table_names: value.task.source_table_names.clone(),
816            unresolved_source_table_names: value.unresolved_source_table_names.clone(),
817            sink_table_name: sink_table_name.clone(),
818            flownode_ids,
819            catalog_name: catalog_name.clone(),
820            query_context: Some(without_scheduled_time_extension(QueryContext::from(
821                value.flow_context.clone(),
822            ))),
823            flow_name: flow_name.clone(),
824            raw_sql: sql.clone(),
825            expire_after,
826            eval_interval_secs: eval_interval,
827            comment: comment.clone(),
828            options,
829            status: if value.is_active() {
830                FlowStatus::Active
831            } else {
832                FlowStatus::PendingSources
833            },
834            created_time: create_time,
835            updated_time: chrono::Utc::now(),
836            eval_schedule: eval_schedule.clone(),
837        };
838
839        (flow_info, flow_routes)
840    }
841}