1mod 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
55pub 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 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 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 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 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 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 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(
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 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 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 async fn on_create_metadata(&mut self) -> Result<Status> {
300 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 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_replace {
337 let old_flow_info = self.data.prev_flow_info_value.as_ref().unwrap();
338
339 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
465pub const DEFER_ON_MISSING_SOURCE_KEY: &str = "defer_on_missing_source";
467
468pub 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 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
539fn 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
557pub 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
586pub(crate) fn resolve_schedule_defaults_into_task(
599 task: &mut CreateFlowTask,
600 prev_flow_info: Option<&FlowInfoValue>,
601) {
602 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 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 let start_secs =
631 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#[derive(Debug, Clone, Serialize, Deserialize, AsRefStr, PartialEq)]
651pub enum CreateFlowState {
652 Prepare,
654 CreateFlows,
656 InvalidateFlowCache,
658 CreateMetadata,
660}
661
662#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
664pub enum FlowType {
665 #[default]
667 Batching,
668 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#[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 #[serde(alias = "query_context")]
702 pub(crate) flow_context: FlowQueryContext,
703 pub(crate) prev_flow_info_value: Option<DeserializedValueWithBytes<FlowInfoValue>>,
706 #[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 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 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 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 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}