risingwave_frontend/handler/
alter_table_column.rs1use 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
41pub 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 let mut definition = original_catalog.create_sql_ast()?;
53
54 {
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 &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 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 let mut table = table;
111 table.vnode_count = VnodeCount::set(old_catalog.vnode_count());
112
113 Ok((source, table, graph, job_type))
114}
115
116pub 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 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 let sql_column_strategy = match operation {
172 AlterTableOperation::AddColumn {
173 column_def: new_column,
174 } => {
175 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 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 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 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 } 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 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 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 assert_eq!(columns.len() + 1, altered_columns.len());
410 assert_eq!(altered_columns["s"].0, DataType::Varchar);
411
412 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 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}