Skip to main content

risingwave_frontend/handler/
alter_source_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 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
29// Note for future drop column:
30// 1. Dependencies of generated columns
31
32/// Handle `ALTER TABLE [ADD] COLUMN` statements.
33pub async fn handle_alter_source_column(
34    handler_args: HandlerArgs,
35    source_name: ObjectName,
36    operation: AlterSourceOperation,
37) -> Result<RwPgResponse> {
38    // Get original definition
39    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    // Currently only allow source without schema registry
73    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            // add column name is from user, so we still have check for reserved column name
118            let mut bound_column = bind_sql_columns(&[column_def], false)?.remove(0);
119            // PARQUET reads variant via the extension type; other alterable encodings cannot
120            // produce variant values, and this path bypasses the CREATE-time gate.
121            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            // No need to update the definition here. It will be done by purification later.
130        }
131        _ => unreachable!(),
132    }
133
134    // update version
135    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        // Check the new column is added.
214        // Check the old columns and IDs are not changed.
215        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        // Check version
246        assert_eq!(source.version + 1, altered_source.version);
247
248        // Check definition
249        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        // Check the new column is added.
262        // Check the old columns and IDs are not changed.
263        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        // Check version
286        assert_eq!(source.version + 1, altered_source.version);
287
288        // Check definition
289        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}