Skip to main content

risingwave_frontend/optimizer/
backfill_order_strategy.rs

1// Copyright 2025 RisingWave Labs
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
15use risingwave_pb::common::Uint32Vector;
16use risingwave_pb::id::RelationId;
17use risingwave_pb::stream_plan::BackfillOrder;
18use risingwave_sqlparser::ast::BackfillOrderStrategy;
19
20use crate::error::Result;
21use crate::optimizer::backfill_order_strategy::auto::plan_auto_strategy;
22use crate::optimizer::backfill_order_strategy::fixed::plan_fixed_strategy;
23use crate::optimizer::plan_node::StreamPlanRef;
24use crate::session::SessionImpl;
25
26pub mod auto {
27    use std::collections::{HashMap, HashSet};
28
29    use risingwave_pb::id::RelationId;
30
31    use crate::optimizer::backfill_order_strategy::common::has_cycle;
32    use crate::optimizer::plan_node::{StreamPlanNodeType, StreamPlanRef};
33    use crate::session::SessionImpl;
34
35    #[derive(Debug)]
36    pub(super) enum BackfillTreeNode {
37        Join {
38            lhs: Box<BackfillTreeNode>,
39            rhs: Box<BackfillTreeNode>,
40        },
41        Scan {
42            id: RelationId,
43        },
44        Union {
45            children: Vec<BackfillTreeNode>,
46        },
47        Ignored,
48    }
49
50    /// TODO: Handle stream share
51    fn plan_graph_to_backfill_tree(
52        session: &SessionImpl,
53        plan: StreamPlanRef,
54    ) -> Option<BackfillTreeNode> {
55        match plan.node_type() {
56            StreamPlanNodeType::StreamHashJoin => {
57                assert_eq!(plan.inputs().len(), 2);
58                let mut inputs = plan.inputs().into_iter();
59                let l = inputs.next().unwrap();
60                let r = inputs.next().unwrap();
61                Some(BackfillTreeNode::Join {
62                    lhs: Box::new(plan_graph_to_backfill_tree(session, l)?),
63                    rhs: Box::new(plan_graph_to_backfill_tree(session, r)?),
64                })
65            }
66            StreamPlanNodeType::StreamTableScan => {
67                let table_scan = plan.as_stream_table_scan().expect("table scan");
68                let relation_id = table_scan.core().table_catalog.id().as_relation_id();
69                Some(BackfillTreeNode::Scan { id: relation_id })
70            }
71            StreamPlanNodeType::StreamSourceScan => {
72                let source_scan = plan.as_stream_source_scan().expect("source scan");
73                let relation_id = source_scan.source_catalog().id.as_relation_id();
74                Some(BackfillTreeNode::Scan { id: relation_id })
75            }
76            StreamPlanNodeType::StreamUnion => {
77                let inputs = plan.inputs();
78                let mut children = Vec::with_capacity(inputs.len());
79                for child in inputs {
80                    let subtree = plan_graph_to_backfill_tree(session, child)?;
81                    if matches!(subtree, BackfillTreeNode::Ignored) {
82                        continue;
83                    }
84                    children.push(subtree);
85                }
86                Some(BackfillTreeNode::Union { children })
87            }
88            node_type => {
89                let inputs = plan.inputs();
90                match inputs.len() {
91                    0 => Some(BackfillTreeNode::Ignored),
92                    1 => {
93                        let mut inputs = inputs.into_iter();
94                        let child = inputs.next().unwrap();
95                        plan_graph_to_backfill_tree(session, child)
96                    }
97                    _ => {
98                        session.notice_to_user(format!(
99                            "Backfill order strategy is not supported for {:?}",
100                            node_type
101                        ));
102                        None
103                    }
104                }
105            }
106        }
107    }
108
109    /// For a given subtree, all the leaf nodes in the leftmost leaf-node node
110    /// must come _after_ all other leaf nodes in the subtree.
111    /// For example, for the following tree:
112    ///
113    /// ```text
114    ///       JOIN (A)
115    ///      /        \
116    ///     JOIN (B)   SCAN (C)
117    ///    /        \
118    ///   /         \
119    /// /           \
120    /// SCAN (D)    SCAN (E)
121    /// ```
122    ///
123    /// D is the leftmost leaf node.
124    /// {C, E} are the other leaf nodes.
125    ///
126    /// So the partial order is:
127    /// {C, E} -> {D}
128    /// Expanded:
129    /// C -> D
130    /// E -> D
131    ///
132    /// Next, we have to consider UNION as well.
133    /// If a UNION node is the leftmost child,
134    /// then for all subtrees in the UNION,
135    /// their leftmost leaf nodes must come after
136    /// all other leaf nodes in the subtree.
137    ///
138    /// ``` text
139    ///         JOIN (A)
140    ///        /        \
141    ///       JOIN (B)   SCAN (C)
142    ///       /       \
143    ///      /         \
144    ///     UNION (D)   SCAN (E)
145    ///    /        \
146    ///   /         \
147    /// SCAN (F)    JOIN (G)
148    ///            /        \
149    ///           /          \
150    ///          SCAN (H)   SCAN (I)
151    /// ```
152    ///
153    /// In this case, {F, H} are the leftmost leaf nodes.
154    /// {C, E} -> {F, H}
155    /// I -> H
156    /// Expanded:
157    /// C -> F
158    /// E -> F
159    /// C -> H
160    /// E -> H
161    /// I -> H
162    fn fold_backfill_tree_to_partial_order(
163        tree: BackfillTreeNode,
164    ) -> HashMap<RelationId, HashSet<RelationId>> {
165        let mut order: HashMap<RelationId, HashSet<RelationId>> = HashMap::new();
166
167        // Returns terminal nodes of the subtree
168        // This is recursive algorithm we use to traverse the tree and compute partial orders.
169        fn traverse_backfill_tree(
170            tree: BackfillTreeNode,
171            order: &mut HashMap<RelationId, HashSet<RelationId>>,
172            is_leftmost_child: bool,
173            mut prior_terminal_nodes: HashSet<RelationId>,
174        ) -> HashSet<RelationId> {
175            match tree {
176                BackfillTreeNode::Ignored => HashSet::new(),
177                BackfillTreeNode::Scan { id } => {
178                    if is_leftmost_child {
179                        for prior_terminal_node in prior_terminal_nodes {
180                            order.entry(prior_terminal_node).or_default().insert(id);
181                        }
182                    }
183                    HashSet::from([id])
184                }
185                BackfillTreeNode::Union { children } => {
186                    let mut terminal_nodes = HashSet::new();
187                    for child in children {
188                        let child_terminal_nodes = traverse_backfill_tree(
189                            child,
190                            order,
191                            is_leftmost_child,
192                            prior_terminal_nodes.clone(),
193                        );
194                        terminal_nodes.extend(child_terminal_nodes);
195                    }
196                    terminal_nodes
197                }
198                BackfillTreeNode::Join { lhs, rhs } => {
199                    let rhs_terminal_nodes =
200                        traverse_backfill_tree(*rhs, order, false, HashSet::new());
201                    prior_terminal_nodes.extend(rhs_terminal_nodes.iter().cloned());
202                    traverse_backfill_tree(*lhs, order, true, prior_terminal_nodes)
203                }
204            }
205        }
206
207        traverse_backfill_tree(tree, &mut order, false, HashSet::new());
208
209        order
210    }
211
212    pub(super) fn plan_auto_strategy(
213        session: &SessionImpl,
214        plan: StreamPlanRef,
215    ) -> HashMap<RelationId, HashSet<RelationId>> {
216        if let Some(tree) = plan_graph_to_backfill_tree(session, plan) {
217            let order = fold_backfill_tree_to_partial_order(tree);
218            if has_cycle(&order) {
219                tracing::warn!(?order, "Backfill order strategy has a cycle");
220                session.notice_to_user("Backfill order strategy has a cycle");
221                return Default::default();
222            }
223            return order;
224        }
225        Default::default()
226    }
227}
228
229mod fixed {
230    use std::collections::{HashMap, HashSet};
231
232    use risingwave_common::bail;
233    use risingwave_pb::id::RelationId;
234    use risingwave_sqlparser::ast::ObjectName;
235
236    use crate::error::Result;
237    use crate::optimizer::backfill_order_strategy::common::{
238        bind_backfill_relation_id_by_name, has_cycle,
239    };
240    use crate::optimizer::plan_node::{StreamPlanNodeType, StreamPlanRef};
241    use crate::session::SessionImpl;
242
243    /// Collect a mapping from bindable relation IDs to relation IDs actually scanned in the plan.
244    ///
245    /// Index selection may replace a scan on a base table with a scan on one of its index tables.
246    /// In that case, both the base table ID and the index table ID map to the actual index table
247    /// ID, so a user can still specify the base table in a fixed backfill order.
248    fn collect_scanned_relation_ids(
249        session: &SessionImpl,
250        plan: StreamPlanRef,
251    ) -> HashMap<RelationId, HashSet<RelationId>> {
252        let mut relation_ids: HashMap<RelationId, HashSet<RelationId>> = HashMap::new();
253
254        fn visit(
255            session: &SessionImpl,
256            plan: StreamPlanRef,
257            relation_ids: &mut HashMap<RelationId, HashSet<RelationId>>,
258        ) {
259            match plan.node_type() {
260                StreamPlanNodeType::StreamTableScan => {
261                    let table_scan = plan.as_stream_table_scan().expect("table scan");
262                    let table_catalog = &table_scan.core().table_catalog;
263                    let scanned_relation_id = table_catalog.id().as_relation_id();
264                    relation_ids
265                        .entry(scanned_relation_id)
266                        .or_default()
267                        .insert(scanned_relation_id);
268
269                    if table_catalog.is_index() {
270                        let reader = session.env().catalog_reader().read_guard();
271                        let schema_catalog = reader
272                            .get_schema_by_id(table_catalog.database_id, table_catalog.schema_id)
273                            .expect("schema of an index table should exist");
274                        let index_catalog = schema_catalog
275                            .iter_index()
276                            .find(|index| index.index_table().id == table_catalog.id)
277                            .expect("index catalog of an index table should exist");
278                        relation_ids
279                            .entry(index_catalog.primary_table.id().as_relation_id())
280                            .or_default()
281                            .insert(scanned_relation_id);
282                    }
283                }
284                StreamPlanNodeType::StreamSourceScan => {
285                    let source_scan = plan.as_stream_source_scan().expect("source scan");
286                    let relation_id = source_scan.source_catalog().id.as_relation_id();
287                    relation_ids
288                        .entry(relation_id)
289                        .or_default()
290                        .insert(relation_id);
291                }
292                _ => {}
293            }
294
295            // Recursively visit all inputs
296            for child in plan.inputs() {
297                visit(session, child, relation_ids);
298            }
299        }
300
301        visit(session, plan, &mut relation_ids);
302        relation_ids
303    }
304
305    pub(super) fn plan_fixed_strategy(
306        session: &SessionImpl,
307        orders: Vec<(ObjectName, ObjectName)>,
308        plan: StreamPlanRef,
309    ) -> Result<HashMap<RelationId, HashSet<RelationId>>> {
310        // Collect all scanned relation IDs from the plan.
311        let scanned_relation_ids = collect_scanned_relation_ids(session, plan);
312
313        let mut order: HashMap<RelationId, HashSet<RelationId>> = HashMap::new();
314        for (start_name, end_name) in orders {
315            let start_relation_id = bind_backfill_relation_id_by_name(session, start_name.clone())?;
316            let end_relation_id = bind_backfill_relation_id_by_name(session, end_name.clone())?;
317
318            // Validate that both relations are present in the query plan
319            let Some(start_scanned_relation_ids) = scanned_relation_ids.get(&start_relation_id)
320            else {
321                bail!(
322                    "Table or source '{}' specified in backfill_order is not used in the query",
323                    start_name
324                );
325            };
326            let Some(end_scanned_relation_ids) = scanned_relation_ids.get(&end_relation_id) else {
327                bail!(
328                    "Table or source '{}' specified in backfill_order is not used in the query",
329                    end_name
330                );
331            };
332
333            for start_scanned_relation_id in start_scanned_relation_ids {
334                order
335                    .entry(*start_scanned_relation_id)
336                    .or_default()
337                    .extend(end_scanned_relation_ids.iter().copied());
338            }
339        }
340        if has_cycle(&order) {
341            bail!("Backfill order strategy has a cycle");
342        }
343        Ok(order)
344    }
345}
346
347mod common {
348    use std::collections::{HashMap, HashSet};
349
350    use risingwave_pb::id::RelationId;
351    use risingwave_sqlparser::ast::ObjectName;
352
353    use crate::Binder;
354    use crate::catalog::CatalogError;
355    use crate::catalog::root_catalog::SchemaPath;
356    use crate::catalog::schema_catalog::SchemaCatalog;
357    use crate::error::Result;
358    use crate::session::SessionImpl;
359
360    /// Check if the backfill order has a cycle.
361    pub(super) fn has_cycle(order: &HashMap<RelationId, HashSet<RelationId>>) -> bool {
362        fn dfs(
363            node: RelationId,
364            order: &HashMap<RelationId, HashSet<RelationId>>,
365            visited: &mut HashSet<RelationId>,
366            stack: &mut HashSet<RelationId>,
367        ) -> bool {
368            if stack.contains(&node) {
369                return true; // Cycle detected
370            }
371
372            if visited.insert(node) {
373                stack.insert(node);
374                if let Some(downstreams) = order.get(&node) {
375                    for neighbor in downstreams {
376                        if dfs(*neighbor, order, visited, stack) {
377                            return true;
378                        }
379                    }
380                }
381                stack.remove(&node);
382            }
383            false
384        }
385
386        let mut visited = HashSet::new();
387        let mut stack = HashSet::new();
388        for &start in order.keys() {
389            if dfs(start, order, &mut visited, &mut stack) {
390                return true;
391            }
392        }
393
394        false
395    }
396
397    pub(super) fn bind_backfill_relation_id_by_name(
398        session: &SessionImpl,
399        name: ObjectName,
400    ) -> Result<RelationId> {
401        let (db_name, schema_name, rel_name) = Binder::resolve_db_schema_qualified_name(&name)?;
402        let db_name = db_name.unwrap_or(session.database());
403
404        let reader = session.env().catalog_reader().read_guard();
405
406        match schema_name {
407            Some(name) => {
408                let schema_catalog = reader.get_schema_by_name(&db_name, &name)?;
409                bind_table(schema_catalog, &rel_name)
410            }
411            None => {
412                let search_path = session.config().search_path();
413                let user_name = session.user_name();
414                let schema_path = SchemaPath::Path(&search_path, &user_name);
415                let result: crate::error::Result<Option<(RelationId, &str)>> = schema_path
416                    .try_find(|schema_name| {
417                        if let Ok(schema_catalog) = reader.get_schema_by_name(&db_name, schema_name)
418                            && let Ok(relation_id) = bind_table(schema_catalog, &rel_name)
419                        {
420                            Ok(Some(relation_id))
421                        } else {
422                            Ok(None)
423                        }
424                    });
425                if let Some((relation_id, _schema_name)) = result? {
426                    return Ok(relation_id);
427                }
428                Err(CatalogError::not_found("table", &rel_name).into())
429            }
430        }
431    }
432
433    fn bind_table(
434        schema_catalog: &SchemaCatalog,
435        name: &String,
436    ) -> crate::error::Result<RelationId> {
437        if let Some(table) = schema_catalog.get_created_table_by_name(name) {
438            Ok(table.id().as_relation_id())
439        } else if let Some(source) = schema_catalog.get_source_by_name(name) {
440            Ok(source.id.as_relation_id())
441        } else {
442            Err(CatalogError::not_found("table or source", name).into())
443        }
444    }
445}
446
447pub mod display {
448    use risingwave_pb::stream_plan::BackfillOrder;
449
450    use crate::session::SessionImpl;
451
452    fn get_table_name(session: &SessionImpl, id: u32) -> crate::error::Result<String> {
453        let catalog_reader = session.env().catalog_reader().read_guard();
454        let table_catalog = catalog_reader.get_any_table_by_id(id.into())?;
455        let table_name = table_catalog.name();
456        let db_id = table_catalog.database_id;
457        let schema_id = table_catalog.schema_id;
458        let schema_catalog = catalog_reader.get_schema_by_id(db_id, schema_id)?;
459        let schema_name = schema_catalog.name();
460        let name = format!("{}.{}", schema_name, table_name);
461        Ok(name)
462    }
463
464    pub(super) fn print_backfill_order_in_dot_format(
465        session: &SessionImpl,
466        order: BackfillOrder,
467    ) -> crate::error::Result<String> {
468        let mut result = String::new();
469        result.push_str("digraph G {\n");
470        // NOTE(kwannoel): This is a hack to make the edge ordering deterministic.
471        // so our planner tests are deterministic.
472        let mut edges = vec![];
473        for (start, end) in order.order {
474            let start_name = get_table_name(session, start.as_raw_id())?;
475            for end in end.data {
476                let end_name = get_table_name(session, end)?;
477                edges.push(format!("  \"{}\" -> \"{}\";\n", start_name, end_name));
478            }
479        }
480        edges.sort();
481        for edge in edges {
482            result.push_str(&edge);
483        }
484        result.push_str("}\n");
485        Ok(result)
486    }
487}
488
489/// We only bind tables and materialized views.
490/// We need to bind sources and indices in the future as well.
491/// For auto backfill strategy,
492/// if a cycle forms due to the same relation being scanned twice in the derived order,
493/// we won't generate any backfill order strategy.
494/// For fixed backfill strategy,
495/// for scans on the same relation id, even though they may be in different fragments,
496/// they will all share the same backfill order.
497pub fn plan_backfill_order(
498    session: &SessionImpl,
499    backfill_order_strategy: BackfillOrderStrategy,
500    plan: StreamPlanRef,
501) -> Result<BackfillOrder> {
502    let order = match backfill_order_strategy {
503        BackfillOrderStrategy::Default | BackfillOrderStrategy::None => Default::default(),
504        BackfillOrderStrategy::Auto => plan_auto_strategy(session, plan),
505        BackfillOrderStrategy::Fixed(orders) => plan_fixed_strategy(session, orders, plan)?,
506    };
507    Ok(BackfillOrder {
508        order: order
509            .into_iter()
510            .map(|(relation_id, dependencies)| {
511                (
512                    relation_id,
513                    Uint32Vector {
514                        data: dependencies
515                            .into_iter()
516                            .map(RelationId::as_raw_id)
517                            .collect(),
518                    },
519                )
520            })
521            .collect(),
522    })
523}
524
525/// Plan the backfill order, and also output the backfill tree.
526pub fn explain_backfill_order_in_dot_format(
527    session: &SessionImpl,
528    backfill_order_strategy: BackfillOrderStrategy,
529    plan: StreamPlanRef,
530) -> Result<String> {
531    let order = plan_backfill_order(session, backfill_order_strategy, plan)?;
532    let dot_formatted_backfill_order = display::print_backfill_order_in_dot_format(session, order)?;
533    Ok(dot_formatted_backfill_order)
534}