Skip to main content

risingwave_frontend/optimizer/plan_node/
stream_table_scan.rs

1// Copyright 2022 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 std::collections::{BTreeMap, HashMap};
16use std::sync::Arc;
17
18use itertools::Itertools;
19use pretty_xmlish::{Pretty, XmlNode};
20use risingwave_common::catalog::Field;
21use risingwave_common::hash::VirtualNode;
22use risingwave_common::types::DataType;
23use risingwave_common::util::iter_util::ZipEqFast;
24use risingwave_common::util::scan_range::ScanRange;
25use risingwave_common::util::sort_util::OrderType;
26use risingwave_pb::stream_plan::stream_node::{PbNodeBody, PbStreamKind};
27use risingwave_pb::stream_plan::{PbStreamNode, StreamScanType};
28
29use super::stream::prelude::*;
30use super::utils::{Distill, childless_record};
31use super::{
32    BackfillType, ExprRewritable, PlanBase, PlanNodeId, StreamNode, StreamPlanRef as PlanRef,
33    generic,
34};
35use crate::TableCatalog;
36use crate::catalog::ColumnId;
37use crate::expr::{ExprRewriter, ExprVisitor, FunctionCall};
38use crate::optimizer::plan_node::expr_visitable::ExprVisitable;
39use crate::optimizer::plan_node::utils::{IndicesDisplay, TableCatalogBuilder};
40use crate::optimizer::property::{Distribution, DistributionDisplay, MonotonicityMap};
41use crate::scheduler::SchedulerResult;
42use crate::stream_fragmenter::BuildFragmentGraphState;
43
44/// `StreamTableScan` is a virtual plan node to represent a stream table scan. It will be converted
45/// to stream scan + merge node (for upstream materialize) + batch table scan when converting to `MView`
46/// creation request.
47#[derive(Debug, Clone, PartialEq, Eq, Hash)]
48pub struct StreamTableScan {
49    pub base: PlanBase<Stream>,
50    core: generic::TableScan,
51    batch_plan_id: PlanNodeId,
52    backfill_type: BackfillType,
53    pk_scan_range: Option<ScanRange>,
54}
55
56impl StreamTableScan {
57    pub const BACKFILL_FINISHED_COLUMN_NAME: &str = "backfill_finished";
58    pub const EPOCH_COLUMN_NAME: &str = "epoch";
59    pub const IS_EPOCH_FINISHED_COLUMN_NAME: &str = "is_epoch_finished";
60    pub const ROW_COUNT_COLUMN_NAME: &str = "row_count";
61    pub const VNODE_COLUMN_NAME: &str = "vnode";
62
63    pub fn new_with_backfill_type(core: generic::TableScan, backfill_type: BackfillType) -> Self {
64        Self::new_with_scan_range(core, backfill_type, None)
65    }
66
67    pub fn new_with_scan_range(
68        core: generic::TableScan,
69        backfill_type: BackfillType,
70        pk_scan_range: Option<ScanRange>,
71    ) -> Self {
72        let batch_plan_id = core.ctx.next_plan_node_id();
73
74        if core.cross_database() {
75            assert!(
76                !(backfill_type == BackfillType::Replicated || backfill_type.without_snapshot()),
77                "cross-database replicated or without-snapshot scan is not supported"
78            );
79        }
80
81        let distribution = {
82            match core.distribution_key() {
83                Some(distribution_key) => {
84                    if distribution_key.is_empty() {
85                        Distribution::Single
86                    } else {
87                        // See also `BatchSeqScan::clone_with_dist`.
88                        Distribution::UpstreamHashShard(distribution_key, core.table_catalog.id)
89                    }
90                }
91                None => Distribution::SomeShard,
92            }
93        };
94
95        let stream_kind = if core.append_only() {
96            StreamKind::AppendOnly
97        } else if backfill_type.without_snapshot() {
98            StreamKind::Upsert
99        } else {
100            StreamKind::Retract
101        };
102
103        let base = PlanBase::new_stream_with_core(
104            &core,
105            distribution,
106            stream_kind,
107            false,
108            core.watermark_columns(),
109            MonotonicityMap::new(),
110        );
111        Self {
112            base,
113            core,
114            batch_plan_id,
115            backfill_type,
116            pk_scan_range,
117        }
118    }
119
120    pub fn table_name(&self) -> &str {
121        self.core.table_name()
122    }
123
124    pub fn core(&self) -> &generic::TableScan {
125        &self.core
126    }
127
128    pub fn to_index_scan(
129        &self,
130        index_table_catalog: Arc<TableCatalog>,
131        primary_to_secondary_mapping: &BTreeMap<usize, usize>,
132        function_mapping: &HashMap<FunctionCall, usize>,
133        backfill_type: BackfillType,
134    ) -> StreamTableScan {
135        let logical_index_scan = self.core.to_index_scan(
136            index_table_catalog,
137            primary_to_secondary_mapping,
138            function_mapping,
139        );
140        logical_index_scan
141            .distribution_key()
142            .expect("distribution key of stream chain must exist in output columns");
143        StreamTableScan::new_with_backfill_type(logical_index_scan, backfill_type)
144    }
145
146    pub fn stream_scan_type(&self) -> StreamScanType {
147        self.backfill_type
148            .to_stream_scan_type(self.core.cross_database())
149    }
150
151    pub fn backfill_type(&self) -> BackfillType {
152        self.backfill_type
153    }
154
155    pub fn pk_scan_range(&self) -> Option<&ScanRange> {
156        self.pk_scan_range.as_ref()
157    }
158
159    // TODO: Add note to reviewer about safety, because of `generic::TableScan` limitation.
160    fn get_upstream_state_table(&self) -> &TableCatalog {
161        self.core.table_catalog.as_ref()
162    }
163
164    /// Build catalog for backfill state
165    ///
166    /// When `stream_scan_type` is not `StreamScanType::SnapshotBackfill`:
167    ///
168    /// Schema: | vnode | pk ... | `backfill_finished` | `row_count` |
169    ///
170    /// key:    | vnode |
171    /// value:  | pk ... | `backfill_finished` | `row_count` |
172    ///
173    /// When we update the backfill progress,
174    /// we update it for all vnodes.
175    ///
176    /// `pk` refers to the upstream pk which we use to track the backfill progress.
177    ///
178    /// `vnode` is the corresponding vnode of the upstream's distribution key.
179    ///         It should also match the vnode of the backfill executor.
180    ///
181    /// `backfill_finished` is a boolean which just indicates if backfill is done.
182    ///
183    /// `row_count` is a count of rows which indicates the # of rows per executor.
184    ///             We used to track this in memory.
185    ///             But for backfill persistence we have to also persist it.
186    ///
187    /// FIXME(kwannoel):
188    /// - Across all vnodes, the values are the same.
189    /// - e.g.
190    ///   | vnode | pk ...  | `backfill_finished` | `row_count` |
191    ///   | 1002 | Int64(1) | t                   | 10          |
192    ///   | 1003 | Int64(1) | t                   | 10          |
193    ///   | 1003 | Int64(1) | t                   | 10          |
194    ///
195    /// Eventually we should track progress per vnode, to support scaling with both mview and
196    /// the corresponding `no_shuffle_backfill`.
197    /// However this is not high priority, since we are working on supporting arrangement backfill,
198    /// which already has this capability.
199    ///
200    ///
201    /// When `stream_scan_type` is `StreamScanType::SnapshotBackfill`:
202    ///
203    /// Schema: | vnode | `epoch` | `row_count` | `is_epoch_finished` | pk ...
204    ///
205    /// key:    | vnode |
206    /// value:  | `epoch` | `row_count` | `is_epoch_finished` | pk ...
207    pub fn build_backfill_state_catalog(
208        &self,
209        state: &mut BuildFragmentGraphState,
210        stream_scan_type: StreamScanType,
211    ) -> TableCatalog {
212        let mut catalog_builder = TableCatalogBuilder::default();
213        let upstream_schema = &self.core.get_table_columns();
214
215        // We use vnode as primary key in state table.
216        // If `Distribution::Single`, vnode will just be `VirtualNode::default()`.
217        catalog_builder.add_column(&Field::with_name(
218            VirtualNode::RW_TYPE,
219            Self::VNODE_COLUMN_NAME,
220        ));
221        catalog_builder.add_order_column(0, OrderType::ascending());
222
223        #[expect(deprecated)]
224        match stream_scan_type {
225            StreamScanType::Chain
226            | StreamScanType::Rearrange
227            | StreamScanType::Backfill
228            | StreamScanType::UpstreamOnly
229            | StreamScanType::ArrangementBackfill => {
230                // pk columns
231                for col_order in self.core.primary_key() {
232                    let col = &upstream_schema[col_order.column_index];
233                    catalog_builder.add_column(&Field::from(&**col));
234                }
235
236                // `backfill_finished` column
237                catalog_builder.add_column(&Field::with_name(
238                    DataType::Boolean,
239                    Self::BACKFILL_FINISHED_COLUMN_NAME,
240                ));
241
242                // `row_count` column
243                catalog_builder.add_column(&Field::with_name(
244                    DataType::Int64,
245                    Self::ROW_COUNT_COLUMN_NAME,
246                ));
247            }
248            StreamScanType::SnapshotBackfill | StreamScanType::CrossDbSnapshotBackfill => {
249                // `epoch` column
250                catalog_builder
251                    .add_column(&Field::with_name(DataType::Int64, Self::EPOCH_COLUMN_NAME));
252
253                // `row_count` column
254                catalog_builder.add_column(&Field::with_name(
255                    DataType::Int64,
256                    Self::ROW_COUNT_COLUMN_NAME,
257                ));
258
259                // `is_finished` column
260                catalog_builder.add_column(&Field::with_name(
261                    DataType::Boolean,
262                    Self::IS_EPOCH_FINISHED_COLUMN_NAME,
263                ));
264
265                // pk columns
266                for col_order in self.core.primary_key() {
267                    let col = &upstream_schema[col_order.column_index];
268                    catalog_builder.add_column(&Field::from(&col.column_desc));
269                }
270            }
271            StreamScanType::Unspecified => unreachable!(),
272        }
273
274        // Reuse the state store pk (vnode) as the vnode as well.
275        catalog_builder.set_vnode_col_idx(0);
276        catalog_builder.set_dist_key_in_pk(vec![0]);
277
278        let num_of_columns = catalog_builder.columns().len();
279        catalog_builder.set_value_indices((1..num_of_columns).collect_vec());
280
281        catalog_builder
282            .build(vec![0], 1)
283            .with_id(state.gen_table_id_wrapped())
284    }
285}
286
287impl_plan_tree_node_for_leaf! { Stream, StreamTableScan }
288
289impl Distill for StreamTableScan {
290    fn distill<'a>(&self) -> XmlNode<'a> {
291        let verbose = self.base.ctx().is_explain_verbose();
292        let mut vec = Vec::with_capacity(4);
293        vec.push(("table", Pretty::from(self.core.table_name().to_owned())));
294        vec.push(("columns", self.core.columns_pretty(verbose)));
295        if let Some(scan_range) = &self.pk_scan_range {
296            let mut parts = Vec::new();
297            let pk_cols = self.core.primary_key();
298            // Display eq_conds
299            for (pk, datum) in pk_cols
300                .iter()
301                .take(scan_range.eq_conds.len())
302                .zip_eq_fast(scan_range.eq_conds.iter())
303            {
304                let field = &self.core.table_catalog.columns()[pk.column_index];
305                parts.push(format!("{} = {:?}", field.name(), datum));
306            }
307            // Display range bounds on the next column
308            let range_col_idx = scan_range.eq_conds.len();
309            if range_col_idx < pk_cols.len() {
310                use std::ops::Bound;
311                let field = &self.core.table_catalog.columns()[pk_cols[range_col_idx].column_index];
312                let fmt_bound_val = |v: &Vec<risingwave_common::types::Datum>| -> String {
313                    v.first().map_or("NULL".to_owned(), |d| format!("{:?}", d))
314                };
315                match &scan_range.range.0 {
316                    Bound::Included(v) => {
317                        parts.push(format!("{} >= {}", field.name(), fmt_bound_val(v)))
318                    }
319                    Bound::Excluded(v) => {
320                        parts.push(format!("{} > {}", field.name(), fmt_bound_val(v)))
321                    }
322                    Bound::Unbounded => {}
323                }
324                match &scan_range.range.1 {
325                    Bound::Included(v) => {
326                        parts.push(format!("{} <= {}", field.name(), fmt_bound_val(v)))
327                    }
328                    Bound::Excluded(v) => {
329                        parts.push(format!("{} < {}", field.name(), fmt_bound_val(v)))
330                    }
331                    Bound::Unbounded => {}
332                }
333            }
334            if !parts.is_empty() {
335                vec.push(("pk_scan_range", Pretty::from(parts.join(" AND "))));
336            }
337        }
338
339        if verbose {
340            vec.push(("stream_scan_type", Pretty::debug(&self.stream_scan_type())));
341            let stream_key = IndicesDisplay {
342                indices: self.stream_key().unwrap_or_default(),
343                schema: self.base.schema(),
344            };
345            vec.push(("stream_key", stream_key.distill()));
346            let pk = IndicesDisplay {
347                indices: &self
348                    .core
349                    .primary_key()
350                    .iter()
351                    .map(|x| x.column_index)
352                    .collect_vec(),
353                schema: &self.core.table_catalog.column_schema(),
354            };
355            vec.push(("pk", pk.distill()));
356            let dist = Pretty::display(&DistributionDisplay {
357                distribution: self.distribution(),
358                input_schema: self.base.schema(),
359            });
360            vec.push(("dist", dist));
361        }
362
363        childless_record("StreamTableScan", vec)
364    }
365}
366
367impl StreamNode for StreamTableScan {
368    fn to_stream_prost_body(&self, _state: &mut BuildFragmentGraphState) -> PbNodeBody {
369        unreachable!(
370            "stream scan cannot be converted into a prost body -- call `adhoc_to_stream_prost` instead."
371        )
372    }
373}
374
375impl StreamTableScan {
376    pub fn adhoc_to_stream_prost(
377        &self,
378        state: &mut BuildFragmentGraphState,
379    ) -> SchedulerResult<PbStreamNode> {
380        use risingwave_pb::stream_plan::*;
381
382        let stream_key = self
383            .stream_key()
384            .unwrap_or(&[])
385            .iter()
386            .map(|x| *x as u32)
387            .collect_vec();
388
389        let stream_scan_type = self.stream_scan_type();
390
391        // The required columns from the table (both scan and upstream).
392        #[expect(deprecated)]
393        let upstream_column_ids = match stream_scan_type {
394            // For backfill, we additionally need the primary key columns.
395            StreamScanType::Backfill
396            | StreamScanType::ArrangementBackfill
397            | StreamScanType::SnapshotBackfill
398            | StreamScanType::CrossDbSnapshotBackfill => self.core.output_and_pk_column_ids(),
399            StreamScanType::Chain | StreamScanType::Rearrange | StreamScanType::UpstreamOnly => {
400                self.core.output_column_ids()
401            }
402            StreamScanType::Unspecified => unreachable!(),
403        }
404        .iter()
405        .map(ColumnId::get_id)
406        .collect_vec();
407
408        // The schema of the snapshot read stream
409        let snapshot_schema = upstream_column_ids
410            .iter()
411            .map(|&id| {
412                let col = self
413                    .core
414                    .get_table_columns()
415                    .iter()
416                    .find(|c| c.column_id.get_id() == id)
417                    .unwrap();
418                Field::from(&col.column_desc).to_prost()
419            })
420            .collect_vec();
421
422        let upstream_schema = snapshot_schema.clone();
423
424        // TODO: snapshot read of upstream mview
425        let batch_plan_node = BatchPlanNode {
426            table_desc: Some(self.core.table_catalog.table_desc().try_to_protobuf()?),
427            column_ids: upstream_column_ids.clone(),
428        };
429
430        let catalog = self
431            .build_backfill_state_catalog(state, stream_scan_type)
432            .to_internal_table_prost();
433
434        // For backfill, we first read pk + output_indices from upstream.
435        // On this, we need to further project `output_indices` to the downstream.
436        // This `output_indices` refers to that.
437        let output_indices = self
438            .core
439            .output_column_ids()
440            .iter()
441            .map(|i| {
442                upstream_column_ids
443                    .iter()
444                    .position(|&x| x == i.get_id())
445                    .unwrap() as u32
446            })
447            .collect_vec();
448
449        let arrangement_table = if stream_scan_type == StreamScanType::ArrangementBackfill {
450            let upstream_table_catalog = self.get_upstream_state_table();
451            Some(upstream_table_catalog.to_internal_table_prost())
452        } else {
453            None
454        };
455
456        let input = if stream_scan_type == StreamScanType::CrossDbSnapshotBackfill {
457            vec![]
458        } else {
459            vec![
460                // Upstream updates
461                // The merge node body will be filled by the `ActorBuilder` on the meta service.
462                PbStreamNode {
463                    node_body: Some(PbNodeBody::Merge(Default::default())),
464                    identity: "Upstream".into(),
465                    fields: upstream_schema,
466                    stream_key: vec![], // not used
467                    ..Default::default()
468                },
469                // Snapshot read
470                PbStreamNode {
471                    node_body: Some(PbNodeBody::BatchPlan(Box::new(batch_plan_node))),
472                    operator_id: self.batch_plan_id.to_stream_node_operator_id(),
473                    identity: "BatchPlanNode".into(),
474                    fields: snapshot_schema,
475                    stream_key: vec![], // not used
476                    input: vec![],
477                    stream_kind: PbStreamKind::AppendOnly as i32,
478                },
479            ]
480        };
481
482        let node_body = PbNodeBody::StreamScan(Box::new(StreamScanNode {
483            table_id: self.core.table_catalog.id,
484            stream_scan_type: stream_scan_type as i32,
485            // The column indices need to be forwarded to the downstream
486            output_indices,
487            upstream_column_ids,
488            // The table desc used by backfill executor
489            table_desc: Some(self.core.table_catalog.table_desc().try_to_protobuf()?),
490            state_table: Some(catalog),
491            arrangement_table,
492            rate_limit: self.base.ctx().overwrite_options().backfill_rate_limit,
493            pk_scan_range: self.pk_scan_range.as_ref().map(|sr| sr.to_protobuf()),
494            ..Default::default()
495        }));
496
497        Ok(PbStreamNode {
498            fields: self.schema().to_prost(),
499            input,
500            node_body: Some(node_body),
501            stream_key,
502            operator_id: self.base.id().to_stream_node_operator_id(),
503            identity: self.distill_to_string(),
504            stream_kind: self.stream_kind().to_protobuf() as i32,
505        })
506    }
507}
508
509impl ExprRewritable<Stream> for StreamTableScan {
510    fn has_rewritable_expr(&self) -> bool {
511        true
512    }
513
514    fn rewrite_exprs(&self, r: &mut dyn ExprRewriter) -> PlanRef {
515        let mut core = self.core.clone();
516        core.rewrite_exprs(r);
517        Self::new_with_scan_range(core, self.backfill_type, self.pk_scan_range.clone()).into()
518    }
519}
520
521impl ExprVisitable for StreamTableScan {
522    fn visit_exprs(&self, v: &mut dyn ExprVisitor) {
523        self.core.visit_exprs(v);
524    }
525}