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