1use std::collections::{BTreeMap, HashMap, HashSet};
16use std::rc::Rc;
17use std::sync::LazyLock;
18
19use anyhow::Context;
20use either::Either;
21use external_schema::debezium::extract_debezium_avro_table_pk_columns;
22use external_schema::nexmark::check_nexmark_schema;
23use itertools::Itertools;
24use maplit::{convert_args, hashmap, hashset};
25use pgwire::pg_response::{PgResponse, StatementType};
26use risingwave_common::array::arrow::{IcebergArrowConvert, arrow_schema_iceberg};
27use risingwave_common::bail_not_implemented;
28use risingwave_common::catalog::{
29 CdcTableDesc, ColumnCatalog, ColumnDesc, ColumnId, INITIAL_SOURCE_VERSION_ID,
30 KAFKA_TIMESTAMP_COLUMN_NAME, ROW_ID_COLUMN_NAME, TableId, debug_assert_column_ids_distinct,
31};
32use risingwave_common::license::Feature;
33use risingwave_common::secret::LocalSecretManager;
34use risingwave_common::system_param::reader::SystemParamsRead;
35use risingwave_common::types::DataType;
36use risingwave_common::util::iter_util::ZipEqFast;
37use risingwave_connector::parser::additional_columns::{
38 build_additional_column_desc, source_add_partition_offset_cols,
39};
40use risingwave_connector::parser::{
41 AvroParserConfig, DEBEZIUM_IGNORE_KEY, DebeziumAvroParserConfig, ProtobufParserConfig,
42 SchemaLocation, SpecificParserConfig, TimestamptzHandling,
43 fetch_json_schema_and_map_to_columns, fetch_pulsar_json_schema_and_map_to_columns,
44};
45use risingwave_connector::schema::AWS_GLUE_SCHEMA_ARN_KEY;
46use risingwave_connector::schema::pulsar_schema::{
47 PULSAR_SCHEMA_AUTH_TOKEN_KEY, PULSAR_SCHEMA_URL_KEY, PulsarSchemaConfig,
48};
49use risingwave_connector::schema::schema_registry::{
50 SCHEMA_REGISTRY_BACKOFF_DURATION_KEY, SCHEMA_REGISTRY_BACKOFF_FACTOR_KEY,
51 SCHEMA_REGISTRY_CA_PEM_PATH, SCHEMA_REGISTRY_MAX_DELAY_KEY, SCHEMA_REGISTRY_PASSWORD,
52 SCHEMA_REGISTRY_RETRIES_MAX_KEY, SCHEMA_REGISTRY_USERNAME, SchemaRegistryConfig,
53 name_strategy_from_str,
54};
55use risingwave_connector::source::cdc::{
56 CDC_BACKFILL_ENABLE_KEY, CDC_MONGODB_STRONG_SCHEMA_KEY, CDC_SHARING_MODE_KEY,
57 CDC_SNAPSHOT_BACKFILL, CDC_SNAPSHOT_MODE_KEY, CDC_TRANSACTIONAL_KEY,
58 CDC_WAIT_FOR_STREAMING_START_TIMEOUT, CdcSourceDdlValidator, CdcSourceRuntimeFlags,
59};
60use risingwave_connector::source::datagen::DATAGEN_CONNECTOR;
61use risingwave_connector::source::iceberg::ICEBERG_CONNECTOR;
62use risingwave_connector::source::nexmark::source::{EventType, get_event_data_types_with_names};
63use risingwave_connector::source::test_source::TEST_CONNECTOR;
64pub use risingwave_connector::source::{
65 ADBC_SNOWFLAKE_CONNECTOR, UPSTREAM_SOURCE_KEY, WEBHOOK_CONNECTOR,
66};
67use risingwave_connector::source::{
68 AZBLOB_CONNECTOR, ConnectorProperties, GCS_CONNECTOR, GOOGLE_PUBSUB_CONNECTOR, KAFKA_CONNECTOR,
69 KINESIS_CONNECTOR, LEGACY_S3_CONNECTOR, MQTT_CONNECTOR, NATS_CONNECTOR, NEXMARK_CONNECTOR,
70 OPENDAL_S3_CONNECTOR, POSIX_FS_CONNECTOR, PULSAR_CONNECTOR,
71};
72use risingwave_connector::{AUTO_SCHEMA_CHANGE_KEY, WithPropertiesExt, dispatch_cdc_source_type};
73use risingwave_pb::catalog::connection_params::PbConnectionType;
74use risingwave_pb::catalog::{PbSchemaRegistryNameStrategy, StreamSourceInfo, WatermarkDesc};
75use risingwave_pb::plan_common::additional_column::ColumnType as AdditionalColumnType;
76use risingwave_pb::plan_common::source_refresh_mode::{RefreshMode, SourceRefreshModeStreaming};
77use risingwave_pb::plan_common::{EncodeType, FormatType, SourceRefreshMode};
78use risingwave_pb::stream_plan::PbStreamFragmentGraph;
79use risingwave_pb::telemetry::TelemetryDatabaseObject;
80use risingwave_sqlparser::ast::{
81 AstString, ColumnDef, ColumnOption, CreateSourceStatement, Encode, Format, FormatEncodeOptions,
82 ObjectName, SourceWatermark, SqlOptionValue, TableConstraint, Value, get_delimiter,
83};
84use risingwave_sqlparser::parser::{IncludeOption, IncludeOptionItem};
85use thiserror_ext::AsReport;
86
87use super::RwPgResponse;
88use crate::binder::Binder;
89use crate::catalog::CatalogError;
90use crate::catalog::root_catalog::SchemaPath;
91use crate::catalog::source_catalog::SourceCatalog;
92use crate::error::ErrorCode::{self, Deprecated, InvalidInputSyntax, NotSupported, ProtocolError};
93use crate::error::{Result, RwError};
94use crate::expr::{Expr, ExprRewriter, SessionTimezone};
95use crate::handler::HandlerArgs;
96use crate::handler::cdc::{
97 bind_cdc_pk_comparisons_externally, bind_cdc_table_schema, bind_cdc_table_schema_externally,
98 derive_connect_properties_for_cdc_table, not_null_check_for_cdc_table,
99 reject_pk_filtered_by_debezium_column_filter, sanity_check_for_table_on_cdc_source,
100};
101use crate::handler::create_table::{
102 ColumnIdGenerator, bind_pk_and_row_id_on_relation, bind_sql_column_constraints,
103 bind_sql_columns, bind_sql_pk_names, bind_table_constraints, check_cdc_source_select_privilege,
104};
105use crate::handler::util::{
106 SourceSchemaCompatExt, check_connector_match_connection_type, ensure_connection_type_allowed,
107 ensure_local_fs_connector_allowed,
108};
109use crate::optimizer::plan_node::generic::SourceNodeKind;
110use crate::optimizer::plan_node::{BackfillType, LogicalSource, ToStream, ToStreamContext};
111use crate::session::SessionImpl;
112use crate::session::current::notice_to_user;
113use crate::utils::{
114 OverwriteOptions, resolve_connection_ref_and_secret_ref, resolve_privatelink_in_with_option,
115 resolve_secret_ref_in_with_options, resolve_source_refresh_mode_in_with_option,
116};
117use crate::{OptimizerContext, WithOptions, WithOptionsSecResolved, bind_data_type, build_graph};
118
119mod external_schema;
120pub use external_schema::{
121 bind_columns_from_source, get_schema_location, schema_has_schema_registry,
122};
123use external_schema::{format_to_prost, row_encode_to_prost};
124mod validate;
125pub use validate::validate_compatibility;
126use validate::{SOURCE_ALLOWED_CONNECTION_CONNECTOR, SOURCE_ALLOWED_CONNECTION_SCHEMA_REGISTRY};
127mod additional_column;
128use additional_column::check_and_add_timestamp_column;
129pub use additional_column::handle_addition_columns;
130use risingwave_common::catalog::ICEBERG_SOURCE_PREFIX;
131use risingwave_common::id::SourceId;
132
133use crate::stream_fragmenter::GraphJobType;
134
135fn non_generated_sql_columns(columns: &[ColumnDef]) -> Vec<ColumnDef> {
136 columns
137 .iter()
138 .filter(|c| !c.is_generated())
139 .cloned()
140 .collect()
141}
142
143fn try_consume_string_from_options(
144 format_encode_options: &mut BTreeMap<String, String>,
145 key: &str,
146) -> Option<AstString> {
147 format_encode_options.remove(key).map(AstString)
148}
149
150fn try_consume_schema_registry_config_from_options(
151 format_encode_options: &mut BTreeMap<String, String>,
152) {
153 [
154 SCHEMA_REGISTRY_USERNAME,
155 SCHEMA_REGISTRY_PASSWORD,
156 SCHEMA_REGISTRY_CA_PEM_PATH,
157 SCHEMA_REGISTRY_MAX_DELAY_KEY,
158 SCHEMA_REGISTRY_BACKOFF_DURATION_KEY,
159 SCHEMA_REGISTRY_BACKOFF_FACTOR_KEY,
160 SCHEMA_REGISTRY_RETRIES_MAX_KEY,
161 ]
162 .iter()
163 .for_each(|key| {
164 try_consume_string_from_options(format_encode_options, key);
165 });
166}
167
168pub(crate) fn uses_pulsar_schema<V>(
170 format_encode_options: &BTreeMap<String, String>,
171 format_encode_secret_refs: &BTreeMap<String, V>,
172) -> bool {
173 format_encode_options.contains_key(PULSAR_SCHEMA_URL_KEY)
174 || format_encode_secret_refs.contains_key(PULSAR_SCHEMA_URL_KEY)
175}
176
177fn try_consume_pulsar_schema_config_from_options(
178 format_encode_options: &mut BTreeMap<String, String>,
179) {
180 for key in [PULSAR_SCHEMA_URL_KEY, PULSAR_SCHEMA_AUTH_TOKEN_KEY] {
181 try_consume_string_from_options(format_encode_options, key);
182 }
183}
184
185fn consume_string_from_options(
186 format_encode_options: &mut BTreeMap<String, String>,
187 key: &str,
188) -> Result<AstString> {
189 try_consume_string_from_options(format_encode_options, key).ok_or(RwError::from(ProtocolError(
190 format!("missing field {} in options", key),
191 )))
192}
193
194fn consume_aws_config_from_options(format_encode_options: &mut BTreeMap<String, String>) {
195 format_encode_options.retain(|key, _| !key.starts_with("aws."))
196}
197
198#[derive(Debug, Clone, Copy, PartialEq, Eq)]
199pub enum CreateSourceType {
200 SharedCdc,
201 SharedNonCdc,
203 NonShared,
204 Table,
206}
207
208impl CreateSourceType {
209 pub fn for_newly_created(
211 session: &SessionImpl,
212 with_properties: &impl WithPropertiesExt,
213 ) -> Self {
214 if with_properties.is_shareable_cdc_connector() {
215 CreateSourceType::SharedCdc
216 } else if with_properties.is_shareable_non_cdc_connector()
217 && session
218 .env()
219 .streaming_config()
220 .developer
221 .enable_shared_source
222 && session.config().streaming_use_shared_source()
223 {
224 CreateSourceType::SharedNonCdc
225 } else {
226 CreateSourceType::NonShared
227 }
228 }
229
230 pub fn for_replace(catalog: &SourceCatalog) -> Self {
231 if !catalog.info.is_shared() {
232 CreateSourceType::NonShared
233 } else if catalog.with_properties.is_shareable_cdc_connector() {
234 CreateSourceType::SharedCdc
235 } else {
236 CreateSourceType::SharedNonCdc
237 }
238 }
239
240 pub fn is_shared(&self) -> bool {
241 matches!(
242 self,
243 CreateSourceType::SharedCdc | CreateSourceType::SharedNonCdc
244 )
245 }
246}
247
248pub(crate) fn bind_all_columns(
250 format_encode: &FormatEncodeOptions,
251 cols_from_source: Option<Vec<ColumnCatalog>>,
252 cols_from_sql: Vec<ColumnCatalog>,
253 col_defs_from_sql: &[ColumnDef],
254 wildcard_idx: Option<usize>,
255 sql_column_strategy: SqlColumnStrategy,
256) -> Result<Vec<ColumnCatalog>> {
257 if let Some(cols_from_source) = cols_from_source {
258 let generated_cols_from_sql = cols_from_sql
261 .iter()
262 .filter(|c| {
263 col_defs_from_sql
264 .iter()
265 .find(|d| d.name.real_value() == c.name())
266 .unwrap()
267 .is_generated()
268 })
269 .cloned()
270 .collect_vec();
271
272 match sql_column_strategy {
273 SqlColumnStrategy::FollowUnchecked => {
275 assert!(
276 wildcard_idx.is_none(),
277 "wildcard still exists while strategy is Follows, not correctly purified?"
278 );
279 return Ok(cols_from_sql);
280 }
281
282 SqlColumnStrategy::Ignore => {}
284
285 SqlColumnStrategy::FollowChecked => {
286 let has_regular_cols_from_sql =
287 generated_cols_from_sql.len() != cols_from_sql.len();
288
289 if has_regular_cols_from_sql {
290 if wildcard_idx.is_some() {
291 return Err(RwError::from(NotSupported(
293 "When there's a wildcard (\"*\"), \
294 only generated columns are allowed in user-defined schema from SQL"
295 .to_owned(),
296 "Remove the non-generated columns".to_owned(),
297 )));
298 } else {
299 for col in &cols_from_sql {
302 if generated_cols_from_sql.contains(col) {
303 continue;
304 }
305 let Some(col_from_source) =
306 cols_from_source.iter().find(|c| c.name() == col.name())
307 else {
308 return Err(RwError::from(ProtocolError(format!(
309 "Column \"{}\" is defined in SQL but not found in the source",
310 col.name()
311 ))));
312 };
313
314 if col_from_source.data_type() != col.data_type() {
315 return Err(RwError::from(ProtocolError(format!(
316 "Data type mismatch for column \"{}\". \
317 Defined in SQL as \"{}\", but found in the source as \"{}\"",
318 col.name(),
319 col.data_type(),
320 col_from_source.data_type()
321 ))));
322 }
323 }
324 return Ok(cols_from_sql);
325 }
326 } else {
327 if wildcard_idx.is_some() {
328 } else {
332 notice_to_user("\
340 Neither wildcard (\"*\") nor regular (non-generated) columns appear in the user-defined schema from SQL. \
341 For backward compatibility, all columns from the source will be included at the beginning. \
342 For clarity, consider adding a wildcard (\"*\") to indicate where the columns from the source should be included, \
343 or specifying the columns you want to include from the source.
344 ");
345 }
346 }
347 }
348 }
349
350 let wildcard_idx = wildcard_idx.unwrap_or(0).min(generated_cols_from_sql.len());
355
356 let mut merged_cols = generated_cols_from_sql;
358 let merged_cols_r = merged_cols.split_off(wildcard_idx);
359 merged_cols.extend(cols_from_source);
360 merged_cols.extend(merged_cols_r);
361
362 Ok(merged_cols)
363 } else {
364 if wildcard_idx.is_some() {
365 return Err(RwError::from(NotSupported(
366 "Wildcard in user-defined schema is only allowed when there exists columns from external schema".to_owned(),
367 "Remove the wildcard or use a source with external schema".to_owned(),
368 )));
369 }
370 let non_generated_sql_defined_columns = non_generated_sql_columns(col_defs_from_sql);
371
372 match (&format_encode.format, &format_encode.row_encode) {
373 (Format::DebeziumMongo, Encode::Json) => {
374 let strong_schema = format_encode
375 .row_options
376 .iter()
377 .find(|k| k.name.real_value().to_lowercase() == CDC_MONGODB_STRONG_SCHEMA_KEY)
378 .map(|k| matches!(k.value, SqlOptionValue::Value(Value::Boolean(true))))
379 .unwrap_or(false);
380
381 if strong_schema {
383 let (_, id_column) = non_generated_sql_defined_columns
384 .iter()
385 .enumerate()
386 .find(|(idx, col)| *idx == 0 && col.name.real_value() == "_id")
387 .ok_or_else(|| {
388 RwError::from(ProtocolError(
389 "The `_id` column of the source with row format DebeziumMongoJson must be defined as the first column in SQL".to_owned(),
390 ))
391 })?;
392
393 let id_data_type = bind_data_type(id_column.data_type.as_ref().unwrap())?;
394 if !matches!(
395 id_data_type,
396 DataType::Varchar | DataType::Int32 | DataType::Int64 | DataType::Jsonb
397 ) {
398 return Err(RwError::from(ProtocolError(
399 "the `_id` column of the source with row format DebeziumMongoJson must be [Jsonb | Varchar | Int32 | Int64]".to_owned(),
400 )));
401 }
402
403 let mut columns = Vec::with_capacity(non_generated_sql_defined_columns.len());
404 columns.push(
405 ColumnCatalog {
407 column_desc: ColumnDesc::named("_id", 0.into(), id_data_type),
408 is_hidden: false,
409 },
410 );
411
412 for (idx, col) in non_generated_sql_defined_columns
414 .into_iter()
415 .skip(1)
417 .enumerate()
418 {
419 columns.push(ColumnCatalog {
420 column_desc: ColumnDesc::named(
421 col.name.real_value(),
422 (idx as i32).into(),
423 bind_data_type(col.data_type.as_ref().unwrap())?,
424 ),
425 is_hidden: false,
426 });
427 }
428
429 return Ok(columns);
430 }
431
432 let mut columns = vec![
433 ColumnCatalog {
434 column_desc: ColumnDesc::named("_id", 0.into(), DataType::Varchar),
435 is_hidden: false,
436 },
437 ColumnCatalog {
438 column_desc: ColumnDesc::named("payload", 0.into(), DataType::Jsonb),
439 is_hidden: false,
440 },
441 ];
442
443 if non_generated_sql_defined_columns.len() != 2
444 || non_generated_sql_defined_columns[0].name.real_value() != columns[0].name()
445 || non_generated_sql_defined_columns[1].name.real_value() != columns[1].name()
446 {
447 return Err(RwError::from(ProtocolError(
448 "the not generated columns of the source with row format DebeziumMongoJson
449 must be (_id [Jsonb | Varchar | Int32 | Int64], payload jsonb)."
450 .to_owned(),
451 )));
452 }
453 let key_data_type = bind_data_type(
455 non_generated_sql_defined_columns[0]
456 .data_type
457 .as_ref()
458 .unwrap(),
459 )?;
460 match key_data_type {
461 DataType::Jsonb | DataType::Varchar | DataType::Int32 | DataType::Int64 => {
462 columns[0].column_desc.data_type = key_data_type;
463 }
464 _ => {
465 return Err(RwError::from(ProtocolError(
466 "the `_id` column of the source with row format DebeziumMongoJson
467 must be [Jsonb | Varchar | Int32 | Int64]"
468 .to_owned(),
469 )));
470 }
471 }
472
473 let value_data_type = bind_data_type(
475 non_generated_sql_defined_columns[1]
476 .data_type
477 .as_ref()
478 .unwrap(),
479 )?;
480 if !matches!(value_data_type, DataType::Jsonb) {
481 return Err(RwError::from(ProtocolError(
482 "the `payload` column of the source with row format DebeziumMongoJson
483 must be Jsonb datatype"
484 .to_owned(),
485 )));
486 }
487 Ok(columns)
488 }
489 (Format::Plain, Encode::Bytes) => {
490 let err = Err(RwError::from(ProtocolError(
491 "ENCODE BYTES only accepts one BYTEA type column".to_owned(),
492 )));
493 if non_generated_sql_defined_columns.len() == 1 {
494 let col_data_type = bind_data_type(
496 non_generated_sql_defined_columns[0]
497 .data_type
498 .as_ref()
499 .unwrap(),
500 )?;
501 if col_data_type == DataType::Bytea {
502 Ok(cols_from_sql)
503 } else {
504 err
505 }
506 } else {
507 err
508 }
509 }
510 (_, _) => Ok(cols_from_sql),
511 }
512 }
513}
514
515fn hint_format_encode(format_encode: &FormatEncodeOptions) -> String {
517 format!(
518 r#"Hint: For FORMAT {0} ENCODE {1}, INCLUDE KEY must be specified and the key column must be used as primary key.
519example:
520 CREATE TABLE <table_name> ( PRIMARY KEY ([rw_key | <key_name>]) )
521 INCLUDE KEY [AS <key_name>]
522 WITH (...)
523 FORMAT {0} ENCODE {1}{2}
524"#,
525 format_encode.format,
526 format_encode.row_encode,
527 if format_encode.row_encode == Encode::Json || format_encode.row_encode == Encode::Bytes {
528 "".to_owned()
529 } else {
530 " (...)".to_owned()
531 }
532 )
533}
534
535pub(crate) async fn bind_source_pk(
538 format_encode: &FormatEncodeOptions,
539 source_info: &StreamSourceInfo,
540 columns: &mut [ColumnCatalog],
541 sql_defined_pk_names: Vec<String>,
542 with_properties: &WithOptionsSecResolved,
543) -> Result<Vec<String>> {
544 if with_properties.is_iceberg_update_source() {
545 if sql_defined_pk_names.is_empty() {
546 return Err(InvalidInputSyntax(
547 "Iceberg streaming_updates requires a SQL PRIMARY KEY for retracting ingestion"
548 .to_owned(),
549 )
550 .into());
551 }
552 return Ok(sql_defined_pk_names);
553 }
554 let sql_defined_pk = !sql_defined_pk_names.is_empty();
555 let include_key_column_name: Option<String> = {
556 columns.iter().find_map(|catalog| {
559 if matches!(
560 catalog.column_desc.additional_column.column_type,
561 Some(AdditionalColumnType::Key(_))
562 ) {
563 Some(catalog.name().to_owned())
564 } else {
565 None
566 }
567 })
568 };
569 let additional_column_names = columns
570 .iter()
571 .filter_map(|col| {
572 if col.column_desc.additional_column.column_type.is_some() {
573 Some(col.name().to_owned())
574 } else {
575 None
576 }
577 })
578 .collect_vec();
579
580 let res = match (&format_encode.format, &format_encode.row_encode) {
581 (Format::Native, Encode::Native) | (Format::None, Encode::None) | (Format::Plain, _) => {
582 sql_defined_pk_names
583 }
584
585 (Format::Upsert, Encode::Json | Encode::Avro | Encode::Protobuf) => {
588 if let Some(ref key_column_name) = include_key_column_name
589 && sql_defined_pk
590 {
591 if sql_defined_pk_names.len() != 1
596 || !key_column_name.eq(sql_defined_pk_names[0].as_str())
597 {
598 return Err(RwError::from(ProtocolError(format!(
599 "Only \"{}\" can be used as primary key\n\n{}",
600 key_column_name,
601 hint_format_encode(format_encode)
602 ))));
603 }
604 sql_defined_pk_names
605 } else {
606 return if let Some(include_key_column_name) = include_key_column_name {
608 Err(RwError::from(ProtocolError(format!(
609 "Primary key must be specified to {}\n\n{}",
610 include_key_column_name,
611 hint_format_encode(format_encode)
612 ))))
613 } else {
614 Err(RwError::from(ProtocolError(format!(
615 "INCLUDE KEY clause not set\n\n{}",
616 hint_format_encode(format_encode)
617 ))))
618 };
619 }
620 }
621
622 (Format::Debezium, Encode::Json) => {
623 if !additional_column_names.is_empty() {
624 return Err(RwError::from(ProtocolError(format!(
625 "FORMAT DEBEZIUM forbids additional columns, but got {:?}",
626 additional_column_names
627 ))));
628 }
629 if !sql_defined_pk {
630 return Err(RwError::from(ProtocolError(
631 "Primary key must be specified when creating source with FORMAT DEBEZIUM."
632 .to_owned(),
633 )));
634 }
635 sql_defined_pk_names
636 }
637 (Format::Debezium, Encode::Avro) => {
638 if !additional_column_names.is_empty() {
639 return Err(RwError::from(ProtocolError(format!(
640 "FORMAT DEBEZIUM forbids additional columns, but got {:?}",
641 additional_column_names
642 ))));
643 }
644 if sql_defined_pk {
645 sql_defined_pk_names
646 } else {
647 let pk_names =
648 extract_debezium_avro_table_pk_columns(source_info, with_properties).await?;
649 for pk_name in &pk_names {
651 columns
652 .iter()
653 .find(|c: &&ColumnCatalog| c.name().eq(pk_name))
654 .ok_or_else(|| {
655 RwError::from(ProtocolError(format!(
656 "avro's key column {} not exists in avro's row schema",
657 pk_name
658 )))
659 })?;
660 }
661 pk_names
662 }
663 }
664 (Format::DebeziumMongo, Encode::Json) => {
665 if sql_defined_pk {
666 sql_defined_pk_names
667 } else {
668 vec!["_id".to_owned()]
669 }
670 }
671
672 (Format::Maxwell, Encode::Json) => {
673 if !additional_column_names.is_empty() {
674 return Err(RwError::from(ProtocolError(format!(
675 "FORMAT MAXWELL forbids additional columns, but got {:?}",
676 additional_column_names
677 ))));
678 }
679 if !sql_defined_pk {
680 return Err(RwError::from(ProtocolError(
681 "Primary key must be specified when creating source with FORMAT MAXWELL ENCODE JSON.".to_owned(),
682 )));
683 }
684 sql_defined_pk_names
685 }
686
687 (Format::Canal, Encode::Json) => {
688 if !additional_column_names.is_empty() {
689 return Err(RwError::from(ProtocolError(format!(
690 "FORMAT CANAL forbids additional columns, but got {:?}",
691 additional_column_names
692 ))));
693 }
694 if !sql_defined_pk {
695 return Err(RwError::from(ProtocolError(
696 "Primary key must be specified when creating source with FORMAT CANAL ENCODE JSON.".to_owned(),
697 )));
698 }
699 sql_defined_pk_names
700 }
701 (format, encoding) => {
702 return Err(RwError::from(ProtocolError(format!(
703 "Unknown combination {:?} {:?}",
704 format, encoding
705 ))));
706 }
707 };
708 Ok(res)
709}
710
711pub(super) fn bind_source_watermark(
712 session: &SessionImpl,
713 name: String,
714 source_watermarks: Vec<SourceWatermark>,
715 column_catalogs: &[ColumnCatalog],
716) -> Result<Vec<WatermarkDesc>> {
717 let mut binder = Binder::new_for_ddl(session);
718 binder.bind_columns_to_context(name.clone(), column_catalogs)?;
719
720 let mut session_tz = SessionTimezone::new(session.config().timezone());
721
722 let watermark_descs = source_watermarks
723 .into_iter()
724 .map(|source_watermark| {
725 let col_name = source_watermark.column.real_value();
726 let watermark_idx = binder.get_column_binding_index(name.clone(), &col_name)?;
727
728 let expr = binder.bind_expr(&source_watermark.expr)?;
729 let expr = session_tz.rewrite_expr(expr);
733 let watermark_col_type = column_catalogs[watermark_idx].data_type();
734 let watermark_expr_type = &expr.return_type();
735 if watermark_col_type != watermark_expr_type {
736 Err(RwError::from(ErrorCode::BindError(
737 format!("The return value type of the watermark expression must be identical to the watermark column data type. Current data type of watermark return value: `{}`, column `{}`",watermark_expr_type, watermark_col_type),
738 )))
739 } else {
740 let expr_proto = expr.to_expr_proto();
741 Ok::<_, RwError>(WatermarkDesc {
742 watermark_idx: watermark_idx as u32,
743 expr: Some(expr_proto),
744 with_ttl: source_watermark.with_ttl,
745 })
746 }
747 })
748 .try_collect()?;
749 Ok(watermark_descs)
750}
751
752pub(super) fn check_format_encode(
758 props: &WithOptionsSecResolved,
759 row_id_index: Option<usize>,
760 columns: &[ColumnCatalog],
761) -> Result<()> {
762 let Some(connector) = props.get_connector() else {
763 return Ok(());
764 };
765
766 if connector == NEXMARK_CONNECTOR {
767 check_nexmark_schema(props, row_id_index, columns)
768 } else {
769 Ok(())
770 }
771}
772
773pub fn bind_connector_props(
774 handler_args: &HandlerArgs,
775 format_encode: &FormatEncodeOptions,
776 is_create_source: bool,
777) -> Result<(WithOptions, SourceRefreshMode)> {
778 let mut with_properties = handler_args.with_options.clone().into_connector_props();
779 if with_properties.is_iceberg_connector()
782 && with_properties
783 .secret_ref()
784 .contains_key(risingwave_connector::source::iceberg::STREAMING_UPDATES_KEY)
785 {
786 return Err(RwError::from(ErrorCode::InvalidParameterValue(
787 "streaming_updates cannot be provided as a SECRET; use a plaintext 'true' or 'false'"
788 .to_owned(),
789 )));
790 }
791 validate_compatibility(format_encode, &mut with_properties)?;
792 let refresh_mode = {
793 let refresh_mode = resolve_source_refresh_mode_in_with_option(&mut with_properties)?;
794 if is_create_source && refresh_mode.is_some() {
795 return Err(RwError::from(ProtocolError(
796 "`refresh_mode` only supported for CREATE TABLE".to_owned(),
797 )));
798 }
799
800 refresh_mode.unwrap_or(SourceRefreshMode {
801 refresh_mode: Some(RefreshMode::Streaming(SourceRefreshModeStreaming {})),
802 })
803 };
804
805 let create_cdc_source_job = with_properties.is_shareable_cdc_connector();
806
807 if !is_create_source && with_properties.is_shareable_cdc_connector() {
808 return Err(RwError::from(ProtocolError(format!(
809 "directly creating a CDC table for connector {} is no longer supported; \
810 please `CREATE SOURCE` to create a shared CDC source first, \
811 then `CREATE TABLE ... FROM <source> TABLE '<database>.<table>'`",
812 with_properties.get_connector().unwrap(),
813 ))));
814 }
815 if is_create_source && create_cdc_source_job {
816 if let Some(value) = with_properties.get(AUTO_SCHEMA_CHANGE_KEY)
817 && value.parse::<bool>().map_err(|_| {
818 ErrorCode::InvalidInputSyntax(format!(
819 "invalid value of '{}' option",
820 AUTO_SCHEMA_CHANGE_KEY
821 ))
822 })?
823 {
824 Feature::CdcAutoSchemaChange.check_available()?;
825 }
826
827 with_properties.insert(CDC_SNAPSHOT_MODE_KEY.into(), CDC_SNAPSHOT_BACKFILL.into());
829 with_properties.insert(CDC_SHARING_MODE_KEY.into(), "true".into());
831 if with_properties.enable_transaction_metadata() {
833 with_properties.insert(CDC_TRANSACTIONAL_KEY.into(), "true".into());
834 }
835 if !with_properties.contains_key(CDC_WAIT_FOR_STREAMING_START_TIMEOUT) {
837 with_properties.insert(
838 CDC_WAIT_FOR_STREAMING_START_TIMEOUT.into(),
839 handler_args
840 .session
841 .config()
842 .cdc_source_wait_streaming_start_timeout()
843 .to_string(),
844 );
845 }
846 }
847 Ok((with_properties, refresh_mode))
848}
849
850pub enum SqlColumnStrategy {
853 FollowUnchecked,
858
859 FollowChecked,
866
867 Ignore,
872}
873
874pub(crate) fn reject_variant_columns(columns: &[ColumnCatalog], context: &str) -> Result<()> {
877 if let Some(col) = columns.iter().find(|c| c.data_type().contains_variant()) {
878 return Err(RwError::from(NotSupported(
879 format!(
880 "VARIANT column \"{}\" is not supported {context} yet",
881 col.name()
882 ),
883 format!("VARIANT columns are not supported {context} yet"),
884 )));
885 }
886 Ok(())
887}
888
889fn reject_variant_columns_for_unsupported_encoding(
895 format_encode: &FormatEncodeOptions,
896 columns_from_sql: &[ColumnCatalog],
897) -> Result<()> {
898 if matches!(
899 (&format_encode.format, &format_encode.row_encode),
900 (Format::None, Encode::None) | (_, Encode::Parquet)
901 ) {
902 return Ok(());
903 }
904 reject_variant_columns(columns_from_sql, "for this source encoding")
905}
906
907#[expect(clippy::too_many_arguments)]
910pub async fn bind_create_source_or_table_with_connector(
911 handler_args: HandlerArgs,
912 full_name: ObjectName,
913 format_encode: FormatEncodeOptions,
914 with_properties: WithOptions,
915 sql_columns_defs: &[ColumnDef],
916 constraints: Vec<TableConstraint>,
917 wildcard_idx: Option<usize>,
918 source_watermarks: Vec<SourceWatermark>,
919 columns_from_resolve_source: Option<Vec<ColumnCatalog>>,
920 source_info: StreamSourceInfo,
921 include_column_options: IncludeOption,
922 col_id_gen: &mut ColumnIdGenerator,
923 create_source_type: CreateSourceType,
924 source_rate_limit: Option<u32>,
925 sql_column_strategy: SqlColumnStrategy,
926 refresh_mode: SourceRefreshMode,
927) -> Result<SourceCatalog> {
928 let session = &handler_args.session;
929 let db_name: &str = &session.database();
930 let (schema_name, source_name) = Binder::resolve_schema_qualified_name(db_name, &full_name)?;
931 let (database_id, schema_id) =
932 session.get_database_and_schema_id_for_create(schema_name.clone())?;
933
934 let is_create_source = create_source_type != CreateSourceType::Table;
935
936 if is_create_source {
937 if with_properties.is_batch_connector() {
939 return Err(ErrorCode::BindError(
940 "can't CREATE SOURCE with refreshable batch connector\n\nHint: use CREATE TABLE instead"
941 .to_owned(),
942 )
943 .into());
944 }
945
946 match format_encode.format {
947 Format::Debezium | Format::DebeziumMongo | Format::Maxwell | Format::Canal => {
949 return Err(ErrorCode::BindError(format!(
950 "can't CREATE SOURCE with FORMAT {}.\n\nHint: use CREATE TABLE instead\n\n{}",
951 format_encode.format,
952 hint_format_encode(&format_encode)
953 ))
954 .into());
955 }
956 Format::Upsert => {
958 notice_to_user(format!(
959 "Streaming queries on sources with `FORMAT {}` may have limitations. If your query isn't supported, consider using `CREATE TABLE` instead.",
960 format_encode.format
961 ));
962 }
963 _ => {}
964 }
965 }
966
967 let sql_pk_names = bind_sql_pk_names(sql_columns_defs, bind_table_constraints(&constraints)?)?;
968
969 if with_properties.is_iceberg_connector() {
970 if is_create_source
971 && !with_properties.is_iceberg_update_source()
972 && !sql_pk_names.is_empty()
973 {
974 return Err(ErrorCode::NotSupported(
975 "PRIMARY KEY is not supported for Iceberg CREATE SOURCE in continuous ingestion mode."
976 .to_owned(),
977 "Iceberg streaming ingestion only supports append-only sources. Remove the PRIMARY KEY clause."
978 .to_owned(),
979 )
980 .into());
981 }
982
983 if !sql_columns_defs.is_empty() {
987 return Err(RwError::from(InvalidInputSyntax(
988 r#"Schema is automatically inferred for iceberg source and should not be specified
989
990HINT: use `CREATE SOURCE <name> WITH (...)` instead of `CREATE SOURCE <name> (<columns>) WITH (...)`."#.to_owned(),
991 )));
992 }
993 }
994
995 if with_properties.is_batch_connector()
997 && with_properties
998 .get(UPSTREAM_SOURCE_KEY)
999 .is_some_and(|s| s.eq_ignore_ascii_case(ADBC_SNOWFLAKE_CONNECTOR))
1000 && !sql_columns_defs.is_empty()
1001 {
1002 return Err(RwError::from(InvalidInputSyntax(
1003 r#"Schema is automatically inferred for ADBC Snowflake source and should not be specified
1004
1005HINT: use `CREATE TABLE <name> WITH (...)` instead of `CREATE TABLE <name> (<columns>) WITH (...)`."#.to_owned(),
1006 )));
1007 }
1008 let columns_from_sql = bind_sql_columns(sql_columns_defs, false)?;
1009
1010 reject_variant_columns_for_unsupported_encoding(&format_encode, &columns_from_sql)?;
1011
1012 let mut columns = bind_all_columns(
1013 &format_encode,
1014 columns_from_resolve_source,
1015 columns_from_sql,
1016 sql_columns_defs,
1017 wildcard_idx,
1018 sql_column_strategy,
1019 )?;
1020
1021 handle_addition_columns(
1023 Some(&format_encode),
1024 &with_properties,
1025 include_column_options,
1026 &mut columns,
1027 false,
1028 )?;
1029
1030 if columns.is_empty() {
1031 return Err(RwError::from(ProtocolError(
1032 "Schema definition is required, either from SQL or schema registry.".to_owned(),
1033 )));
1034 }
1035
1036 if is_create_source {
1038 check_and_add_timestamp_column(&with_properties, &mut columns);
1040
1041 if create_source_type == CreateSourceType::SharedNonCdc {
1044 let (columns_exist, additional_columns) = source_add_partition_offset_cols(
1045 &columns,
1046 &with_properties.get_connector().unwrap(),
1047 true, );
1049 for (existed, c) in columns_exist.into_iter().zip_eq_fast(additional_columns) {
1050 if !existed {
1051 columns.push(ColumnCatalog::hidden(c));
1052 }
1053 }
1054 }
1055 }
1056
1057 let mut with_properties = with_properties;
1059 resolve_privatelink_in_with_option(&mut with_properties)?;
1060
1061 if session
1063 .env()
1064 .system_params_manager()
1065 .get_params()
1066 .load()
1067 .enforce_secret()
1068 && Feature::SecretManagement.check_available().is_ok()
1069 {
1070 ConnectorProperties::enforce_secret_source(&with_properties)?;
1072 }
1073
1074 let (with_properties, connection_type, connector_conn_ref) =
1075 resolve_connection_ref_and_secret_ref(
1076 with_properties,
1077 session,
1078 Some(TelemetryDatabaseObject::Source),
1079 )?;
1080 ensure_connection_type_allowed(connection_type, &SOURCE_ALLOWED_CONNECTION_CONNECTOR)?;
1081
1082 if !matches!(connection_type, PbConnectionType::Unspecified) {
1084 let Some(connector) = with_properties.get_connector() else {
1085 return Err(RwError::from(ProtocolError(format!(
1086 "missing field '{}' in WITH clause",
1087 UPSTREAM_SOURCE_KEY
1088 ))));
1089 };
1090 check_connector_match_connection_type(connector.as_str(), &connection_type)?;
1091 }
1092
1093 let pk_names = bind_source_pk(
1094 &format_encode,
1095 &source_info,
1096 &mut columns,
1097 sql_pk_names,
1098 &with_properties,
1099 )
1100 .await?;
1101
1102 if let Some(duplicated_name) = columns.iter().map(|c| c.name()).duplicates().next() {
1105 return Err(ErrorCode::InvalidInputSyntax(format!(
1106 "column \"{}\" specified more than once",
1107 duplicated_name
1108 ))
1109 .into());
1110 }
1111
1112 for c in &mut columns {
1114 let original_data_type = c.data_type().clone();
1115 col_id_gen.generate(c)?;
1116 if is_create_source {
1120 c.column_desc.data_type = original_data_type;
1121 }
1122 }
1123 debug_assert_column_ids_distinct(&columns);
1124
1125 let (mut columns, pk_col_ids, row_id_index) =
1126 bind_pk_and_row_id_on_relation(columns, pk_names, true)?;
1127
1128 let watermark_descs =
1129 bind_source_watermark(session, source_name.clone(), source_watermarks, &columns)?;
1130 assert!(watermark_descs.len() <= 1);
1132 if is_create_source && watermark_descs.iter().any(|d| d.with_ttl) {
1133 return Err(ErrorCode::NotSupported(
1134 "WITH TTL is not supported in WATERMARK clause for CREATE SOURCE.".to_owned(),
1135 "Use `CREATE TABLE ... WATERMARK ... WITH TTL` instead.".to_owned(),
1136 )
1137 .into());
1138 }
1139
1140 let append_only = row_id_index.is_some();
1141 if is_create_source && !append_only && !watermark_descs.is_empty() {
1142 return Err(ErrorCode::NotSupported(
1143 "Defining watermarks on source requires the source connector to be append only."
1144 .to_owned(),
1145 "Use the key words `FORMAT PLAIN`".to_owned(),
1146 )
1147 .into());
1148 }
1149
1150 bind_sql_column_constraints(
1151 session,
1152 source_name.clone(),
1153 &mut columns,
1154 sql_columns_defs,
1156 &pk_col_ids,
1157 )?;
1158 check_format_encode(&with_properties, row_id_index, &columns)?;
1159
1160 let definition = handler_args.normalized_sql.clone();
1161
1162 let associated_table_id = if is_create_source {
1163 None
1164 } else {
1165 Some(TableId::placeholder())
1166 };
1167 let source = SourceCatalog {
1168 id: SourceId::placeholder(),
1169 name: source_name,
1170 schema_id,
1171 database_id,
1172 columns,
1173 pk_col_ids,
1174 append_only,
1175 owner: session.user_id(),
1176 info: source_info,
1177 row_id_index,
1178 with_properties,
1179 watermark_descs,
1180 associated_table_id,
1181 definition,
1182 connection_id: connector_conn_ref,
1183 created_at_epoch: None,
1184 initialized_at_epoch: None,
1185 version: INITIAL_SOURCE_VERSION_ID,
1186 created_at_cluster_version: None,
1187 initialized_at_cluster_version: None,
1188 rate_limit: source_rate_limit,
1189 refresh_mode: Some(refresh_mode),
1190 };
1191 source.validate_iceberg_update_source()?;
1192 Ok(source)
1193}
1194
1195pub async fn handle_create_source(
1196 mut handler_args: HandlerArgs,
1197 stmt: CreateSourceStatement,
1198) -> Result<RwPgResponse> {
1199 let session = handler_args.session.clone();
1200
1201 if let Either::Right(resp) = session.check_relation_name_duplicated(
1202 stmt.source_name.clone(),
1203 StatementType::CREATE_SOURCE,
1204 stmt.if_not_exists,
1205 )? {
1206 return Ok(resp);
1207 }
1208
1209 if stmt
1210 .source_name
1211 .base_name()
1212 .starts_with(ICEBERG_SOURCE_PREFIX)
1213 {
1214 return Err(RwError::from(InvalidInputSyntax(format!(
1215 "Source name cannot start with reserved prefix '{}'",
1216 ICEBERG_SOURCE_PREFIX
1217 ))));
1218 }
1219
1220 if stmt.cdc_table_info.is_some() {
1221 return handle_create_cdc_table_source(handler_args, stmt).await;
1222 }
1223
1224 let overwrite_options = OverwriteOptions::new(&mut handler_args)?;
1225
1226 if handler_args.with_options.is_empty() {
1227 return Err(RwError::from(InvalidInputSyntax(
1228 "missing WITH clause".to_owned(),
1229 )));
1230 }
1231
1232 if overwrite_options.source_rate_limit == Some(0)
1233 && dispatch_cdc_source_type!(
1234 handler_args.with_options.cdc_source_type(),
1235 T,
1236 T::NEEDS_NON_EMPTY_INITIAL_OFFSET,
1237 false
1238 )
1239 {
1240 let connector = handler_args.with_options.get_connector().unwrap();
1241 return Err(RwError::from(ErrorCode::InvalidParameterValue(format!(
1242 "`source_rate_limit` cannot be 0 when creating a `{connector}` source because source creation must wait for the initial CDC offset."
1243 ))));
1244 }
1245
1246 let format_encode = stmt.format_encode.into_v2_with_warning();
1247 let (with_properties, refresh_mode) =
1248 bind_connector_props(&handler_args, &format_encode, true)?;
1249 if let Some(connector) = with_properties.get_connector() {
1250 ensure_local_fs_connector_allowed(&session, &connector)?;
1251 }
1252
1253 let (schema_name, _) =
1256 Binder::resolve_schema_qualified_name(&session.database(), &stmt.source_name)?;
1257 session.get_database_and_schema_id_for_create(schema_name)?;
1258
1259 let create_source_type = CreateSourceType::for_newly_created(&session, &*with_properties);
1260 let (columns_from_resolve_source, source_info) = bind_columns_from_source(
1261 &session,
1262 &format_encode,
1263 Either::Left(&with_properties),
1264 create_source_type,
1265 )
1266 .await?;
1267 let mut col_id_gen = ColumnIdGenerator::new_initial();
1268
1269 if stmt.columns.iter().any(|col| {
1270 col.options
1271 .iter()
1272 .any(|def| matches!(def.option, ColumnOption::NotNull))
1273 }) {
1274 return Err(RwError::from(InvalidInputSyntax(
1275 "NOT NULL constraint is not supported in source schema".to_owned(),
1276 )));
1277 }
1278
1279 let source_catalog = bind_create_source_or_table_with_connector(
1280 handler_args.clone(),
1281 stmt.source_name,
1282 format_encode,
1283 with_properties,
1284 &stmt.columns,
1285 stmt.constraints,
1286 stmt.wildcard_idx,
1287 stmt.source_watermarks,
1288 columns_from_resolve_source,
1289 source_info,
1290 stmt.include_column_options,
1291 &mut col_id_gen,
1292 create_source_type,
1293 overwrite_options.source_rate_limit,
1294 SqlColumnStrategy::FollowChecked,
1295 refresh_mode,
1296 )
1297 .await?;
1298
1299 if stmt.temporary {
1301 if session.get_temporary_source(&source_catalog.name).is_some() {
1302 return Err(CatalogError::duplicated("source", source_catalog.name.clone()).into());
1303 }
1304 session.create_temporary_source(source_catalog);
1305 return Ok(PgResponse::empty_result(StatementType::CREATE_SOURCE));
1306 }
1307
1308 let source = source_catalog.to_prost();
1309
1310 let catalog_writer = session.catalog_writer()?;
1311
1312 if create_source_type.is_shared() {
1313 let graph = generate_stream_graph_for_source(handler_args, source_catalog)?;
1314 catalog_writer
1315 .create_source(source, Some(graph), stmt.if_not_exists)
1316 .await?;
1317 } else {
1318 catalog_writer
1320 .create_source(source, None, stmt.if_not_exists)
1321 .await?;
1322 }
1323
1324 Ok(PgResponse::empty_result(StatementType::CREATE_SOURCE))
1325}
1326
1327async fn handle_create_cdc_table_source(
1328 handler_args: HandlerArgs,
1329 stmt: CreateSourceStatement,
1330) -> Result<RwPgResponse> {
1331 let session = handler_args.session.clone();
1332 let cdc_table_info = stmt.cdc_table_info.as_ref().expect("checked by caller");
1333
1334 if stmt.temporary {
1335 return Err(ErrorCode::NotSupported(
1336 "temporary CDC table sources are not supported".to_owned(),
1337 "Remove the TEMPORARY clause".to_owned(),
1338 )
1339 .into());
1340 }
1341
1342 if handler_args.with_options.len() != 1
1343 || !handler_args
1344 .with_options
1345 .value_eq_ignore_case(CDC_BACKFILL_ENABLE_KEY, "false")
1346 || !handler_args.with_options.secret_ref().is_empty()
1347 || !handler_args.with_options.connection_ref().is_empty()
1348 {
1349 return Err(ErrorCode::InvalidInputSyntax(
1350 "CDC table sources currently require exactly `WITH (snapshot = 'false')`".to_owned(),
1351 )
1352 .into());
1353 }
1354
1355 if !stmt.source_watermarks.is_empty() {
1356 return Err(ErrorCode::NotSupported(
1357 "watermarks on CDC table sources are not supported".to_owned(),
1358 "Remove the WATERMARK clause".to_owned(),
1359 )
1360 .into());
1361 }
1362 if stmt.columns.iter().any(|column| column.is_generated()) {
1363 return Err(ErrorCode::NotSupported(
1364 "generated columns on CDC table sources are not supported".to_owned(),
1365 "Define generated expressions in the downstream streaming query".to_owned(),
1366 )
1367 .into());
1368 }
1369 for column in &stmt.columns {
1370 for option in &column.options {
1371 if matches!(
1372 option.option,
1373 ColumnOption::DefaultValue(_) | ColumnOption::DefaultValueInternal { .. }
1374 ) {
1375 return Err(ErrorCode::NotSupported(
1376 "default values on CDC table sources are not supported".to_owned(),
1377 "Remove the default value expression".to_owned(),
1378 )
1379 .into());
1380 }
1381 }
1382 }
1383
1384 sanity_check_for_table_on_cdc_source(
1385 false,
1386 &stmt.columns,
1387 &stmt.wildcard_idx,
1388 &stmt.constraints,
1389 &stmt.source_watermarks,
1390 )?;
1391 not_null_check_for_cdc_table(&stmt.wildcard_idx, &stmt.columns)?;
1392
1393 let db_name = session.database();
1394 let user_name = session.user_name();
1395 let search_path = session.config().search_path();
1396 let (schema_name, source_name) =
1397 Binder::resolve_schema_qualified_name(&db_name, &stmt.source_name)?;
1398 let (database_id, schema_id) =
1399 session.get_database_and_schema_id_for_create(schema_name.clone())?;
1400
1401 let (upstream_schema, upstream_source_name) =
1402 Binder::resolve_schema_qualified_name(&db_name, &cdc_table_info.source_name)?;
1403 let upstream_source = {
1404 let catalog_reader = session.env().catalog_reader().read_guard();
1405 let schema_path = SchemaPath::new(upstream_schema.as_deref(), &search_path, &user_name);
1406 let (source, _) = catalog_reader.get_source_by_name(
1407 &db_name,
1408 schema_path,
1409 upstream_source_name.as_str(),
1410 )?;
1411 source.clone()
1412 };
1413 check_cdc_source_select_privilege(&session, &upstream_source)?;
1414 if !upstream_source.info.is_shared()
1415 || !upstream_source.with_properties.is_shareable_cdc_connector()
1416 || upstream_source.info.external_table.is_some()
1417 {
1418 return Err(ErrorCode::InvalidInputSyntax(format!(
1419 "source `{}` is not a shared CDC source",
1420 cdc_table_info.source_name
1421 ))
1422 .into());
1423 }
1424
1425 let (cdc_with_options, external_table_name) = derive_connect_properties_for_cdc_table(
1426 &upstream_source.with_properties,
1427 cdc_table_info.external_table_name.clone(),
1428 )?;
1429 let (mut columns, pk_names, pk_comparisons) = match stmt.wildcard_idx {
1430 Some(_) => bind_cdc_table_schema_externally(cdc_with_options.clone()).await?,
1431 None => {
1432 let (columns, pk_names) =
1433 bind_cdc_table_schema(&stmt.columns, &stmt.constraints, false)?;
1434 let pk_comparisons = Box::pin(bind_cdc_pk_comparisons_externally(
1435 cdc_with_options.clone(),
1436 &pk_names,
1437 ))
1438 .await?;
1439 (columns, pk_names, pk_comparisons)
1440 }
1441 };
1442 if pk_names.is_empty() {
1443 return Err(ErrorCode::NotSupported(
1444 "CDC table source without a primary key is not supported".to_owned(),
1445 "Define a primary key on the upstream table or in the source schema".to_owned(),
1446 )
1447 .into());
1448 }
1449 reject_pk_filtered_by_debezium_column_filter(&pk_names, &cdc_with_options)?;
1450
1451 handle_addition_columns(
1452 None,
1453 &cdc_with_options,
1454 stmt.include_column_options.clone(),
1455 &mut columns,
1456 true,
1457 )?;
1458 let mut col_id_gen = ColumnIdGenerator::new_initial();
1459 for column in &mut columns {
1460 col_id_gen.generate(column)?;
1461 }
1462 debug_assert_column_ids_distinct(&columns);
1463 let (columns, pk_col_ids, row_id_index) =
1464 bind_pk_and_row_id_on_relation(columns, pk_names, false)?;
1465 debug_assert!(row_id_index.is_none());
1466
1467 let (connect_properties, secret_refs) = cdc_with_options.into_parts();
1468 let id_to_index = columns
1469 .iter()
1470 .enumerate()
1471 .map(|(index, column)| (column.column_id(), index))
1472 .collect::<HashMap<_, _>>();
1473 let stream_key = pk_col_ids.iter().map(|id| id_to_index[id]).collect_vec();
1474 let pk = stream_key
1475 .iter()
1476 .map(|index| {
1477 risingwave_common::util::sort_util::ColumnOrder::new(
1478 *index,
1479 risingwave_common::util::sort_util::OrderType::ascending(),
1480 )
1481 })
1482 .collect();
1483 let cdc_table_desc = CdcTableDesc {
1484 table_id: TableId::placeholder(),
1485 source_id: upstream_source.id,
1486 external_table_name,
1487 pk,
1488 pk_comparisons,
1489 columns: columns
1490 .iter()
1491 .map(|column| column.column_desc.clone())
1492 .collect(),
1493 stream_key,
1494 connect_properties,
1499 secret_refs,
1500 };
1501
1502 let mut source_info = upstream_source.info.clone();
1503 source_info.cdc_source_job = false;
1504 source_info.is_distributed = false;
1505 source_info.external_table = Some(cdc_table_desc.to_protobuf());
1506
1507 let catalog_with_properties = WithOptionsSecResolved::new(
1510 BTreeMap::from([
1511 (
1512 UPSTREAM_SOURCE_KEY.to_owned(),
1513 upstream_source
1514 .with_properties
1515 .get_connector()
1516 .expect("validated as a CDC source"),
1517 ),
1518 (CDC_BACKFILL_ENABLE_KEY.to_owned(), "false".to_owned()),
1519 ]),
1520 BTreeMap::new(),
1521 );
1522
1523 let source_catalog = SourceCatalog {
1524 id: SourceId::placeholder(),
1525 name: source_name,
1526 schema_id,
1527 database_id,
1528 columns,
1529 pk_col_ids,
1530 append_only: false,
1531 owner: session.user_id(),
1532 info: source_info,
1533 row_id_index: None,
1534 with_properties: catalog_with_properties,
1535 watermark_descs: vec![],
1536 associated_table_id: None,
1537 definition: handler_args.normalized_sql,
1538 connection_id: None,
1539 created_at_epoch: None,
1540 initialized_at_epoch: None,
1541 version: INITIAL_SOURCE_VERSION_ID,
1542 created_at_cluster_version: None,
1543 initialized_at_cluster_version: None,
1544 rate_limit: None,
1545 refresh_mode: upstream_source.refresh_mode,
1546 };
1547
1548 session
1549 .catalog_writer()?
1550 .create_source(source_catalog.to_prost(), None, stmt.if_not_exists)
1551 .await?;
1552 Ok(PgResponse::empty_result(StatementType::CREATE_SOURCE))
1553}
1554
1555pub(super) fn generate_stream_graph_for_source(
1556 handler_args: HandlerArgs,
1557 source_catalog: SourceCatalog,
1558) -> Result<PbStreamFragmentGraph> {
1559 let context = OptimizerContext::from_handler_args(handler_args)?;
1560 let source_node = LogicalSource::with_catalog(
1561 Rc::new(source_catalog),
1562 SourceNodeKind::CreateSharedSource,
1563 context.into(),
1564 None,
1565 )?;
1566
1567 let stream_plan = source_node.to_stream(&mut ToStreamContext::new_with_backfill_type(
1568 false,
1569 BackfillType::ArrangementBackfill,
1572 ))?;
1573 let graph = build_graph(stream_plan, Some(GraphJobType::Source))?;
1574 Ok(graph)
1575}
1576
1577#[cfg(all(test, not(madsim)))]
1578#[path = "create_source/iceberg_update_test.rs"]
1579mod iceberg_update_tests;
1580
1581#[cfg(test)]
1582pub mod tests {
1583 use std::collections::HashMap;
1584 use std::sync::Arc;
1585
1586 use risingwave_common::catalog::{
1587 DEFAULT_DATABASE_NAME, DEFAULT_SCHEMA_NAME, ROW_ID_COLUMN_NAME,
1588 };
1589 use risingwave_common::config::FrontendConfig;
1590 use risingwave_common::types::{DataType, StructType};
1591 use risingwave_connector::parser::additional_columns::get_kafka_header_item_datatype;
1592 use risingwave_pb::plan_common::EncodeType;
1593
1594 use crate::catalog::root_catalog::SchemaPath;
1595 use crate::catalog::source_catalog::SourceCatalog;
1596 use crate::test_utils::{LocalFrontend, PROTO_FILE_DATA, create_proto_file};
1597
1598 const GET_COLUMN_FROM_CATALOG: fn(&Arc<SourceCatalog>) -> HashMap<&str, DataType> =
1599 |catalog: &Arc<SourceCatalog>| -> HashMap<&str, DataType> {
1600 catalog
1601 .columns
1602 .iter()
1603 .map(|col| (col.name(), col.data_type().clone()))
1604 .collect::<HashMap<&str, DataType>>()
1605 };
1606
1607 #[tokio::test]
1608 async fn test_create_source_handler() {
1609 let proto_file = create_proto_file(PROTO_FILE_DATA);
1610 let sql = format!(
1611 r#"CREATE SOURCE t
1612 WITH (connector = 'kinesis')
1613 FORMAT PLAIN ENCODE PROTOBUF (message = '.test.TestRecord', schema.location = 'file://{}')"#,
1614 proto_file.path().to_str().unwrap()
1615 );
1616 let frontend = LocalFrontend::new(Default::default()).await;
1617 frontend.run_sql(sql).await.unwrap();
1618
1619 let session = frontend.session_ref();
1620 let catalog_reader = session.env().catalog_reader().read_guard();
1621 let schema_path = SchemaPath::Name(DEFAULT_SCHEMA_NAME);
1622
1623 let (source, _) = catalog_reader
1625 .get_source_by_name(DEFAULT_DATABASE_NAME, schema_path, "t")
1626 .unwrap();
1627 assert_eq!(source.name, "t");
1628
1629 let columns = GET_COLUMN_FROM_CATALOG(source);
1630
1631 let city_type = StructType::new(vec![
1632 ("address", DataType::Varchar),
1633 ("zipcode", DataType::Varchar),
1634 ])
1635 .into();
1637 let expected_columns = maplit::hashmap! {
1638 ROW_ID_COLUMN_NAME => DataType::Serial,
1639 "id" => DataType::Int32,
1640 "zipcode" => DataType::Int64,
1641 "rate" => DataType::Float32,
1642 "country" => StructType::new(
1643 vec![("address", DataType::Varchar),("city", city_type),("zipcode", DataType::Varchar)],
1644 )
1645 .into(),
1647 };
1648 assert_eq!(columns, expected_columns, "{columns:#?}");
1649 }
1650
1651 #[tokio::test]
1652 async fn test_create_mqtt_source_with_protobuf() {
1653 let proto_file = create_proto_file(PROTO_FILE_DATA);
1654 let sql = format!(
1655 r#"CREATE SOURCE t_mqtt
1656 WITH (
1657 connector = 'mqtt',
1658 url = 'mqtt://localhost:1883',
1659 topic = 'test_topic'
1660 )
1661 FORMAT PLAIN ENCODE PROTOBUF (
1662 message = '.test.TestRecord',
1663 schema.location = 'file://{}'
1664 )"#,
1665 proto_file.path().to_str().unwrap()
1666 );
1667 let frontend = LocalFrontend::new(Default::default()).await;
1668 frontend.run_sql(sql).await.unwrap();
1669
1670 let session = frontend.session_ref();
1671 let catalog_reader = session.env().catalog_reader().read_guard();
1672 let schema_path = SchemaPath::Name(DEFAULT_SCHEMA_NAME);
1673
1674 let (source, _) = catalog_reader
1675 .get_source_by_name(DEFAULT_DATABASE_NAME, schema_path, "t_mqtt")
1676 .unwrap();
1677
1678 assert_eq!(source.name, "t_mqtt");
1679 assert_eq!(source.info.row_encode, EncodeType::Protobuf as i32);
1680 }
1681
1682 #[tokio::test]
1683 async fn test_create_posix_fs_source_requires_frontend_config() {
1684 let frontend = LocalFrontend::with_frontend_config(
1685 Default::default(),
1686 FrontendConfig {
1687 unsafe_enable_local_fs_connector: false,
1688 ..Default::default()
1689 },
1690 )
1691 .await;
1692 let err = frontend
1693 .run_sql(
1694 r#"CREATE SOURCE local_files (
1695 line VARCHAR
1696 ) WITH (
1697 connector = 'posix_fs',
1698 posix_fs.root = '/tmp',
1699 match_pattern = '*.csv'
1700 ) FORMAT PLAIN ENCODE CSV (without_header = 'true')"#
1701 .to_owned(),
1702 )
1703 .await
1704 .unwrap_err();
1705
1706 assert!(
1707 err.to_string()
1708 .contains("frontend.unsafe_enable_local_fs_connector = true"),
1709 "{err:?}"
1710 );
1711 }
1712
1713 #[tokio::test]
1714 async fn test_duplicate_props_options() {
1715 let proto_file = create_proto_file(PROTO_FILE_DATA);
1716 let sql = format!(
1717 r#"CREATE SOURCE t
1718 WITH (
1719 connector = 'kinesis',
1720 aws.region='user_test_topic',
1721 endpoint='172.10.1.1:9090,172.10.1.2:9090',
1722 aws.credentials.access_key_id = 'your_access_key_1',
1723 aws.credentials.secret_access_key = 'your_secret_key_1'
1724 )
1725 FORMAT PLAIN ENCODE PROTOBUF (
1726 message = '.test.TestRecord',
1727 aws.credentials.access_key_id = 'your_access_key_2',
1728 aws.credentials.secret_access_key = 'your_secret_key_2',
1729 schema.location = 'file://{}',
1730 )"#,
1731 proto_file.path().to_str().unwrap()
1732 );
1733 let frontend = LocalFrontend::new(Default::default()).await;
1734 frontend.run_sql(sql).await.unwrap();
1735
1736 let session = frontend.session_ref();
1737 let catalog_reader = session.env().catalog_reader().read_guard();
1738 let schema_path = SchemaPath::Name(DEFAULT_SCHEMA_NAME);
1739
1740 let (source, _) = catalog_reader
1742 .get_source_by_name(DEFAULT_DATABASE_NAME, schema_path, "t")
1743 .unwrap();
1744 assert_eq!(source.name, "t");
1745
1746 assert_eq!(
1748 source
1749 .info
1750 .format_encode_options
1751 .get("aws.credentials.access_key_id")
1752 .unwrap(),
1753 "your_access_key_2"
1754 );
1755 assert_eq!(
1756 source
1757 .info
1758 .format_encode_options
1759 .get("aws.credentials.secret_access_key")
1760 .unwrap(),
1761 "your_secret_key_2"
1762 );
1763
1764 assert_eq!(
1766 source
1767 .with_properties
1768 .get("aws.credentials.access_key_id")
1769 .unwrap(),
1770 "your_access_key_1"
1771 );
1772 assert_eq!(
1773 source
1774 .with_properties
1775 .get("aws.credentials.secret_access_key")
1776 .unwrap(),
1777 "your_secret_key_1"
1778 );
1779
1780 assert!(!source.with_properties.contains_key("schema.location"));
1782 }
1783
1784 #[tokio::test]
1785 async fn test_multi_table_cdc_create_source_handler() {
1786 let sql =
1787 "CREATE SOURCE t2 WITH (connector = 'mysql-cdc') FORMAT PLAIN ENCODE JSON".to_owned();
1788 let frontend = LocalFrontend::new(Default::default()).await;
1789 let session = frontend.session_ref();
1790
1791 frontend
1792 .run_sql_with_session(session.clone(), sql)
1793 .await
1794 .unwrap();
1795 let catalog_reader = session.env().catalog_reader().read_guard();
1796 let schema_path = SchemaPath::Name(DEFAULT_SCHEMA_NAME);
1797
1798 let (source, _) = catalog_reader
1800 .get_source_by_name(DEFAULT_DATABASE_NAME, schema_path, "t2")
1801 .unwrap();
1802 assert_eq!(source.name, "t2");
1803
1804 let columns = source
1805 .columns
1806 .iter()
1807 .map(|col| (col.name(), col.data_type().clone()))
1808 .collect::<Vec<(&str, DataType)>>();
1809
1810 expect_test::expect![[r#"
1811 [
1812 (
1813 "payload",
1814 Jsonb,
1815 ),
1816 (
1817 "_rw_offset",
1818 Varchar,
1819 ),
1820 (
1821 "_rw_table_name",
1822 Varchar,
1823 ),
1824 (
1825 "_row_id",
1826 Serial,
1827 ),
1828 ]
1829 "#]]
1830 .assert_debug_eq(&columns);
1831 }
1832
1833 #[tokio::test]
1834 async fn test_reject_zero_source_rate_limit_when_cdc_must_wait_for_offset() {
1835 let frontend = LocalFrontend::new(Default::default()).await;
1836 let session = frontend.session_ref();
1837
1838 for (source_name, connector) in [
1839 ("mysql_explicit_zero", "mysql-cdc"),
1840 ("sqlserver_explicit_zero", "sqlserver-cdc"),
1841 ] {
1842 let err = frontend
1843 .run_sql_with_session(
1844 session.clone(),
1845 format!(
1846 "CREATE SOURCE {source_name} WITH (connector = '{connector}', source_rate_limit = '0') FORMAT PLAIN ENCODE JSON"
1847 ),
1848 )
1849 .await
1850 .unwrap_err();
1851 let message = err.to_string();
1852 assert!(
1853 message.contains("source creation must wait for the initial CDC offset"),
1854 "{message}"
1855 );
1856 }
1857
1858 frontend
1859 .run_sql_with_session(session.clone(), "SET source_rate_limit TO 0;")
1860 .await
1861 .unwrap();
1862
1863 for (source_name, connector) in [
1864 ("mysql_session_zero", "mysql-cdc"),
1865 ("sqlserver_session_zero", "sqlserver-cdc"),
1866 ] {
1867 let err = frontend
1868 .run_sql_with_session(
1869 session.clone(),
1870 format!(
1871 "CREATE SOURCE {source_name} WITH (connector = '{connector}') FORMAT PLAIN ENCODE JSON"
1872 ),
1873 )
1874 .await
1875 .unwrap_err();
1876 assert!(
1877 err.to_string()
1878 .contains("source creation must wait for the initial CDC offset"),
1879 "{err}"
1880 );
1881 }
1882
1883 frontend
1884 .run_sql_with_session(
1885 session.clone(),
1886 "CREATE SOURCE mysql_positive_override WITH (connector = 'mysql-cdc', source_rate_limit = '1') FORMAT PLAIN ENCODE JSON",
1887 )
1888 .await
1889 .unwrap();
1890
1891 frontend
1892 .run_sql_with_session(
1893 session,
1894 "CREATE SOURCE postgres_zero WITH (connector = 'postgres-cdc') FORMAT PLAIN ENCODE JSON",
1895 )
1896 .await
1897 .unwrap();
1898 }
1899
1900 #[tokio::test]
1901 async fn test_source_addition_columns() {
1902 let sql =
1904 "CREATE SOURCE s (v1 int) include key as _rw_kafka_key with (connector = 'kafka') format plain encode json".to_owned();
1905 let frontend = LocalFrontend::new(Default::default()).await;
1906 frontend.run_sql(sql).await.unwrap();
1907 let session = frontend.session_ref();
1908 let catalog_reader = session.env().catalog_reader().read_guard();
1909 let (source, _) = catalog_reader
1910 .get_source_by_name(
1911 DEFAULT_DATABASE_NAME,
1912 SchemaPath::Name(DEFAULT_SCHEMA_NAME),
1913 "s",
1914 )
1915 .unwrap();
1916 assert_eq!(source.name, "s");
1917
1918 let columns = source
1919 .columns
1920 .iter()
1921 .map(|col| (col.name(), col.data_type().clone()))
1922 .collect::<Vec<(&str, DataType)>>();
1923
1924 expect_test::expect![[r#"
1925 [
1926 (
1927 "v1",
1928 Int32,
1929 ),
1930 (
1931 "_rw_kafka_key",
1932 Bytea,
1933 ),
1934 (
1935 "_rw_kafka_timestamp",
1936 Timestamptz,
1937 ),
1938 (
1939 "_rw_kafka_partition",
1940 Varchar,
1941 ),
1942 (
1943 "_rw_kafka_offset",
1944 Varchar,
1945 ),
1946 (
1947 "_row_id",
1948 Serial,
1949 ),
1950 ]
1951 "#]]
1952 .assert_debug_eq(&columns);
1953 drop(catalog_reader);
1954
1955 let sql =
1956 "CREATE SOURCE s_pulsar (v1 int) include header 'tenant' as pulsar_header with (connector = 'pulsar') format plain encode json".to_owned();
1957 frontend.run_sql(sql).await.unwrap();
1958 let catalog_reader = session.env().catalog_reader().read_guard();
1959 let (source, _) = catalog_reader
1960 .get_source_by_name(
1961 DEFAULT_DATABASE_NAME,
1962 SchemaPath::Name(DEFAULT_SCHEMA_NAME),
1963 "s_pulsar",
1964 )
1965 .unwrap();
1966 assert_eq!(source.name, "s_pulsar");
1967
1968 let columns = source
1969 .columns
1970 .iter()
1971 .map(|col| (col.name(), col.data_type().clone()))
1972 .collect::<Vec<(&str, DataType)>>();
1973
1974 expect_test::expect![[r#"
1975 [
1976 (
1977 "v1",
1978 Int32,
1979 ),
1980 (
1981 "pulsar_header",
1982 Bytea,
1983 ),
1984 (
1985 "_row_id",
1986 Serial,
1987 ),
1988 ]
1989 "#]]
1990 .assert_debug_eq(&columns);
1991 drop(catalog_reader);
1992
1993 let sql =
1994 "CREATE SOURCE s_nats (v1 int) include header 'trace-id' as nats_header with (connector = 'nats') format plain encode json".to_owned();
1995 frontend.run_sql(sql).await.unwrap();
1996 let catalog_reader = session.env().catalog_reader().read_guard();
1997 let (source, _) = catalog_reader
1998 .get_source_by_name(
1999 DEFAULT_DATABASE_NAME,
2000 SchemaPath::Name(DEFAULT_SCHEMA_NAME),
2001 "s_nats",
2002 )
2003 .unwrap();
2004 assert_eq!(source.name, "s_nats");
2005
2006 let columns = source
2007 .columns
2008 .iter()
2009 .map(|col| (col.name(), col.data_type().clone()))
2010 .collect::<Vec<(&str, DataType)>>();
2011
2012 expect_test::expect![[r#"
2013 [
2014 (
2015 "v1",
2016 Int32,
2017 ),
2018 (
2019 "nats_header",
2020 Bytea,
2021 ),
2022 (
2023 "_row_id",
2024 Serial,
2025 ),
2026 ]
2027 "#]]
2028 .assert_debug_eq(&columns);
2029 drop(catalog_reader);
2030
2031 let sql =
2033 "CREATE SOURCE s_nats_all (v1 int) include header as nats_all_headers with (connector = 'nats') format plain encode json".to_owned();
2034 frontend.run_sql(sql).await.unwrap();
2035 let catalog_reader = session.env().catalog_reader().read_guard();
2036 let (source, _) = catalog_reader
2037 .get_source_by_name(
2038 DEFAULT_DATABASE_NAME,
2039 SchemaPath::Name(DEFAULT_SCHEMA_NAME),
2040 "s_nats_all",
2041 )
2042 .unwrap();
2043 assert_eq!(source.name, "s_nats_all");
2044 let all_headers_col = source
2045 .columns
2046 .iter()
2047 .find(|c| c.name() == "nats_all_headers")
2048 .expect("nats_all_headers column present");
2049 assert_eq!(
2050 all_headers_col.data_type(),
2051 &DataType::list(get_kafka_header_item_datatype()),
2052 );
2053 drop(catalog_reader);
2054
2055 let sql =
2056 "CREATE SOURCE s3 (v1 int) include timestamp 'header1' as header_col with (connector = 'kafka') format plain encode json".to_owned();
2057 match frontend.run_sql(sql).await {
2058 Err(e) => {
2059 assert_eq!(
2060 e.to_string(),
2061 "Protocol error: Only header column can have inner field, but got \"timestamp\""
2062 )
2063 }
2064 _ => unreachable!(),
2065 }
2066 }
2067
2068 #[tokio::test]
2072 async fn test_create_privilege_checked_before_schema_fetch() {
2073 let frontend = LocalFrontend::new(Default::default()).await;
2074 frontend
2075 .run_sql("CREATE USER no_create_user")
2076 .await
2077 .unwrap();
2078 let user_id = frontend
2079 .session_ref()
2080 .env()
2081 .user_info_reader()
2082 .read_guard()
2083 .get_user_by_name("no_create_user")
2084 .unwrap()
2085 .id;
2086 let user_session = frontend.session_user_ref(
2087 DEFAULT_DATABASE_NAME.to_owned(),
2088 "no_create_user".to_owned(),
2089 user_id,
2090 );
2091
2092 let with_and_format = "WITH (connector = 'kafka', properties.bootstrap.server = '127.0.0.1:1', topic = 't') \
2095 FORMAT PLAIN ENCODE AVRO (schema.registry = 'http://127.0.0.1:1')";
2096 let create_source = format!("CREATE SOURCE s {with_and_format}");
2097 let create_table = format!("CREATE TABLE t {with_and_format}");
2098
2099 for sql in [create_source, create_table] {
2100 let err = frontend
2101 .run_sql_with_session(user_session.clone(), sql)
2102 .await
2103 .unwrap_err();
2104 assert!(
2105 err.to_string()
2106 .contains("permission denied for schema \"public\": \"CREATE\""),
2107 "{err:?}"
2108 );
2109 }
2110 }
2111}