Skip to main content

risingwave_frontend/handler/
alter_table_column.rs

1// Copyright 2023 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::sync::Arc;
16
17use itertools::Itertools;
18use pgwire::pg_response::{PgResponse, StatementType};
19use risingwave_common::catalog::ColumnCatalog;
20use risingwave_common::hash::VnodeCount;
21use risingwave_common::license::Feature;
22use risingwave_common::{bail, bail_not_implemented};
23use risingwave_pb::ddl_service::TableJobType;
24use risingwave_pb::stream_plan::StreamFragmentGraph;
25use risingwave_sqlparser::ast::{
26    AlterColumnOperation, AlterTableOperation, ColumnOption, ObjectName, Statement,
27};
28
29use super::create_source::SqlColumnStrategy;
30use super::create_table::{ColumnIdGenerator, generate_stream_graph_for_replace_table};
31use super::{HandlerArgs, RwPgResponse};
32use crate::catalog::purify::try_purify_table_source_create_sql_ast;
33use crate::catalog::root_catalog::SchemaPath;
34use crate::catalog::source_catalog::SourceCatalog;
35use crate::catalog::table_catalog::TableType;
36use crate::error::{ErrorCode, Result, RwError};
37use crate::expr::ExprImpl;
38use crate::session::SessionImpl;
39use crate::{Binder, TableCatalog};
40
41/// Used in auto schema change process
42pub async fn get_new_table_definition_for_cdc_table(
43    original_catalog: Arc<TableCatalog>,
44    new_columns: &[ColumnCatalog],
45) -> Result<Statement> {
46    assert_eq!(
47        original_catalog.row_id_index, None,
48        "primary key of cdc table must be user defined"
49    );
50
51    // Retrieve the original table definition.
52    let mut definition = original_catalog.create_sql_ast()?;
53
54    // Clear the original columns field, so that we'll follow `new_columns` to generate a
55    // purified definition.
56    {
57        let Statement::CreateTable {
58            columns,
59            constraints,
60            ..
61        } = &mut definition
62        else {
63            panic!("unexpected statement: {:?}", definition);
64        };
65
66        columns.clear();
67        constraints.clear();
68    }
69
70    let new_definition = try_purify_table_source_create_sql_ast(
71        definition,
72        new_columns,
73        None,
74        // The IDs of `new_columns` may not be consistently maintained at this point.
75        // So we use the column names to identify the primary key columns.
76        &original_catalog.pk_column_names(),
77    )?;
78
79    Ok(new_definition)
80}
81
82pub async fn get_replace_table_plan(
83    session: &Arc<SessionImpl>,
84    table_name: ObjectName,
85    new_definition: Statement,
86    old_catalog: &Arc<TableCatalog>,
87    sql_column_strategy: SqlColumnStrategy,
88) -> Result<(
89    Option<SourceCatalog>,
90    TableCatalog,
91    StreamFragmentGraph,
92    TableJobType,
93)> {
94    // Create handler args as if we're creating a new table with the altered definition.
95    let handler_args = HandlerArgs::new(session.clone(), &new_definition, Arc::from(""))?;
96    let col_id_gen = ColumnIdGenerator::new_alter(old_catalog);
97
98    let (graph, table, source, job_type) = Box::pin(generate_stream_graph_for_replace_table(
99        session,
100        table_name,
101        old_catalog,
102        handler_args.clone(),
103        new_definition,
104        col_id_gen,
105        sql_column_strategy,
106    ))
107    .await?;
108
109    // Set some fields ourselves so that the meta service does not need to maintain them.
110    let mut table = table;
111    table.vnode_count = VnodeCount::set(old_catalog.vnode_count());
112
113    Ok((source, table, graph, job_type))
114}
115
116/// Handle `ALTER TABLE [ADD|DROP] COLUMN` statements. The `operation` must be either `AddColumn` or
117/// `DropColumn`.
118pub async fn handle_alter_table_column(
119    handler_args: HandlerArgs,
120    table_name: ObjectName,
121    operation: AlterTableOperation,
122) -> Result<RwPgResponse> {
123    let session = handler_args.session;
124    let (original_catalog, has_incoming_sinks) =
125        fetch_table_catalog_for_alter(session.as_ref(), &table_name)?;
126
127    if original_catalog.is_iceberg_engine_table()
128        && matches!(
129            &operation,
130            AlterTableOperation::AddColumn { .. } | AlterTableOperation::DropColumn { .. }
131        )
132    {
133        Feature::SinkAutoSchemaChange.check_available()?;
134    }
135
136    if original_catalog.webhook_info.is_some() {
137        return Err(RwError::from(ErrorCode::BindError(
138            "Adding/dropping a column of a table with webhook has not been implemented.".to_owned(),
139        )));
140    }
141
142    // Retrieve the original table definition and parse it to AST.
143    let mut definition = original_catalog.create_sql_ast_purified()?;
144    let Statement::CreateTable { columns, .. } = &mut definition else {
145        panic!("unexpected statement: {:?}", definition);
146    };
147
148    if has_incoming_sinks && matches!(operation, AlterTableOperation::DropColumn { .. }) {
149        Err(ErrorCode::InvalidInputSyntax(
150            "dropping columns in target table of sinks is not supported".to_owned(),
151        ))?;
152    }
153
154    // The `sql_column_strategy` will be `FollowChecked` if the operation is `AddColumn`, and
155    // `FollowUnchecked` if the operation is `DropColumn`.
156    //
157    // Consider the following example:
158    // - There was a column `foo` and a generated column `gen` that references `foo`.
159    // - The external schema is updated to remove `foo`.
160    // - The user tries to drop `foo` from the table.
161    //
162    // Dropping `foo` directly will fail because `gen` references `foo`. However, dropping `gen`
163    // first will also be rejected because `foo` does not exist any more. Also, executing
164    // `REFRESH SCHEMA` will not help because it keeps the generated column. The user gets stuck.
165    //
166    // `FollowUnchecked` workarounds this issue. There are also some alternatives:
167    // - Allow dropping multiple columns at once.
168    // - Check against the persisted schema, instead of resolving again.
169    //
170    // Applied only to tables with schema registry.
171    let sql_column_strategy = match operation {
172        AlterTableOperation::AddColumn {
173            column_def: new_column,
174        } => {
175            // Duplicated names can actually be checked by `StreamMaterialize`. We do here for
176            // better error reporting.
177            let new_column_name = new_column.name.real_value();
178            if columns
179                .iter()
180                .any(|c| c.name.real_value() == new_column_name)
181            {
182                Err(ErrorCode::InvalidInputSyntax(format!(
183                    "column \"{new_column_name}\" of table \"{table_name}\" already exists"
184                )))?
185            }
186
187            if new_column
188                .options
189                .iter()
190                .any(|x| matches!(x.option, ColumnOption::GeneratedColumns(_)))
191            {
192                Err(ErrorCode::InvalidInputSyntax(
193                    "alter table add generated columns is not supported".to_owned(),
194                ))?
195            }
196
197            if new_column
198                .options
199                .iter()
200                .any(|x| matches!(x.option, ColumnOption::NotNull))
201                && !new_column
202                    .options
203                    .iter()
204                    .any(|x| matches!(x.option, ColumnOption::DefaultValue(_)))
205            {
206                Err(ErrorCode::InvalidInputSyntax(
207                    "alter table add NOT NULL columns must have default value".to_owned(),
208                ))?;
209            }
210
211            // Add the new column to the table definition if it is not created by `create table (*)` syntax.
212            columns.push(new_column);
213
214            SqlColumnStrategy::FollowChecked
215        }
216
217        AlterTableOperation::DropColumn {
218            column_name,
219            if_exists,
220            cascade,
221        } => {
222            if cascade {
223                bail_not_implemented!(issue = 6903, "drop column cascade");
224            }
225
226            // Check if the column to drop is referenced by any generated columns.
227            for column in original_catalog.columns() {
228                if let Some(expr) = column.generated_expr() {
229                    let expr = ExprImpl::from_expr_proto(expr)?;
230                    let refs = expr.collect_input_refs(original_catalog.columns().len());
231                    for idx in refs.ones() {
232                        let refed_column = &original_catalog.columns()[idx];
233                        if refed_column.name() == column_name.real_value() {
234                            bail!(format!(
235                                "failed to drop column \"{}\" because it's referenced by a generated column \"{}\"",
236                                column_name,
237                                column.name()
238                            ))
239                        }
240                    }
241                }
242            }
243
244            // Locate the column by name and remove it.
245            let column_name = column_name.real_value();
246            let removed_column = columns
247                .extract_if(.., |c| c.name.real_value() == column_name)
248                .at_most_one()
249                .ok()
250                .unwrap();
251
252            if removed_column.is_some() {
253                // PASS
254            } else if if_exists {
255                return Ok(PgResponse::builder(StatementType::ALTER_TABLE)
256                    .notice(format!(
257                        "column \"{}\" does not exist, skipping",
258                        column_name
259                    ))
260                    .into());
261            } else {
262                Err(ErrorCode::InvalidInputSyntax(format!(
263                    "column \"{}\" of table \"{}\" does not exist",
264                    column_name, table_name
265                )))?
266            }
267
268            SqlColumnStrategy::FollowUnchecked
269        }
270
271        AlterTableOperation::AlterColumn { column_name, op } => {
272            let AlterColumnOperation::SetDataType {
273                data_type,
274                using: None,
275            } = op
276            else {
277                bail_not_implemented!(issue = 6903, "{op}");
278            };
279
280            // Locate the column by name and update its data type.
281            let column_name = column_name.real_value();
282            let column = columns
283                .iter_mut()
284                .find(|c| c.name.real_value() == column_name)
285                .ok_or_else(|| {
286                    ErrorCode::InvalidInputSyntax(format!(
287                        "column \"{}\" of table \"{}\" does not exist",
288                        column_name, table_name
289                    ))
290                })?;
291
292            column.data_type = Some(data_type);
293
294            SqlColumnStrategy::FollowChecked
295        }
296
297        _ => unreachable!(),
298    };
299    let (source, table, graph, job_type) = Box::pin(get_replace_table_plan(
300        &session,
301        table_name,
302        definition,
303        &original_catalog,
304        sql_column_strategy,
305    ))
306    .await?;
307
308    let catalog_writer = session.catalog_writer()?;
309
310    catalog_writer
311        .replace_table(
312            source.map(|x| x.to_prost()),
313            table.to_prost(),
314            graph,
315            job_type,
316        )
317        .await?;
318    Ok(PgResponse::empty_result(StatementType::ALTER_TABLE))
319}
320
321pub fn fetch_table_catalog_for_alter(
322    session: &SessionImpl,
323    table_name: &ObjectName,
324) -> Result<(Arc<TableCatalog>, bool)> {
325    let db_name = &session.database();
326    let (schema_name, real_table_name) =
327        Binder::resolve_schema_qualified_name(db_name, table_name)?;
328    let search_path = session.config().search_path();
329    let user_name = &session.user_name();
330
331    let schema_path = SchemaPath::new(schema_name.as_deref(), &search_path, user_name);
332
333    {
334        let reader = session.env().catalog_reader().read_guard();
335        let (table, schema_name) =
336            reader.get_created_table_by_name(db_name, schema_path, &real_table_name)?;
337
338        match table.table_type() {
339            TableType::Table => {}
340
341            _ => Err(ErrorCode::InvalidInputSyntax(format!(
342                "\"{table_name}\" is not a table or cannot be altered"
343            )))?,
344        }
345
346        session.check_privilege_for_drop_alter(schema_name, &**table)?;
347
348        let has_incoming_sinks = reader
349            .get_schema_by_id(table.database_id, table.schema_id)?
350            .table_incoming_sinks(table.id)
351            .map(|sinks| !sinks.is_empty())
352            .unwrap_or(false);
353
354        Ok((table.clone(), has_incoming_sinks))
355    }
356}
357
358#[cfg(test)]
359mod tests {
360    use std::collections::HashMap;
361
362    use risingwave_common::catalog::{
363        DEFAULT_DATABASE_NAME, DEFAULT_SCHEMA_NAME, ROW_ID_COLUMN_NAME,
364    };
365    use risingwave_common::types::DataType;
366
367    use crate::catalog::root_catalog::SchemaPath;
368    use crate::test_utils::LocalFrontend;
369
370    #[tokio::test]
371    async fn test_add_column_handler() {
372        let frontend = LocalFrontend::new(Default::default()).await;
373        let session = frontend.session_ref();
374        let schema_path = SchemaPath::Name(DEFAULT_SCHEMA_NAME);
375
376        let sql = "create table t (i int, r real);";
377        frontend.run_sql(sql).await.unwrap();
378
379        let get_table = || {
380            let catalog_reader = session.env().catalog_reader().read_guard();
381            catalog_reader
382                .get_created_table_by_name(DEFAULT_DATABASE_NAME, schema_path, "t")
383                .unwrap()
384                .0
385                .clone()
386        };
387
388        let table = get_table();
389
390        let columns: HashMap<_, _> = table
391            .columns
392            .iter()
393            .map(|col| (col.name(), (col.data_type().clone(), col.column_id())))
394            .collect();
395
396        // Alter the table.
397        let sql = "alter table t add column s text;";
398        frontend.run_sql(sql).await.unwrap();
399
400        let altered_table = get_table();
401
402        let altered_columns: HashMap<_, _> = altered_table
403            .columns
404            .iter()
405            .map(|col| (col.name(), (col.data_type().clone(), col.column_id())))
406            .collect();
407
408        // Check the new column.
409        assert_eq!(columns.len() + 1, altered_columns.len());
410        assert_eq!(altered_columns["s"].0, DataType::Varchar);
411
412        // Check the old columns and IDs are not changed.
413        assert_eq!(columns["i"], altered_columns["i"]);
414        assert_eq!(columns["r"], altered_columns["r"]);
415        assert_eq!(
416            columns[ROW_ID_COLUMN_NAME],
417            altered_columns[ROW_ID_COLUMN_NAME]
418        );
419
420        // Check the version is updated.
421        assert_eq!(
422            table.version.as_ref().unwrap().version_id + 1,
423            altered_table.version.as_ref().unwrap().version_id
424        );
425        assert_eq!(
426            table.version.as_ref().unwrap().next_column_id.next(),
427            altered_table.version.as_ref().unwrap().next_column_id
428        );
429    }
430}