1use std::sync::Arc;
16
17use either::Either;
18use itertools::Itertools;
19use pgwire::pg_response::StatementType;
20use risingwave_common::bail_not_implemented;
21use risingwave_common::catalog::{ColumnCatalog, max_column_id};
22use risingwave_connector::WithPropertiesExt;
23use risingwave_pb::catalog::StreamSourceInfo;
24use risingwave_pb::plan_common::{EncodeType, FormatType};
25use risingwave_sqlparser::ast::{
26 CompatibleFormatEncode, CreateSourceStatement, Encode, Format, FormatEncodeOptions, ObjectName,
27 SqlOption, Statement,
28};
29
30use super::create_source::{
31 generate_stream_graph_for_source, schema_has_schema_registry, validate_compatibility,
32};
33use super::util::SourceSchemaCompatExt;
34use super::{HandlerArgs, RwPgResponse};
35use crate::catalog::root_catalog::SchemaPath;
36use crate::catalog::source_catalog::SourceCatalog;
37use crate::error::{ErrorCode, Result};
38use crate::handler::create_source::{CreateSourceType, bind_columns_from_source};
39use crate::session::SessionImpl;
40use crate::utils::resolve_secret_ref_in_with_options;
41use crate::{Binder, WithOptions};
42
43fn format_type_to_format(from: FormatType) -> Option<Format> {
44 Some(match from {
45 FormatType::Unspecified => return None,
46 FormatType::Native => Format::Native,
47 FormatType::Debezium => Format::Debezium,
48 FormatType::DebeziumMongo => Format::DebeziumMongo,
49 FormatType::Maxwell => Format::Maxwell,
50 FormatType::Canal => Format::Canal,
51 FormatType::Upsert => Format::Upsert,
52 FormatType::Plain => Format::Plain,
53 FormatType::None => Format::None,
54 })
55}
56
57fn encode_type_to_encode(from: EncodeType) -> Option<Encode> {
58 Some(match from {
59 EncodeType::Unspecified => return None,
60 EncodeType::Native => Encode::Native,
61 EncodeType::Avro => Encode::Avro,
62 EncodeType::Csv => Encode::Csv,
63 EncodeType::Protobuf => Encode::Protobuf,
64 EncodeType::Json => Encode::Json,
65 EncodeType::Bytes => Encode::Bytes,
66 EncodeType::Template => Encode::Template,
67 EncodeType::Parquet => Encode::Parquet,
68 EncodeType::None => Encode::None,
69 EncodeType::Text => Encode::Text,
70 })
71}
72
73fn columns_minus(columns_a: &[ColumnCatalog], columns_b: &[ColumnCatalog]) -> Vec<ColumnCatalog> {
85 columns_a
86 .iter()
87 .filter(|col_a| {
88 !col_a.is_hidden()
89 && !col_a.is_connector_additional_column()
90 && !col_a.is_generated()
91 && !columns_b.iter().any(|col_b| {
92 col_a.name() == col_b.name() && col_a.data_type() == col_b.data_type()
93 })
94 })
95 .cloned()
96 .collect()
97}
98
99pub fn fetch_source_catalog_with_db_schema_id(
101 session: &SessionImpl,
102 name: &ObjectName,
103) -> Result<Arc<SourceCatalog>> {
104 let db_name = &session.database();
105 let (schema_name, real_source_name) = Binder::resolve_schema_qualified_name(db_name, name)?;
106 let search_path = session.config().search_path();
107 let user_name = &session.user_name();
108
109 let schema_path = SchemaPath::new(schema_name.as_deref(), &search_path, user_name);
110
111 let reader = session.env().catalog_reader().read_guard();
112 let (source, schema_name) =
113 reader.get_source_by_name(db_name, schema_path, &real_source_name)?;
114
115 session.check_privilege_for_drop_alter(schema_name, &**source)?;
116
117 Ok(Arc::clone(source))
118}
119
120pub fn check_format_encode(
123 original_source: &SourceCatalog,
124 new_format_encode: &FormatEncodeOptions,
125) -> Result<()> {
126 let StreamSourceInfo {
127 format, row_encode, ..
128 } = original_source.info;
129 let (Some(old_format), Some(old_row_encode)) = (
130 format_type_to_format(FormatType::try_from(format).unwrap()),
131 encode_type_to_encode(EncodeType::try_from(row_encode).unwrap()),
132 ) else {
133 return Err(ErrorCode::NotSupported(
134 "altering a legacy source which is not created using `FORMAT .. ENCODE ..` Clause"
135 .to_owned(),
136 "try this feature by creating a fresh source".to_owned(),
137 )
138 .into());
139 };
140
141 if new_format_encode.format != old_format || new_format_encode.row_encode != old_row_encode {
142 bail_not_implemented!(
143 "the original definition is FORMAT {:?} ENCODE {:?}, and altering them is not supported yet",
144 &old_format,
145 &old_row_encode,
146 );
147 }
148
149 Ok(())
150}
151
152pub async fn refresh_sr_and_get_columns_diff(
154 original_source: &SourceCatalog,
155 format_encode: &FormatEncodeOptions,
156 session: &Arc<SessionImpl>,
157) -> Result<(StreamSourceInfo, Vec<ColumnCatalog>, Vec<ColumnCatalog>)> {
158 let mut with_properties = original_source.with_properties.clone();
159 validate_compatibility(format_encode, &mut with_properties)?;
160
161 if with_properties.is_cdc_connector() {
162 bail_not_implemented!("altering a cdc source is not supported");
163 }
164
165 let (Some(columns_from_resolve_source), source_info) = bind_columns_from_source(
166 session,
167 format_encode,
168 Either::Right(&with_properties),
169 CreateSourceType::for_replace(original_source),
170 )
171 .await?
172 else {
173 unreachable!("source without schema registry is rejected")
175 };
176
177 let mut added_columns = columns_minus(&columns_from_resolve_source, &original_source.columns);
178 let mut next_col_id = max_column_id(&original_source.columns).next();
180 for col in &mut added_columns {
181 col.column_desc.column_id = next_col_id;
182 next_col_id = next_col_id.next();
183 }
184 let dropped_columns = columns_minus(&original_source.columns, &columns_from_resolve_source);
185 tracing::debug!(
186 ?added_columns,
187 ?dropped_columns,
188 ?columns_from_resolve_source,
189 original_source = ?original_source.columns
190 );
191
192 Ok((source_info, added_columns, dropped_columns))
193}
194
195fn get_format_encode_from_source(source: &SourceCatalog) -> Result<FormatEncodeOptions> {
196 let stmt = source.create_sql_ast()?;
197 let Statement::CreateSource {
198 stmt: CreateSourceStatement { format_encode, .. },
199 } = stmt
200 else {
201 return Err(ErrorCode::InternalError(format!(
202 "source \"{}\" has a non-CREATE SOURCE definition",
203 source.name
204 ))
205 .into());
206 };
207 Ok(format_encode.into_v2_with_warning())
208}
209
210fn reject_associated_table(source: &SourceCatalog) -> Result<()> {
211 if source.associated_table_id.is_some() {
212 return Err(ErrorCode::NotSupported(
213 "alter table with connector using ALTER SOURCE statement".to_owned(),
214 "try to use ALTER TABLE instead".to_owned(),
215 )
216 .into());
217 }
218
219 Ok(())
220}
221
222pub async fn handler_refresh_schema(
223 handler_args: HandlerArgs,
224 name: ObjectName,
225) -> Result<RwPgResponse> {
226 let source = fetch_source_catalog_with_db_schema_id(&handler_args.session, &name)?;
227 reject_associated_table(&source)?;
228 if source.is_cdc_table_source() {
229 return Err(ErrorCode::NotSupported(
230 "refreshing the schema of a CDC table source is not supported".to_owned(),
231 "Drop and recreate the CDC table source".to_owned(),
232 )
233 .into());
234 }
235 let format_encode = get_format_encode_from_source(&source)?;
236 handle_alter_source_with_sr(handler_args, name, format_encode).await
237}
238
239pub async fn handle_alter_source_with_sr(
240 handler_args: HandlerArgs,
241 name: ObjectName,
242 format_encode: FormatEncodeOptions,
243) -> Result<RwPgResponse> {
244 let session = handler_args.session.clone();
245 let source = fetch_source_catalog_with_db_schema_id(&session, &name)?;
246 let mut source = source.as_ref().clone();
247
248 reject_associated_table(&source)?;
249 if source.is_cdc_table_source() {
250 return Err(ErrorCode::NotSupported(
251 "altering the format or schema of a CDC table source is not supported".to_owned(),
252 "Drop and recreate the CDC table source".to_owned(),
253 )
254 .into());
255 }
256
257 check_format_encode(&source, &format_encode)?;
258
259 if !schema_has_schema_registry(&format_encode) {
260 return Err(ErrorCode::NotSupported(
261 "altering a source without schema registry".to_owned(),
262 "try `ALTER SOURCE .. ADD COLUMN ...` instead".to_owned(),
263 )
264 .into());
265 }
266
267 let (source_info, added_columns, dropped_columns) =
268 refresh_sr_and_get_columns_diff(&source, &format_encode, &session).await?;
269
270 if !dropped_columns.is_empty() {
271 bail_not_implemented!(
272 "this altering statement will drop columns, which is not supported yet: {}",
273 dropped_columns
274 .iter()
275 .map(|col| format!("({}: {})", col.name(), col.data_type()))
276 .join(", ")
277 );
278 }
279
280 source.info = source_info;
281 source.columns.extend(added_columns);
282 source.definition = alter_definition_format_encode(
283 source.create_sql_ast_purified()?,
284 format_encode.row_options.clone(),
285 )?;
286
287 let (format_encode_options, format_encode_secret_ref) = resolve_secret_ref_in_with_options(
288 WithOptions::try_from(format_encode.row_options())?,
289 session.as_ref(),
290 )?
291 .into_parts();
292 source
293 .info
294 .format_encode_options
295 .extend(format_encode_options);
296
297 source
298 .info
299 .format_encode_secret_refs
300 .extend(format_encode_secret_ref);
301
302 source.version += 1;
304
305 let pb_source = source.to_prost();
306 let catalog_writer = session.catalog_writer()?;
307 if source.info.is_shared() {
308 let graph = generate_stream_graph_for_source(handler_args, source.clone())?;
309 catalog_writer.replace_source(pb_source, graph).await?
310 } else {
311 catalog_writer.alter_source(pb_source).await?;
312 }
313 Ok(RwPgResponse::empty_result(StatementType::ALTER_SOURCE))
314}
315
316pub fn alter_definition_format_encode(
318 mut stmt: Statement,
319 format_encode_options: Vec<SqlOption>,
320) -> Result<String> {
321 match &mut stmt {
322 Statement::CreateSource {
323 stmt: CreateSourceStatement { format_encode, .. },
324 }
325 | Statement::CreateTable {
326 format_encode: Some(format_encode),
327 ..
328 } => match format_encode {
329 CompatibleFormatEncode::V2(schema) => {
330 schema.row_options = format_encode_options;
331 }
332 },
333 _ => unreachable!(),
334 }
335
336 Ok(stmt.to_string())
337}
338
339#[cfg(test)]
340pub mod tests {
341 use risingwave_common::catalog::{DEFAULT_DATABASE_NAME, DEFAULT_SCHEMA_NAME};
342 use risingwave_common::types::DataType;
343
344 use crate::catalog::root_catalog::SchemaPath;
345 use crate::test_utils::{LocalFrontend, PROTO_FILE_DATA, create_proto_file};
346
347 #[tokio::test]
348 async fn test_refresh_schema_rejects_associated_table() {
349 let proto_file = create_proto_file(PROTO_FILE_DATA);
350 let sql = format!(
351 r#"CREATE TABLE t
352 WITH (
353 connector = 'kafka',
354 topic = 'test-topic',
355 properties.bootstrap.server = 'localhost:29092'
356 )
357 FORMAT PLAIN ENCODE PROTOBUF (
358 message = '.test.TestRecord',
359 schema.location = 'file://{}'
360 )"#,
361 proto_file.path().to_str().unwrap()
362 );
363 let frontend = LocalFrontend::new(Default::default()).await;
364 frontend.run_sql(sql).await.unwrap();
365
366 let error = frontend
367 .run_sql("ALTER SOURCE t REFRESH SCHEMA")
368 .await
369 .unwrap_err();
370 assert!(error.to_string().contains("try to use ALTER TABLE instead"));
371 }
372
373 #[tokio::test]
374 async fn test_alter_source_with_sr_handler() {
375 let proto_file = create_proto_file(PROTO_FILE_DATA);
376 let sql = format!(
377 r#"CREATE SOURCE src
378 WITH (
379 connector = 'kafka',
380 topic = 'test-topic',
381 properties.bootstrap.server = 'localhost:29092'
382 )
383 FORMAT PLAIN ENCODE PROTOBUF (
384 message = '.test.TestRecord',
385 schema.location = 'file://{}'
386 )"#,
387 proto_file.path().to_str().unwrap()
388 );
389 let frontend = LocalFrontend::new(Default::default()).await;
390 let session = frontend.session_ref();
391 let schema_path = SchemaPath::Name(DEFAULT_SCHEMA_NAME);
392
393 frontend
394 .run_sql_with_session(session.clone(), "SET streaming_use_shared_source TO false;")
395 .await
396 .unwrap();
397 frontend
398 .run_sql_with_session(session.clone(), sql)
399 .await
400 .unwrap();
401
402 let get_source = || {
403 let catalog_reader = session.env().catalog_reader().read_guard();
404 catalog_reader
405 .get_source_by_name(DEFAULT_DATABASE_NAME, schema_path, "src")
406 .unwrap()
407 .0
408 .clone()
409 };
410
411 let source = get_source();
412 expect_test::expect!["CREATE SOURCE src (id INT, country STRUCT<address CHARACTER VARYING, city STRUCT<address CHARACTER VARYING, zipcode CHARACTER VARYING>, zipcode CHARACTER VARYING>, zipcode BIGINT, rate REAL) WITH (connector = 'kafka', topic = 'test-topic', properties.bootstrap.server = 'localhost:29092') FORMAT PLAIN ENCODE PROTOBUF (message = '.test.TestRecord', schema.location = 'file://')"].assert_eq(&source.create_sql_purified().replace(proto_file.path().to_str().unwrap(), ""));
413
414 let sql = format!(
415 r#"ALTER SOURCE src FORMAT UPSERT ENCODE PROTOBUF (
416 message = '.test.TestRecord',
417 schema.location = 'file://{}'
418 )"#,
419 proto_file.path().to_str().unwrap()
420 );
421 assert!(
422 frontend
423 .run_sql(sql)
424 .await
425 .unwrap_err()
426 .to_string()
427 .contains("the original definition is FORMAT Plain ENCODE Protobuf")
428 );
429
430 let sql = format!(
431 r#"ALTER SOURCE src FORMAT PLAIN ENCODE PROTOBUF (
432 message = '.test.TestRecordAlterType',
433 schema.location = 'file://{}'
434 )"#,
435 proto_file.path().to_str().unwrap()
436 );
437 let res_str = frontend.run_sql(sql).await.unwrap_err().to_string();
438 assert!(res_str.contains("id: integer"));
439 assert!(res_str.contains("zipcode: bigint"));
440
441 let sql = format!(
442 r#"ALTER SOURCE src FORMAT PLAIN ENCODE PROTOBUF (
443 message = '.test.TestRecordExt',
444 schema.location = 'file://{}'
445 )"#,
446 proto_file.path().to_str().unwrap()
447 );
448 frontend.run_sql(sql).await.unwrap();
449
450 let altered_source = get_source();
451
452 let name_column = altered_source
453 .columns
454 .iter()
455 .find(|col| col.column_desc.name == "name")
456 .unwrap();
457 assert_eq!(name_column.column_desc.data_type, DataType::Varchar);
458
459 expect_test::expect!["CREATE SOURCE src (id INT, country STRUCT<address CHARACTER VARYING, city STRUCT<address CHARACTER VARYING, zipcode CHARACTER VARYING>, zipcode CHARACTER VARYING>, zipcode BIGINT, rate REAL, name CHARACTER VARYING) WITH (connector = 'kafka', topic = 'test-topic', properties.bootstrap.server = 'localhost:29092') FORMAT PLAIN ENCODE PROTOBUF (message = '.test.TestRecordExt', schema.location = 'file://')"].assert_eq(&altered_source.create_sql_purified().replace(proto_file.path().to_str().unwrap(), ""));
460 }
461}