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