risingwave_frontend/optimizer/plan_node/generic/
source.rs1use std::rc::Rc;
16
17use educe::Educe;
18use risingwave_common::catalog::{ColumnCatalog, ColumnDesc, Field, Schema};
19use risingwave_common::types::DataType;
20use risingwave_common::util::sort_util::OrderType;
21use risingwave_connector::WithPropertiesExt;
22use risingwave_sqlparser::ast::AsOf;
23
24use super::super::utils::TableCatalogBuilder;
25use super::GenericPlanNode;
26use crate::TableCatalog;
27use crate::catalog::source_catalog::SourceCatalog;
28use crate::optimizer::optimizer_context::OptimizerContextRef;
29use crate::optimizer::property::{FunctionalDependencySet, StreamKind};
30
31#[derive(Debug, Clone, PartialEq, Eq, Hash)]
33#[expect(clippy::enum_variant_names)]
34pub enum SourceNodeKind {
35 CreateTable,
37 CreateSharedSource,
39 CreateMViewOrBatch,
45}
46
47#[derive(Debug, Clone, Educe)]
49#[educe(PartialEq, Eq, Hash)]
50pub struct Source {
51 pub catalog: Option<Rc<SourceCatalog>>,
53
54 pub column_catalog: Vec<ColumnCatalog>,
58 pub row_id_index: Option<usize>,
59
60 pub kind: SourceNodeKind,
61
62 #[educe(PartialEq(ignore))]
63 #[educe(Hash(ignore))]
64 pub ctx: OptimizerContextRef,
65
66 pub as_of: Option<AsOf>,
67}
68
69impl GenericPlanNode for Source {
70 fn schema(&self) -> Schema {
71 let fields = self
72 .column_catalog
73 .iter()
74 .map(|c| (&c.column_desc).into())
75 .collect();
76 Schema { fields }
77 }
78
79 fn stream_key(&self) -> Option<Vec<usize>> {
80 if let Some(idx) = self.row_id_index {
83 Some(vec![idx])
84 } else if let Some(catalog) = &self.catalog {
85 catalog
86 .pk_col_ids
87 .iter()
88 .map(|id| {
89 self.column_catalog
90 .iter()
91 .position(|c| c.column_id() == *id)
92 })
93 .collect::<Option<Vec<_>>>()
94 } else {
95 None
96 }
97 }
98
99 fn ctx(&self) -> OptimizerContextRef {
100 self.ctx.clone()
101 }
102
103 fn functional_dependency(&self) -> FunctionalDependencySet {
104 let pk_indices = self.stream_key();
105 match pk_indices {
106 Some(pk_indices) => {
107 debug_assert!(
108 pk_indices
109 .iter()
110 .all(|idx| *idx < self.column_catalog.len())
111 );
112 FunctionalDependencySet::with_key(self.column_catalog.len(), &pk_indices)
113 }
114 None => FunctionalDependencySet::new(self.column_catalog.len()),
115 }
116 }
117}
118
119impl Source {
120 pub fn stream_kind(&self) -> StreamKind {
121 if let Some(catalog) = &self.catalog {
122 if catalog.is_iceberg_update_source() {
123 StreamKind::Retract
124 } else if catalog.append_only {
125 StreamKind::AppendOnly
126 } else {
127 StreamKind::Upsert
130 }
131 } else {
132 StreamKind::AppendOnly
134 }
135 }
136
137 pub fn file_list_node(core: Self) -> Self {
139 let column_catalog = if core.is_iceberg_update_source() {
140 Self::iceberg_update_task_fields()
142 .iter()
143 .enumerate()
144 .map(|(id, field)| ColumnCatalog {
145 column_desc: ColumnDesc::from_field_with_column_id(field, id as i32),
146 is_hidden: false,
147 })
148 .collect()
149 } else if core.is_iceberg_connector() {
150 vec![
151 ColumnCatalog {
152 column_desc: ColumnDesc::from_field_with_column_id(
153 &Field {
154 name: "file_path".to_owned(),
155 data_type: DataType::Varchar,
156 },
157 0,
158 ),
159 is_hidden: false,
160 },
161 ColumnCatalog {
162 column_desc: ColumnDesc::from_field_with_column_id(
163 &Field {
164 name: "file_scan_task".to_owned(),
165 data_type: DataType::Jsonb,
166 },
167 1,
168 ),
169 is_hidden: false,
170 },
171 ]
172 } else if core.is_batch_connector() {
173 vec![
174 ColumnCatalog {
175 column_desc: ColumnDesc::from_field_with_column_id(
176 &Field {
177 name: "batch_task_id".to_owned(),
178 data_type: DataType::Varchar,
179 },
180 0,
181 ),
182 is_hidden: false,
183 },
184 ColumnCatalog {
185 column_desc: ColumnDesc::from_field_with_column_id(
186 &Field {
187 name: "batch_task_info".to_owned(),
188 data_type: DataType::Jsonb,
189 },
190 1,
191 ),
192 is_hidden: false,
193 },
194 ]
195 } else if core.is_new_fs_connector() {
196 vec![
197 ColumnCatalog {
198 column_desc: ColumnDesc::from_field_with_column_id(
199 &Field {
200 name: "filename".to_owned(),
201 data_type: DataType::Varchar,
202 },
203 0,
204 ),
205 is_hidden: false,
206 },
207 ColumnCatalog {
209 column_desc: ColumnDesc::from_field_with_column_id(
210 &Field {
211 name: "last_edit_time".to_owned(),
212 data_type: DataType::Timestamptz,
213 },
214 1,
215 ),
216 is_hidden: false,
217 },
218 ColumnCatalog {
219 column_desc: ColumnDesc::from_field_with_column_id(
220 &Field {
221 name: "file_size".to_owned(),
222 data_type: DataType::Int64,
223 },
224 2,
225 ),
226 is_hidden: false,
227 },
228 ]
229 } else {
230 unreachable!()
231 };
232 Self {
233 column_catalog,
234 row_id_index: None,
235 ..core
236 }
237 }
238
239 pub fn is_new_fs_connector(&self) -> bool {
240 self.catalog
241 .as_ref()
242 .is_some_and(|catalog| catalog.with_properties.is_new_fs_connector())
243 }
244
245 pub fn is_iceberg_connector(&self) -> bool {
246 self.catalog
247 .as_ref()
248 .is_some_and(|catalog| catalog.with_properties.is_iceberg_connector())
249 }
250
251 pub fn is_iceberg_update_source(&self) -> bool {
252 self.catalog
253 .as_ref()
254 .is_some_and(|catalog| catalog.is_iceberg_update_source())
255 }
256
257 pub fn is_kafka_connector(&self) -> bool {
258 self.catalog
259 .as_ref()
260 .is_some_and(|catalog| catalog.with_properties.is_kafka_connector())
261 }
262
263 pub fn is_batch_connector(&self) -> bool {
264 self.catalog
265 .as_ref()
266 .is_some_and(|catalog| catalog.with_properties.is_batch_connector())
267 }
268
269 pub fn requires_singleton(&self) -> bool {
270 self.is_iceberg_connector() || self.is_batch_connector()
271 }
272
273 pub fn support_time_travel(&self) -> bool {
275 self.is_iceberg_connector()
276 }
277
278 pub fn exclude_iceberg_hidden_columns(mut self) -> Self {
279 if self.is_iceberg_update_source() {
280 return self;
281 }
282 let Some(catalog) = &mut self.catalog else {
283 return self;
284 };
285 if catalog.info.is_shared() {
286 return self;
288 }
289 if self.kind != SourceNodeKind::CreateMViewOrBatch {
290 return self;
291 }
292
293 let prune = |col: &ColumnCatalog| col.is_hidden() && !col.is_row_id_column();
294
295 self.row_id_index = self.row_id_index.map(|idx| {
297 let mut cnt = 0;
298 for col in self.column_catalog.iter().take(idx + 1) {
299 if prune(col) {
300 cnt += 1;
301 }
302 }
303 idx - cnt
304 });
305 self.column_catalog.retain(|c| !prune(c));
306 self
307 }
308
309 pub fn exclude_generated_columns(mut self) -> (Self, Option<usize>) {
312 let original_row_id_index = self.row_id_index;
313 self.row_id_index = original_row_id_index.map(|idx| {
315 let mut cnt = 0;
316 for col in self.column_catalog.iter().take(idx + 1) {
317 if col.is_generated() {
318 cnt += 1;
319 }
320 }
321 idx - cnt
322 });
323 self.column_catalog.retain(|c| !c.is_generated());
324 (self, original_row_id_index)
325 }
326
327 pub fn infer_internal_table_catalog(is_distributed: bool) -> TableCatalog {
348 let mut builder = TableCatalogBuilder::default();
349
350 let key = Field {
351 data_type: DataType::Varchar,
352 name: "partition_id".to_owned(),
353 };
354 let value = Field {
355 data_type: DataType::Jsonb,
356 name: "offset_info".to_owned(),
357 };
358
359 let ordered_col_idx = builder.add_column(&key);
360 builder.add_column(&value);
361 builder.add_order_column(ordered_col_idx, OrderType::ascending());
362
363 builder.build(
364 if is_distributed {
365 vec![ordered_col_idx]
366 } else {
367 vec![]
368 },
369 1,
370 )
371 }
372
373 fn iceberg_update_task_fields() -> [Field; 3] {
374 [
375 Field {
376 data_type: DataType::Int64,
377 name: "route".to_owned(),
378 },
379 Field {
380 data_type: DataType::Int64,
381 name: "seq".to_owned(),
382 },
383 Field {
384 data_type: DataType::Jsonb,
385 name: "task".to_owned(),
386 },
387 ]
388 }
389
390 pub fn infer_iceberg_update_fetch_table_catalog() -> TableCatalog {
394 let mut builder = TableCatalogBuilder::default();
395 let [route, seq, task] = Self::iceberg_update_task_fields();
396 let route = builder.add_column(&route);
397 let seq = builder.add_column(&seq);
398 builder.add_column(&task);
399 builder.add_order_column(route, OrderType::ascending());
400 builder.add_order_column(seq, OrderType::ascending());
401 builder.build(vec![route], 2)
402 }
403
404 pub fn clone_with_column_catalog(&self, column_catalog: Vec<ColumnCatalog>) -> Self {
405 let row_id_index = column_catalog.iter().position(|c| c.is_row_id_column());
406 Self {
407 catalog: self.catalog.clone(),
408 column_catalog,
409 row_id_index,
410 kind: self.kind.clone(),
411 ctx: self.ctx.clone(),
412 as_of: self.as_of.clone(),
413 }
414 }
415}