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