1use std::assert_matches;
16use std::num::NonZeroU32;
17
18use fixedbitset::FixedBitSet;
19use itertools::Itertools;
20use pretty_xmlish::{Pretty, XmlNode};
21use risingwave_common::catalog::{
22 ColumnCatalog, ConflictBehavior, CreateType, Engine, StreamJobStatus, TableId,
23};
24use risingwave_common::hash::VnodeCount;
25use risingwave_common::id::FragmentId;
26use risingwave_common::types::DataType;
27use risingwave_common::util::column_index_mapping::ColIndexMapping;
28use risingwave_common::util::iter_util::ZipEqFast;
29use risingwave_common::util::sort_util::{ColumnOrder, OrderType};
30use risingwave_pb::catalog::PbWebhookSourceInfo;
31use risingwave_pb::stream_plan::stream_node::PbNodeBody;
32
33use super::derive::derive_columns;
34use super::stream::prelude::*;
35use super::utils::{Distill, TableCatalogBuilder, childless_record};
36use super::{
37 ExprRewritable, PlanTreeNodeUnary, StreamNode, StreamPlanRef as PlanRef, reorganize_elements_id,
38};
39use crate::catalog::table_catalog::{TableCatalog, TableType, TableVersion};
40use crate::catalog::{DatabaseId, SchemaId};
41use crate::error::Result;
42use crate::optimizer::StreamOptimizedLogicalPlanRoot;
43use crate::optimizer::plan_node::derive::derive_pk;
44use crate::optimizer::plan_node::expr_visitable::ExprVisitable;
45use crate::optimizer::plan_node::utils::plan_can_use_background_ddl;
46use crate::optimizer::plan_node::{PlanBase, PlanNodeMeta};
47use crate::optimizer::property::{Cardinality, Distribution, Order, RequiredDist};
48use crate::optimizer::variant_key::variant_key_error;
49use crate::stream_fragmenter::BuildFragmentGraphState;
50
51#[derive(Debug, Clone, PartialEq, Eq, Hash)]
53pub struct StreamMaterialize {
54 pub base: PlanBase<Stream>,
55 input: PlanRef,
57 table: TableCatalog,
58 staging_table: Option<TableCatalog>,
60 refresh_progress_table: Option<TableCatalog>,
62}
63
64impl StreamMaterialize {
65 pub fn new(input: PlanRef, table: TableCatalog) -> Result<Self> {
66 Self::new_with_staging_and_progress(input, table, None, None)
67 }
68
69 pub fn new_with_staging_and_progress(
70 input: PlanRef,
71 table: TableCatalog,
72 staging_table: Option<TableCatalog>,
73 refresh_progress_table: Option<TableCatalog>,
74 ) -> Result<Self> {
75 let kind = match table.conflict_behavior() {
76 ConflictBehavior::NoCheck => {
77 reject_upsert_input!(input, "Materialize without conflict handling")
78 }
79
80 ConflictBehavior::Overwrite
82 | ConflictBehavior::IgnoreConflict
83 | ConflictBehavior::DoUpdateIfNotNull => match input.stream_kind() {
84 StreamKind::AppendOnly => StreamKind::AppendOnly,
85 StreamKind::Retract | StreamKind::Upsert => StreamKind::Retract,
86 },
87 };
88 let base = PlanBase::new_stream(
89 input.ctx(),
90 input.schema().clone(),
91 Some(table.stream_key()),
92 input.functional_dependency().clone(),
93 input.distribution().clone(),
94 kind,
95 input.emit_on_window_close(),
96 input.watermark_columns().clone(),
97 input.columns_monotonicity().clone(),
98 );
99
100 Ok(Self {
101 base,
102 input,
103 table,
104 staging_table,
105 refresh_progress_table,
106 })
107 }
108
109 pub fn create(
114 StreamOptimizedLogicalPlanRoot {
115 plan: input,
116 required_dist: user_distributed_by,
117 required_order: user_order_by,
118 out_fields: user_cols,
119 out_names,
120 ..
121 }: StreamOptimizedLogicalPlanRoot,
122 name: String,
123 database_id: DatabaseId,
124 schema_id: SchemaId,
125 definition: String,
126 table_type: TableType,
127 cardinality: Cardinality,
128 retention_seconds: Option<NonZeroU32>,
129 ) -> Result<Self> {
130 let input = Self::rewrite_input(input, user_distributed_by.clone(), table_type)?;
131 let input = reorganize_elements_id(input);
133 let columns = derive_columns(input.schema(), out_names, &user_cols)?;
134
135 let create_type = if matches!(table_type, TableType::MaterializedView)
136 && input.ctx().session_ctx().config().background_ddl()
137 && plan_can_use_background_ddl(&input)
138 {
139 CreateType::Background
140 } else {
141 CreateType::Foreground
142 };
143
144 let conflict_behavior = match input.stream_kind() {
146 StreamKind::Retract | StreamKind::AppendOnly => ConflictBehavior::NoCheck,
147 StreamKind::Upsert => ConflictBehavior::Overwrite,
148 };
149
150 let table = Self::derive_table_catalog(
151 input.clone(),
152 name,
153 database_id,
154 schema_id,
155 user_distributed_by,
156 user_order_by,
157 columns,
158 definition,
159 conflict_behavior,
160 vec![],
161 None,
162 vec![],
163 None,
164 table_type,
165 None,
166 cardinality,
167 retention_seconds,
168 create_type,
169 None,
170 Engine::Hummock,
171 false,
172 )?;
173
174 Self::new(input, table)
175 }
176
177 #[expect(clippy::too_many_arguments)]
183 pub fn create_for_table(
184 input: PlanRef,
185 name: String,
186 database_id: DatabaseId,
187 schema_id: SchemaId,
188 user_distributed_by: RequiredDist,
189 user_order_by: Order,
190 columns: Vec<ColumnCatalog>,
191 definition: String,
192 conflict_behavior: ConflictBehavior,
193 version_column_indices: Vec<usize>,
194 pk_column_indices: Vec<usize>,
195 ttl_watermark_indices: Vec<usize>,
196 row_id_index: Option<usize>,
197 version: TableVersion,
198 retention_seconds: Option<NonZeroU32>,
199 webhook_info: Option<PbWebhookSourceInfo>,
200 engine: Engine,
201 refreshable: bool,
202 ) -> Result<Self> {
203 let input = Self::rewrite_input(input, user_distributed_by.clone(), TableType::Table)?;
204
205 let table = Self::derive_table_catalog(
206 input.clone(),
207 name.clone(),
208 database_id,
209 schema_id,
210 user_distributed_by,
211 user_order_by,
212 columns,
213 definition,
214 conflict_behavior,
215 version_column_indices,
216 Some(pk_column_indices),
217 ttl_watermark_indices,
218 row_id_index,
219 TableType::Table,
220 Some(version),
221 Cardinality::unknown(), retention_seconds,
223 CreateType::Foreground,
224 webhook_info,
225 engine,
226 refreshable,
227 )?;
228
229 let (staging_table, refresh_progress_table) = if refreshable {
231 let staging = Some(Self::derive_staging_table_catalog(table.clone()));
232 let progress = Some(Self::derive_refresh_progress_table_catalog(table.clone()));
233 (staging, progress)
234 } else {
235 (None, None)
236 };
237
238 tracing::info!(
239 table_name = %name,
240 refreshable = %refreshable,
241 has_staging_table = %staging_table.is_some(),
242 has_progress_table = %refresh_progress_table.is_some(),
243 "Creating StreamMaterialize with staging and progress table info"
244 );
245
246 Self::new_with_staging_and_progress(input, table, staging_table, refresh_progress_table)
247 }
248
249 fn rewrite_input(
251 input: PlanRef,
252 user_distributed_by: RequiredDist,
253 table_type: TableType,
254 ) -> Result<PlanRef> {
255 let required_dist = match input.distribution() {
256 Distribution::Single => RequiredDist::single(),
257 _ => match table_type {
258 TableType::Table => {
259 assert_matches!(
260 user_distributed_by,
261 RequiredDist::ShardByKey(_) | RequiredDist::ShardByExactKey(_)
262 );
263 user_distributed_by
264 }
265 TableType::MaterializedView => {
266 assert_matches!(user_distributed_by, RequiredDist::Any);
267 let required_dist =
269 RequiredDist::shard_by_key(input.schema().len(), input.expect_stream_key());
270
271 let is_stream_join = matches!(input.as_stream_hash_join(), Some(_join))
276 || matches!(input.as_stream_temporal_join(), Some(_join))
277 || matches!(input.as_stream_delta_join(), Some(_join));
278
279 if is_stream_join {
280 return Ok(required_dist.stream_enforce(input));
281 }
282
283 required_dist
284 }
285 TableType::Index => {
286 assert_matches!(
287 user_distributed_by,
288 RequiredDist::PhysicalDist(Distribution::HashShard(_))
289 );
290 user_distributed_by
291 }
292 TableType::VectorIndex => {
293 unreachable!("VectorIndex should not be created by StreamMaterialize")
294 }
295 TableType::Internal => unreachable!(),
296 },
297 };
298
299 required_dist.streaming_enforce_if_not_satisfies(input)
300 }
301
302 #[expect(clippy::too_many_arguments)]
307 fn derive_table_catalog(
308 rewritten_input: PlanRef,
309 name: String,
310 database_id: DatabaseId,
311 schema_id: SchemaId,
312 user_distributed_by: RequiredDist,
313 user_order_by: Order,
314 columns: Vec<ColumnCatalog>,
315 definition: String,
316 conflict_behavior: ConflictBehavior,
317 version_column_indices: Vec<usize>,
318 pk_column_indices: Option<Vec<usize>>, ttl_watermark_indices: Vec<usize>,
320 row_id_index: Option<usize>,
321 table_type: TableType,
322 version: Option<TableVersion>,
323 cardinality: Cardinality,
324 retention_seconds: Option<NonZeroU32>,
325 create_type: CreateType,
326 webhook_info: Option<PbWebhookSourceInfo>,
327 engine: Engine,
328 refreshable: bool,
329 ) -> Result<TableCatalog> {
330 let input = rewritten_input;
331
332 let value_indices = (0..columns.len()).collect_vec();
333 let distribution_key = input.distribution().dist_column_indices().to_vec();
334 let append_only = input.append_only();
335 let watermark_columns = input.watermark_columns().indices().collect();
338
339 let (table_pk, mut stream_key) = if let Some(pk_column_indices) = pk_column_indices {
340 let table_pk = pk_column_indices
341 .iter()
342 .map(|idx| ColumnOrder::new(*idx, OrderType::ascending()))
343 .collect();
344 (table_pk, pk_column_indices)
346 } else {
347 derive_pk(input, user_distributed_by, user_order_by, &columns)
348 };
349
350 for order in &table_pk {
353 let column = &columns[order.column_index];
354 if column.data_type().contains_variant() {
355 return Err(variant_key_error(format!(
356 "VARIANT column \"{}\" is part of the storage primary key",
357 column.name(),
358 )));
359 }
360 }
361
362 for idx in ttl_watermark_indices.iter().copied() {
367 if !stream_key.contains(&idx) {
368 stream_key.push(idx);
369 }
370 }
371
372 let read_prefix_len_hint = table_pk.len();
373 Ok(TableCatalog {
374 id: TableId::placeholder(),
375 schema_id,
376 database_id,
377 associated_source_id: None,
378 name,
379 columns,
380 pk: table_pk,
381 stream_key,
382 distribution_key,
383 table_type,
384 append_only,
385 owner: risingwave_common::catalog::DEFAULT_SUPER_USER_ID,
386 fragment_id: FragmentId::placeholder(),
387 dml_fragment_id: None,
388 vnode_col_index: None,
389 row_id_index,
390 value_indices,
391 definition,
392 conflict_behavior,
393 version_column_indices,
394 read_prefix_len_hint,
395 version,
396 watermark_columns,
397 dist_key_in_pk: vec![],
398 cardinality,
399 created_at_epoch: None,
400 initialized_at_epoch: None,
401 create_type,
402 stream_job_status: StreamJobStatus::Creating,
403 description: None,
404 initialized_at_cluster_version: None,
405 created_at_cluster_version: None,
406 retention_seconds: retention_seconds.map(|i| i.into()),
407 cdc_table_id: None,
408 vnode_count: VnodeCount::Placeholder, webhook_info,
410 job_id: None,
411 engine: match table_type {
412 TableType::Table => engine,
413 TableType::MaterializedView
414 | TableType::Index
415 | TableType::Internal
416 | TableType::VectorIndex => {
417 assert_eq!(engine, Engine::Hummock);
418 engine
419 }
420 },
421 clean_watermark_index_in_pk: None, clean_watermark_indices: ttl_watermark_indices,
423 refreshable,
424 vector_index_info: None,
425 cdc_table_type: None,
426 })
427 }
428
429 fn derive_staging_table_catalog(
431 TableCatalog {
432 id,
433 schema_id,
434 database_id,
435 associated_source_id,
436 name,
437 columns,
438 pk,
439 stream_key,
440 table_type: _,
441 distribution_key,
442 append_only,
443 cardinality,
444 owner,
445 retention_seconds,
446 fragment_id,
447 dml_fragment_id: _,
448 vnode_col_index,
449 row_id_index,
450 value_indices: _,
451 definition,
452 conflict_behavior,
453 version_column_indices,
454 read_prefix_len_hint: _,
455 version,
456 watermark_columns: _,
457 dist_key_in_pk,
458 created_at_epoch,
459 initialized_at_epoch,
460 create_type,
461 stream_job_status,
462 description,
463 created_at_cluster_version,
464 initialized_at_cluster_version,
465 cdc_table_id,
466 vnode_count,
467 webhook_info,
468 job_id,
469 engine,
470 clean_watermark_index_in_pk,
471 clean_watermark_indices,
472 refreshable,
473 vector_index_info,
474 cdc_table_type,
475 }: TableCatalog,
476 ) -> TableCatalog {
477 tracing::info!(
478 table_name = %name,
479 "Creating staging table for refreshable table"
480 );
481
482 assert!(row_id_index.is_none());
483 assert!(retention_seconds.is_none());
484 assert!(refreshable);
485
486 let pk_col_indices = pk.iter().map(|pk| pk.column_index).collect_vec();
489 let pk_cols = pk_col_indices
490 .iter()
491 .map(|&col_idx| columns[col_idx].clone())
492 .collect_vec();
493 let mapping = ColIndexMapping::with_remaining_columns(&pk_col_indices, columns.len());
494 TableCatalog {
495 id,
496 schema_id,
497 database_id,
498 associated_source_id,
499 name,
500 value_indices: (0..pk_cols.len()).collect(),
501 columns: pk_cols,
502 pk: pk
503 .iter()
504 .map(|pk| ColumnOrder::new(mapping.map(pk.column_index), pk.order_type))
505 .collect(),
506 stream_key: mapping.try_map_all(stream_key).unwrap(),
507 vnode_col_index: vnode_col_index.map(|i| mapping.map(i)),
508 dist_key_in_pk,
510 distribution_key: mapping.try_map_all(distribution_key).unwrap(),
511 table_type: TableType::Internal,
512 watermark_columns: FixedBitSet::new(),
513 append_only,
514 cardinality,
515 owner,
516 retention_seconds: None,
517 fragment_id,
518 dml_fragment_id: None,
519 row_id_index: None,
520 definition,
521 conflict_behavior,
522 version_column_indices,
523 read_prefix_len_hint: 0,
527 version,
528 created_at_epoch,
529 initialized_at_epoch,
530 create_type,
531 stream_job_status,
532 description,
533 created_at_cluster_version,
534 initialized_at_cluster_version,
535 cdc_table_id,
536 vnode_count,
537 webhook_info,
538 job_id,
539 engine,
540 clean_watermark_index_in_pk,
541 clean_watermark_indices,
542 refreshable: false,
543 vector_index_info,
544 cdc_table_type,
545 }
546 }
547
548 fn derive_refresh_progress_table_catalog(table: TableCatalog) -> TableCatalog {
552 tracing::debug!(
553 table_name = %table.name,
554 "Creating refresh progress table for refreshable table"
555 );
556
557 let mut columns = vec![ColumnCatalog {
560 column_desc: risingwave_common::catalog::ColumnDesc::named(
561 "vnode",
562 0.into(),
563 DataType::Int16,
564 ),
565 is_hidden: false,
566 }];
567
568 let mut col_index = 1;
570 for pk_col in &table.pk {
571 let upstream_col = &table.columns[pk_col.column_index];
572 columns.push(ColumnCatalog {
573 column_desc: risingwave_common::catalog::ColumnDesc::named(
574 format!("pos_{}", upstream_col.name()),
575 col_index.into(),
576 upstream_col.data_type().clone(),
577 ),
578 is_hidden: false,
579 });
580 col_index += 1;
581 }
582
583 for (name, data_type) in [
585 ("is_completed", DataType::Boolean),
586 ("processed_rows", DataType::Int64),
587 ] {
588 columns.push(ColumnCatalog {
589 column_desc: risingwave_common::catalog::ColumnDesc::named(
590 name,
591 col_index.into(),
592 data_type,
593 ),
594 is_hidden: false,
595 });
596 col_index += 1;
597 }
598
599 let mut builder = TableCatalogBuilder::default();
600
601 for column in &columns {
603 builder.add_column(&(&column.column_desc).into());
604 }
605
606 builder.add_order_column(0, OrderType::ascending());
608 builder.set_vnode_col_idx(0);
609 builder.set_value_indices((0..columns.len()).collect());
610 builder.set_dist_key_in_pk(vec![0]);
611
612 builder.build(vec![0], 1)
613 }
614
615 #[must_use]
617 pub fn table(&self) -> &TableCatalog {
618 &self.table
619 }
620
621 #[must_use]
623 pub fn staging_table(&self) -> Option<&TableCatalog> {
624 self.staging_table.as_ref()
625 }
626
627 #[must_use]
629 pub fn refresh_progress_table(&self) -> Option<&TableCatalog> {
630 self.refresh_progress_table.as_ref()
631 }
632
633 pub fn name(&self) -> &str {
634 self.table.name()
635 }
636}
637
638impl Distill for StreamMaterialize {
639 fn distill<'a>(&self) -> XmlNode<'a> {
640 let table = self.table();
641
642 let column_names = (table.columns.iter())
643 .map(|col| col.name_with_hidden().to_string())
644 .map(Pretty::from)
645 .collect();
646
647 let stream_key = (table.stream_key().iter())
648 .map(|&k| table.columns[k].name().to_owned())
649 .map(Pretty::from)
650 .collect();
651
652 let pk_columns = (table.pk.iter())
653 .map(|o| table.columns[o.column_index].name().to_owned())
654 .map(Pretty::from)
655 .collect();
656 let mut vec = Vec::with_capacity(5);
657 vec.push(("columns", Pretty::Array(column_names)));
658 vec.push(("stream_key", Pretty::Array(stream_key)));
659 vec.push(("pk_columns", Pretty::Array(pk_columns)));
660 let pk_conflict_behavior = self.table.conflict_behavior().debug_to_string();
661
662 vec.push(("pk_conflict", Pretty::from(pk_conflict_behavior)));
663
664 let watermark_columns = &self.base.watermark_columns();
665 if self.base.watermark_columns().n_indices() > 0 {
666 let watermark_column_names = watermark_columns
668 .indices()
669 .map(|i| table.columns()[i].name_with_hidden().to_string())
670 .map(Pretty::from)
671 .collect();
672 vec.push(("watermark_columns", Pretty::Array(watermark_column_names)));
673 };
674 childless_record("StreamMaterialize", vec)
675 }
676}
677
678impl PlanTreeNodeUnary<Stream> for StreamMaterialize {
679 fn input(&self) -> PlanRef {
680 self.input.clone()
681 }
682
683 fn clone_with_input(&self, input: PlanRef) -> Self {
684 let new = Self::new_with_staging_and_progress(
685 input,
686 self.table().clone(),
687 self.staging_table.clone(),
688 self.refresh_progress_table.clone(),
689 )
690 .unwrap();
691 new.base
692 .schema()
693 .fields
694 .iter()
695 .zip_eq_fast(self.base.schema().fields.iter())
696 .for_each(|(a, b)| {
697 assert_eq!(a.data_type, b.data_type);
698 });
699 assert_eq!(new.plan_base().stream_key(), self.plan_base().stream_key());
700 new
701 }
702}
703
704impl_plan_tree_node_for_unary! { Stream, StreamMaterialize }
705
706impl StreamNode for StreamMaterialize {
707 fn to_stream_prost_body(&self, state: &mut BuildFragmentGraphState) -> PbNodeBody {
708 use risingwave_pb::stream_plan::*;
709
710 tracing::debug!(
711 table_name = %self.table().name(),
712 refreshable = %self.table().refreshable,
713 has_staging_table = %self.staging_table.is_some(),
714 has_progress_table = %self.refresh_progress_table.is_some(),
715 staging_table_name = ?self.staging_table.as_ref().map(|t| (&t.id, &t.name)),
716 progress_table_name = ?self.refresh_progress_table.as_ref().map(|t| (&t.id, &t.name)),
717 "Converting StreamMaterialize to protobuf"
718 );
719
720 let staging_table_prost = self
721 .staging_table
722 .clone()
723 .map(|t| t.with_id(state.gen_table_id_wrapped()).to_prost());
724
725 let refresh_progress_table_prost = self
726 .refresh_progress_table
727 .clone()
728 .map(|t| t.with_id(state.gen_table_id_wrapped()).to_prost());
729
730 PbNodeBody::Materialize(Box::new(MaterializeNode {
731 table_id: 0.into(),
734 table: None,
735 staging_table: staging_table_prost,
737 refresh_progress_table: refresh_progress_table_prost,
739
740 column_orders: self
741 .table()
742 .pk()
743 .iter()
744 .copied()
745 .map(ColumnOrder::to_protobuf)
746 .collect(),
747
748 cleaned_by_ttl_watermark: !self.table.clean_watermark_indices.is_empty(),
751 }))
752 }
753}
754
755impl ExprRewritable<Stream> for StreamMaterialize {}
756
757impl ExprVisitable for StreamMaterialize {}