Skip to main content

risingwave_frontend/catalog/
table_catalog.rs

1// Copyright 2022 RisingWave Labs
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15use 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/// `TableCatalog` Includes full information about a table.
52///
53/// Here `Table` is an internal concept, corresponding to _a table in storage_, all of which can be `SELECT`ed.
54/// It is not the same as a user-side table created by `CREATE TABLE`.
55///
56/// Use [`Self::table_type()`] to determine the [`TableType`] of the table.
57///
58/// # Column ID & Column Index
59///
60/// [`ColumnId`](risingwave_common::catalog::ColumnId) (with type `i32`) is the unique identifier of
61/// a column in a table. It is used to access storage.
62///
63/// Column index, or idx, (with type `usize`) is the relative position inside the `Vec` of columns.
64///
65/// A tip to avoid making mistakes is never do casting - i32 as usize or vice versa.
66///
67/// # Keys
68///
69/// All the keys are represented as column indices.
70///
71/// - **Primary Key** (pk): unique identifier of a row.
72///
73/// - **Order Key**: the primary key for storage, used to sort and access data.
74///
75///   For an MV, the columns in `ORDER BY` clause will be put at the beginning of the order key
76///   until the stream key is covered. The remaining stream-key columns will follow behind.
77///
78///   If there's no `ORDER BY` clause, the order key will be the same as pk.
79///
80/// - **Distribution Key**: the columns used to partition the data. It must be a subset of the order
81///   key.
82#[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>, // TODO: use SourceId
92
93    pub name: String,
94
95    /// All columns in this table.
96    pub columns: Vec<ColumnCatalog>,
97
98    /// Key used as materialize's storage key prefix, including MV order columns and `stream_key`.
99    pub pk: Vec<ColumnOrder>,
100
101    /// `pk_indices` of the corresponding materialize operator's output.
102    /// For the backward compatibility, we should use `stream_key()` method to get the stream key.
103    /// never use this field directly.
104    pub stream_key: Vec<usize>,
105
106    /// Type of the table. Used to distinguish user-created tables, materialized views, index
107    /// tables, and internal tables.
108    pub table_type: TableType,
109
110    /// Distribution key column indices.
111    pub distribution_key: Vec<usize>,
112
113    /// The append-only attribute is derived from `StreamMaterialize` and `StreamTableScan` relies
114    /// on this to derive an append-only stream plan.
115    pub append_only: bool,
116
117    /// The cardinality of the table.
118    pub cardinality: Cardinality,
119
120    /// Owner of the table.
121    pub owner: UserId,
122
123    // TTL of the record in the table, to ensure the consistency with other tables in the streaming plan, it only applies to append-only tables.
124    pub retention_seconds: Option<u32>,
125
126    /// The fragment id of the `Materialize` operator for this table.
127    pub fragment_id: FragmentId,
128
129    /// The fragment id of the `DML` operator for this table.
130    pub dml_fragment_id: Option<FragmentId>,
131
132    /// An optional column index which is the vnode of each row computed by the table's consistent
133    /// hash distribution.
134    pub vnode_col_index: Option<usize>,
135
136    /// An optional column index of row id. If the primary key is specified by users, this will be
137    /// `None`.
138    pub row_id_index: Option<usize>,
139
140    /// The column indices which are stored in the state store's value with row-encoding.
141    pub value_indices: Vec<usize>,
142
143    /// The full `CREATE TABLE` or `CREATE MATERIALIZED VIEW` definition of the table.
144    pub definition: String,
145
146    /// The behavior of handling incoming pk conflict from source executor, we can overwrite or
147    /// ignore conflict pk. For normal materialize executor and other executors, this field will be
148    /// `No Check`.
149    pub conflict_behavior: ConflictBehavior,
150
151    pub version_column_indices: Vec<usize>,
152
153    pub read_prefix_len_hint: usize,
154
155    /// Per-table catalog version, used by schema change. `None` for internal tables and tests.
156    pub version: Option<TableVersion>,
157
158    /// The column indices which could receive watermarks.
159    pub watermark_columns: FixedBitSet,
160
161    /// Optional field specifies the distribution key indices in pk.
162    /// See <https://github.com/risingwavelabs/risingwave/issues/8377> for more information.
163    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    /// Indicate whether to create table in background or foreground.
170    pub create_type: CreateType,
171
172    /// Indicate the stream job status, whether it is created or creating.
173    /// If it is creating, we should hide it.
174    pub stream_job_status: StreamJobStatus,
175
176    /// description of table, set by `comment on`.
177    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    /// Total vnode count of the table.
186    ///
187    /// Can be [`VnodeCount::Placeholder`] if the catalog is generated by the frontend and the
188    /// corresponding job is still in `Creating` status, in which case calling [`Self::vnode_count`]
189    /// will panic.
190    ///
191    /// [`StreamMaterialize::derive_table_catalog`]: crate::optimizer::plan_node::StreamMaterialize::derive_table_catalog
192    /// [`TableCatalogBuilder::build`]: crate::optimizer::plan_node::utils::TableCatalogBuilder::build
193    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    /// Indices of watermark columns in all columns that should be used for state cleaning.
204    /// This replaces `clean_watermark_index_in_pk` but is kept for backward compatibility.
205    /// When set, this takes precedence over `clean_watermark_index_in_pk`.
206    pub clean_watermark_indices: Vec<usize>,
207
208    /// Whether the table supports manual refresh operations
209    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    /// Tables created by `CREATE TABLE`.
220    #[cfg_attr(test, default)]
221    Table,
222    /// Tables created by `CREATE MATERIALIZED VIEW`.
223    MaterializedView,
224    /// Tables serving as index for `TableType::Table` or `TableType::MaterializedView`.
225    /// An index has both a `TableCatalog` and an `IndexCatalog`.
226    Index,
227    VectorIndex,
228    /// Internal tables for executors.
229    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/// The version of a table, used by schema change. See [`PbTableVersion`] for more details.
256#[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    /// Create an initial version for a table, with the given max column id.
264    #[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    /// Returns the SQL definition when the table was created, purified with best effort
302    /// if it's a table.
303    ///
304    /// See [`Self::create_sql_ast_purified`] for more details.
305    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    /// Returns the parsed SQL definition when the table was created, purified with best effort
312    /// if it's a table.
313    ///
314    /// Returns error if it's invalid.
315    pub fn create_sql_ast_purified(&self) -> Result<ast::Statement> {
316        // Purification is only applicable to tables.
317        if let TableType::Table = self.table_type() {
318            let base = if self.definition.is_empty() {
319                // Created by `CREATE TABLE AS`, create a skeleton `CREATE TABLE` statement.
320                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    /// Get a reference to the table catalog's table id.
348    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    /// Returns an error if `DROP` statements are used on the wrong type of table.
400    #[must_use]
401    pub fn bad_drop_error(&self) -> RwError {
402        RelationKind::from(self.table_type).bad_drop_error().into()
403    }
404
405    /// Get the table catalog's associated source id.
406    #[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    /// Get a reference to the table catalog's columns.
416    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    /// Get a reference to the table catalog's pk desc.
429    pub fn pk(&self) -> &[ColumnOrder] {
430        self.pk.as_ref()
431    }
432
433    /// Get the column IDs of the primary key.
434    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    /// Get the column names of the primary key.
442    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    /// Derive the stream key, which must include all distribution keys.
450    /// For backward compatibility, if distribution key is not in stream key(e.g. indexes created in old versions),
451    /// we need to add them to stream key.
452    pub fn stream_key(&self) -> Vec<usize> {
453        // if distribution key is not in stream key, we need to add them to stream key
454        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    /// Get a [`TableDesc`] of the table.
473    ///
474    /// Note: this must be called on existing tables, otherwise it will fail to get the vnode count
475    /// (which is determined by the meta service) and panic.
476    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    /// Get a reference to the table catalog's name.
499    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    /// Returns the SQL definition when the table was created.
512    ///
513    /// See [`Self::create_sql_ast`] for more details.
514    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    /// Returns the parsed SQL definition when the table was created.
521    ///
522    /// Re-create the table with this statement may have different schema if the schema is derived
523    /// from external systems (like schema registry) or it's created by `CREATE TABLE AS`. If this
524    /// is not desired, use [`Self::create_sql_ast_purified`] instead.
525    ///
526    /// Returns error if it's invalid.
527    pub fn create_sql_ast(&self) -> Result<ast::Statement> {
528        if let TableType::Table = self.table_type()
529            && self.definition.is_empty()
530        {
531            // Always fix definition for `CREATE TABLE AS`.
532            self.create_sql_ast_purified()
533        } else {
534            // Directly parse the persisted definition.
535            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    /// Get a reference to the table catalog's version.
544    pub fn version(&self) -> Option<&TableVersion> {
545        self.version.as_ref()
546    }
547
548    /// Get the table's version id. Returns `None` if the table has no version field.
549    pub fn version_id(&self) -> Option<TableVersionId> {
550        self.version().map(|v| v.version_id)
551    }
552
553    /// Get the total vnode count of the table.
554    pub fn vnode_count(&self) -> usize {
555        if self.id().is_placeholder() {
556            0
557        } else {
558            // Panics if it's called on an incomplete (and not yet persisted) table catalog.
559            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            // ignore `_rw_timestamp` when serializing
570            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    /// Get insertable columns together with the shifted `row_id_index` used by DML.
637    ///
638    /// The returned columns exclude hidden columns and generated columns. The shifted
639    /// `row_id_index` rules out generated columns before the row-id column, matching the DML
640    /// input schema.
641    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        // Set materialize key as arrange key + pk
782        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            // Only allow placeholder vnode count for creating tables.
805            // After the table is created, an `Update` notification will be used to update the vnode count field.
806            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            // Add system column `_rw_timestamp` to every table, but notice that this column is never persisted.
822            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}