Skip to main content

flow/
adapter.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//! Flow source schema management and stateless streaming execution.
16
17use std::collections::BTreeMap;
18use std::sync::Arc;
19#[cfg(test)]
20use std::sync::atomic::AtomicUsize;
21use std::sync::atomic::{AtomicBool, Ordering};
22
23use common_base::memory_limit::MemoryLimit;
24use common_config::Configurable;
25use common_error::ext::BoxedError;
26use common_meta::distributed_time_constants::BASE_HEARTBEAT_INTERVAL;
27use common_meta::key::TableMetadataManagerRef;
28use common_options::memory::MemoryOptions;
29use common_recordbatch::map_dictionary_to_values_data_type;
30use common_stat::get_total_cpu_cores;
31use common_telemetry::logging::{LoggingOptions, TracingOptions};
32use common_telemetry::{error, info};
33use datafusion_common::TableReference;
34use datafusion_common::tree_node::TreeNode;
35use datafusion_expr::logical_plan::Distinct;
36use datafusion_expr::{Expr, LogicalPlan};
37use datatypes::schema::{ColumnSchema, SchemaRef};
38use itertools::Itertools;
39use meta_client::MetaClientOptions;
40use query::QueryEngine;
41use query::options::QueryOptions;
42use serde::{Deserialize, Serialize};
43use servers::grpc::GrpcOptions;
44use servers::http::HttpOptions;
45use snafu::{IntoError, OptionExt, ResultExt, ensure};
46use store_api::storage::{ConcreteDataType, RegionId};
47use tokio::sync::RwLock;
48use tokio::time::Instant;
49
50use crate::adapter::stateless::StatelessFlow;
51use crate::adapter::table_source::ManagedTableSource;
52use crate::adapter::util::{
53    relation_desc_to_column_schemas_with_fallback, table_info_value_to_relation_desc,
54};
55use crate::batching_mode::BatchingModeOptions;
56use crate::batching_mode::frontend_client::FrontendClient;
57use crate::batching_mode::utils::sql_to_df_plan;
58use crate::error::{
59    DatafusionSnafu, Error, ExternalSnafu, FlowNotFoundSnafu, InsertIntoFlowSnafu, InternalSnafu,
60    InvalidQuerySnafu, UnexpectedSnafu,
61};
62use crate::repr::{ColumnType, DiffRow, RelationDesc, Row};
63use crate::{CreateFlowArgs, FlowId, TableName};
64
65pub(crate) mod flownode_impl;
66pub(crate) mod stateless;
67pub(crate) mod table_source;
68#[cfg(test)]
69mod tests;
70pub(crate) mod util;
71
72/// Converts a retained plan's output to logical schemas. Only a direct source
73/// column (optionally wrapped in an alias) carries source semantics; computed
74/// expressions deliberately use the physical output field metadata instead.
75fn output_column_schemas(
76    plan: &LogicalPlan,
77    source_schema: &SchemaRef,
78) -> Result<(Vec<ColumnSchema>, Vec<Option<usize>>), Error> {
79    // Plain DISTINCT preserves the selected columns and their source semantics. Inspect its
80    // input for lineage, while leaving computed expressions without source metadata.
81    let expressions = match plan {
82        LogicalPlan::Projection(projection) => Some(&projection.expr),
83        LogicalPlan::Distinct(Distinct::All(input)) => match input.as_ref() {
84            LogicalPlan::Projection(projection) => Some(&projection.expr),
85            _ => None,
86        },
87        _ => None,
88    };
89    let is_pass_through = matches!(plan, LogicalPlan::TableScan(_) | LogicalPlan::Filter(_))
90        || matches!(plan, LogicalPlan::Distinct(Distinct::All(input)) if matches!(input.as_ref(), LogicalPlan::TableScan(_) | LogicalPlan::Filter(_)));
91    let mut lineage = Vec::new();
92    let columns = plan
93        .schema()
94        .fields()
95        .iter()
96        .enumerate()
97        .map(|(idx, field)| {
98            let mut column = ColumnSchema::try_from(field.as_ref())
99                .map_err(BoxedError::new)
100                .context(ExternalSnafu)?;
101            let source_index = expressions
102                .and_then(|exprs| {
103                    let expr = exprs.get(idx)?;
104                    let expr = match expr {
105                        Expr::Column(column) => Some(column),
106                        Expr::Alias(alias) => match alias.expr.as_ref() {
107                            Expr::Column(column) => Some(column),
108                            _ => None,
109                        },
110                        _ => None,
111                    }?;
112                    source_schema.column_index_by_name(&expr.name)
113                })
114                .or_else(|| {
115                    is_pass_through
116                        .then(|| source_schema.column_index_by_name(field.name()))
117                        .flatten()
118                });
119            if let Some(source_index) = source_index {
120                let mut source = source_schema.column_schemas()[source_index].clone();
121                source.name = field.name().clone();
122                source.data_type = map_dictionary_to_values_data_type(&source.data_type);
123                column = source;
124            } else {
125                column.data_type = map_dictionary_to_values_data_type(&column.data_type);
126                // DataFusion may propagate field metadata through a computed expression.  Such
127                // an expression has no source-column lineage and must not inherit its PK/time
128                // semantics into the sink relation.
129                if expressions.is_some() {
130                    column = column.with_time_index(false);
131                }
132            }
133            lineage.push(source_index);
134            Ok(column)
135        })
136        .collect::<Result<Vec<_>, Error>>()?;
137    Ok((columns, lineage))
138}
139
140fn relation_desc_from_output(
141    columns: &[ColumnSchema],
142    lineage: &[Option<usize>],
143    source_primary_key_indices: &[usize],
144    plan: &LogicalPlan,
145) -> RelationDesc {
146    let mut keys = source_primary_key_indices
147        .iter()
148        .filter_map(|source_index| {
149            lineage
150                .iter()
151                .position(|index| index == &Some(*source_index))
152        })
153        .collect_vec();
154    // DISTINCT defines identity from every output tuple; the source timestamp participates
155    // separately as the relation time index, so it is excluded from the tag key.
156    if matches!(plan, LogicalPlan::Distinct(Distinct::All(_))) {
157        keys = columns
158            .iter()
159            .positions(|column| !column.is_time_index())
160            .collect();
161    }
162    let time_index = columns.iter().position(ColumnSchema::is_time_index);
163    RelationDesc {
164        typ: crate::repr::RelationType {
165            column_types: columns
166                .iter()
167                .map(|column| ColumnType::new(column.data_type.clone(), column.is_nullable()))
168                .collect(),
169            keys: if keys.is_empty() {
170                vec![]
171            } else {
172                vec![crate::repr::Key::from(keys)]
173            },
174            time_index,
175            auto_columns: vec![],
176        },
177        names: columns
178            .iter()
179            .map(|column| Some(column.name.clone()))
180            .collect(),
181    }
182}
183
184fn default_num_workers() -> usize {
185    get_total_cpu_cores().div_ceil(2)
186}
187
188/// Returns whether an existing sink uses the legacy explicit source-time-index layout.
189///
190/// This check is deliberately separate from [`resolve_sink_layout`]: ordinary auto-column
191/// resolution remains the first choice, and this compatibility path is only for a sink whose
192/// trailing column is a real, user-defined time index.
193pub(crate) fn is_explicit_source_timestamp_compatibility(
194    output_schema: &[ColumnSchema],
195    output_lineage: &[Option<usize>],
196    sink_schema: &[ColumnSchema],
197    source_schema: &SchemaRef,
198) -> bool {
199    if sink_schema.len() != output_schema.len() + 1 || output_lineage.len() != output_schema.len() {
200        return false;
201    }
202    if !output_schema
203        .iter()
204        .zip(&sink_schema[..output_schema.len()])
205        .all(|(output, sink)| output.data_type == sink.data_type)
206    {
207        return false;
208    }
209
210    let Some(source_timestamp_index) = source_schema.timestamp_index() else {
211        return false;
212    };
213    let sink_timestamp = &sink_schema[output_schema.len()];
214    sink_timestamp.data_type == source_schema.column_schemas()[source_timestamp_index].data_type
215        && sink_timestamp.data_type.is_timestamp()
216        && sink_timestamp.is_time_index()
217        && sink_timestamp.default_constraint().is_some()
218        && sink_timestamp.name != AUTO_CREATED_UPDATE_AT_TS_COL
219        && sink_timestamp.name != AUTO_CREATED_PLACEHOLDER_TS_COL
220        && !output_lineage.contains(&Some(source_timestamp_index))
221}
222
223pub const AUTO_CREATED_PLACEHOLDER_TS_COL: &str = "__ts_placeholder";
224pub const AUTO_CREATED_UPDATE_AT_TS_COL: &str = "update_at";
225
226/// Resolves the columns appended by the flow, validating the complete output/sink layout.
227///
228/// A sink with the same arity as the query is an ordinary sink, even when its last
229/// column happens to be named `update_at`. Auto columns are only inferred from the
230/// arity difference and their exact trailing layout.
231pub(crate) fn resolve_sink_layout(
232    output_schema: &[ColumnSchema],
233    sink_schema: &[ColumnSchema],
234) -> Result<Vec<ColumnSchema>, Error> {
235    ensure!(
236        sink_schema.len() >= output_schema.len() && sink_schema.len() - output_schema.len() <= 2,
237        InvalidQuerySnafu {
238            reason: format!(
239                "Flow output has {} columns, but sink has {} columns; only zero, one, or two trailing auto columns are supported",
240                output_schema.len(),
241                sink_schema.len()
242            )
243        }
244    );
245    let suffix_len = sink_schema.len() - output_schema.len();
246    for (idx, (output, sink)) in output_schema.iter().zip(sink_schema.iter()).enumerate() {
247        ensure!(
248            output.data_type == sink.data_type,
249            InvalidQuerySnafu {
250                reason: format!(
251                    "Flow output column {idx} has type {:?}, but sink column {} has type {:?}",
252                    output.data_type, sink.name, sink.data_type
253                )
254            }
255        );
256    }
257    let suffix = &sink_schema[output_schema.len()..];
258    match suffix_len {
259        0 => Ok(vec![]),
260        1 => {
261            let column = &suffix[0];
262            ensure!(
263                column.name == AUTO_CREATED_UPDATE_AT_TS_COL && column.data_type.is_timestamp(),
264                InvalidQuerySnafu {
265                    reason: format!(
266                        "The trailing sink column must be timestamp {}",
267                        AUTO_CREATED_UPDATE_AT_TS_COL
268                    )
269                }
270            );
271            Ok(suffix.to_vec())
272        }
273        2 => {
274            let update_at = &suffix[0];
275            let placeholder = &suffix[1];
276            ensure!(
277                update_at.name == AUTO_CREATED_UPDATE_AT_TS_COL
278                    && update_at.data_type.is_timestamp()
279                    && placeholder.name == AUTO_CREATED_PLACEHOLDER_TS_COL
280                    && placeholder.data_type.is_timestamp()
281                    && placeholder.is_time_index(),
282                InvalidQuerySnafu {
283                    reason: "The two trailing sink columns must be timestamp update_at followed by timestamp time-index __ts_placeholder".to_string()
284                }
285            );
286            Ok(suffix.to_vec())
287        }
288        _ => unreachable!(),
289    }
290}
291
292/// Legacy helper for callers that only have a sink schema. New stateless flows
293/// resolve the suffix against both schemas and store it in `StatelessFlow`.
294pub(crate) fn sink_output_column_count(sink_schema: &[ColumnSchema]) -> Result<usize, Error> {
295    let mut count = sink_schema.len();
296    if sink_schema
297        .last()
298        .is_some_and(|column| column.name == AUTO_CREATED_PLACEHOLDER_TS_COL)
299    {
300        let placeholder = &sink_schema[count - 1];
301        ensure!(
302            placeholder.data_type.is_timestamp() && placeholder.is_time_index(),
303            InvalidQuerySnafu {
304                reason: format!(
305                    "Auto-created sink column {} must be a timestamp time index",
306                    AUTO_CREATED_PLACEHOLDER_TS_COL
307                )
308            }
309        );
310        count -= 1;
311    }
312    if sink_schema
313        .get(count.saturating_sub(1))
314        .is_some_and(|column| column.name == AUTO_CREATED_UPDATE_AT_TS_COL)
315    {
316        ensure!(
317            sink_schema[count - 1].data_type.is_timestamp(),
318            InvalidQuerySnafu {
319                reason: format!(
320                    "Auto-created sink column {} must be a timestamp",
321                    AUTO_CREATED_UPDATE_AT_TS_COL
322                )
323            }
324        );
325        count -= 1;
326    }
327    Ok(count)
328}
329
330pub(crate) fn validate_sink_layout(
331    output_schema: &[ColumnSchema],
332    sink_schema: &[ColumnSchema],
333) -> Result<(), Error> {
334    resolve_sink_layout(output_schema, sink_schema).map(|_| ())
335}
336
337pub(crate) fn validate_auto_column_names(output_schema: &[ColumnSchema]) -> Result<(), Error> {
338    for column in output_schema {
339        ensure!(
340            column.name != AUTO_CREATED_UPDATE_AT_TS_COL
341                && column.name != AUTO_CREATED_PLACEHOLDER_TS_COL,
342            InvalidQuerySnafu {
343                reason: format!(
344                    "Flow output column {} is reserved for an auto-created sink column",
345                    column.name
346                )
347            }
348        );
349    }
350    Ok(())
351}
352
353pub(crate) fn validate_sink_layout_with_suffix(
354    output_schema: &[ColumnSchema],
355    sink_schema: &[ColumnSchema],
356    suffix: &[ColumnSchema],
357) -> Result<(), Error> {
358    ensure!(
359        sink_schema.len() == output_schema.len() + suffix.len()
360            && sink_schema[output_schema.len()..] == *suffix,
361        InvalidQuerySnafu {
362            reason: "Stored sink auto-column layout no longer matches the sink schema".to_string()
363        }
364    );
365    for (idx, (output, sink)) in output_schema
366        .iter()
367        .zip(&sink_schema[..output_schema.len()])
368        .enumerate()
369    {
370        ensure!(
371            output.data_type == sink.data_type,
372            InvalidQuerySnafu {
373                reason: format!(
374                    "Flow output column {idx} has type {:?}, but sink column {} has type {:?}",
375                    output.data_type, sink.name, sink.data_type
376                )
377            }
378        );
379    }
380    Ok(())
381}
382
383#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
384#[serde(default)]
385pub struct FlowConfig {
386    /// Deprecated and ignored. Flow workers have been removed.
387    #[deprecated(note = "flow workers have been removed; this field is ignored")]
388    #[serde(default = "default_num_workers")]
389    pub num_workers: usize,
390    pub batching_mode: BatchingModeOptions,
391}
392
393#[allow(deprecated)]
394impl Default for FlowConfig {
395    fn default() -> Self {
396        Self {
397            num_workers: get_total_cpu_cores().div_ceil(2),
398            batching_mode: BatchingModeOptions::default(),
399        }
400    }
401}
402
403#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
404#[serde(default)]
405pub struct FlownodeOptions {
406    pub node_id: Option<u64>,
407    pub flow: FlowConfig,
408    pub grpc: GrpcOptions,
409    pub http: HttpOptions,
410    pub meta_client: Option<MetaClientOptions>,
411    pub logging: LoggingOptions,
412    pub tracing: TracingOptions,
413    pub query: QueryOptions,
414    pub memory: MemoryOptions,
415}
416
417impl Default for FlownodeOptions {
418    fn default() -> Self {
419        Self {
420            node_id: None,
421            flow: FlowConfig::default(),
422            grpc: GrpcOptions::default().with_bind_addr("127.0.0.1:3004"),
423            http: HttpOptions::default(),
424            meta_client: None,
425            logging: LoggingOptions::default(),
426            tracing: TracingOptions::default(),
427            query: QueryOptions {
428                parallelism: 1,
429                allow_query_fallback: false,
430                memory_pool_size: MemoryLimit::default(),
431                enable_per_region_metrics: false,
432                ..Default::default()
433            },
434            memory: MemoryOptions::default(),
435        }
436    }
437}
438
439impl Configurable for FlownodeOptions {
440    fn validate_sanitize(&mut self) -> common_config::error::Result<()> {
441        Ok(())
442    }
443}
444
445pub type FlowStreamingEngineRef = Arc<StreamingEngine>;
446
447#[derive(Default)]
448struct StatelessFlowRuntime {
449    flow: Option<Arc<StatelessFlow>>,
450    failed_rebuild: Option<(u32, Instant)>,
451}
452
453struct StatelessFlowSlot {
454    runtime: Arc<RwLock<StatelessFlowRuntime>>,
455    /// Cleared at registry-detach time, fencing creators that already captured this slot.
456    active: std::sync::atomic::AtomicBool,
457    #[cfg(test)]
458    rebuild_attempts: AtomicUsize,
459}
460
461fn validate_captured_slot(
462    slot: &StatelessFlowSlot,
463    current_source_table_id: Option<table::metadata::TableId>,
464    expected_table_id: table::metadata::TableId,
465    flow_id: FlowId,
466) -> Result<(), Error> {
467    ensure!(
468        slot.active.load(Ordering::Acquire),
469        FlowNotFoundSnafu { id: flow_id }
470    );
471    let current_source_table_id =
472        current_source_table_id.context(FlowNotFoundSnafu { id: flow_id })?;
473    ensure!(
474        current_source_table_id == expected_table_id,
475        InvalidQuerySnafu {
476            reason: format!("Flow {flow_id} source table changed while it was selected")
477        }
478    );
479    Ok(())
480}
481
482pub struct StreamingEngine {
483    pub query_engine: Arc<dyn QueryEngine>,
484    pub frontend_client: Arc<FrontendClient>,
485    table_info_source: ManagedTableSource,
486    stateless_flows: RwLock<BTreeMap<FlowId, Arc<StatelessFlowSlot>>>,
487    pub node_id: Option<u32>,
488}
489
490impl StreamingEngine {
491    pub fn new(
492        node_id: Option<u32>,
493        query_engine: Arc<dyn QueryEngine>,
494        table_meta: TableMetadataManagerRef,
495        frontend_client: Arc<FrontendClient>,
496    ) -> Self {
497        let table_info_source = ManagedTableSource::new(
498            table_meta.table_info_manager().clone(),
499            table_meta.table_name_manager().clone(),
500        );
501        Self {
502            query_engine,
503            frontend_client,
504            table_info_source,
505            stateless_flows: Default::default(),
506            node_id,
507        }
508    }
509
510    pub async fn handle_write_request(
511        &self,
512        region_id: RegionId,
513        rows: Vec<DiffRow>,
514        batch_datatypes: &[ConcreteDataType],
515        source_schema_version: u32,
516    ) -> Result<(), Error> {
517        let table_id = region_id.table_id();
518        let flow_ids = self.flow_ids_for_table(table_id).await;
519        let mut failed_flow_ids = Vec::new();
520        let mut first_error = None;
521        for (flow_id, slot) in flow_ids {
522            let result = self
523                .execute_flow(
524                    flow_id,
525                    slot,
526                    table_id,
527                    rows.clone(),
528                    batch_datatypes,
529                    source_schema_version,
530                )
531                .await;
532            if let Err(err) = result {
533                error!(err; "Failed to insert into flow={}, region_id={}", flow_id, region_id);
534                failed_flow_ids.push(flow_id);
535                if first_error.is_none() {
536                    first_error = Some(BoxedError::new(err));
537                }
538            }
539        }
540        match first_error {
541            Some(source) => Err(InsertIntoFlowSnafu {
542                region_id: u64::from(region_id),
543                flow_ids: failed_flow_ids,
544            }
545            .into_error(source)),
546            None => Ok(()),
547        }
548    }
549
550    async fn flow_ids_for_table(
551        &self,
552        table_id: table::metadata::TableId,
553    ) -> Vec<(FlowId, Arc<StatelessFlowSlot>)> {
554        let slots = self
555            .stateless_flows
556            .read()
557            .await
558            .iter()
559            .map(|(id, slot)| (*id, Arc::clone(slot)))
560            .collect::<Vec<_>>();
561        let mut flows = Vec::new();
562        for (id, slot) in slots {
563            if slot
564                .runtime
565                .read()
566                .await
567                .flow
568                .as_ref()
569                .is_some_and(|flow| flow.source_table_id == table_id)
570            {
571                flows.push((id, slot));
572            }
573        }
574        flows
575    }
576
577    async fn execute_flow(
578        &self,
579        flow_id: FlowId,
580        slot: Arc<StatelessFlowSlot>,
581        expected_table_id: table::metadata::TableId,
582        rows: Vec<DiffRow>,
583        batch_datatypes: &[ConcreteDataType],
584        source_schema_version: u32,
585    ) -> Result<usize, Error> {
586        // Keep the captured lifecycle slot rather than resolving the ID again after a drop.
587        let mut guard = slot.runtime.clone().read_owned().await;
588        let current = guard
589            .flow
590            .as_ref()
591            .context(FlowNotFoundSnafu { id: flow_id })?;
592        validate_captured_slot(
593            &slot,
594            Some(current.source_table_id),
595            expected_table_id,
596            flow_id,
597        )?;
598        if current.source_schema_version != source_schema_version {
599            if guard
600                .failed_rebuild
601                .as_ref()
602                .is_some_and(|(version, until)| {
603                    *version == source_schema_version && *until > Instant::now()
604                })
605            {
606                return InvalidQuerySnafu {
607                    reason: format!(
608                        "Flow {flow_id} schema rebuild for source version {source_schema_version} is cooling down"
609                    ),
610                }
611                .fail();
612            }
613            drop(guard);
614            // Serialize rebuilds with definition replacement using the existing publication
615            // lease. Another write may already have rebuilt this schema while we waited.
616            let mut published = slot.runtime.clone().write_owned().await;
617            let current = published
618                .flow
619                .as_ref()
620                .context(FlowNotFoundSnafu { id: flow_id })?;
621            validate_captured_slot(
622                &slot,
623                Some(current.source_table_id),
624                expected_table_id,
625                flow_id,
626            )?;
627            if current.source_schema_version != source_schema_version {
628                if published
629                    .failed_rebuild
630                    .as_ref()
631                    .is_some_and(|(version, until)| {
632                        *version == source_schema_version && *until > Instant::now()
633                    })
634                {
635                    return InvalidQuerySnafu {
636                        reason: format!(
637                            "Flow {flow_id} schema rebuild for source version {source_schema_version} is cooling down"
638                        ),
639                    }
640                    .fail();
641                }
642                #[cfg(test)]
643                slot.rebuild_attempts.fetch_add(1, Ordering::Relaxed);
644                let replacement = self
645                    .build_stateless_flow(&current.create_args, false)
646                    .await
647                    .and_then(|replacement| {
648                        ensure!(
649                            replacement.source_table_id == expected_table_id
650                                && replacement.source_schema_version == source_schema_version,
651                            InvalidQuerySnafu {
652                                reason: format!(
653                                    "Source schema changed while rebuilding flow {flow_id}"
654                                )
655                            }
656                        );
657                        Ok(replacement)
658                    });
659                match replacement {
660                    Ok(replacement) => {
661                        published.flow = Some(Arc::new(replacement));
662                        published.failed_rebuild = None;
663                    }
664                    Err(error) => {
665                        // Reuse the default heartbeat retry baseline; this is neither a
666                        // negotiated interval nor a cache-freshness guarantee.
667                        published.failed_rebuild = Some((
668                            source_schema_version,
669                            Instant::now() + BASE_HEARTBEAT_INTERVAL,
670                        ));
671                        return Err(error);
672                    }
673                }
674            }
675            // Do not open a replacement/drop gap between preparation and sink execution.
676            guard = tokio::sync::OwnedRwLockWriteGuard::downgrade(published);
677        }
678        let flow = guard
679            .flow
680            .as_ref()
681            .context(FlowNotFoundSnafu { id: flow_id })?;
682        // This is the last check before planning.  In particular, a request normalized against
683        // an old source schema is never allowed to reach a newly published plan.
684        let latest = self
685            .table_info_source
686            .get_table_info_value(&flow.source_table_id)
687            .await?
688            .context(UnexpectedSnafu {
689                reason: "Source table metadata is missing",
690            })?
691            .table_info
692            .meta
693            .schema
694            .version();
695        ensure!(
696            source_schema_version == latest && flow.source_schema_version == latest,
697            InvalidQuerySnafu {
698                reason: format!("Source schema version changed before flow {flow_id} execution")
699            }
700        );
701        stateless::execute(
702            flow,
703            &rows,
704            batch_datatypes,
705            &self.query_engine,
706            &self.frontend_client,
707            latest,
708        )
709        .await
710    }
711
712    pub async fn remove_flow_inner(&self, flow_id: FlowId) -> Result<(), Error> {
713        // Detach first: this is the tombstone/linearization point. A concurrent create must
714        // install a different slot and can never republish into the removed one.
715        let slot = self
716            .stateless_flows
717            .write()
718            .await
719            .remove(&flow_id)
720            .context(FlowNotFoundSnafu { id: flow_id })?;
721        slot.active.store(false, Ordering::Release);
722        let mut runtime = slot.runtime.write().await;
723        runtime.flow.take();
724        runtime.failed_rebuild = None;
725        Ok(())
726    }
727
728    async fn publish_initial_flow(
729        &self,
730        slot: Arc<StatelessFlowSlot>,
731        flow: Arc<StatelessFlow>,
732        create_if_not_exists: bool,
733        or_replace: bool,
734    ) -> Result<bool, Error> {
735        let mut runtime = slot.runtime.write().await;
736        ensure!(
737            slot.active.load(Ordering::Acquire),
738            FlowNotFoundSnafu {
739                id: flow.create_args.flow_id
740            }
741        );
742        if runtime.flow.is_some() && !or_replace {
743            if create_if_not_exists {
744                return Ok(false);
745            }
746            return crate::error::FlowAlreadyExistSnafu {
747                id: flow.create_args.flow_id,
748            }
749            .fail();
750        }
751        let latest = self
752            .table_info_source
753            .get_table_info_value(&flow.source_table_id)
754            .await?
755            .context(UnexpectedSnafu {
756                reason: "Source table metadata is missing",
757            })?
758            .table_info
759            .meta
760            .schema
761            .version();
762        ensure!(
763            latest == flow.source_schema_version,
764            InvalidQuerySnafu {
765                reason: format!(
766                    "Source schema changed while building flow: built version {}, current version {latest}",
767                    flow.source_schema_version
768                )
769            }
770        );
771        runtime.flow = Some(flow);
772        runtime.failed_rebuild = None;
773        Ok(true)
774    }
775
776    pub async fn create_flow_inner(&self, args: CreateFlowArgs) -> Result<Option<FlowId>, Error> {
777        let flow_id = args.flow_id;
778        let flow = Arc::new(self.build_stateless_flow(&args, true).await?);
779        let slot = {
780            let mut slots = self.stateless_flows.write().await;
781            if let Some(slot) = slots.get(&flow_id) {
782                Arc::clone(slot)
783            } else {
784                let slot = Arc::new(StatelessFlowSlot {
785                    runtime: Arc::new(RwLock::new(StatelessFlowRuntime::default())),
786                    active: AtomicBool::new(true),
787                    #[cfg(test)]
788                    rebuild_attempts: AtomicUsize::new(0),
789                });
790                slots.insert(flow_id, Arc::clone(&slot));
791                slot
792            }
793        };
794        let published = self
795            .publish_initial_flow(
796                slot.clone(),
797                flow,
798                args.create_if_not_exists,
799                args.or_replace,
800            )
801            .await?;
802        ensure!(
803            self.stateless_flows
804                .read()
805                .await
806                .get(&flow_id)
807                .is_some_and(|current| Arc::ptr_eq(current, &slot)),
808            FlowNotFoundSnafu { id: flow_id }
809        );
810        if published {
811            info!("Successfully create flow with id={flow_id}");
812            Ok(Some(flow_id))
813        } else {
814            Ok(None)
815        }
816    }
817
818    async fn build_stateless_flow(
819        &self,
820        args: &CreateFlowArgs,
821        create_sink: bool,
822    ) -> Result<StatelessFlow, Error> {
823        let CreateFlowArgs {
824            flow_id,
825            sink_table_name,
826            source_table_ids,
827            sql,
828            query_ctx,
829            ..
830        } = args;
831        ensure!(
832            source_table_ids.len() == 1,
833            InvalidQuerySnafu {
834                reason: "Stateless streaming flow does not support multiple source tables",
835            }
836        );
837
838        let query_ctx = query_ctx.clone().map(Arc::new).context(UnexpectedSnafu {
839            reason: "Query context is missing",
840        })?;
841        let source_table_id = source_table_ids[0];
842        let source_table_name = self
843            .table_info_source
844            .get_table_name(&source_table_id)
845            .await?;
846        let source_table_info = self
847            .table_info_source
848            .get_table_info_value(&source_table_id)
849            .await?
850            .context(UnexpectedSnafu {
851                reason: "Source table metadata is missing",
852            })?;
853        let flow_plan =
854            sql_to_df_plan(query_ctx.clone(), self.query_engine.clone(), sql, true).await?;
855        stateless::validate_plan(&flow_plan)?;
856        let source_meta = source_table_info.table_info.meta;
857        let source_schema = source_meta.schema;
858        let source_primary_key_indices = source_meta.primary_key_indices;
859        stateless::validate_source_scan(&flow_plan, source_table_id, &source_schema)?;
860        let (inferred_schema, lineage) = output_column_schemas(&flow_plan, &source_schema)?;
861        let inferred_relation = relation_desc_from_output(
862            &inferred_schema,
863            &lineage,
864            &source_primary_key_indices,
865            &flow_plan,
866        );
867        let sink_exists = self.fetch_table_pk_schema(sink_table_name).await?.is_some();
868        if !sink_exists {
869            validate_auto_column_names(&inferred_schema)?;
870        }
871        if !sink_exists
872            && create_sink
873            && !self
874                .create_table_from_relation(
875                    &format!("flow-id={flow_id}"),
876                    sink_table_name,
877                    &inferred_relation,
878                )
879                .await?
880        {
881            return UnexpectedSnafu {
882                reason: format!("Failed to create table {sink_table_name:?}"),
883            }
884            .fail();
885        }
886        ensure!(
887            sink_exists || create_sink,
888            UnexpectedSnafu {
889                reason: format!("Sink table metadata is missing: {sink_table_name:?}"),
890            }
891        );
892
893        // Fetch the metadata after validation or auto-creation. The sink's layout is the insert
894        // contract: flow output aliases must not leak into the request schema.
895        let (sink_primary_keys, _, sink_schema) = self
896            .fetch_table_pk_schema(sink_table_name)
897            .await?
898            .context(UnexpectedSnafu {
899                reason: format!("Sink table metadata is missing: {sink_table_name:?}"),
900            })?;
901        let (auto_columns, plan) = match resolve_sink_layout(&inferred_schema, &sink_schema) {
902            Ok(auto_columns) => (auto_columns, flow_plan),
903            Err(normal_error) => {
904                // Appending a hidden source timestamp changes DISTINCT's key.  It is therefore
905                // never a valid compatibility rewrite for a DISTINCT flow.
906                let has_distinct = {
907                    let mut found = false;
908                    flow_plan
909                        .apply(|node| {
910                            if matches!(node, LogicalPlan::Distinct(Distinct::All(_))) {
911                                found = true;
912                            }
913                            Ok(datafusion_common::tree_node::TreeNodeRecursion::Continue)
914                        })
915                        .context(DatafusionSnafu {
916                            context: "Failed to inspect flow plan",
917                        })?;
918                    found
919                };
920                if has_distinct
921                    || !sink_exists
922                    || !is_explicit_source_timestamp_compatibility(
923                        &inferred_schema,
924                        &lineage,
925                        &sink_schema,
926                        &source_schema,
927                    )
928                {
929                    return Err(normal_error);
930                }
931                let source_timestamp_index = source_schema.timestamp_index().unwrap();
932                let source_timestamp_name =
933                    &source_schema.column_schemas()[source_timestamp_index].name;
934                let plan = stateless::rewrite_source_timestamp(
935                    flow_plan,
936                    &TableReference::full(
937                        source_table_name[0].clone(),
938                        source_table_name[1].clone(),
939                        source_table_name[2].clone(),
940                    ),
941                    source_timestamp_name,
942                )?;
943                let (effective_output, _) = output_column_schemas(&plan, &source_schema)?;
944                ensure!(
945                    effective_output.len() == sink_schema.len()
946                        && effective_output
947                            .iter()
948                            .zip(&sink_schema)
949                            .all(|(output, sink)| output.data_type == sink.data_type),
950                    InvalidQuerySnafu {
951                        reason: "Compatibility plan output does not match the full sink schema"
952                    }
953                );
954                (vec![], plan)
955            }
956        };
957
958        Ok(StatelessFlow {
959            source_table_id,
960            source_table_name,
961            source_schema_version: source_schema.version(),
962            source_schema,
963            sink_table_name: sink_table_name.clone(),
964            sink_schema,
965            sink_primary_keys,
966            auto_columns,
967            plan,
968            query_ctx,
969            create_args: args.clone(),
970        })
971    }
972
973    pub async fn flush_flow_inner(&self, _flow_id: FlowId) -> Result<usize, Error> {
974        Ok(0)
975    }
976
977    pub(crate) async fn stateless_flow_ids(&self) -> Vec<FlowId> {
978        let slots = self.stateless_flows.read().await;
979        let mut ids = Vec::new();
980        for (id, slot) in slots.iter() {
981            if slot.runtime.read().await.flow.is_some() {
982                ids.push(*id);
983            }
984        }
985        ids
986    }
987
988    pub async fn flow_exist_inner(&self, flow_id: FlowId) -> Result<bool, Error> {
989        let slot = self.stateless_flows.read().await.get(&flow_id).cloned();
990        Ok(match slot {
991            Some(slot) => slot.runtime.read().await.flow.is_some(),
992            None => false,
993        })
994    }
995
996    async fn fetch_table_pk_schema(
997        &self,
998        table_name: &TableName,
999    ) -> Result<Option<(Vec<String>, Option<usize>, Vec<ColumnSchema>)>, Error> {
1000        if let Some(table_id) = self
1001            .table_info_source
1002            .get_opt_table_id_from_name(table_name)
1003            .await?
1004        {
1005            let table_info = self
1006                .table_info_source
1007                .get_table_info_value(&table_id)
1008                .await?
1009                .unwrap();
1010            let meta = table_info.table_info.meta;
1011            let schema = meta.schema.column_schemas().to_vec();
1012            let primary_keys = meta
1013                .primary_key_indices
1014                .into_iter()
1015                .map(|i| schema[i].name.clone())
1016                .collect_vec();
1017            Ok(Some((primary_keys, meta.schema.timestamp_index(), schema)))
1018        } else {
1019            Ok(None)
1020        }
1021    }
1022
1023    async fn adjust_auto_created_table_schema(
1024        &self,
1025        schema: &RelationDesc,
1026    ) -> Result<(Vec<String>, Vec<ColumnSchema>, bool), Error> {
1027        let primary_keys = schema
1028            .typ()
1029            .keys
1030            .first()
1031            .map(|key| {
1032                key.column_indices
1033                    .iter()
1034                    .map(|i| {
1035                        schema
1036                            .get_name(*i)
1037                            .clone()
1038                            .unwrap_or_else(|| format!("col_{i}"))
1039                    })
1040                    .collect_vec()
1041            })
1042            .unwrap_or_default();
1043        let mut columns = relation_desc_to_column_schemas_with_fallback(schema);
1044        columns.push(ColumnSchema::new(
1045            AUTO_CREATED_UPDATE_AT_TS_COL,
1046            ConcreteDataType::timestamp_millisecond_datatype(),
1047            true,
1048        ));
1049        let no_time_index = schema.typ().time_index.is_none();
1050        if no_time_index {
1051            columns.push(
1052                ColumnSchema::new(
1053                    AUTO_CREATED_PLACEHOLDER_TS_COL,
1054                    ConcreteDataType::timestamp_millisecond_datatype(),
1055                    true,
1056                )
1057                .with_time_index(true),
1058            );
1059        }
1060        Ok((primary_keys, columns, no_time_index))
1061    }
1062}
1063
1064impl StreamingEngine {
1065    async fn handle_inserts_inner(
1066        &self,
1067        request: api::v1::region::InsertRequests,
1068    ) -> Result<(), Error> {
1069        // A mirrored envelope can contain several regions of one source.  Normalize each request,
1070        // then concatenate by source so DISTINCT is evaluated once over the complete envelope.
1071        let mut grouped: BTreeMap<_, (_, Vec<DiffRow>, Vec<ConcreteDataType>, u32)> =
1072            BTreeMap::new();
1073        let mut poisoned_tables = std::collections::HashSet::new();
1074        let mut first_error = None;
1075        for write_request in request.requests {
1076            let region_id = RegionId::from(write_request.region_id);
1077            let table_id = region_id.table_id();
1078            if poisoned_tables.contains(&table_id) {
1079                continue;
1080            }
1081            match self.handle_insert_request(write_request).await {
1082                Ok((rows, types, version)) => {
1083                    let entry = grouped
1084                        .entry(table_id)
1085                        .or_insert_with(|| (region_id, vec![], types.clone(), version));
1086                    if entry.2 != types || entry.3 != version {
1087                        poisoned_tables.insert(table_id);
1088                        grouped.remove(&table_id);
1089                        let err = InvalidQuerySnafu { reason: format!("Source table {table_id} metadata changed within one insert envelope") }.build();
1090                        let ids: Vec<FlowId> = self
1091                            .flow_ids_for_table(table_id)
1092                            .await
1093                            .into_iter()
1094                            .map(|(id, _)| id)
1095                            .collect();
1096                        let err = InsertIntoFlowSnafu {
1097                            region_id: u64::from(region_id),
1098                            flow_ids: ids,
1099                        }
1100                        .into_error(BoxedError::new(err));
1101                        error!(err; "Failed to normalize flow insert request for region_id={region_id}");
1102                        if first_error.is_none() {
1103                            first_error = Some(err);
1104                        }
1105                    } else {
1106                        entry.1.extend(rows);
1107                    }
1108                }
1109                Err(err) => {
1110                    let ids: Vec<FlowId> = self
1111                        .flow_ids_for_table(table_id)
1112                        .await
1113                        .into_iter()
1114                        .map(|(id, _)| id)
1115                        .collect();
1116                    poisoned_tables.insert(table_id);
1117                    grouped.remove(&table_id);
1118                    let err = InsertIntoFlowSnafu {
1119                        region_id: u64::from(region_id),
1120                        flow_ids: ids,
1121                    }
1122                    .into_error(BoxedError::new(err));
1123                    error!(err; "Failed to normalize flow insert request for region_id={region_id}");
1124                    if first_error.is_none() {
1125                        first_error = Some(err);
1126                    }
1127                }
1128            }
1129        }
1130        for (_, (region_id, rows, types, version)) in grouped {
1131            if let Err(err) = self
1132                .handle_write_request(region_id, rows, &types, version)
1133                .await
1134                && first_error.is_none()
1135            {
1136                first_error = Some(err);
1137            }
1138        }
1139        match first_error {
1140            Some(err) => Err(err),
1141            None => Ok(()),
1142        }
1143    }
1144
1145    async fn handle_insert_request(
1146        &self,
1147        write_request: api::v1::region::InsertRequest,
1148    ) -> Result<(Vec<DiffRow>, Vec<ConcreteDataType>, u32), Error> {
1149        let region_id = write_request.region_id;
1150        let table_id = RegionId::from(region_id).table_id();
1151        let (insert_schema, rows_proto) = write_request
1152            .rows
1153            .map(|r| (r.schema, r.rows))
1154            .unwrap_or_default();
1155        let now = common_time::util::current_time_millis();
1156        let (table_types, fetch_order, source_schema_version) = {
1157            // Fetch the source metadata once for both current-schema normalization and
1158            // retained-plan validation. In particular, do not execute a retained plan
1159            // against rows normalized with a newer schema.
1160            let table_info = self
1161                .table_info_source
1162                .get_table_info_value(&table_id)
1163                .await?
1164                .context(UnexpectedSnafu {
1165                    reason: format!("Table metadata is missing for table id {table_id}"),
1166                })?;
1167            let source_schema_version = table_info.table_info.meta.schema.version();
1168            let table_schema = table_info_value_to_relation_desc(table_info)?;
1169            let defaults = table_schema
1170                .default_values
1171                .iter()
1172                .zip(table_schema.relation_desc.typ().column_types.iter())
1173                .map(|(value, ty)| {
1174                    value.as_ref().and_then(|value| {
1175                        value.create_default(ty.scalar_type(), ty.nullable()).ok()
1176                    })
1177                })
1178                .collect_vec();
1179            let types = table_schema
1180                .relation_desc
1181                .typ()
1182                .column_types
1183                .iter()
1184                .map(|ty| ty.scalar_type.clone())
1185                .collect_vec();
1186            let names = table_schema
1187                .relation_desc
1188                .names
1189                .iter()
1190                .enumerate()
1191                .map(|(idx, name)| {
1192                    name.clone().context(InternalSnafu {
1193                        reason: format!("Column {idx} of table {table_id} has no name"),
1194                    })
1195                })
1196                .collect::<Result<Vec<_>, _>>()?;
1197            let input_columns = insert_schema
1198                .iter()
1199                .enumerate()
1200                .map(|(idx, column)| (&column.column_name, idx))
1201                .collect::<std::collections::HashMap<_, _>>();
1202            let order = names
1203                .iter()
1204                .zip(defaults)
1205                .map(|(name, default)| {
1206                    input_columns
1207                        .get(name)
1208                        .copied()
1209                        .map(FetchFromRow::Idx)
1210                        .or_else(|| default.map(FetchFromRow::Default))
1211                        .with_context(|| UnexpectedSnafu {
1212                            reason: format!("Column not found: {name}"),
1213                        })
1214                })
1215                .collect::<Result<Vec<_>, _>>()?;
1216            (types, order, source_schema_version)
1217        };
1218        let rows = rows_proto
1219            .into_iter()
1220            .map(|row| {
1221                let row = Row::from(row);
1222                let row = fetch_order
1223                    .iter()
1224                    .map(|item| item.fetch(&row))
1225                    .collect_vec();
1226                (Row::new(row), now, 1)
1227            })
1228            .collect_vec();
1229        Ok((rows, table_types, source_schema_version))
1230    }
1231}
1232
1233#[derive(Debug, Clone)]
1234enum FetchFromRow {
1235    Idx(usize),
1236    Default(datatypes::value::Value),
1237}
1238
1239impl FetchFromRow {
1240    fn fetch(&self, row: &Row) -> datatypes::value::Value {
1241        match self {
1242            Self::Idx(idx) => row
1243                .get(*idx)
1244                .cloned()
1245                .unwrap_or(datatypes::value::Value::Null),
1246            Self::Default(value) => value.clone(),
1247        }
1248    }
1249}