1use std::assert_matches;
16use std::collections::{HashMap, HashSet};
17
18use fixedbitset::FixedBitSet;
19use itertools::Itertools;
20use risingwave_common::catalog::{
21 ColumnCatalog, ColumnDesc, ConflictBehavior, CreateType, Engine, Field, ICEBERG_SINK_PREFIX,
22 ICEBERG_SOURCE_PREFIX, Schema, StreamJobStatus, TableDesc, TableId, TableVersionId,
23};
24use risingwave_common::hash::{VnodeCount, VnodeCountCompat};
25use risingwave_common::id::{JobId, SourceId};
26use risingwave_common::util::epoch::Epoch;
27use risingwave_common::util::sort_util::ColumnOrder;
28use risingwave_connector::source::cdc::external::ExternalCdcTableType;
29use risingwave_pb::catalog::table::{
30 CdcTableType as PbCdcTableType, PbEngine, PbTableType, PbTableVersion,
31};
32use risingwave_pb::catalog::{
33 PbCreateType, PbStreamJobStatus, PbTable, PbVectorIndexInfo, PbWebhookSourceInfo,
34};
35use risingwave_pb::common::PbColumnOrder;
36use risingwave_pb::plan_common::DefaultColumnDesc;
37use risingwave_pb::plan_common::column_desc::GeneratedOrDefaultColumn;
38use risingwave_sqlparser::ast;
39use risingwave_sqlparser::parser::Parser;
40use thiserror_ext::AsReport as _;
41
42use super::purify::try_purify_table_source_create_sql_ast;
43use super::{ColumnId, DatabaseId, FragmentId, OwnedByUserCatalog, SchemaId};
44use crate::catalog::RelationKind;
45use crate::error::{Result, RwError};
46use crate::expr::ExprImpl;
47use crate::optimizer::property::Cardinality;
48use crate::session::current::notice_to_user;
49use crate::user::UserId;
50
51#[derive(Clone, Debug, PartialEq, Eq, Hash)]
83#[cfg_attr(test, derive(Default))]
84pub struct TableCatalog {
85 pub id: TableId,
86
87 pub schema_id: SchemaId,
88
89 pub database_id: DatabaseId,
90
91 pub associated_source_id: Option<SourceId>, pub name: String,
94
95 pub columns: Vec<ColumnCatalog>,
97
98 pub pk: Vec<ColumnOrder>,
100
101 pub stream_key: Vec<usize>,
105
106 pub table_type: TableType,
109
110 pub distribution_key: Vec<usize>,
112
113 pub append_only: bool,
116
117 pub cardinality: Cardinality,
119
120 pub owner: UserId,
122
123 pub retention_seconds: Option<u32>,
125
126 pub fragment_id: FragmentId,
128
129 pub dml_fragment_id: Option<FragmentId>,
131
132 pub vnode_col_index: Option<usize>,
135
136 pub row_id_index: Option<usize>,
139
140 pub value_indices: Vec<usize>,
142
143 pub definition: String,
145
146 pub conflict_behavior: ConflictBehavior,
150
151 pub version_column_indices: Vec<usize>,
152
153 pub read_prefix_len_hint: usize,
154
155 pub version: Option<TableVersion>,
157
158 pub watermark_columns: FixedBitSet,
160
161 pub dist_key_in_pk: Vec<usize>,
164
165 pub created_at_epoch: Option<Epoch>,
166
167 pub initialized_at_epoch: Option<Epoch>,
168
169 pub create_type: CreateType,
171
172 pub stream_job_status: StreamJobStatus,
175
176 pub description: Option<String>,
178
179 pub created_at_cluster_version: Option<String>,
180
181 pub initialized_at_cluster_version: Option<String>,
182
183 pub cdc_table_id: Option<String>,
184
185 pub vnode_count: VnodeCount,
194
195 pub webhook_info: Option<PbWebhookSourceInfo>,
196
197 pub job_id: Option<JobId>,
198
199 pub engine: Engine,
200
201 pub clean_watermark_index_in_pk: Option<usize>,
202
203 pub clean_watermark_indices: Vec<usize>,
207
208 pub refreshable: bool,
210
211 pub vector_index_info: Option<PbVectorIndexInfo>,
212
213 pub cdc_table_type: Option<ExternalCdcTableType>,
214}
215
216#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
217#[cfg_attr(test, derive(Default))]
218pub enum TableType {
219 #[cfg_attr(test, default)]
221 Table,
222 MaterializedView,
224 Index,
227 VectorIndex,
228 Internal,
230}
231
232impl TableType {
233 fn from_prost(prost: PbTableType) -> Self {
234 match prost {
235 PbTableType::Table => Self::Table,
236 PbTableType::MaterializedView => Self::MaterializedView,
237 PbTableType::Index => Self::Index,
238 PbTableType::Internal => Self::Internal,
239 PbTableType::VectorIndex => Self::VectorIndex,
240 PbTableType::Unspecified => unreachable!(),
241 }
242 }
243
244 pub(crate) fn to_prost(self) -> PbTableType {
245 match self {
246 Self::Table => PbTableType::Table,
247 Self::MaterializedView => PbTableType::MaterializedView,
248 Self::Index => PbTableType::Index,
249 Self::VectorIndex => PbTableType::VectorIndex,
250 Self::Internal => PbTableType::Internal,
251 }
252 }
253}
254
255#[derive(Clone, Debug, PartialEq, Eq, Hash)]
257pub struct TableVersion {
258 pub version_id: TableVersionId,
259 pub next_column_id: ColumnId,
260}
261
262impl TableVersion {
263 #[cfg(test)]
265 pub fn new_initial_for_test(max_column_id: ColumnId) -> Self {
266 use risingwave_common::catalog::INITIAL_TABLE_VERSION_ID;
267
268 Self {
269 version_id: INITIAL_TABLE_VERSION_ID,
270 next_column_id: max_column_id.next(),
271 }
272 }
273
274 pub fn from_prost(prost: PbTableVersion) -> Self {
275 Self {
276 version_id: prost.version,
277 next_column_id: ColumnId::from(prost.next_column_id),
278 }
279 }
280
281 pub fn to_prost(&self) -> PbTableVersion {
282 PbTableVersion {
283 version: self.version_id,
284 next_column_id: self.next_column_id.into(),
285 }
286 }
287}
288
289impl From<TableType> for RelationKind {
290 fn from(table_type: TableType) -> Self {
291 match table_type {
292 TableType::Table => Self::Table,
293 TableType::MaterializedView => Self::MaterializedView,
294 TableType::Index | TableType::VectorIndex => Self::Index,
295 TableType::Internal => Self::Internal,
296 }
297 }
298}
299
300impl TableCatalog {
301 pub fn create_sql_purified(&self) -> String {
306 self.create_sql_ast_purified()
307 .and_then(|stmt| stmt.try_to_string().map_err(Into::into))
308 .unwrap_or_else(|_| self.create_sql())
309 }
310
311 pub fn create_sql_ast_purified(&self) -> Result<ast::Statement> {
316 if let TableType::Table = self.table_type() {
318 let base = if self.definition.is_empty() {
319 let name = ast::ObjectName(vec![self.name.as_str().into()]);
321 ast::Statement::default_create_table(name)
322 } else {
323 self.create_sql_ast_from_persisted()?
324 };
325
326 match try_purify_table_source_create_sql_ast(
327 base,
328 self.columns(),
329 self.row_id_index,
330 &self.pk_column_ids(),
331 ) {
332 Ok(stmt) => return Ok(stmt),
333 Err(e) => notice_to_user(format!(
334 "error occurred while purifying definition for table \"{}\", \
335 results may be inaccurate: {}",
336 self.name,
337 e.as_report()
338 )),
339 }
340 }
341
342 self.create_sql_ast_from_persisted()
343 }
344}
345
346impl TableCatalog {
347 pub fn id(&self) -> TableId {
349 self.id
350 }
351
352 pub fn with_id(mut self, id: TableId) -> Self {
353 self.id = id;
354 self
355 }
356
357 pub fn conflict_behavior(&self) -> ConflictBehavior {
358 self.conflict_behavior
359 }
360
361 pub fn table_type(&self) -> TableType {
362 self.table_type
363 }
364
365 pub fn engine(&self) -> Engine {
366 self.engine
367 }
368
369 pub fn iceberg_source_name(&self) -> Option<String> {
370 match self.engine {
371 Engine::Iceberg => Some(format!("{}{}", ICEBERG_SOURCE_PREFIX, self.name)),
372 Engine::Hummock => None,
373 }
374 }
375
376 pub fn iceberg_sink_name(&self) -> Option<String> {
377 match self.engine {
378 Engine::Iceberg => Some(format!("{}{}", ICEBERG_SINK_PREFIX, self.name)),
379 Engine::Hummock => None,
380 }
381 }
382
383 pub fn is_user_table(&self) -> bool {
384 self.table_type == TableType::Table
385 }
386
387 pub fn is_internal_table(&self) -> bool {
388 self.table_type == TableType::Internal
389 }
390
391 pub fn is_mview(&self) -> bool {
392 self.table_type == TableType::MaterializedView
393 }
394
395 pub fn is_index(&self) -> bool {
396 self.table_type == TableType::Index
397 }
398
399 #[must_use]
401 pub fn bad_drop_error(&self) -> RwError {
402 RelationKind::from(self.table_type).bad_drop_error().into()
403 }
404
405 #[must_use]
407 pub fn associated_source_id(&self) -> Option<SourceId> {
408 self.associated_source_id
409 }
410
411 pub fn has_associated_source(&self) -> bool {
412 self.associated_source_id.is_some()
413 }
414
415 pub fn columns(&self) -> &[ColumnCatalog] {
417 &self.columns
418 }
419
420 pub fn columns_without_rw_timestamp(&self) -> Vec<ColumnCatalog> {
421 self.columns
422 .iter()
423 .filter(|c| !c.is_rw_timestamp_column())
424 .cloned()
425 .collect()
426 }
427
428 pub fn pk(&self) -> &[ColumnOrder] {
430 self.pk.as_ref()
431 }
432
433 pub fn pk_column_ids(&self) -> Vec<ColumnId> {
435 self.pk
436 .iter()
437 .map(|x| self.columns[x.column_index].column_id())
438 .collect()
439 }
440
441 pub fn pk_column_names(&self) -> Vec<&str> {
443 self.pk
444 .iter()
445 .map(|x| self.columns[x.column_index].name())
446 .collect()
447 }
448
449 pub fn stream_key(&self) -> Vec<usize> {
453 if self
455 .distribution_key
456 .iter()
457 .any(|dist_key| !self.stream_key.contains(dist_key))
458 {
459 let mut new_stream_key = self.distribution_key.clone();
460 let mut seen: HashSet<usize> = self.distribution_key.iter().copied().collect();
461 for &key in &self.stream_key {
462 if seen.insert(key) {
463 new_stream_key.push(key);
464 }
465 }
466 new_stream_key
467 } else {
468 self.stream_key.clone()
469 }
470 }
471
472 pub fn table_desc(&self) -> TableDesc {
477 use risingwave_common::catalog::TableOption;
478
479 let table_options = TableOption::new(self.retention_seconds);
480
481 TableDesc {
482 table_id: self.id,
483 pk: self.pk.clone(),
484 stream_key: self.stream_key(),
485 columns: self.columns.iter().map(|c| c.column_desc.clone()).collect(),
486 distribution_key: self.distribution_key.clone(),
487 append_only: self.append_only,
488 retention_seconds: table_options.retention_seconds,
489 value_indices: self.value_indices.clone(),
490 read_prefix_len_hint: self.read_prefix_len_hint,
491 watermark_columns: self.watermark_columns.clone(),
492 versioned: self.version.is_some(),
493 vnode_col_index: self.vnode_col_index,
494 vnode_count: self.vnode_count(),
495 }
496 }
497
498 pub fn name(&self) -> &str {
500 self.name.as_ref()
501 }
502
503 pub fn distribution_key(&self) -> &[usize] {
504 self.distribution_key.as_ref()
505 }
506
507 pub fn to_internal_table_prost(&self) -> PbTable {
508 self.to_prost()
509 }
510
511 pub fn create_sql(&self) -> String {
515 self.create_sql_ast()
516 .and_then(|stmt| stmt.try_to_string().map_err(Into::into))
517 .unwrap_or_else(|_| self.definition.clone())
518 }
519
520 pub fn create_sql_ast(&self) -> Result<ast::Statement> {
528 if let TableType::Table = self.table_type()
529 && self.definition.is_empty()
530 {
531 self.create_sql_ast_purified()
533 } else {
534 self.create_sql_ast_from_persisted()
536 }
537 }
538
539 fn create_sql_ast_from_persisted(&self) -> Result<ast::Statement> {
540 Ok(Parser::parse_exactly_one(&self.definition)?)
541 }
542
543 pub fn version(&self) -> Option<&TableVersion> {
545 self.version.as_ref()
546 }
547
548 pub fn version_id(&self) -> Option<TableVersionId> {
550 self.version().map(|v| v.version_id)
551 }
552
553 pub fn vnode_count(&self) -> usize {
555 if self.id().is_placeholder() {
556 0
557 } else {
558 self.vnode_count.value()
560 }
561 }
562
563 pub fn to_prost(&self) -> PbTable {
564 PbTable {
565 id: self.id,
566 schema_id: self.schema_id,
567 database_id: self.database_id,
568 name: self.name.clone(),
569 columns: self
571 .columns_without_rw_timestamp()
572 .iter()
573 .map(|c| c.to_protobuf())
574 .collect(),
575 pk: self.pk.iter().map(|o| o.to_protobuf()).collect(),
576 stream_key: self.stream_key().iter().map(|x| *x as _).collect(),
577 optional_associated_source_id: self.associated_source_id.map(Into::into),
578 table_type: self.table_type.to_prost() as i32,
579 distribution_key: self
580 .distribution_key
581 .iter()
582 .map(|k| *k as i32)
583 .collect_vec(),
584 append_only: self.append_only,
585 owner: self.owner,
586 fragment_id: self.fragment_id,
587 dml_fragment_id: self.dml_fragment_id,
588 vnode_col_index: self.vnode_col_index.map(|i| i as _),
589 row_id_index: self.row_id_index.map(|i| i as _),
590 value_indices: self.value_indices.iter().map(|x| *x as _).collect(),
591 definition: self.definition.clone(),
592 read_prefix_len_hint: self.read_prefix_len_hint as u32,
593 version: self.version.as_ref().map(TableVersion::to_prost),
594 watermark_indices: self.watermark_columns.ones().map(|x| x as _).collect_vec(),
595 dist_key_in_pk: self.dist_key_in_pk.iter().map(|x| *x as _).collect(),
596 handle_pk_conflict_behavior: self.conflict_behavior.to_protobuf().into(),
597 version_column_indices: self
598 .version_column_indices
599 .iter()
600 .map(|&idx| idx as u32)
601 .collect(),
602 cardinality: Some(self.cardinality.to_protobuf()),
603 initialized_at_epoch: self.initialized_at_epoch.map(|epoch| epoch.0),
604 created_at_epoch: self.created_at_epoch.map(|epoch| epoch.0),
605 #[expect(deprecated)]
606 cleaned_by_watermark: false,
607 stream_job_status: self.stream_job_status.to_proto().into(),
608 create_type: self.create_type.to_proto().into(),
609 description: self.description.clone(),
610 #[expect(deprecated)]
611 incoming_sinks: vec![],
612 created_at_cluster_version: self.created_at_cluster_version.clone(),
613 initialized_at_cluster_version: self.initialized_at_cluster_version.clone(),
614 retention_seconds: self.retention_seconds,
615 cdc_table_id: self.cdc_table_id.clone(),
616 maybe_vnode_count: self.vnode_count.to_protobuf(),
617 webhook_info: self.webhook_info.clone(),
618 job_id: self.job_id,
619 engine: Some(self.engine.to_protobuf().into()),
620 #[expect(deprecated)]
621 clean_watermark_index_in_pk: self.clean_watermark_index_in_pk.map(|x| x as i32),
622 clean_watermark_indices: self
623 .clean_watermark_indices
624 .iter()
625 .map(|&x| x as u32)
626 .collect(),
627 refreshable: self.refreshable,
628 vector_index_info: self.vector_index_info,
629 cdc_table_type: self
630 .cdc_table_type
631 .clone()
632 .map(|t| PbCdcTableType::from(t) as i32),
633 }
634 }
635
636 pub fn columns_to_insert(
642 &self,
643 ) -> (
644 impl Iterator<Item = (&ColumnCatalog, bool)> + '_,
645 Option<usize>,
646 ) {
647 let pk_column_indices: HashSet<_> =
648 self.pk.iter().map(|column| column.column_index).collect();
649 let columns = self
650 .columns
651 .iter()
652 .enumerate()
653 .filter(|(_, c)| !c.is_hidden() && !c.is_generated())
654 .map(move |(idx, column)| (column, pk_column_indices.contains(&idx)));
655 let row_id_index = self.row_id_index.map(|row_id_index| {
656 let generated_column_count = self
657 .columns()
658 .iter()
659 .take(row_id_index + 1)
660 .filter(|column| column.is_generated())
661 .count();
662 row_id_index - generated_column_count
663 });
664 (columns, row_id_index)
665 }
666
667 pub fn generated_column_names(&self) -> impl Iterator<Item = &str> {
668 self.columns
669 .iter()
670 .filter(|c| c.is_generated())
671 .map(|c| c.name())
672 }
673
674 pub fn first_generated_column(&self) -> Option<(usize, &str)> {
675 self.columns
676 .iter()
677 .filter(|c| !c.is_hidden())
678 .enumerate()
679 .find(|(_, c)| c.is_generated())
680 .map(|(idx, c)| (idx, c.name()))
681 }
682
683 pub fn generated_col_idxes(&self) -> impl Iterator<Item = usize> + '_ {
684 self.columns
685 .iter()
686 .enumerate()
687 .filter(|(_, c)| c.is_generated())
688 .map(|(i, _)| i)
689 }
690
691 pub fn default_column_expr(&self, col_idx: usize) -> ExprImpl {
692 if let Some(GeneratedOrDefaultColumn::DefaultColumn(DefaultColumnDesc { expr, .. })) = self
693 .columns[col_idx]
694 .column_desc
695 .generated_or_default_column
696 .as_ref()
697 {
698 ExprImpl::from_expr_proto(expr.as_ref().unwrap())
699 .expect("expr in default columns corrupted")
700 } else {
701 ExprImpl::literal_null(self.columns[col_idx].data_type().clone())
702 }
703 }
704
705 pub fn default_column_exprs(columns: &[ColumnCatalog]) -> Vec<ExprImpl> {
706 columns
707 .iter()
708 .map(|c| {
709 if let Some(GeneratedOrDefaultColumn::DefaultColumn(DefaultColumnDesc {
710 expr,
711 ..
712 })) = c.column_desc.generated_or_default_column.as_ref()
713 {
714 ExprImpl::from_expr_proto(expr.as_ref().unwrap())
715 .expect("expr in default columns corrupted")
716 } else {
717 ExprImpl::literal_null(c.data_type().clone())
718 }
719 })
720 .collect()
721 }
722
723 pub fn default_columns(&self) -> impl Iterator<Item = (usize, ExprImpl)> + '_ {
724 self.columns.iter().enumerate().filter_map(|(i, c)| {
725 if let Some(GeneratedOrDefaultColumn::DefaultColumn(DefaultColumnDesc {
726 expr, ..
727 })) = c.column_desc.generated_or_default_column.as_ref()
728 {
729 Some((
730 i,
731 ExprImpl::from_expr_proto(expr.as_ref().unwrap())
732 .expect("expr in default columns corrupted"),
733 ))
734 } else {
735 None
736 }
737 })
738 }
739
740 pub fn has_generated_column(&self) -> bool {
741 self.columns.iter().any(|c| c.is_generated())
742 }
743
744 pub fn has_rw_timestamp_column(&self) -> bool {
745 self.columns.iter().any(|c| c.is_rw_timestamp_column())
746 }
747
748 pub fn column_schema(&self) -> Schema {
749 Schema::new(
750 self.columns
751 .iter()
752 .map(|c| Field::from(&c.column_desc))
753 .collect(),
754 )
755 }
756
757 pub fn is_created(&self) -> bool {
758 self.stream_job_status == StreamJobStatus::Created
759 }
760
761 pub fn is_iceberg_engine_table(&self) -> bool {
762 self.engine == Engine::Iceberg
763 }
764
765 pub fn order_column_indices(&self) -> impl Iterator<Item = usize> + '_ {
766 self.pk.iter().map(|col| col.column_index)
767 }
768
769 pub fn get_id_to_op_idx_mapping(&self) -> HashMap<ColumnId, usize> {
770 ColumnDesc::get_id_to_op_idx_mapping(&self.columns, None)
771 }
772
773 pub fn order_column_ids(&self) -> Vec<ColumnId> {
774 self.pk
775 .iter()
776 .map(|col| self.columns[col.column_index].column_desc.column_id)
777 .collect()
778 }
779
780 pub fn arrange_key_orders_protobuf(&self) -> Vec<PbColumnOrder> {
781 self.pk.iter().map(|x| x.to_protobuf()).collect()
783 }
784}
785
786impl From<PbTable> for TableCatalog {
787 fn from(tb: PbTable) -> Self {
788 let id = tb.id;
789 let tb_conflict_behavior = tb.handle_pk_conflict_behavior();
790 let tb_engine = tb
791 .get_engine()
792 .map(|engine| PbEngine::try_from(*engine).expect("Invalid engine"))
793 .unwrap_or(PbEngine::Hummock);
794 let table_type = tb.get_table_type().unwrap();
795 let stream_job_status = tb
796 .get_stream_job_status()
797 .unwrap_or(PbStreamJobStatus::Created);
798 let create_type = tb.get_create_type().unwrap_or(PbCreateType::Foreground);
799 let associated_source_id = tb.optional_associated_source_id.map(Into::into);
800 let name = tb.name.clone();
801
802 let vnode_count = tb.vnode_count_inner();
803 if let VnodeCount::Placeholder = vnode_count {
804 assert_matches!(stream_job_status, PbStreamJobStatus::Creating);
807 }
808
809 let mut col_names = HashSet::new();
810 let mut col_index: HashMap<i32, usize> = HashMap::new();
811
812 let conflict_behavior = ConflictBehavior::from_protobuf(&tb_conflict_behavior);
813 let version_column_indices: Vec<usize> = tb
814 .version_column_indices
815 .iter()
816 .map(|&idx| idx as usize)
817 .collect();
818 let mut columns: Vec<ColumnCatalog> =
819 tb.columns.into_iter().map(ColumnCatalog::from).collect();
820 if columns.iter().all(|c| !c.is_rw_timestamp_column()) {
821 columns.push(ColumnCatalog::rw_timestamp_column());
823 }
824 for (idx, catalog) in columns.clone().into_iter().enumerate() {
825 let col_name = catalog.name();
826 if !col_names.insert(col_name.to_owned()) {
827 panic!("duplicated column name {} in table {} ", col_name, tb.name)
828 }
829
830 let col_id = catalog.column_desc.column_id.get_id();
831 col_index.insert(col_id, idx);
832 }
833
834 let pk = tb.pk.iter().map(ColumnOrder::from_protobuf).collect();
835 let mut watermark_columns = FixedBitSet::with_capacity(columns.len());
836 for idx in &tb.watermark_indices {
837 watermark_columns.insert(*idx as _);
838 }
839 let engine = Engine::from_protobuf(&tb_engine);
840
841 Self {
842 id,
843 schema_id: tb.schema_id,
844 database_id: tb.database_id,
845 associated_source_id,
846 name,
847 pk,
848 columns,
849 table_type: TableType::from_prost(table_type),
850 distribution_key: tb
851 .distribution_key
852 .iter()
853 .map(|k| *k as usize)
854 .collect_vec(),
855 stream_key: tb.stream_key.iter().map(|x| *x as _).collect(),
856 append_only: tb.append_only,
857 owner: tb.owner,
858 fragment_id: tb.fragment_id,
859 dml_fragment_id: tb.dml_fragment_id,
860 vnode_col_index: tb.vnode_col_index.map(|x| x as usize),
861 row_id_index: tb.row_id_index.map(|x| x as usize),
862 value_indices: tb.value_indices.iter().map(|x| *x as _).collect(),
863 definition: tb.definition,
864 conflict_behavior,
865 version_column_indices,
866 read_prefix_len_hint: tb.read_prefix_len_hint as usize,
867 version: tb.version.map(TableVersion::from_prost),
868 watermark_columns,
869 dist_key_in_pk: tb.dist_key_in_pk.iter().map(|x| *x as _).collect(),
870 cardinality: tb
871 .cardinality
872 .map(|c| Cardinality::from_protobuf(&c))
873 .unwrap_or_else(Cardinality::unknown),
874 created_at_epoch: tb.created_at_epoch.map(Epoch::from),
875 initialized_at_epoch: tb.initialized_at_epoch.map(Epoch::from),
876 create_type: CreateType::from_proto(create_type),
877 stream_job_status: StreamJobStatus::from_proto(stream_job_status),
878 description: tb.description,
879 created_at_cluster_version: tb.created_at_cluster_version.clone(),
880 initialized_at_cluster_version: tb.initialized_at_cluster_version.clone(),
881 retention_seconds: tb.retention_seconds,
882 cdc_table_id: tb.cdc_table_id,
883 vnode_count,
884 webhook_info: tb.webhook_info,
885 job_id: tb.job_id,
886 engine,
887 #[expect(deprecated)]
888 clean_watermark_index_in_pk: tb.clean_watermark_index_in_pk.map(|x| x as usize),
889 clean_watermark_indices: tb
890 .clean_watermark_indices
891 .iter()
892 .map(|&x| x as usize)
893 .collect(),
894
895 refreshable: tb.refreshable,
896 vector_index_info: tb.vector_index_info,
897 cdc_table_type: tb
898 .cdc_table_type
899 .and_then(|t| PbCdcTableType::try_from(t).ok())
900 .map(ExternalCdcTableType::from),
901 }
902 }
903}
904
905impl From<&PbTable> for TableCatalog {
906 fn from(tb: &PbTable) -> Self {
907 tb.clone().into()
908 }
909}
910
911impl OwnedByUserCatalog for TableCatalog {
912 fn owner(&self) -> UserId {
913 self.owner
914 }
915}
916
917#[cfg(test)]
918mod tests {
919 use risingwave_common::catalog::{ColumnDesc, ColumnId};
920 use risingwave_common::test_prelude::*;
921 use risingwave_common::types::*;
922 use risingwave_common::util::sort_util::OrderType;
923 use risingwave_pb::catalog::table::PbEngine;
924 use risingwave_pb::plan_common::{
925 AdditionalColumn, ColumnDescVersion, PbColumnCatalog, PbColumnDesc,
926 };
927
928 use super::*;
929
930 #[test]
931 fn test_into_table_catalog() {
932 let table: TableCatalog = PbTable {
933 id: 0.into(),
934 schema_id: 0.into(),
935 database_id: 0.into(),
936 name: "test".to_owned(),
937 table_type: PbTableType::Table as i32,
938 columns: vec![
939 ColumnCatalog::row_id_column().to_protobuf(),
940 PbColumnCatalog {
941 column_desc: Some(PbColumnDesc::new(
942 DataType::from(StructType::new([
943 ("address", DataType::Varchar),
944 ("zipcode", DataType::Varchar),
945 ]))
946 .to_protobuf(),
947 "country",
948 1,
949 )),
950 is_hidden: false,
951 },
952 ],
953 pk: vec![ColumnOrder::new(0, OrderType::ascending()).to_protobuf()],
954 stream_key: vec![0],
955 distribution_key: vec![0],
956 optional_associated_source_id: Some(SourceId::new(233).into()),
957 append_only: false,
958 owner: risingwave_common::catalog::DEFAULT_SUPER_USER_ID,
959 retention_seconds: Some(300),
960 fragment_id: 0.into(),
961 dml_fragment_id: None,
962 initialized_at_epoch: None,
963 value_indices: vec![0],
964 definition: "".into(),
965 read_prefix_len_hint: 0,
966 vnode_col_index: None,
967 row_id_index: None,
968 version: Some(PbTableVersion {
969 version: 0,
970 next_column_id: 2,
971 }),
972 watermark_indices: vec![],
973 handle_pk_conflict_behavior: 3,
974 dist_key_in_pk: vec![0],
975 cardinality: None,
976 created_at_epoch: None,
977 #[expect(deprecated)]
978 cleaned_by_watermark: false,
979 stream_job_status: PbStreamJobStatus::Created.into(),
980 create_type: PbCreateType::Foreground.into(),
981 description: Some("description".to_owned()),
982 #[expect(deprecated)]
983 incoming_sinks: vec![],
984 created_at_cluster_version: None,
985 initialized_at_cluster_version: None,
986 version_column_indices: Vec::new(),
987 cdc_table_id: None,
988 maybe_vnode_count: VnodeCount::set(233).to_protobuf(),
989 webhook_info: None,
990 job_id: None,
991 engine: Some(PbEngine::Hummock as i32),
992 #[expect(deprecated)]
993 clean_watermark_index_in_pk: None,
994 clean_watermark_indices: vec![],
995
996 refreshable: false,
997 vector_index_info: None,
998 cdc_table_type: None,
999 }
1000 .into();
1001
1002 assert_eq!(
1003 table,
1004 TableCatalog {
1005 id: TableId::new(0),
1006 schema_id: 0.into(),
1007 database_id: 0.into(),
1008 associated_source_id: Some(SourceId::new(233)),
1009 name: "test".to_owned(),
1010 table_type: TableType::Table,
1011 columns: vec![
1012 ColumnCatalog::row_id_column(),
1013 ColumnCatalog {
1014 column_desc: ColumnDesc {
1015 data_type: StructType::new(vec![
1016 ("address", DataType::Varchar),
1017 ("zipcode", DataType::Varchar)
1018 ],)
1019 .into(),
1020 column_id: ColumnId::new(1),
1021 name: "country".to_owned(),
1022 description: None,
1023 generated_or_default_column: None,
1024 additional_column: AdditionalColumn { column_type: None },
1025 version: ColumnDescVersion::LATEST,
1026 system_column: None,
1027 nullable: true,
1028 },
1029 is_hidden: false
1030 },
1031 ColumnCatalog::rw_timestamp_column(),
1032 ],
1033 stream_key: vec![0],
1034 pk: vec![ColumnOrder::new(0, OrderType::ascending())],
1035 distribution_key: vec![0],
1036 append_only: false,
1037 owner: risingwave_common::catalog::DEFAULT_SUPER_USER_ID,
1038 retention_seconds: Some(300),
1039 fragment_id: 0.into(),
1040 dml_fragment_id: None,
1041 vnode_col_index: None,
1042 row_id_index: None,
1043 value_indices: vec![0],
1044 definition: "".into(),
1045 conflict_behavior: ConflictBehavior::NoCheck,
1046 read_prefix_len_hint: 0,
1047 version: Some(TableVersion::new_initial_for_test(ColumnId::new(1))),
1048 watermark_columns: FixedBitSet::with_capacity(3),
1049 dist_key_in_pk: vec![0],
1050 cardinality: Cardinality::unknown(),
1051 created_at_epoch: None,
1052 initialized_at_epoch: None,
1053 stream_job_status: StreamJobStatus::Created,
1054 create_type: CreateType::Foreground,
1055 description: Some("description".to_owned()),
1056 created_at_cluster_version: None,
1057 initialized_at_cluster_version: None,
1058 version_column_indices: Vec::new(),
1059 cdc_table_id: None,
1060 vnode_count: VnodeCount::set(233),
1061 webhook_info: None,
1062 job_id: None,
1063 engine: Engine::Hummock,
1064 clean_watermark_index_in_pk: None,
1065 clean_watermark_indices: vec![],
1066
1067 refreshable: false,
1068 vector_index_info: None,
1069 cdc_table_type: None,
1070 }
1071 );
1072 assert_eq!(table, TableCatalog::from(table.to_prost()));
1073 }
1074}