Skip to main content

risingwave_frontend/optimizer/plan_node/
logical_changelog.rs

1// Copyright 2024 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 itertools::Itertools;
16
17use super::expr_visitable::ExprVisitable;
18use super::generic::{_CHANGELOG_ROW_ID, CHANGELOG_OP, GenericPlanRef};
19use super::utils::impl_distill_by_unit;
20use super::{
21    BatchPlanRef, ColPrunable, ColumnPruningContext, ExprRewritable, Logical,
22    LogicalPlanRef as PlanRef, LogicalProject, PlanBase, PlanTreeNodeUnary, PredicatePushdown,
23    RewriteStreamContext, StreamChangeLog, StreamPlanRef, ToBatch, ToStream, ToStreamContext,
24    gen_filter_and_pushdown, generic,
25};
26use crate::error::ErrorCode::BindError;
27use crate::error::Result;
28use crate::optimizer::plan_node::generic::PhysicalPlanRef;
29use crate::optimizer::property::{Distribution, RequiredDist};
30use crate::utils::{ColIndexMapping, Condition};
31
32#[derive(Debug, Clone, PartialEq, Eq, Hash)]
33pub struct LogicalChangeLog {
34    pub base: PlanBase<Logical>,
35    core: generic::ChangeLog<PlanRef>,
36}
37
38impl LogicalChangeLog {
39    pub fn create(
40        input: PlanRef,
41        key_indices: Option<Vec<usize>>,
42        distribution_key_indices: Option<Vec<usize>>,
43    ) -> PlanRef {
44        Self::new(input, key_indices, distribution_key_indices, true, true).into()
45    }
46
47    pub fn new(
48        input: PlanRef,
49        key_indices: Option<Vec<usize>>,
50        distribution_key_indices: Option<Vec<usize>>,
51        need_op: bool,
52        need_changelog_row_id: bool,
53    ) -> Self {
54        let core = generic::ChangeLog::new(
55            input,
56            key_indices,
57            distribution_key_indices,
58            need_op,
59            need_changelog_row_id,
60        );
61        Self::with_core(core)
62    }
63
64    pub fn with_core(core: generic::ChangeLog<PlanRef>) -> Self {
65        let base = PlanBase::new_logical_with_core(&core);
66        LogicalChangeLog { base, core }
67    }
68}
69
70impl PlanTreeNodeUnary<Logical> for LogicalChangeLog {
71    fn input(&self) -> PlanRef {
72        self.core.input.clone()
73    }
74
75    fn clone_with_input(&self, input: PlanRef) -> Self {
76        let core = self.core.clone_with_input(input);
77
78        Self::with_core(core)
79    }
80
81    fn rewrite_with_input(
82        &self,
83        input: PlanRef,
84        input_col_change: ColIndexMapping,
85    ) -> (Self, ColIndexMapping) {
86        let key_indices = self
87            .core
88            .key_indices
89            .as_ref()
90            .map(|key| input_col_change.map_all(key));
91        let distribution_key_indices = self
92            .core
93            .distribution_key_indices
94            .as_ref()
95            .map(|key| input_col_change.map_all(key));
96        let changelog = Self::new(
97            input,
98            key_indices,
99            distribution_key_indices,
100            self.core.need_op,
101            true,
102        );
103
104        let out_col_change = if self.core.need_op {
105            let (mut output_vec, len) = input_col_change.into_parts();
106            output_vec.push(Some(len));
107            ColIndexMapping::new(output_vec, len + 1)
108        } else {
109            input_col_change
110        };
111
112        let (mut output_vec, len) = out_col_change.into_parts();
113        let out_col_change = if self.core.need_changelog_row_id {
114            output_vec.push(Some(len));
115            ColIndexMapping::new(output_vec, len + 1)
116        } else {
117            ColIndexMapping::new(output_vec, len + 1)
118        };
119
120        (changelog, out_col_change)
121    }
122}
123
124impl_plan_tree_node_for_unary! { Logical, LogicalChangeLog}
125impl_distill_by_unit!(LogicalChangeLog, core, "LogicalChangeLog");
126
127impl ExprRewritable<Logical> for LogicalChangeLog {}
128
129impl ExprVisitable for LogicalChangeLog {}
130
131impl PredicatePushdown for LogicalChangeLog {
132    fn predicate_pushdown(
133        &self,
134        predicate: Condition,
135        ctx: &mut super::PredicatePushdownContext,
136    ) -> PlanRef {
137        gen_filter_and_pushdown(self, predicate, Condition::true_cond(), ctx)
138    }
139}
140
141impl ColPrunable for LogicalChangeLog {
142    fn prune_col(&self, required_cols: &[usize], ctx: &mut ColumnPruningContext) -> PlanRef {
143        let fields = self.schema().fields();
144        let mut need_op = false;
145        let mut need_changelog_row_id = false;
146        // columns required as input from children,
147        // generated columns like op and event_id are removed
148        let mut input_required_cols = required_cols
149            .iter()
150            .filter_map(|a| {
151                if let Some(f) = fields.get(*a) {
152                    if f.name == CHANGELOG_OP {
153                        need_op = true;
154                        None
155                    } else if f.name == _CHANGELOG_ROW_ID {
156                        need_changelog_row_id = true;
157                        None
158                    } else {
159                        Some(*a)
160                    }
161                } else {
162                    Some(*a)
163                }
164            })
165            .collect_vec();
166
167        // `StreamChangeLog` must see the input stream key to co-locate changes for the same row.
168        // Keep key columns internally even when they are not selected from the changelog CTE, and
169        // project them away above the operator afterwards.
170        if let Some(stream_key) = self.input().stream_key() {
171            for &key in stream_key {
172                if !input_required_cols.contains(&key) {
173                    input_required_cols.push(key);
174                }
175            }
176        }
177
178        // add declared business keys to input request
179        if let Some(key) = &self.core.key_indices {
180            for &index in key {
181                if !input_required_cols.contains(&index) {
182                    input_required_cols.push(index);
183                }
184            }
185        }
186
187        // Distribution columns must survive pruning even when the query does not select them.
188        if let Some(key) = &self.core.distribution_key_indices {
189            for &index in key {
190                if !input_required_cols.contains(&index) {
191                    input_required_cols.push(index);
192                }
193            }
194        }
195
196        let new_input = self.input().prune_col(&input_required_cols, ctx);
197        let input_mapping = ColIndexMapping::with_remaining_columns(
198            &input_required_cols,
199            self.input().schema().len(),
200        );
201        let key_indices = self
202            .core
203            .key_indices
204            .as_ref()
205            .map(|key| input_mapping.map_all(key));
206
207        let distribution_key_indices = self
208            .core
209            .distribution_key_indices
210            .as_ref()
211            .map(|key| input_mapping.map_all(key));
212
213        // updated plan with new inputs
214        let changelog: PlanRef = Self::new(
215            new_input,
216            key_indices,
217            distribution_key_indices,
218            need_op,
219            need_changelog_row_id,
220        )
221        .into();
222
223        // output now contains business keys as well, we need to hide it if not requested by the parent
224        let (mut output_mapping, new_output_len) = input_mapping.into_parts();
225
226        if self.core.need_op {
227            output_mapping.push(need_op.then_some(new_output_len));
228        }
229
230        if self.core.need_changelog_row_id {
231            output_mapping
232                .push(need_changelog_row_id.then_some(new_output_len + usize::from(need_op)));
233        }
234
235        let output_len = changelog.schema().len();
236        let output_mapping = ColIndexMapping::new(output_mapping, output_len);
237        let output_required_cols = output_mapping.map_all(required_cols);
238
239        if output_required_cols.iter().copied().eq(0..output_len) {
240            changelog
241        } else {
242            let output_mapping =
243                ColIndexMapping::with_remaining_columns(&output_required_cols, output_len);
244
245            LogicalProject::with_mapping(changelog, output_mapping).into()
246        }
247    }
248}
249
250impl ToBatch for LogicalChangeLog {
251    fn to_batch(&self) -> Result<BatchPlanRef> {
252        Err(BindError("With changelog cte only support with create mv/sink".to_owned()).into())
253    }
254}
255
256impl ToStream for LogicalChangeLog {
257    fn to_stream(&self, ctx: &mut ToStreamContext) -> Result<StreamPlanRef> {
258        let input = self.input().to_stream(ctx)?;
259
260        let input = if let Some(key) = &self.core.distribution_key_indices {
261            RequiredDist::hash_shard(key).streaming_enforce_if_not_satisfies(input)?
262        } else if let Some(key) = &self.core.key_indices {
263            RequiredDist::hash_shard(key).streaming_enforce_if_not_satisfies(input)?
264        } else if matches!(input.distribution(), Distribution::SomeShard) {
265            RequiredDist::hash_shard(input.expect_stream_key())
266                .streaming_enforce_if_not_satisfies(input)?
267        } else {
268            input
269        };
270
271        let row_id_index = self.schema().fields().len() - 1;
272        // The changelog row id encodes the vnode of the input row, so the output of a
273        // hash-distributed input is hash-distributed by the row id. A singleton input stays
274        // in its singleton fragment.
275        let (distribution_keys, dist) = match input.distribution() {
276            Distribution::HashShard(distribution_keys)
277            | Distribution::UpstreamHashShard(distribution_keys, _) => (
278                distribution_keys.clone(),
279                Distribution::HashShard(vec![row_id_index]),
280            ),
281            Distribution::Single => (vec![], Distribution::Single),
282            dist => {
283                return Err(BindError(format!(
284                    "ChangeLog requires input to be hash distributed, single, but got {:?}",
285                    dist
286                ))
287                .into());
288            }
289        };
290        let core = self.core.clone_with_input(input);
291        let plan = StreamChangeLog::new_with_dist(
292            core,
293            dist,
294            distribution_keys.into_iter().map(|k| k as u32).collect(),
295        )
296        .into();
297
298        Ok(plan)
299    }
300
301    fn logical_rewrite_for_stream(
302        &self,
303        ctx: &mut RewriteStreamContext,
304    ) -> Result<(PlanRef, ColIndexMapping)> {
305        let (input, input_col_change) = self.input().logical_rewrite_for_stream(ctx)?;
306        let (changelog, out_col_change) = self.rewrite_with_input(input, input_col_change);
307        Ok((changelog.into(), out_col_change))
308    }
309}
310
311#[cfg(test)]
312mod tests {
313    use std::rc::Rc;
314
315    use risingwave_common::catalog::{CdcTableDesc, ColumnDesc, ColumnId, TableId};
316    use risingwave_common::id::SourceId;
317    use risingwave_common::types::DataType;
318    use risingwave_common::util::sort_util::{ColumnOrder, OrderType};
319    use risingwave_connector::source::cdc::CdcScanOptions;
320
321    use super::*;
322    use crate::optimizer::optimizer_context::OptimizerContext;
323    use crate::optimizer::plan_node::{BackfillType, LogicalCdcScan, PlanTreeNodeUnary};
324
325    #[test]
326    fn test_changelog_on_cdc_scan_enforces_hash_distribution() {
327        let desc = CdcTableDesc {
328            table_id: TableId::new(1),
329            source_id: SourceId::new(2),
330            external_table_name: "mydb.orders".to_owned(),
331            pk: vec![ColumnOrder::new(0, OrderType::ascending())],
332            pk_comparisons: vec![risingwave_common::catalog::CdcKeyComparison::Native],
333            columns: vec![
334                ColumnDesc::named("id", ColumnId::new(1), DataType::Int32),
335                ColumnDesc::named("payload", ColumnId::new(2), DataType::Varchar),
336            ],
337            stream_key: vec![0],
338            ..Default::default()
339        };
340        let scan = LogicalCdcScan::create(
341            "orders".to_owned(),
342            Rc::new(desc),
343            OptimizerContext::mock(),
344            CdcScanOptions {
345                disable_backfill: true,
346                ..Default::default()
347            },
348        );
349        let changelog = LogicalChangeLog::create(scan.clone().into(), None, None);
350        let stream = changelog
351            .to_stream(&mut ToStreamContext::new_with_backfill_type(
352                false,
353                BackfillType::Replicated,
354            ))
355            .unwrap();
356
357        let input = stream.as_stream_change_log().unwrap().input();
358        assert!(input.as_stream_exchange().is_some());
359        assert_eq!(input.distribution(), &Distribution::HashShard(vec![0]));
360
361        // The primary key is retained internally even when it is not selected from the changelog.
362        let changelog = LogicalChangeLog::create(scan.into(), None, None);
363        let pruned = changelog.prune_col(
364            &[1, 2, 3],
365            &mut ColumnPruningContext::new(changelog.clone()),
366        );
367        let stream = pruned
368            .to_stream(&mut ToStreamContext::new_with_backfill_type(
369                false,
370                BackfillType::Replicated,
371            ))
372            .unwrap();
373        let changelog = stream.as_stream_project().unwrap().input();
374        let input = changelog.as_stream_change_log().unwrap().input();
375        assert!(input.as_stream_exchange().is_some());
376        assert_eq!(input.distribution(), &Distribution::HashShard(vec![1]));
377    }
378}