risingwave_frontend/handler/
alter_source_column.rs1use pgwire::pg_response::{PgResponse, StatementType};
16use risingwave_common::catalog::max_column_id;
17use risingwave_connector::source::{SourceEncode, SourceStruct, extract_source_struct};
18use risingwave_sqlparser::ast::{AlterSourceOperation, ObjectName};
19
20use super::create_source::{
21 generate_stream_graph_for_source, reject_variant_columns, uses_pulsar_schema,
22};
23use super::create_table::bind_sql_columns;
24use super::{HandlerArgs, RwPgResponse};
25use crate::Binder;
26use crate::catalog::root_catalog::SchemaPath;
27use crate::error::{ErrorCode, Result, RwError};
28
29pub async fn handle_alter_source_column(
34 handler_args: HandlerArgs,
35 source_name: ObjectName,
36 operation: AlterSourceOperation,
37) -> Result<RwPgResponse> {
38 let session = handler_args.session.clone();
40 let db_name = &session.database();
41 let (schema_name, real_source_name) =
42 Binder::resolve_schema_qualified_name(db_name, &source_name)?;
43 let search_path = session.config().search_path();
44 let user_name = &session.user_name();
45
46 let schema_path = SchemaPath::new(schema_name.as_deref(), &search_path, user_name);
47
48 let mut catalog = {
49 let reader = session.env().catalog_reader().read_guard();
50 let (source, schema_name) =
51 reader.get_source_by_name(db_name, schema_path, &real_source_name)?;
52 session.check_privilege_for_drop_alter(schema_name, &**source)?;
53
54 (**source).clone()
55 };
56
57 if catalog.associated_table_id.is_some() {
58 return Err(ErrorCode::NotSupported(
59 "alter table with connector with ALTER SOURCE statement".to_owned(),
60 "try to use ALTER TABLE instead".to_owned(),
61 )
62 .into());
63 };
64 if catalog.is_cdc_table_source() {
65 return Err(ErrorCode::NotSupported(
66 "altering columns of a CDC table source is not supported".to_owned(),
67 "Drop and recreate the CDC table source".to_owned(),
68 )
69 .into());
70 }
71
72 let SourceStruct { encode, .. } = extract_source_struct(&catalog.info)?;
74 match encode {
75 SourceEncode::Avro | SourceEncode::Protobuf => {
76 return Err(ErrorCode::NotSupported(
77 "alter source with schema registry".to_owned(),
78 "try `ALTER SOURCE .. FORMAT .. ENCODE .. (...)` instead".to_owned(),
79 )
80 .into());
81 }
82 SourceEncode::Json
83 if catalog.info.use_schema_registry
84 || uses_pulsar_schema(
85 &catalog.info.format_encode_options,
86 &catalog.info.format_encode_secret_refs,
87 ) =>
88 {
89 return Err(ErrorCode::NotSupported(
90 "alter source with schema registry".to_owned(),
91 "try `ALTER SOURCE .. FORMAT .. ENCODE .. (...)` instead".to_owned(),
92 )
93 .into());
94 }
95 SourceEncode::Invalid | SourceEncode::Native | SourceEncode::None => {
96 return Err(RwError::from(ErrorCode::NotSupported(
97 format!("alter source with encode {:?}", encode),
98 "Only source with encode JSON | BYTES | CSV | PARQUET can be altered".into(),
99 )));
100 }
101 SourceEncode::Json | SourceEncode::Csv | SourceEncode::Bytes | SourceEncode::Parquet => {}
102 }
103
104 let columns = &mut catalog.columns;
105 match operation {
106 AlterSourceOperation::AddColumn { column_def } => {
107 let new_column_name = column_def.name.real_value();
108 if columns
109 .iter()
110 .any(|c| c.column_desc.name == new_column_name)
111 {
112 Err(ErrorCode::InvalidInputSyntax(format!(
113 "column \"{new_column_name}\" of source \"{source_name}\" already exists"
114 )))?
115 }
116
117 let mut bound_column = bind_sql_columns(&[column_def], false)?.remove(0);
119 if encode != SourceEncode::Parquet {
122 reject_variant_columns(
123 std::slice::from_ref(&bound_column),
124 "for this source encoding",
125 )?;
126 }
127 bound_column.column_desc.column_id = max_column_id(columns).next();
128 columns.push(bound_column);
129 }
131 _ => unreachable!(),
132 }
133
134 catalog.version += 1;
136 catalog.fill_purified_create_sql();
137
138 let catalog_writer = session.catalog_writer()?;
139 if catalog.info.is_shared() {
140 let graph = generate_stream_graph_for_source(handler_args, catalog.clone())?;
141 catalog_writer
142 .replace_source(catalog.to_prost(), graph)
143 .await?
144 } else {
145 catalog_writer.alter_source(catalog.to_prost()).await?
146 };
147
148 Ok(PgResponse::empty_result(StatementType::ALTER_SOURCE))
149}
150
151#[cfg(test)]
152pub mod tests {
153 use std::collections::BTreeMap;
154
155 use risingwave_common::catalog::{DEFAULT_DATABASE_NAME, DEFAULT_SCHEMA_NAME};
156
157 use crate::catalog::root_catalog::SchemaPath;
158 use crate::test_utils::LocalFrontend;
159
160 #[tokio::test]
161 async fn test_alter_source_column_handler() {
162 let frontend = LocalFrontend::new(Default::default()).await;
163 let session = frontend.session_ref();
164 let schema_path = SchemaPath::Name(DEFAULT_SCHEMA_NAME);
165
166 let sql = r#"create source s_shared (v1 int) with (
167 connector = 'kafka',
168 topic = 'abc',
169 properties.bootstrap.server = 'localhost:29092',
170 ) FORMAT PLAIN ENCODE JSON;"#;
171
172 frontend
173 .run_sql_with_session(session.clone(), sql)
174 .await
175 .unwrap();
176
177 frontend
178 .run_sql_with_session(session.clone(), "SET streaming_use_shared_source TO false;")
179 .await
180 .unwrap();
181 let sql = r#"create source s (v1 int) with (
182 connector = 'kafka',
183 topic = 'abc',
184 properties.bootstrap.server = 'localhost:29092',
185 ) FORMAT PLAIN ENCODE JSON;"#;
186
187 frontend
188 .run_sql_with_session(session.clone(), sql)
189 .await
190 .unwrap();
191
192 let get_source = |name: &str| {
193 let catalog_reader = session.env().catalog_reader().read_guard();
194 catalog_reader
195 .get_source_by_name(DEFAULT_DATABASE_NAME, schema_path, name)
196 .unwrap()
197 .0
198 .clone()
199 };
200
201 let source = get_source("s");
202
203 let sql = "alter source s_shared add column v2 varchar;";
204 frontend.run_sql(sql).await.unwrap();
205
206 let altered_source = get_source("s_shared");
207 let altered_columns: BTreeMap<_, _> = altered_source
208 .columns
209 .iter()
210 .map(|col| (col.name(), (col.data_type().clone(), col.column_id())))
211 .collect();
212
213 expect_test::expect![[r#"
216 {
217 "_row_id": (
218 Serial,
219 #0,
220 ),
221 "_rw_kafka_offset": (
222 Varchar,
223 #4,
224 ),
225 "_rw_kafka_partition": (
226 Varchar,
227 #3,
228 ),
229 "_rw_kafka_timestamp": (
230 Timestamptz,
231 #2,
232 ),
233 "v1": (
234 Int32,
235 #1,
236 ),
237 "v2": (
238 Varchar,
239 #5,
240 ),
241 }
242 "#]]
243 .assert_debug_eq(&altered_columns);
244
245 assert_eq!(source.version + 1, altered_source.version);
247
248 expect_test::expect!["CREATE SOURCE s_shared (v1 INT, v2 CHARACTER VARYING) WITH (connector = 'kafka', topic = 'abc', properties.bootstrap.server = 'localhost:29092') FORMAT PLAIN ENCODE JSON"].assert_eq(&altered_source.definition);
250
251 let sql = "alter source s add column v2 varchar;";
252 frontend.run_sql(sql).await.unwrap();
253
254 let altered_source = get_source("s");
255 let altered_columns: BTreeMap<_, _> = altered_source
256 .columns
257 .iter()
258 .map(|col| (col.name(), (col.data_type().clone(), col.column_id())))
259 .collect();
260
261 expect_test::expect![[r#"
264 {
265 "_row_id": (
266 Serial,
267 #0,
268 ),
269 "_rw_kafka_timestamp": (
270 Timestamptz,
271 #2,
272 ),
273 "v1": (
274 Int32,
275 #1,
276 ),
277 "v2": (
278 Varchar,
279 #3,
280 ),
281 }
282 "#]]
283 .assert_debug_eq(&altered_columns);
284
285 assert_eq!(source.version + 1, altered_source.version);
287
288 expect_test::expect!["CREATE SOURCE s (v1 INT, v2 CHARACTER VARYING) WITH (connector = 'kafka', topic = 'abc', properties.bootstrap.server = 'localhost:29092') FORMAT PLAIN ENCODE JSON"].assert_eq(&altered_source.definition);
290 }
291}