risingwave_frontend/optimizer/plan_node/
logical_changelog.rs1use 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(input: PlanRef, key_indices: Option<Vec<usize>>) -> PlanRef {
40 Self::new(input, key_indices, true, true).into()
41 }
42
43 pub fn new(
44 input: PlanRef,
45 key_indices: Option<Vec<usize>>,
46 need_op: bool,
47 need_changelog_row_id: bool,
48 ) -> Self {
49 let core = generic::ChangeLog::new(input, key_indices, need_op, need_changelog_row_id);
50 Self::with_core(core)
51 }
52
53 pub fn with_core(core: generic::ChangeLog<PlanRef>) -> Self {
54 let base = PlanBase::new_logical_with_core(&core);
55 LogicalChangeLog { base, core }
56 }
57}
58
59impl PlanTreeNodeUnary<Logical> for LogicalChangeLog {
60 fn input(&self) -> PlanRef {
61 self.core.input.clone()
62 }
63
64 fn clone_with_input(&self, input: PlanRef) -> Self {
65 let core = self.core.clone_with_input(input);
66
67 Self::with_core(core)
68 }
69
70 fn rewrite_with_input(
71 &self,
72 input: PlanRef,
73 input_col_change: ColIndexMapping,
74 ) -> (Self, ColIndexMapping) {
75 let key_indices = self
76 .core
77 .key_indices
78 .as_ref()
79 .map(|key| input_col_change.map_all(key));
80 let changelog = Self::new(input, key_indices, self.core.need_op, true);
81
82 let out_col_change = if self.core.need_op {
83 let (mut output_vec, len) = input_col_change.into_parts();
84 output_vec.push(Some(len));
85 ColIndexMapping::new(output_vec, len + 1)
86 } else {
87 input_col_change
88 };
89
90 let (mut output_vec, len) = out_col_change.into_parts();
91 let out_col_change = if self.core.need_changelog_row_id {
92 output_vec.push(Some(len));
93 ColIndexMapping::new(output_vec, len + 1)
94 } else {
95 ColIndexMapping::new(output_vec, len + 1)
96 };
97
98 (changelog, out_col_change)
99 }
100}
101
102impl_plan_tree_node_for_unary! { Logical, LogicalChangeLog}
103impl_distill_by_unit!(LogicalChangeLog, core, "LogicalChangeLog");
104
105impl ExprRewritable<Logical> for LogicalChangeLog {}
106
107impl ExprVisitable for LogicalChangeLog {}
108
109impl PredicatePushdown for LogicalChangeLog {
110 fn predicate_pushdown(
111 &self,
112 predicate: Condition,
113 ctx: &mut super::PredicatePushdownContext,
114 ) -> PlanRef {
115 gen_filter_and_pushdown(self, predicate, Condition::true_cond(), ctx)
116 }
117}
118
119impl ColPrunable for LogicalChangeLog {
120 fn prune_col(&self, required_cols: &[usize], ctx: &mut ColumnPruningContext) -> PlanRef {
121 let fields = self.schema().fields();
122 let mut need_op = false;
123 let mut need_changelog_row_id = false;
124 let mut input_required_cols = required_cols
127 .iter()
128 .filter_map(|a| {
129 if let Some(f) = fields.get(*a) {
130 if f.name == CHANGELOG_OP {
131 need_op = true;
132 None
133 } else if f.name == _CHANGELOG_ROW_ID {
134 need_changelog_row_id = true;
135 None
136 } else {
137 Some(*a)
138 }
139 } else {
140 Some(*a)
141 }
142 })
143 .collect_vec();
144
145 if let Some(stream_key) = self.input().stream_key() {
149 for &key in stream_key {
150 if !input_required_cols.contains(&key) {
151 input_required_cols.push(key);
152 }
153 }
154 }
155
156 if let Some(key) = &self.core.key_indices {
158 for &index in key {
159 if !input_required_cols.contains(&index) {
160 input_required_cols.push(index);
161 }
162 }
163 }
164
165 let new_input = self.input().prune_col(&input_required_cols, ctx);
166 let input_mapping = ColIndexMapping::with_remaining_columns(
167 &input_required_cols,
168 self.input().schema().len(),
169 );
170 let key_indices = self
171 .core
172 .key_indices
173 .as_ref()
174 .map(|key| input_mapping.map_all(key));
175
176 let changelog: PlanRef =
178 Self::new(new_input, key_indices, need_op, need_changelog_row_id).into();
179
180 let (mut output_mapping, new_output_len) = input_mapping.into_parts();
182
183 if self.core.need_op {
184 output_mapping.push(need_op.then_some(new_output_len));
185 }
186
187 if self.core.need_changelog_row_id {
188 output_mapping
189 .push(need_changelog_row_id.then_some(new_output_len + usize::from(need_op)));
190 }
191
192 let output_len = changelog.schema().len();
193 let output_mapping = ColIndexMapping::new(output_mapping, output_len);
194 let output_required_cols = output_mapping.map_all(required_cols);
195
196 if output_required_cols.iter().copied().eq(0..output_len) {
197 changelog
198 } else {
199 let output_mapping =
200 ColIndexMapping::with_remaining_columns(&output_required_cols, output_len);
201
202 LogicalProject::with_mapping(changelog, output_mapping).into()
203 }
204 }
205}
206
207impl ToBatch for LogicalChangeLog {
208 fn to_batch(&self) -> Result<BatchPlanRef> {
209 Err(BindError("With changelog cte only support with create mv/sink".to_owned()).into())
210 }
211}
212
213impl ToStream for LogicalChangeLog {
214 fn to_stream(&self, ctx: &mut ToStreamContext) -> Result<StreamPlanRef> {
215 let input = self.input().to_stream(ctx)?;
216
217 let input = if let Some(key) = &self.core.key_indices {
218 RequiredDist::hash_shard(key).streaming_enforce_if_not_satisfies(input)?
219 } else if matches!(input.distribution(), Distribution::SomeShard) {
220 RequiredDist::hash_shard(input.expect_stream_key())
221 .streaming_enforce_if_not_satisfies(input)?
222 } else {
223 input
224 };
225
226 let dist = input.distribution();
227 let distribution_keys = match dist {
228 Distribution::HashShard(distribution_keys)
229 | Distribution::UpstreamHashShard(distribution_keys, _) => distribution_keys.clone(),
230 Distribution::Single => {
231 vec![]
232 }
233 _ => {
234 return Err(BindError(format!(
235 "ChangeLog requires input to be hash distributed, single, but got {:?}",
236 dist
237 ))
238 .into());
239 }
240 };
241 let core = self.core.clone_with_input(input);
242 let row_id_index = self.schema().fields().len() - 1;
243 let plan = StreamChangeLog::new_with_dist(
244 core,
245 Distribution::HashShard(vec![row_id_index]),
246 distribution_keys.into_iter().map(|k| k as u32).collect(),
247 )
248 .into();
249
250 Ok(plan)
251 }
252
253 fn logical_rewrite_for_stream(
254 &self,
255 ctx: &mut RewriteStreamContext,
256 ) -> Result<(PlanRef, ColIndexMapping)> {
257 let (input, input_col_change) = self.input().logical_rewrite_for_stream(ctx)?;
258 let (changelog, out_col_change) = self.rewrite_with_input(input, input_col_change);
259 Ok((changelog.into(), out_col_change))
260 }
261}
262
263#[cfg(test)]
264mod tests {
265 use std::rc::Rc;
266
267 use risingwave_common::catalog::{CdcTableDesc, ColumnDesc, ColumnId, TableId};
268 use risingwave_common::id::SourceId;
269 use risingwave_common::types::DataType;
270 use risingwave_common::util::sort_util::{ColumnOrder, OrderType};
271 use risingwave_connector::source::cdc::CdcScanOptions;
272
273 use super::*;
274 use crate::optimizer::optimizer_context::OptimizerContext;
275 use crate::optimizer::plan_node::{BackfillType, LogicalCdcScan, PlanTreeNodeUnary};
276
277 #[test]
278 fn test_changelog_on_cdc_scan_enforces_hash_distribution() {
279 let desc = CdcTableDesc {
280 table_id: TableId::new(1),
281 source_id: SourceId::new(2),
282 external_table_name: "mydb.orders".to_owned(),
283 pk: vec![ColumnOrder::new(0, OrderType::ascending())],
284 pk_comparisons: vec![risingwave_common::catalog::CdcKeyComparison::Native],
285 columns: vec![
286 ColumnDesc::named("id", ColumnId::new(1), DataType::Int32),
287 ColumnDesc::named("payload", ColumnId::new(2), DataType::Varchar),
288 ],
289 stream_key: vec![0],
290 ..Default::default()
291 };
292 let scan = LogicalCdcScan::create(
293 "orders".to_owned(),
294 Rc::new(desc),
295 OptimizerContext::mock(),
296 CdcScanOptions {
297 disable_backfill: true,
298 ..Default::default()
299 },
300 );
301 let changelog = LogicalChangeLog::create(scan.clone().into(), None);
302 let stream = changelog
303 .to_stream(&mut ToStreamContext::new_with_backfill_type(
304 false,
305 BackfillType::Replicated,
306 ))
307 .unwrap();
308
309 let input = stream.as_stream_change_log().unwrap().input();
310 assert!(input.as_stream_exchange().is_some());
311 assert_eq!(input.distribution(), &Distribution::HashShard(vec![0]));
312
313 let changelog = LogicalChangeLog::create(scan.into(), None);
315 let pruned = changelog.prune_col(
316 &[1, 2, 3],
317 &mut ColumnPruningContext::new(changelog.clone()),
318 );
319 let stream = pruned
320 .to_stream(&mut ToStreamContext::new_with_backfill_type(
321 false,
322 BackfillType::Replicated,
323 ))
324 .unwrap();
325 let changelog = stream.as_stream_project().unwrap().input();
326 let input = changelog.as_stream_change_log().unwrap().input();
327 assert!(input.as_stream_exchange().is_some());
328 assert_eq!(input.distribution(), &Distribution::HashShard(vec![1]));
329 }
330}