1use itertools::Itertools;
16use pretty_xmlish::{Pretty, XmlNode};
17use risingwave_common::catalog::{ColumnCatalog, Field};
18use risingwave_common::types::DataType;
19use risingwave_common::util::sort_util::OrderType;
20use risingwave_connector::source::cdc::normalize_simple_postgres_quoted_table_name;
21use risingwave_pb::stream_plan::PbStreamNode;
22use risingwave_pb::stream_plan::stream_node::{PbNodeBody, PbStreamKind};
23
24use super::stream::prelude::*;
25use super::utils::{Distill, childless_record};
26use super::{ExprRewritable, PlanBase, StreamNode, StreamPlanRef as PlanRef, generic};
27use crate::catalog::ColumnId;
28use crate::expr::{Expr, ExprImpl, ExprRewriter, ExprType, ExprVisitor, FunctionCall, InputRef};
29use crate::optimizer::plan_node::expr_visitable::ExprVisitable;
30use crate::optimizer::plan_node::utils::{IndicesDisplay, TableCatalogBuilder};
31use crate::optimizer::property::{Distribution, DistributionDisplay};
32use crate::scheduler::SchedulerResult;
33use crate::stream_fragmenter::BuildFragmentGraphState;
34use crate::{Explain, TableCatalog};
35
36#[derive(Debug, Clone, PartialEq, Eq, Hash)]
39pub struct StreamCdcTableScan {
40 pub base: PlanBase<Stream>,
41 core: generic::CdcScan,
42}
43
44impl StreamCdcTableScan {
45 pub fn new(core: generic::CdcScan) -> Self {
46 let distribution = Distribution::SomeShard;
47 let base = PlanBase::new_stream_with_core(
48 &core,
49 distribution,
50 StreamKind::Upsert,
54 false,
55 core.watermark_columns(),
56 core.columns_monotonicity(),
57 );
58 Self { base, core }
59 }
60
61 pub fn table_name(&self) -> &str {
62 &self.core.table_name
63 }
64
65 pub fn core(&self) -> &generic::CdcScan {
66 &self.core
67 }
68
69 pub fn build_backfill_state_catalog(
78 &self,
79 state: &mut BuildFragmentGraphState,
80 is_parallelized_backfill: bool,
81 ) -> TableCatalog {
82 if is_parallelized_backfill {
83 let mut catalog_builder = TableCatalogBuilder::default();
84 catalog_builder.add_column(&Field::with_name(DataType::Int64, "split_id"));
86 catalog_builder.add_order_column(0, OrderType::ascending());
87 catalog_builder.add_column(&Field::with_name(DataType::Boolean, "backfill_finished"));
88 catalog_builder.add_column(&Field::with_name(DataType::Int64, "row_count"));
90 catalog_builder.add_column(&Field::with_name(DataType::Jsonb, "cdc_offset_low"));
91 catalog_builder.add_column(&Field::with_name(DataType::Jsonb, "cdc_offset_high"));
92 catalog_builder
93 .build(vec![], 1)
94 .with_id(state.gen_table_id_wrapped())
95 } else {
96 let mut catalog_builder = TableCatalogBuilder::default();
97 let upstream_schema = &self.core.get_table_columns();
98
99 catalog_builder.add_column(&Field::with_name(DataType::Varchar, "split_id"));
102 catalog_builder.add_order_column(0, OrderType::ascending());
103
104 for col_order in self.core.primary_key() {
106 let col = &upstream_schema[col_order.column_index];
107 catalog_builder.add_column(&Field::from(col));
108 }
109
110 catalog_builder.add_column(&Field::with_name(DataType::Boolean, "backfill_finished"));
111
112 catalog_builder.add_column(&Field::with_name(DataType::Int64, "row_count"));
114
115 catalog_builder.add_column(&Field::with_name(DataType::Jsonb, "cdc_offset"));
117
118 catalog_builder
120 .build(vec![], 1)
121 .with_id(state.gen_table_id_wrapped())
122 }
123 }
124}
125
126impl_plan_tree_node_for_leaf! { Stream, StreamCdcTableScan }
127
128impl Distill for StreamCdcTableScan {
129 fn distill<'a>(&self) -> XmlNode<'a> {
130 let verbose = self.base.ctx().is_explain_verbose();
131 let mut vec = Vec::with_capacity(4);
132 vec.push(("table", Pretty::from(self.core.table_name.clone())));
133 vec.push(("columns", self.core.columns_pretty(verbose)));
134
135 if verbose {
136 let pk = IndicesDisplay {
137 indices: self.stream_key().unwrap_or_default(),
138 schema: self.base.schema(),
139 };
140 vec.push(("pk", pk.distill()));
141 let dist = Pretty::display(&DistributionDisplay {
142 distribution: self.distribution(),
143 input_schema: self.base.schema(),
144 });
145 vec.push(("dist", dist));
146 }
147
148 childless_record("StreamCdcTableScan", vec)
149 }
150}
151
152impl StreamNode for StreamCdcTableScan {
153 fn to_stream_prost_body(&self, _state: &mut BuildFragmentGraphState) -> PbNodeBody {
154 unreachable!(
155 "stream scan cannot be converted into a prost body -- call `adhoc_to_stream_prost` instead."
156 )
157 }
158}
159
160impl StreamCdcTableScan {
161 pub fn adhoc_to_stream_prost(
163 &self,
164 state: &mut BuildFragmentGraphState,
165 ) -> SchedulerResult<PbStreamNode> {
166 use risingwave_pb::stream_plan::*;
167
168 let stream_key = self
169 .stream_key()
170 .unwrap_or_else(|| {
171 panic!(
172 "should always have a stream key in the stream plan but not, sub plan: {}",
173 PlanRef::from(self.clone()).explain_to_string()
174 )
175 })
176 .iter()
177 .map(|x| *x as u32)
178 .collect_vec();
179
180 let cdc_source_schema = ColumnCatalog::debezium_cdc_source_cols()
182 .into_iter()
183 .map(|c| Field::from(c.column_desc).to_prost())
184 .collect_vec();
185
186 let catalog = self
187 .build_backfill_state_catalog(state, self.core.options.is_parallelized_backfill())
188 .to_internal_table_prost();
189
190 let upstream_source_id = self.core.cdc_table_desc.source_id;
192
193 let filter_expr =
195 Self::build_cdc_filter_expr(self.core.cdc_table_desc.external_table_name.as_str());
196
197 let filter_operator_id = self.core.ctx.next_plan_node_id();
198 let filter_stream_node = StreamNode {
200 operator_id: filter_operator_id.to_stream_node_operator_id(),
201 input: vec![
202 PbStreamNode {
204 node_body: Some(PbNodeBody::Merge(Default::default())),
205 identity: "Upstream".into(),
206 fields: cdc_source_schema.clone(),
207 stream_key: vec![], ..Default::default()
209 },
210 ],
211 stream_key: vec![], stream_kind: PbStreamKind::AppendOnly as _,
213 identity: "StreamCdcFilter".to_owned(),
214 fields: cdc_source_schema.clone(),
215 node_body: Some(PbNodeBody::CdcFilter(Box::new(CdcFilterNode {
216 search_condition: Some(filter_expr.to_expr_proto()),
217 upstream_source_id,
218 }))),
219 };
220
221 let exchange_operator_id = self.core.ctx.next_plan_node_id();
222 let strategy = if self.core.options.is_parallelized_backfill() {
223 DispatchStrategy {
224 r#type: DispatcherType::Broadcast as _,
225 dist_key_indices: vec![],
226 output_mapping: PbDispatchOutputMapping::identical(cdc_source_schema.len()).into(),
227 }
228 } else {
229 DispatchStrategy {
230 r#type: DispatcherType::Simple as _,
231 dist_key_indices: vec![], output_mapping: PbDispatchOutputMapping::identical(cdc_source_schema.len()).into(),
233 }
234 };
235 let exchange_stream_node = StreamNode {
237 operator_id: exchange_operator_id.to_stream_node_operator_id(),
238 input: vec![filter_stream_node],
239 stream_key: vec![], stream_kind: PbStreamKind::AppendOnly as _,
241 identity: "Exchange".to_owned(),
242 fields: cdc_source_schema,
243 node_body: Some(PbNodeBody::Exchange(Box::new(ExchangeNode {
244 strategy: Some(strategy),
245 }))),
246 };
247
248 let upstream_column_ids = self
250 .core
251 .output_and_pk_column_ids()
252 .iter()
253 .map(ColumnId::get_id)
254 .collect_vec();
255
256 let output_indices = self
257 .core
258 .output_column_ids()
259 .iter()
260 .map(|i| {
261 upstream_column_ids
262 .iter()
263 .position(|&x| x == i.get_id())
264 .unwrap() as u32
265 })
266 .collect_vec();
267
268 tracing::debug!(
269 output_column_ids=?self.core.output_column_ids(),
270 ?upstream_column_ids,
271 ?output_indices,
272 "stream cdc table scan output indices"
273 );
274
275 let options = self.core.options.to_proto();
276 let stream_scan_body = PbNodeBody::StreamCdcScan(Box::new(StreamCdcScanNode {
277 table_id: upstream_source_id.as_cdc_table_id(),
278 upstream_column_ids,
279 output_indices,
280 state_table: Some(catalog),
282 cdc_table_desc: Some(self.core.cdc_table_desc.to_protobuf()),
283 rate_limit: self.base.ctx().overwrite_options().backfill_rate_limit,
284 disable_backfill: options.disable_backfill,
285 options: Some(options),
286 }));
287
288 Ok(PbStreamNode {
290 fields: self.schema().to_prost(),
291 input: vec![exchange_stream_node],
292 node_body: Some(stream_scan_body),
293 stream_key,
294 operator_id: self.base.id().to_stream_node_operator_id(),
295 identity: self.distill_to_string(),
296 stream_kind: self.stream_kind().to_protobuf() as i32,
297 })
298 }
299
300 pub fn build_cdc_filter_expr(cdc_table_name: &str) -> ExprImpl {
302 let table_name_ref: ExprImpl = InputRef::new(2, DataType::Varchar).into();
303 let mut filter_expr = build_cdc_table_name_eq_expr(table_name_ref.clone(), cdc_table_name);
304
305 if let Some(compat_table_name) = normalize_simple_postgres_quoted_table_name(cdc_table_name)
306 {
307 filter_expr = FunctionCall::new(
314 ExprType::Or,
315 vec![
316 filter_expr,
317 build_cdc_table_name_eq_expr(table_name_ref, &compat_table_name),
318 ],
319 )
320 .unwrap()
321 .into();
322 }
323
324 filter_expr
325 }
326}
327
328fn build_cdc_table_name_eq_expr(table_name_ref: ExprImpl, cdc_table_name: &str) -> ExprImpl {
329 FunctionCall::new(
330 ExprType::Equal,
331 vec![
332 table_name_ref,
333 ExprImpl::literal_varchar(cdc_table_name.into()),
334 ],
335 )
336 .unwrap()
337 .into()
338}
339
340impl ExprRewritable<Stream> for StreamCdcTableScan {
341 fn has_rewritable_expr(&self) -> bool {
342 true
343 }
344
345 fn rewrite_exprs(&self, r: &mut dyn ExprRewriter) -> PlanRef {
346 let core = self.core.clone();
347 core.rewrite_exprs(r);
348 Self::new(core).into()
349 }
350}
351
352impl ExprVisitable for StreamCdcTableScan {
353 fn visit_exprs(&self, v: &mut dyn ExprVisitor) {
354 self.core.visit_exprs(v);
355 }
356}
357
358#[cfg(test)]
359mod tests {
360 use std::str::FromStr;
361
362 use risingwave_common::row::OwnedRow;
363 use risingwave_common::types::{JsonbVal, ScalarImpl};
364
365 use super::*;
366
367 #[tokio::test]
368 async fn test_cdc_filter_expr() {
369 let t1_json = JsonbVal::from_str(r#"{ "before": null, "after": { "v": 111, "v2": 222.2 }, "source": { "version": "2.2.0.Alpha3", "connector": "mysql", "name": "dbserver1", "ts_ms": 1678428689000, "snapshot": "false", "db": "inventory", "sequence": null, "table": "t1", "server_id": 223344, "gtid": null, "file": "mysql-bin.000003", "pos": 774, "row": 0, "thread": 8, "query": null }, "op": "c", "ts_ms": 1678428689389, "transaction": null }"#).unwrap();
370 let t2_json = JsonbVal::from_str(r#"{ "before": null, "after": { "v": 333, "v2": 666.6 }, "source": { "version": "2.2.0.Alpha3", "connector": "mysql", "name": "dbserver1", "ts_ms": 1678428689000, "snapshot": "false", "db": "inventory", "sequence": null, "table": "t2", "server_id": 223344, "gtid": null, "file": "mysql-bin.000003", "pos": 884, "row": 0, "thread": 8, "query": null }, "op": "c", "ts_ms": 1678428689389, "transaction": null }"#).unwrap();
371
372 let trx_json = JsonbVal::from_str(r#"{"data_collections": null, "event_count": null, "id": "35319:3962662584", "status": "BEGIN", "ts_ms": 1704263537068}"#).unwrap();
374 let row1 = OwnedRow::new(vec![
375 Some(t1_json.into()),
376 Some(r#"{"file": "1.binlog", "pos": 100}"#.into()),
377 Some("public.t2".into()),
378 ]);
379 let row2 = OwnedRow::new(vec![
380 Some(t2_json.into()),
381 Some(r#"{"file": "2.binlog", "pos": 100}"#.into()),
382 Some("abs.t2".into()),
383 ]);
384
385 let row3 = OwnedRow::new(vec![
386 Some(trx_json.into()),
387 Some(r#"{"file": "3.binlog", "pos": 100}"#.into()),
388 Some("public.t2".into()),
389 ]);
390
391 let filter_expr = StreamCdcTableScan::build_cdc_filter_expr("public.t2");
392 assert_eq!(
393 filter_expr.eval_row(&row1).await.unwrap(),
394 Some(ScalarImpl::Bool(true))
395 );
396 assert_eq!(
397 filter_expr.eval_row(&row2).await.unwrap(),
398 Some(ScalarImpl::Bool(false))
399 );
400 assert_eq!(
401 filter_expr.eval_row(&row3).await.unwrap(),
402 Some(ScalarImpl::Bool(true))
403 )
404 }
405
406 #[tokio::test]
407 async fn test_cdc_filter_expr_postgres_quoted_table_name_compat() {
408 let row_with_unquoted_schema_table =
409 OwnedRow::new(vec![None, None, Some("public.QuotedNoteE2e".into())]);
410 let row_with_other_table = OwnedRow::new(vec![None, None, Some("public.other".into())]);
411
412 let filter_expr = StreamCdcTableScan::build_cdc_filter_expr(r#"public."QuotedNoteE2e""#);
413 assert_eq!(
414 filter_expr
415 .eval_row(&row_with_unquoted_schema_table)
416 .await
417 .unwrap(),
418 Some(ScalarImpl::Bool(true))
419 );
420 assert_eq!(
421 filter_expr.eval_row(&row_with_other_table).await.unwrap(),
422 Some(ScalarImpl::Bool(false))
423 );
424 }
425}