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