Skip to main content

risingwave_frontend/handler/create_source/
external_schema.rs

1// Copyright 2024 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
15//! bind columns from external schema
16
17use risingwave_connector::source::ADBC_SNOWFLAKE_CONNECTOR;
18
19use super::*;
20
21mod json;
22use json::*;
23mod avro;
24use avro::extract_avro_table_schema;
25pub mod debezium;
26pub mod iceberg;
27use iceberg::extract_iceberg_columns;
28mod protobuf;
29use protobuf::extract_protobuf_table_schema;
30pub mod nexmark;
31
32/// Define a feature-gated function from a module.
33///
34/// This macro conditionally includes a module and re-exports a function from it.
35/// When the feature is disabled, it generates a stub function that returns an error.
36macro_rules! feature_gated_function {
37    (
38        mod $mod_name:ident,
39        $feature_name:literal,
40        async fn $func_name:ident ( $( $param_name:ident : $param_type:ty ),* $(,)? ) -> $ret_type:ty
41    ) => {
42        #[cfg(feature = $feature_name)]
43        pub mod $mod_name;
44        #[cfg(feature = $feature_name)]
45        use $mod_name::$func_name;
46
47        #[cfg(not(feature = $feature_name))]
48        #[allow(unused_variables)]
49        pub async fn $func_name( $( $param_name : $param_type ),* ) -> $ret_type {
50            Err(anyhow::anyhow!(
51                "Feature `{}` is not enabled at compile time. \
52                Please enable it in `Cargo.toml` and rebuild.",
53                $feature_name
54            ))
55        }
56    };
57}
58
59feature_gated_function!(
60    mod adbc_snowflake,
61    "source-adbc_snowflake",
62    async fn extract_adbc_snowflake_columns(
63        with_properties: &WithOptionsSecResolved,
64    ) -> anyhow::Result<Vec<ColumnCatalog>>
65);
66
67/// Resolves the schema of the source from external schema file.
68/// See <https://docs.risingwave.com/sql/commands/sql-create-source> for more information.
69///
70/// Note: the returned schema strictly corresponds to the schema.
71/// Other special columns like additional columns (`INCLUDE`), and `row_id` column are not included.
72pub async fn bind_columns_from_source(
73    session: &SessionImpl,
74    format_encode: &FormatEncodeOptions,
75    with_properties: Either<&WithOptions, &WithOptionsSecResolved>,
76    create_source_type: CreateSourceType,
77) -> Result<(Option<Vec<ColumnCatalog>>, StreamSourceInfo)> {
78    let (columns_from_resolve_source, mut source_info) =
79        if create_source_type == CreateSourceType::SharedCdc {
80            bind_columns_from_source_for_cdc(session, format_encode)?
81        } else {
82            bind_columns_from_source_for_non_cdc(session, format_encode, with_properties).await?
83        };
84    if create_source_type.is_shared() {
85        // Note: this field should be called is_shared. Check field doc for more details.
86        source_info.cdc_source_job = true;
87        source_info.is_distributed = create_source_type == CreateSourceType::SharedNonCdc;
88    }
89    Ok((columns_from_resolve_source, source_info))
90}
91
92async fn bind_columns_from_source_for_non_cdc(
93    session: &SessionImpl,
94    format_encode: &FormatEncodeOptions,
95    with_properties: Either<&WithOptions, &WithOptionsSecResolved>,
96) -> Result<(Option<Vec<ColumnCatalog>>, StreamSourceInfo)> {
97    const MESSAGE_NAME_KEY: &str = "message";
98    const KEY_MESSAGE_NAME_KEY: &str = "key.message";
99    const NAME_STRATEGY_KEY: &str = "schema.registry.name.strategy";
100
101    let options_with_secret = match with_properties {
102        Either::Left(options) => {
103            let (sec_resolve_props, connection_type, _) = resolve_connection_ref_and_secret_ref(
104                options.clone(),
105                session,
106                Some(TelemetryDatabaseObject::Source),
107            )?;
108            if !SOURCE_ALLOWED_CONNECTION_CONNECTOR.contains(&connection_type) {
109                return Err(RwError::from(ProtocolError(format!(
110                    "connection type {:?} is not allowed, allowed types: {:?}",
111                    connection_type, SOURCE_ALLOWED_CONNECTION_CONNECTOR
112                ))));
113            }
114
115            sec_resolve_props
116        }
117        Either::Right(options_with_secret) => options_with_secret.clone(),
118    };
119
120    let is_kafka: bool = options_with_secret.is_kafka_connector();
121
122    // todo: need to resolve connection ref for schema registry
123    let (sec_resolve_props, connection_type, schema_registry_conn_ref) =
124        resolve_connection_ref_and_secret_ref(
125            WithOptions::try_from(format_encode.row_options())?,
126            session,
127            Some(TelemetryDatabaseObject::Source),
128        )?;
129    ensure_connection_type_allowed(connection_type, &SOURCE_ALLOWED_CONNECTION_SCHEMA_REGISTRY)?;
130
131    let (format_encode_options, format_encode_secret_refs) = sec_resolve_props.into_parts();
132    // Need real secret to access the schema registry
133    let mut format_encode_options_to_consume = LocalSecretManager::global().fill_secrets(
134        format_encode_options.clone(),
135        format_encode_secret_refs.clone(),
136    )?;
137
138    fn get_key_message_name(options: &mut BTreeMap<String, String>) -> Option<String> {
139        consume_string_from_options(options, KEY_MESSAGE_NAME_KEY)
140            .map(|ele| Some(ele.0))
141            .unwrap_or(None)
142    }
143    fn get_sr_name_strategy_check(
144        options: &mut BTreeMap<String, String>,
145        use_sr: bool,
146    ) -> Result<Option<i32>> {
147        let name_strategy = get_name_strategy_or_default(try_consume_string_from_options(
148            options,
149            NAME_STRATEGY_KEY,
150        ))?;
151        if !use_sr && name_strategy.is_some() {
152            return Err(RwError::from(ProtocolError(
153                "schema registry name strategy only works with schema registry enabled".to_owned(),
154            )));
155        }
156        Ok(name_strategy)
157    }
158
159    let mut stream_source_info = StreamSourceInfo {
160        format: format_to_prost(&format_encode.format) as i32,
161        row_encode: row_encode_to_prost(&format_encode.row_encode) as i32,
162        format_encode_options,
163        format_encode_secret_refs,
164        connection_id: schema_registry_conn_ref,
165        ..Default::default()
166    };
167
168    if format_encode.format == Format::Debezium {
169        try_consume_string_from_options(&mut format_encode_options_to_consume, DEBEZIUM_IGNORE_KEY);
170    }
171
172    let columns = match (&format_encode.format, &format_encode.row_encode) {
173        (Format::Native, Encode::Native)
174        | (Format::Plain, Encode::Bytes)
175        | (Format::DebeziumMongo, Encode::Json) => None,
176        (Format::Plain, Encode::Protobuf) | (Format::Upsert, Encode::Protobuf) => {
177            let (row_schema_location, use_schema_registry) =
178                get_schema_location(&mut format_encode_options_to_consume)?;
179            let message_name = consume_string_from_options(
180                &mut format_encode_options_to_consume,
181                MESSAGE_NAME_KEY,
182            )?;
183            let name_strategy = get_sr_name_strategy_check(
184                &mut format_encode_options_to_consume,
185                use_schema_registry,
186            )?;
187
188            stream_source_info.use_schema_registry = use_schema_registry;
189            stream_source_info
190                .row_schema_location
191                .clone_from(&row_schema_location.0);
192            stream_source_info
193                .proto_message_name
194                .clone_from(&message_name.0);
195            stream_source_info.key_message_name =
196                get_key_message_name(&mut format_encode_options_to_consume);
197            stream_source_info.name_strategy =
198                name_strategy.unwrap_or(PbSchemaRegistryNameStrategy::Unspecified as i32);
199
200            Some(
201                extract_protobuf_table_schema(
202                    &stream_source_info,
203                    &options_with_secret,
204                    &mut format_encode_options_to_consume,
205                )
206                .await?,
207            )
208        }
209        (format @ (Format::Plain | Format::Upsert | Format::Debezium), Encode::Avro) => {
210            if format_encode_options_to_consume.contains_key(PULSAR_SCHEMA_URL_KEY) {
211                // Pulsar schema options are parsed by `SpecificParserConfig` and remain in the
212                // catalog format options. They deliberately do not populate the legacy
213                // Confluent-oriented `row_schema_location` fields.
214            } else if format_encode_options_to_consume
215                .remove(AWS_GLUE_SCHEMA_ARN_KEY)
216                .is_none()
217            {
218                // Legacy logic that assumes either `schema.location` or confluent `schema.registry`.
219                // The handling of newly added aws glue is centralized in `connector::parser`.
220                // TODO(xiangjinwu): move these option parsing to `connector::parser` as well.
221
222                let (row_schema_location, use_schema_registry) =
223                    get_schema_location(&mut format_encode_options_to_consume)?;
224
225                if matches!(format, Format::Debezium) && !use_schema_registry {
226                    return Err(RwError::from(ProtocolError(
227                        "schema location for DEBEZIUM_AVRO row format is not supported".to_owned(),
228                    )));
229                }
230
231                let message_name = try_consume_string_from_options(
232                    &mut format_encode_options_to_consume,
233                    MESSAGE_NAME_KEY,
234                );
235                let name_strategy = get_sr_name_strategy_check(
236                    &mut format_encode_options_to_consume,
237                    use_schema_registry,
238                )?;
239
240                stream_source_info.use_schema_registry = use_schema_registry;
241                stream_source_info
242                    .row_schema_location
243                    .clone_from(&row_schema_location.0);
244                stream_source_info.proto_message_name =
245                    message_name.unwrap_or(AstString("".into())).0;
246                stream_source_info.key_message_name =
247                    get_key_message_name(&mut format_encode_options_to_consume);
248                stream_source_info.name_strategy =
249                    name_strategy.unwrap_or(PbSchemaRegistryNameStrategy::Unspecified as i32);
250            }
251
252            Some(
253                extract_avro_table_schema(
254                    &stream_source_info,
255                    &options_with_secret,
256                    &mut format_encode_options_to_consume,
257                    matches!(format, Format::Debezium),
258                )
259                .await?,
260            )
261        }
262        (Format::Plain, Encode::Csv) => {
263            let chars =
264                consume_string_from_options(&mut format_encode_options_to_consume, "delimiter")?.0;
265            let delimiter = get_delimiter(chars.as_str()).context("failed to parse delimiter")?;
266            let has_header = try_consume_string_from_options(
267                &mut format_encode_options_to_consume,
268                "without_header",
269            )
270            .map(|s| s.0 == "false")
271            .unwrap_or(true);
272
273            if is_kafka && has_header {
274                return Err(RwError::from(ProtocolError(
275                    "CSV HEADER is not supported when creating table with Kafka connector"
276                        .to_owned(),
277                )));
278            }
279
280            stream_source_info.csv_delimiter = delimiter as i32;
281            stream_source_info.csv_has_header = has_header;
282
283            None
284        }
285        // For parquet format, this step is implemented in parquet parser.
286        (Format::Plain, Encode::Parquet) => None,
287        (
288            Format::Plain | Format::Upsert | Format::Maxwell | Format::Canal | Format::Debezium,
289            Encode::Json,
290        ) => {
291            if matches!(
292                format_encode.format,
293                Format::Plain | Format::Upsert | Format::Debezium
294            ) {
295                // Parse the value but throw it away.
296                // It would be too late to report error in `SpecificParserConfig::new`,
297                // which leads to recovery loop.
298                // TODO: rely on SpecificParserConfig::new to validate, like Avro
299                if let Some(value) =
300                    format_encode_options_to_consume.get(TimestamptzHandling::OPTION_KEY)
301                {
302                    TimestamptzHandling::from_options(value)
303                        .map_err(|err| InvalidInputSyntax(err.message))?;
304                }
305                try_consume_string_from_options(
306                    &mut format_encode_options_to_consume,
307                    TimestamptzHandling::OPTION_KEY,
308                );
309            }
310
311            if let Some(config) =
312                PulsarSchemaConfig::from_options(&format_encode_options_to_consume)?
313            {
314                Some(
315                    extract_pulsar_json_table_schema(
316                        &config,
317                        &options_with_secret,
318                        &mut format_encode_options_to_consume,
319                    )
320                    .await?,
321                )
322            } else {
323                let schema_config =
324                    get_json_schema_location(&mut format_encode_options_to_consume)?;
325                stream_source_info.use_schema_registry =
326                    json_schema_infer_use_schema_registry(&schema_config);
327
328                extract_json_table_schema(
329                    &schema_config,
330                    &options_with_secret,
331                    &mut format_encode_options_to_consume,
332                )
333                .await?
334            }
335        }
336        (Format::None, Encode::None) => {
337            if options_with_secret.is_iceberg_connector() {
338                Some(
339                    extract_iceberg_columns(&options_with_secret)
340                        .await
341                        .map_err(|err| ProtocolError(err.to_report_string()))?,
342                )
343            } else if options_with_secret
344                .get(UPSTREAM_SOURCE_KEY)
345                .is_some_and(|s| s.eq_ignore_ascii_case(ADBC_SNOWFLAKE_CONNECTOR))
346            {
347                Some(
348                    extract_adbc_snowflake_columns(&options_with_secret)
349                        .await
350                        .map_err(|err| ProtocolError(err.to_report_string()))?,
351                )
352            } else {
353                None
354            }
355        }
356        (format, encoding) => {
357            return Err(RwError::from(ProtocolError(format!(
358                "Unknown combination {:?} {:?}",
359                format, encoding
360            ))));
361        }
362    };
363
364    if !format_encode_options_to_consume.is_empty() {
365        let err_string = format!(
366            "Get unknown format_encode_options for {:?} {:?}: {}",
367            format_encode.format,
368            format_encode.row_encode,
369            format_encode_options_to_consume
370                .keys()
371                .cloned()
372                .collect::<Vec<String>>()
373                .join(","),
374        );
375        session.notice_to_user(err_string);
376    }
377    Ok((columns, stream_source_info))
378}
379
380fn bind_columns_from_source_for_cdc(
381    session: &SessionImpl,
382    format_encode: &FormatEncodeOptions,
383) -> Result<(Option<Vec<ColumnCatalog>>, StreamSourceInfo)> {
384    let with_options = WithOptions::try_from(format_encode.row_options())?;
385    if !with_options.connection_ref().is_empty() {
386        return Err(RwError::from(NotSupported(
387            "CDC connector does not support connection ref yet".to_owned(),
388            "Explicitly specify the connection in WITH clause".to_owned(),
389        )));
390    }
391    let (format_encode_options, format_encode_secret_refs) =
392        resolve_secret_ref_in_with_options(with_options, session)?.into_parts();
393
394    // Need real secret to access the schema registry
395    let mut format_encode_options_to_consume = LocalSecretManager::global().fill_secrets(
396        format_encode_options.clone(),
397        format_encode_secret_refs.clone(),
398    )?;
399
400    match (&format_encode.format, &format_encode.row_encode) {
401        (Format::Plain, Encode::Json) => (),
402        (format, encoding) => {
403            // Note: parser will also check this. Just be extra safe here
404            return Err(RwError::from(ProtocolError(format!(
405                "Row format for CDC connectors should be either omitted or set to `FORMAT PLAIN ENCODE JSON`, got: {:?} {:?}",
406                format, encoding
407            ))));
408        }
409    };
410
411    let columns = ColumnCatalog::debezium_cdc_source_cols().to_vec();
412    let schema_config = get_json_schema_location(&mut format_encode_options_to_consume)?;
413
414    let stream_source_info = StreamSourceInfo {
415        format: format_to_prost(&format_encode.format) as i32,
416        row_encode: row_encode_to_prost(&format_encode.row_encode) as i32,
417        format_encode_options,
418        use_schema_registry: json_schema_infer_use_schema_registry(&schema_config),
419        cdc_source_job: true,
420        is_distributed: false,
421        format_encode_secret_refs,
422        ..Default::default()
423    };
424    if !format_encode_options_to_consume.is_empty() {
425        let err_string = format!(
426            "Get unknown format_encode_options for {:?} {:?}: {}",
427            format_encode.format,
428            format_encode.row_encode,
429            format_encode_options_to_consume
430                .keys()
431                .cloned()
432                .collect::<Vec<String>>()
433                .join(","),
434        );
435        session.notice_to_user(err_string);
436    }
437    Ok((Some(columns), stream_source_info))
438}
439
440pub(super) fn format_to_prost(format: &Format) -> FormatType {
441    match format {
442        Format::Native => FormatType::Native,
443        Format::Plain => FormatType::Plain,
444        Format::Upsert => FormatType::Upsert,
445        Format::Debezium => FormatType::Debezium,
446        Format::DebeziumMongo => FormatType::DebeziumMongo,
447        Format::Maxwell => FormatType::Maxwell,
448        Format::Canal => FormatType::Canal,
449        Format::None => FormatType::None,
450    }
451}
452pub(super) fn row_encode_to_prost(row_encode: &Encode) -> EncodeType {
453    match row_encode {
454        Encode::Native => EncodeType::Native,
455        Encode::Json => EncodeType::Json,
456        Encode::Avro => EncodeType::Avro,
457        Encode::Protobuf => EncodeType::Protobuf,
458        Encode::Csv => EncodeType::Csv,
459        Encode::Bytes => EncodeType::Bytes,
460        Encode::Template => EncodeType::Template,
461        Encode::Parquet => EncodeType::Parquet,
462        Encode::None => EncodeType::None,
463        Encode::Text => EncodeType::Text,
464    }
465}
466
467pub fn get_schema_location(
468    format_encode_options: &mut BTreeMap<String, String>,
469) -> Result<(AstString, bool)> {
470    let schema_location = try_consume_string_from_options(format_encode_options, "schema.location");
471    let schema_registry = try_consume_string_from_options(format_encode_options, "schema.registry");
472    match (schema_location, schema_registry) {
473        (None, None) => Err(RwError::from(ProtocolError(
474            "missing either a schema location or a schema registry".to_owned(),
475        ))),
476        (None, Some(schema_registry)) => Ok((schema_registry, true)),
477        (Some(schema_location), None) => Ok((schema_location, false)),
478        (Some(_), Some(_)) => Err(RwError::from(ProtocolError(
479            "only need either the schema location or the schema registry".to_owned(),
480        ))),
481    }
482}
483
484pub fn schema_has_schema_registry(schema: &FormatEncodeOptions) -> bool {
485    match schema.row_encode {
486        Encode::Avro | Encode::Protobuf => true,
487        Encode::Json => {
488            let mut options = WithOptions::try_from(schema.row_options()).unwrap();
489            uses_pulsar_schema(&options, options.secret_ref())
490                || matches!(get_json_schema_location(options.inner_mut()), Ok(Some(_)))
491        }
492        _ => false,
493    }
494}
495
496#[inline]
497fn get_name_strategy_or_default(name_strategy: Option<AstString>) -> Result<Option<i32>> {
498    match name_strategy {
499        None => Ok(None),
500        Some(name) => Ok(Some(name_strategy_from_str(name.0.as_str())
501            .ok_or_else(|| RwError::from(ProtocolError(format!("\
502            expect strategy name in topic_name_strategy, record_name_strategy and topic_record_name_strategy, but got {}", name))))? as i32)),
503    }
504}