risingwave_frontend/optimizer/plan_node/
logical_source.rs1use std::rc::Rc;
16
17use pretty_xmlish::{Pretty, XmlNode};
18use risingwave_common::bail;
19use risingwave_common::catalog::{
20 ColumnCatalog, ICEBERG_FILE_PATH_COLUMN_NAME, ICEBERG_FILE_POS_COLUMN_NAME,
21 ICEBERG_SEQUENCE_NUM_COLUMN_NAME, ROW_ID_COLUMN_NAME,
22};
23use risingwave_pb::plan_common::GeneratedColumnDesc;
24use risingwave_pb::plan_common::column_desc::GeneratedOrDefaultColumn;
25use risingwave_pb::plan_common::source_refresh_mode::RefreshMode;
26use risingwave_sqlparser::ast::AsOf;
27
28use super::generic::{GenericPlanRef, SourceNodeKind};
29use super::stream_watermark_filter::StreamWatermarkFilter;
30use super::utils::{Distill, childless_record};
31use super::{
32 BatchProject, BatchSource, ColPrunable, ExprRewritable, Logical, LogicalFilter,
33 LogicalPlanRef as PlanRef, LogicalProject, PlanBase, PredicatePushdown, StreamPlanRef,
34 StreamProject, StreamRowIdGen, StreamSource, StreamSourceScan, ToBatch, ToStream, generic,
35};
36use crate::catalog::source_catalog::SourceCatalog;
37use crate::error::Result;
38use crate::expr::{ExprImpl, ExprRewriter, ExprVisitor, InputRef};
39use crate::optimizer::optimizer_context::OptimizerContextRef;
40use crate::optimizer::plan_node::expr_visitable::ExprVisitable;
41use crate::optimizer::plan_node::stream_fs_fetch::StreamFsFetch;
42use crate::optimizer::plan_node::utils::column_names_pretty;
43use crate::optimizer::plan_node::{
44 ColumnPruningContext, PredicatePushdownContext, RewriteStreamContext, StreamDedup,
45 ToStreamContext,
46};
47use crate::optimizer::property::Distribution::HashShard;
48use crate::optimizer::property::{
49 Distribution, MonotonicityMap, RequiredDist, StreamKind, WatermarkColumns,
50};
51use crate::utils::{ColIndexMapping, Condition, IndexRewriter};
52
53#[derive(Debug, Clone, PartialEq, Eq, Hash)]
55pub struct LogicalSource {
56 pub base: PlanBase<Logical>,
57 pub core: generic::Source,
58
59 pub(crate) output_exprs: Option<Vec<ExprImpl>>,
62 pub(crate) output_row_id_index: Option<usize>,
65}
66
67impl LogicalSource {
68 pub fn new(
69 source_catalog: Option<Rc<SourceCatalog>>,
70 column_catalog: Vec<ColumnCatalog>,
71 row_id_index: Option<usize>,
72 kind: SourceNodeKind,
73 ctx: OptimizerContextRef,
74 as_of: Option<AsOf>,
75 ) -> Result<Self> {
76 let core = generic::Source {
83 catalog: source_catalog,
84 column_catalog,
85 row_id_index,
86 kind,
87 ctx,
88 as_of,
89 };
90
91 if core.as_of.is_some() && !core.support_time_travel() {
92 bail!("Time travel is not supported for the source")
93 }
94
95 let base = PlanBase::new_logical_with_core(&core);
96
97 let output_exprs = Self::derive_output_exprs_from_generated_columns(&core.column_catalog)?;
98 let (core, output_row_id_index) = core.exclude_generated_columns();
99
100 Ok(LogicalSource {
101 base,
102 core,
103 output_exprs,
104 output_row_id_index,
105 })
106 }
107
108 pub fn with_catalog(
109 source_catalog: Rc<SourceCatalog>,
110 kind: SourceNodeKind,
111 ctx: OptimizerContextRef,
112 as_of: Option<AsOf>,
113 ) -> Result<Self> {
114 let column_catalogs = source_catalog.columns.clone();
115 let row_id_index = source_catalog.row_id_index;
116 if !source_catalog.append_only {
117 assert!(row_id_index.is_none());
118 }
119
120 Self::new(
121 Some(source_catalog),
122 column_catalogs,
123 row_id_index,
124 kind,
125 ctx,
126 as_of,
127 )
128 }
129
130 pub fn derive_output_exprs_from_generated_columns(
135 columns: &[ColumnCatalog],
136 ) -> Result<Option<Vec<ExprImpl>>> {
137 if !columns.iter().any(|c| c.is_generated()) {
138 return Ok(None);
139 }
140
141 let col_mapping = {
142 let mut mapping = vec![None; columns.len()];
143 let mut cur = 0;
144 for (idx, column) in columns.iter().enumerate() {
145 if !column.is_generated() {
146 mapping[idx] = Some(cur);
147 cur += 1;
148 } else {
149 mapping[idx] = None;
150 }
151 }
152 ColIndexMapping::new(mapping, columns.len())
153 };
154
155 let mut rewriter = IndexRewriter::new(col_mapping);
156 let mut exprs = Vec::with_capacity(columns.len());
157 let mut cur = 0;
158 for column in columns {
159 let column_desc = &column.column_desc;
160 let ret_data_type = column_desc.data_type.clone();
161
162 if let Some(GeneratedOrDefaultColumn::GeneratedColumn(generated_column)) =
163 &column_desc.generated_or_default_column
164 {
165 let GeneratedColumnDesc { expr } = generated_column;
166 let proj_expr =
168 rewriter.rewrite_expr(ExprImpl::from_expr_proto(expr.as_ref().unwrap())?);
169 let casted_expr = proj_expr.cast_assign(&ret_data_type)?;
170 exprs.push(casted_expr);
171 } else {
172 let input_ref = InputRef {
173 data_type: ret_data_type,
174 index: cur,
175 };
176 cur += 1;
177 exprs.push(ExprImpl::InputRef(Box::new(input_ref)));
178 }
179 }
180
181 Ok(Some(exprs))
182 }
183
184 fn create_non_shared_source_plan(core: generic::Source) -> Result<StreamPlanRef> {
185 let mut plan;
186 if core.is_new_fs_connector() {
187 let dedup = !Self::is_full_reload_refresh(&core);
191 plan = Self::create_list_plan(core.clone(), dedup)?;
192 plan = StreamFsFetch::new(plan, core).into();
193 } else if core.is_iceberg_connector() || core.is_batch_connector() {
194 plan = Self::create_list_plan(core.clone(), false)?;
195 plan = StreamFsFetch::new(plan, core).into();
196 } else {
197 plan = StreamSource::new(core).into()
198 }
199 Ok(plan)
200 }
201
202 fn is_full_reload_refresh(core: &generic::Source) -> bool {
203 core.catalog.as_ref().is_some_and(|catalog| {
204 catalog.refresh_mode.as_ref().is_some_and(|refresh_mode| {
205 matches!(refresh_mode.refresh_mode, Some(RefreshMode::FullReload(_)))
206 })
207 })
208 }
209
210 fn create_list_plan(core: generic::Source, dedup: bool) -> Result<StreamPlanRef> {
212 let downstream_columns = core.column_catalog.clone();
213 let logical_source = generic::Source::file_list_node(core);
214 let mut list_plan: StreamPlanRef = StreamSource {
215 base: PlanBase::new_stream_with_core(
216 &logical_source,
217 Distribution::Single,
218 StreamKind::AppendOnly, false,
220 WatermarkColumns::new(),
221 MonotonicityMap::new(),
222 ),
223 core: logical_source,
224 downstream_columns: Some(downstream_columns),
225 }
226 .into();
227 list_plan = RequiredDist::shard_by_key(list_plan.schema().len(), &[0])
228 .streaming_enforce_if_not_satisfies(list_plan)?;
229 if dedup {
230 list_plan = StreamDedup::new(generic::Dedup {
231 input: list_plan,
232 dedup_cols: vec![0],
233 })
234 .into();
235 }
236
237 Ok(list_plan)
238 }
239
240 pub fn source_catalog(&self) -> Option<Rc<SourceCatalog>> {
241 self.core.catalog.clone()
242 }
243
244 pub fn clone_with_column_catalog(&self, column_catalog: Vec<ColumnCatalog>) -> Result<Self> {
245 let row_id_index = column_catalog.iter().position(|c| c.is_row_id_column());
246 let kind = self.core.kind.clone();
247 let ctx = self.core.ctx.clone();
248 let as_of = self.core.as_of.clone();
249 Self::new(
250 self.source_catalog(),
251 column_catalog,
252 row_id_index,
253 kind,
254 ctx,
255 as_of,
256 )
257 }
258
259 fn prune_col_for_iceberg_source(&self, required_cols: &[usize]) -> PlanRef {
260 assert!(self.core.is_iceberg_connector());
261 let schema_len = self.schema().len();
266 assert!(
267 schema_len >= 4,
268 "Iceberg source must have at least 4 columns (3 iceberg hidden + 1 row_id)"
269 );
270
271 assert_eq!(
272 self.core.column_catalog[schema_len - 4].name(),
273 ICEBERG_SEQUENCE_NUM_COLUMN_NAME
274 );
275 assert_eq!(
276 self.core.column_catalog[schema_len - 3].name(),
277 ICEBERG_FILE_PATH_COLUMN_NAME
278 );
279 assert_eq!(
280 self.core.column_catalog[schema_len - 2].name(),
281 ICEBERG_FILE_POS_COLUMN_NAME
282 );
283 assert_eq!(
284 self.core.column_catalog[schema_len - 1].name(),
285 ROW_ID_COLUMN_NAME
286 );
287 assert_eq!(self.output_row_id_index, Some(self.schema().len() - 1));
288
289 let iceberg_start_idx = schema_len - 4;
290 let row_id_idx = schema_len - 1;
291
292 let mut source_cols = Vec::new();
294
295 for &idx in required_cols {
297 if idx < iceberg_start_idx {
298 source_cols.push(idx);
300 }
301 }
302
303 source_cols.extend([
305 iceberg_start_idx,
306 iceberg_start_idx + 1,
307 iceberg_start_idx + 2,
308 row_id_idx,
309 ]);
310
311 let mut core = self.core.clone();
313 core.column_catalog = source_cols
314 .iter()
315 .map(|idx| core.column_catalog[*idx].clone())
316 .collect();
317 core.row_id_index = Some(source_cols.len() - 1);
319
320 let base = PlanBase::new_logical_with_core(&core);
321 let output_exprs =
322 Self::derive_output_exprs_from_generated_columns(&core.column_catalog).unwrap();
323 let (core, _) = core.exclude_generated_columns();
324
325 let pruned_source = LogicalSource {
326 base,
327 core,
328 output_exprs,
329 output_row_id_index: Some(source_cols.len() - 1),
330 };
331
332 let mut old_to_new = vec![None; self.schema().len()];
334 for (new_idx, &old_idx) in source_cols.iter().enumerate() {
335 old_to_new[old_idx] = Some(new_idx);
336 }
337
338 let new_required: Vec<_> = required_cols
340 .iter()
341 .map(|&old_idx| old_to_new[old_idx].unwrap())
342 .collect();
343
344 let mapping =
345 ColIndexMapping::with_remaining_columns(&new_required, pruned_source.schema().len());
346 LogicalProject::with_mapping(pruned_source.into(), mapping).into()
347 }
348
349 pub fn is_shared_source(&self) -> bool {
350 self.source_catalog().is_some_and(|c| c.info.is_shared())
354 }
355}
356
357impl_plan_tree_node_for_leaf! { Logical, LogicalSource}
358impl Distill for LogicalSource {
359 fn distill<'a>(&self) -> XmlNode<'a> {
360 let fields = if let Some(catalog) = self.source_catalog() {
361 let src = Pretty::from(catalog.name.clone());
362 let mut fields = vec![
363 ("source", src),
364 ("is_shared", Pretty::debug(&catalog.info.is_shared())),
365 ("columns", column_names_pretty(self.schema())),
366 ];
367 if let Some(as_of) = &self.core.as_of {
368 fields.push(("as_of", Pretty::debug(as_of)));
369 }
370 fields
371 } else {
372 vec![]
373 };
374 childless_record("LogicalSource", fields)
375 }
376}
377
378impl ColPrunable for LogicalSource {
379 fn prune_col(&self, required_cols: &[usize], _ctx: &mut ColumnPruningContext) -> PlanRef {
380 if self.core.is_iceberg_connector() && !Self::is_full_reload_refresh(&self.core) {
382 self.prune_col_for_iceberg_source(required_cols)
383 } else {
384 let mapping =
386 ColIndexMapping::with_remaining_columns(required_cols, self.schema().len());
387 LogicalProject::with_mapping(self.clone().into(), mapping).into()
388 }
389 }
390}
391
392impl ExprRewritable<Logical> for LogicalSource {
393 fn has_rewritable_expr(&self) -> bool {
394 self.output_exprs.is_some()
395 }
396
397 fn rewrite_exprs(&self, r: &mut dyn ExprRewriter) -> PlanRef {
398 let mut output_exprs = self.output_exprs.clone();
399
400 for expr in output_exprs.iter_mut().flatten() {
401 *expr = r.rewrite_expr(expr.clone());
402 }
403
404 Self {
405 output_exprs,
406 ..self.clone()
407 }
408 .into()
409 }
410}
411
412impl ExprVisitable for LogicalSource {
413 fn visit_exprs(&self, v: &mut dyn ExprVisitor) {
414 self.output_exprs
415 .iter()
416 .flatten()
417 .for_each(|e| v.visit_expr(e));
418 }
419}
420
421impl PredicatePushdown for LogicalSource {
422 fn predicate_pushdown(
423 &self,
424 predicate: Condition,
425 _ctx: &mut PredicatePushdownContext,
426 ) -> PlanRef {
427 LogicalFilter::create(self.clone().into(), predicate)
428 }
429}
430
431impl ToBatch for LogicalSource {
432 fn to_batch(&self) -> Result<crate::optimizer::plan_node::BatchPlanRef> {
433 assert!(
434 !self.core.is_kafka_connector(),
435 "LogicalSource with a kafka property should be converted to LogicalKafkaScan"
436 );
437 assert!(
438 !self.core.is_iceberg_connector(),
439 "LogicalSource with a iceberg property should be converted to LogicalIcebergScan"
440 );
441 let mut plan = BatchSource::new(self.core.clone()).into();
442
443 if let Some(exprs) = &self.output_exprs {
444 let logical_project = generic::Project::new(exprs.clone(), plan);
445 plan = BatchProject::new(logical_project).into();
446 }
447
448 Ok(plan)
449 }
450}
451
452impl ToStream for LogicalSource {
453 fn to_stream(
454 &self,
455 _ctx: &mut ToStreamContext,
456 ) -> Result<crate::optimizer::plan_node::StreamPlanRef> {
457 let mut plan;
458
459 match self.core.kind {
460 SourceNodeKind::CreateTable | SourceNodeKind::CreateSharedSource => {
461 plan = Self::create_non_shared_source_plan(self.core.clone())?;
464 }
465 SourceNodeKind::CreateMViewOrBatch => {
466 if self.is_shared_source() {
467 plan = StreamSourceScan::new(self.core.clone()).into();
468 } else {
469 plan = Self::create_non_shared_source_plan(self.core.clone())?;
471 }
472
473 if let Some(exprs) = &self.output_exprs {
474 let logical_project = generic::Project::new(exprs.clone(), plan);
475 plan = StreamProject::new(logical_project).into();
476 }
477
478 if let Some(row_id_index) = self.output_row_id_index {
479 plan = StreamRowIdGen::new_with_dist(
480 plan,
481 row_id_index,
482 HashShard(vec![row_id_index]),
483 )
484 .into();
485 }
486
487 if let Some(catalog) = self.source_catalog()
488 && !catalog.watermark_descs.is_empty()
489 {
490 plan = StreamWatermarkFilter::new(plan, catalog.watermark_descs.clone()).into();
491 }
492 }
493 }
494 Ok(plan)
495 }
496
497 fn logical_rewrite_for_stream(
498 &self,
499 _ctx: &mut RewriteStreamContext,
500 ) -> Result<(PlanRef, ColIndexMapping)> {
501 Ok((
502 self.clone().into(),
503 ColIndexMapping::identity(self.schema().len()),
504 ))
505 }
506}