1use std::collections::{BTreeMap, HashMap, HashSet};
16use std::rc::Rc;
17use std::sync::Arc;
18
19use anyhow::Context;
20use clap::ValueEnum;
21use either::Either;
22use fixedbitset::FixedBitSet;
23use iceberg::spec::FormatVersion;
24use itertools::Itertools;
25use percent_encoding::percent_decode_str;
26use pgwire::pg_response::{PgResponse, StatementType};
27use prost::Message as _;
28use risingwave_common::acl::AclMode;
29use risingwave_common::catalog::{
30 CdcKeyComparison, CdcTableDesc, ColumnCatalog, ColumnDesc, ConflictBehavior,
31 DEFAULT_SCHEMA_NAME, Engine, ICEBERG_SINK_PREFIX, ICEBERG_SOURCE_PREFIX,
32 RISINGWAVE_ICEBERG_ROW_ID, ROW_ID_COLUMN_NAME, TableId,
33};
34use risingwave_common::config::MetaBackend;
35use risingwave_common::global_jvm::Jvm;
36use risingwave_common::session_config::sink_decouple::SinkDecouple;
37use risingwave_common::types::DataType;
38use risingwave_common::util::sort_util::{ColumnOrder, OrderType};
39use risingwave_common::util::value_encoding::DatumToProtoExt;
40use risingwave_common::{bail, bail_not_implemented};
41use risingwave_connector::source::cdc::external::ExternalCdcTableType;
42use risingwave_connector::source::cdc::{CdcSourceExternalTableIdentifier, build_cdc_table_id};
43use risingwave_connector::{
44 AUTO_SCHEMA_CHANGE_KEY, WithOptionsSecResolved, WithPropertiesExt, dispatch_cdc_source_type,
45};
46use risingwave_pb::catalog::connection::Info as ConnectionInfo;
47use risingwave_pb::catalog::connection_params::ConnectionType;
48use risingwave_pb::catalog::{PbSource, PbWebhookSourceInfo, WatermarkDesc};
49use risingwave_pb::ddl_service::{PbTableJobType, TableJobType};
50use risingwave_pb::plan_common::column_desc::GeneratedOrDefaultColumn;
51use risingwave_pb::plan_common::{
52 AdditionalColumn, ColumnDescVersion, DefaultColumnDesc, GeneratedColumnDesc,
53};
54use risingwave_pb::secret::PbSecretRef;
55use risingwave_pb::secret::secret_ref::PbRefAsType;
56use risingwave_pb::stream_plan::StreamFragmentGraph;
57use risingwave_sqlparser::ast::{
58 CdcTableInfo, ColumnDef, ColumnOption, CompatibleFormatEncode, ConnectionRefValue, CreateSink,
59 CreateSinkStatement, CreateSourceStatement, DataType as AstDataType, ExplainOptions, Format,
60 FormatEncodeOptions, Ident, ObjectName, OnConflict, SecretRefAsType, SourceWatermark,
61 Statement, TableConstraint, WebhookSourceInfo, WithProperties,
62};
63use risingwave_sqlparser::parser::IncludeOption;
64use thiserror_ext::AsReport;
65
66use super::RwPgResponse;
67use super::create_source::{CreateSourceType, SqlColumnStrategy, bind_columns_from_source};
68use crate::binder::{Clause, SecureCompareContext, WEBHOOK_PAYLOAD_FIELD_NAME, bind_data_type};
69use crate::catalog::root_catalog::SchemaPath;
70use crate::catalog::source_catalog::SourceCatalog;
71use crate::catalog::table_catalog::TableVersion;
72use crate::catalog::{ColumnId, DatabaseId, SchemaId, SourceId, check_column_name_not_reserved};
73use crate::error::{ErrorCode, Result, RwError, bail_bind_error};
74use crate::expr::{Expr, ExprImpl, ExprRewriter};
75use crate::handler::HandlerArgs;
76use crate::handler::cdc::{
77 bind_cdc_pk_comparisons_externally, bind_cdc_table_schema, bind_cdc_table_schema_externally,
78 derive_connect_properties_for_cdc_table, not_null_check_for_cdc_table,
79 reject_pk_filtered_by_debezium_column_filter, sanity_check_for_table_on_cdc_source,
80};
81use crate::handler::create_source::{
82 bind_connector_props, bind_create_source_or_table_with_connector, bind_source_watermark,
83 handle_addition_columns,
84};
85use crate::handler::privilege::ObjectCheckItem;
86use crate::handler::util::{
87 LongRunningNotificationAction, SourceSchemaCompatExt, execute_with_long_running_notification,
88};
89use crate::optimizer::plan_node::generic::{SourceNodeKind, build_cdc_scan_options_with_options};
90use crate::optimizer::plan_node::{
91 LogicalCdcScan, LogicalPlanRef, LogicalSource, StreamPlanRef as PlanRef,
92 ensure_sync_log_store_fragment_root,
93};
94use crate::optimizer::property::{Order, RequiredDist};
95use crate::optimizer::{OptimizerContext, OptimizerContextRef, PlanRoot};
96use crate::session::SessionImpl;
97use crate::session::current::notice_to_user;
98use crate::stream_fragmenter::{GraphJobType, build_graph};
99use crate::utils::OverwriteOptions;
100use crate::{Binder, Explain, TableCatalog, WithOptions};
101
102mod col_id_gen;
103pub use col_id_gen::*;
104use risingwave_connector::sink::SinkParam;
105use risingwave_connector::sink::iceberg::{
106 ENABLE_COMPACTION, ENABLE_MANIFEST_REWRITE, IcebergConfig, IcebergSink,
107 is_iceberg_engine_option, parse_partition_by_exprs, validate_order_key_columns,
108};
109use risingwave_pb::ddl_service::create_iceberg_table_request::{PbSinkJobInfo, PbTableJobInfo};
110
111use crate::handler::create_sink::{SinkPlanContext, gen_sink_plan};
112
113fn ensure_column_options_supported(c: &ColumnDef) -> Result<()> {
114 for option_def in &c.options {
115 match option_def.option {
116 ColumnOption::GeneratedColumns(_) => {}
117 ColumnOption::DefaultValue(_) => {}
118 ColumnOption::DefaultValueInternal { .. } => {}
119 ColumnOption::Unique { is_primary: true } => {}
120 ColumnOption::Null => {}
121 ColumnOption::NotNull => {}
122 _ => bail_not_implemented!("column constraints \"{}\"", option_def),
123 }
124 }
125 Ok(())
126}
127
128pub fn bind_sql_columns(
132 column_defs: &[ColumnDef],
133 is_for_drop_table_connector: bool,
134) -> Result<Vec<ColumnCatalog>> {
135 let mut columns = Vec::with_capacity(column_defs.len());
136
137 for column in column_defs {
138 ensure_column_options_supported(column)?;
139 let ColumnDef {
143 name,
144 data_type,
145 collation,
146 options,
147 ..
148 } = column;
149
150 let data_type = data_type
151 .clone()
152 .ok_or_else(|| ErrorCode::InvalidInputSyntax("data type is not specified".into()))?;
153 if let Some(collation) = collation {
154 if !["C", "POSIX"].contains(&collation.real_value().as_str()) {
160 bail_not_implemented!(
161 "Collate collation other than `C` or `POSIX` is not implemented"
162 );
163 }
164
165 match data_type {
166 AstDataType::Text | AstDataType::Varchar | AstDataType::Char(_) => {}
167 _ => {
168 return Err(ErrorCode::NotSupported(
169 format!("{} is not a collatable data type", data_type),
170 "The only built-in collatable data types are `varchar`, please check your type".into(),
171 ).into());
172 }
173 }
174 }
175
176 if !is_for_drop_table_connector {
177 check_column_name_not_reserved(&name.real_value())?;
181 }
182
183 let nullable: bool = !options
184 .iter()
185 .any(|def| matches!(def.option, ColumnOption::NotNull));
186
187 columns.push(ColumnCatalog {
188 column_desc: ColumnDesc {
189 data_type: bind_data_type(&data_type)?,
190 column_id: ColumnId::placeholder(),
191 name: name.real_value(),
192 generated_or_default_column: None,
193 description: None,
194 additional_column: AdditionalColumn { column_type: None },
195 version: ColumnDescVersion::LATEST,
196 system_column: None,
197 nullable,
198 },
199 is_hidden: false,
200 });
201 }
202
203 Ok(columns)
204}
205
206fn check_generated_column_constraints(
207 column_name: &String,
208 column_id: ColumnId,
209 expr: &ExprImpl,
210 column_catalogs: &[ColumnCatalog],
211 generated_column_names: &[String],
212 pk_column_ids: &[ColumnId],
213) -> Result<()> {
214 let input_refs = expr.collect_input_refs(column_catalogs.len());
215 for idx in input_refs.ones() {
216 let referred_generated_column = &column_catalogs[idx].column_desc.name;
217 if generated_column_names
218 .iter()
219 .any(|c| c == referred_generated_column)
220 {
221 return Err(ErrorCode::BindError(format!(
222 "A generated column cannot reference another generated column. \
223 Generated column \"{}\" references generated column \"{}\"",
224 column_name, referred_generated_column
225 ))
226 .into());
227 }
228 }
229
230 if pk_column_ids.contains(&column_id) && expr.is_impure() {
231 return Err(ErrorCode::BindError(format!(
232 "Generated columns with impure expressions should not be part of the primary key. \
233 Here column \"{}\" is defined as part of the primary key.",
234 column_name
235 ))
236 .into());
237 }
238
239 Ok(())
240}
241
242pub fn bind_sql_column_constraints(
245 session: &SessionImpl,
246 table_name: String,
247 column_catalogs: &mut [ColumnCatalog],
248 columns: &[ColumnDef],
249 pk_column_ids: &[ColumnId],
250) -> Result<()> {
251 let generated_column_names = {
252 let mut names = vec![];
253 for column in columns {
254 for option_def in &column.options {
255 if let ColumnOption::GeneratedColumns(_) = option_def.option {
256 names.push(column.name.real_value());
257 break;
258 }
259 }
260 }
261 names
262 };
263
264 let mut binder = Binder::new_for_ddl(session);
265 binder.bind_columns_to_context(table_name, column_catalogs)?;
266
267 for column in columns {
268 let Some(idx) = column_catalogs
269 .iter()
270 .position(|c| c.name() == column.name.real_value())
271 else {
272 continue;
275 };
276
277 for option_def in &column.options {
278 match &option_def.option {
279 ColumnOption::GeneratedColumns(expr) => {
280 binder.set_clause(Some(Clause::GeneratedColumn));
281
282 let expr_impl = binder.bind_expr(expr).with_context(|| {
283 format!(
284 "failed to bind the expression in generated column \"{}\"",
285 column.name.real_value()
286 )
287 })?;
288
289 check_generated_column_constraints(
290 &column.name.real_value(),
291 column_catalogs[idx].column_id(),
292 &expr_impl,
293 column_catalogs,
294 &generated_column_names,
295 pk_column_ids,
296 )?;
297
298 column_catalogs[idx].column_desc.generated_or_default_column = Some(
299 GeneratedOrDefaultColumn::GeneratedColumn(GeneratedColumnDesc {
300 expr: Some(expr_impl.to_expr_proto()),
301 }),
302 );
303 binder.set_clause(None);
304 }
305 ColumnOption::DefaultValue(expr) => {
306 let expr_impl = binder
307 .bind_expr(expr)?
308 .cast_assign(column_catalogs[idx].data_type())?;
309
310 let rewritten_expr_impl = session
318 .pinned_snapshot()
319 .inline_now_proc_time()
320 .rewrite_expr(expr_impl.clone());
321
322 if let Some(snapshot_value) = rewritten_expr_impl.try_fold_const() {
323 let snapshot_value = snapshot_value?;
324
325 column_catalogs[idx].column_desc.generated_or_default_column =
326 Some(GeneratedOrDefaultColumn::DefaultColumn(DefaultColumnDesc {
327 snapshot_value: Some(snapshot_value.to_protobuf()),
328 expr: Some(expr_impl.to_expr_proto()),
329 }));
331 } else {
332 return Err(ErrorCode::BindError(format!(
333 "Default expression used in column `{}` cannot be evaluated. \
334 Use generated columns instead if you mean to reference other columns.",
335 column.name
336 ))
337 .into());
338 }
339 }
340 ColumnOption::DefaultValueInternal { persisted, expr: _ } => {
341 if persisted.is_empty() {
346 bail_bind_error!(
347 "DEFAULT INTERNAL is only used for internal purposes, \
348 please specify a concrete default value"
349 );
350 }
351
352 let desc = DefaultColumnDesc::decode(&**persisted)
353 .expect("failed to decode persisted `DefaultColumnDesc`");
354
355 column_catalogs[idx].column_desc.generated_or_default_column =
356 Some(GeneratedOrDefaultColumn::DefaultColumn(desc));
357 }
358 _ => {}
359 }
360 }
361 }
362 Ok(())
363}
364
365pub fn bind_table_constraints(table_constraints: &[TableConstraint]) -> Result<Vec<String>> {
367 let mut pk_column_names = vec![];
368
369 for constraint in table_constraints {
370 match constraint {
371 TableConstraint::Unique {
372 name: _,
373 columns,
374 is_primary: true,
375 } => {
376 if !pk_column_names.is_empty() {
377 return Err(multiple_pk_definition_err());
378 }
379 pk_column_names = columns.iter().map(|c| c.real_value()).collect_vec();
380 }
381 _ => bail_not_implemented!("table constraint \"{}\"", constraint),
382 }
383 }
384 Ok(pk_column_names)
385}
386
387pub fn bind_sql_pk_names(
388 columns_defs: &[ColumnDef],
389 pk_names_from_table_constraints: Vec<String>,
390) -> Result<Vec<String>> {
391 let mut pk_column_names = pk_names_from_table_constraints;
392
393 for column in columns_defs {
394 for option_def in &column.options {
395 if let ColumnOption::Unique { is_primary: true } = option_def.option {
396 if !pk_column_names.is_empty() {
397 return Err(multiple_pk_definition_err());
398 }
399 pk_column_names.push(column.name.real_value());
400 };
401 }
402 }
403
404 Ok(pk_column_names)
405}
406
407fn multiple_pk_definition_err() -> RwError {
408 ErrorCode::BindError("multiple primary keys are not allowed".into()).into()
409}
410
411pub fn bind_pk_and_row_id_on_relation(
416 mut columns: Vec<ColumnCatalog>,
417 pk_names: Vec<String>,
418 must_need_pk: bool,
419) -> Result<(Vec<ColumnCatalog>, Vec<ColumnId>, Option<usize>)> {
420 for c in &columns {
421 assert!(c.column_id() != ColumnId::placeholder());
422 }
423
424 let name_to_id = columns
426 .iter()
427 .map(|c| (c.name(), c.column_id()))
428 .collect::<HashMap<_, _>>();
429
430 let mut pk_column_ids: Vec<_> = pk_names
431 .iter()
432 .map(|name| {
433 name_to_id.get(name.as_str()).copied().ok_or_else(|| {
434 ErrorCode::BindError(format!("column \"{name}\" named in key does not exist"))
435 })
436 })
437 .try_collect()?;
438
439 let need_row_id = pk_column_ids.is_empty() && must_need_pk;
441
442 let row_id_index = need_row_id.then(|| {
443 let column = ColumnCatalog::row_id_column();
444 let index = columns.len();
445 pk_column_ids = vec![column.column_id()];
446 columns.push(column);
447 index
448 });
449
450 if let Some(col) = columns.iter().map(|c| c.name()).duplicates().next() {
451 Err(ErrorCode::InvalidInputSyntax(format!(
452 "column \"{col}\" specified more than once"
453 )))?;
454 }
455
456 Ok((columns, pk_column_ids, row_id_index))
457}
458
459#[allow(clippy::too_many_arguments)]
462pub(crate) async fn gen_create_table_plan_with_source(
463 mut handler_args: HandlerArgs,
464 explain_options: ExplainOptions,
465 table_name: ObjectName,
466 column_defs: Vec<ColumnDef>,
467 wildcard_idx: Option<usize>,
468 constraints: Vec<TableConstraint>,
469 format_encode: FormatEncodeOptions,
470 source_watermarks: Vec<SourceWatermark>,
471 mut col_id_gen: ColumnIdGenerator,
472 include_column_options: IncludeOption,
473 props: CreateTableProps,
474 sql_column_strategy: SqlColumnStrategy,
475) -> Result<(PlanRef, Option<SourceCatalog>, TableCatalog)> {
476 if props.append_only
477 && format_encode.format != Format::Plain
478 && format_encode.format != Format::Native
479 {
480 return Err(ErrorCode::BindError(format!(
481 "Append only table does not support format {}.",
482 format_encode.format
483 ))
484 .into());
485 }
486
487 let session = &handler_args.session;
488 let (with_properties, refresh_mode) =
489 bind_connector_props(&handler_args, &format_encode, false)?;
490 if with_properties.is_shareable_cdc_connector() {
491 generated_columns_check_for_cdc_table(&column_defs)?;
492 not_null_check_for_cdc_table(&wildcard_idx, &column_defs)?;
493 } else if column_defs.iter().any(|col| {
494 col.options
495 .iter()
496 .any(|def| matches!(def.option, ColumnOption::NotNull))
497 }) {
498 notice_to_user(
500 "The table contains columns with NOT NULL constraints. Any rows from upstream violating the constraints will be ignored silently.",
501 );
502 }
503
504 let db_name: &str = &session.database();
505 let (schema_name, _) = Binder::resolve_schema_qualified_name(db_name, &table_name)?;
506 session.get_database_and_schema_id_for_create(schema_name.clone())?;
509
510 let (columns_from_resolve_source, source_info) = bind_columns_from_source(
512 session,
513 &format_encode,
514 Either::Left(&with_properties),
515 CreateSourceType::Table,
516 )
517 .await?;
518
519 let overwrite_options = OverwriteOptions::new(&mut handler_args);
520 let rate_limit = overwrite_options.source_rate_limit;
521 let source = bind_create_source_or_table_with_connector(
522 handler_args.clone(),
523 table_name,
524 format_encode,
525 with_properties,
526 &column_defs,
527 constraints,
528 wildcard_idx,
529 source_watermarks,
530 columns_from_resolve_source,
531 source_info,
532 include_column_options,
533 &mut col_id_gen,
534 CreateSourceType::Table,
535 rate_limit,
536 sql_column_strategy,
537 refresh_mode,
538 )
539 .await?;
540
541 let context = OptimizerContext::new(handler_args, explain_options);
542
543 let (plan, table) = gen_table_plan_with_source(
544 context.into(),
545 schema_name,
546 source.clone(),
547 col_id_gen.into_version(),
548 props,
549 )?;
550
551 Ok((plan, Some(source), table))
552}
553
554#[allow(clippy::too_many_arguments)]
557pub(crate) fn gen_create_table_plan(
558 context: OptimizerContext,
559 table_name: ObjectName,
560 column_defs: Vec<ColumnDef>,
561 constraints: Vec<TableConstraint>,
562 mut col_id_gen: ColumnIdGenerator,
563 source_watermarks: Vec<SourceWatermark>,
564 props: CreateTableProps,
565 is_for_replace_plan: bool,
566) -> Result<(PlanRef, TableCatalog)> {
567 let mut columns = bind_sql_columns(&column_defs, is_for_replace_plan)?;
568 for c in &mut columns {
569 col_id_gen.generate(c)?;
570 }
571
572 let (_, secret_refs, connection_refs) = context.with_options().clone().into_parts();
573 if !secret_refs.is_empty() || !connection_refs.is_empty() {
574 return Err(crate::error::ErrorCode::InvalidParameterValue("Secret reference and Connection reference are not allowed in options when creating table without external source".to_owned()).into());
575 }
576
577 gen_create_table_plan_without_source(
578 context,
579 table_name,
580 columns,
581 column_defs,
582 constraints,
583 source_watermarks,
584 col_id_gen.into_version(),
585 props,
586 )
587}
588
589#[allow(clippy::too_many_arguments)]
590pub(crate) fn gen_create_table_plan_without_source(
591 context: OptimizerContext,
592 table_name: ObjectName,
593 columns: Vec<ColumnCatalog>,
594 column_defs: Vec<ColumnDef>,
595 constraints: Vec<TableConstraint>,
596 source_watermarks: Vec<SourceWatermark>,
597 version: TableVersion,
598 props: CreateTableProps,
599) -> Result<(PlanRef, TableCatalog)> {
600 let pk_names = bind_sql_pk_names(&column_defs, bind_table_constraints(&constraints)?)?;
602 let (mut columns, pk_column_ids, row_id_index) =
603 bind_pk_and_row_id_on_relation(columns, pk_names, true)?;
604
605 let watermark_descs = bind_source_watermark(
606 context.session_ctx(),
607 table_name.real_value(),
608 source_watermarks,
609 &columns,
610 )?;
611
612 bind_sql_column_constraints(
613 context.session_ctx(),
614 table_name.real_value(),
615 &mut columns,
616 &column_defs,
617 &pk_column_ids,
618 )?;
619 let session = context.session_ctx().clone();
620
621 let db_name = &session.database();
622 let (schema_name, table_name) = Binder::resolve_schema_qualified_name(db_name, &table_name)?;
623
624 let info = CreateTableInfo {
625 columns,
626 pk_column_ids,
627 row_id_index,
628 watermark_descs,
629 source_catalog: None,
630 version,
631 };
632
633 gen_table_plan_inner(context.into(), schema_name, table_name, info, props)
634}
635
636fn gen_table_plan_with_source(
637 context: OptimizerContextRef,
638 schema_name: Option<String>,
639 source_catalog: SourceCatalog,
640 version: TableVersion,
641 props: CreateTableProps,
642) -> Result<(PlanRef, TableCatalog)> {
643 let table_name = source_catalog.name.clone();
644
645 let info = CreateTableInfo {
646 columns: source_catalog.columns.clone(),
647 pk_column_ids: source_catalog.pk_col_ids.clone(),
648 row_id_index: source_catalog.row_id_index,
649 watermark_descs: source_catalog.watermark_descs.clone(),
650 source_catalog: Some(source_catalog),
651 version,
652 };
653
654 gen_table_plan_inner(context, schema_name, table_name, info, props)
655}
656
657#[derive(Clone, Copy)]
659pub enum EitherOnConflict {
660 Ast(Option<OnConflict>),
661 Resolved(ConflictBehavior),
662}
663
664impl From<Option<OnConflict>> for EitherOnConflict {
665 fn from(v: Option<OnConflict>) -> Self {
666 Self::Ast(v)
667 }
668}
669
670impl From<ConflictBehavior> for EitherOnConflict {
671 fn from(v: ConflictBehavior) -> Self {
672 Self::Resolved(v)
673 }
674}
675
676impl EitherOnConflict {
677 pub fn to_behavior(self, append_only: bool, row_id_as_pk: bool) -> Result<ConflictBehavior> {
679 let conflict_behavior = match self {
680 EitherOnConflict::Ast(on_conflict) => {
681 if append_only {
682 if row_id_as_pk {
683 ConflictBehavior::NoCheck
685 } else {
686 if let Some(on_conflict) = on_conflict
688 && on_conflict != OnConflict::Nothing
689 {
690 return Err(ErrorCode::InvalidInputSyntax(
691 "When PRIMARY KEY constraint applied to an APPEND ONLY table, \
692 the ON CONFLICT behavior must be DO NOTHING."
693 .to_owned(),
694 )
695 .into());
696 }
697 ConflictBehavior::IgnoreConflict
698 }
699 } else {
700 match on_conflict.unwrap_or(OnConflict::UpdateFull) {
702 OnConflict::UpdateFull => ConflictBehavior::Overwrite,
703 OnConflict::Nothing => ConflictBehavior::IgnoreConflict,
704 OnConflict::UpdateIfNotNull => ConflictBehavior::DoUpdateIfNotNull,
705 }
706 }
707 }
708 EitherOnConflict::Resolved(b) => b,
709 };
710
711 Ok(conflict_behavior)
712 }
713}
714
715pub struct CreateTableInfo {
720 pub columns: Vec<ColumnCatalog>,
721 pub pk_column_ids: Vec<ColumnId>,
722 pub row_id_index: Option<usize>,
723 pub watermark_descs: Vec<WatermarkDesc>,
724 pub source_catalog: Option<SourceCatalog>,
725 pub version: TableVersion,
726}
727
728pub struct CreateTableProps {
733 pub definition: String,
734 pub append_only: bool,
735 pub on_conflict: EitherOnConflict,
736 pub with_version_columns: Vec<String>,
737 pub webhook_info: Option<PbWebhookSourceInfo>,
738 pub engine: Engine,
739}
740
741#[allow(clippy::too_many_arguments)]
742fn gen_table_plan_inner(
743 context: OptimizerContextRef,
744 schema_name: Option<String>,
745 table_name: String,
746 info: CreateTableInfo,
747 props: CreateTableProps,
748) -> Result<(PlanRef, TableCatalog)> {
749 let CreateTableInfo {
750 ref columns,
751 row_id_index,
752 ref watermark_descs,
753 ref source_catalog,
754 ..
755 } = info;
756 let CreateTableProps { append_only, .. } = props;
757
758 let (database_id, schema_id) = context
759 .session_ctx()
760 .get_database_and_schema_id_for_create(schema_name)?;
761
762 let session = context.session_ctx().clone();
763 let retention_seconds = context.with_options().retention_seconds();
764
765 let source_node: LogicalPlanRef = LogicalSource::new(
766 source_catalog.clone().map(Rc::new),
767 columns.clone(),
768 row_id_index,
769 SourceNodeKind::CreateTable,
770 context.clone(),
771 None,
772 )?
773 .into();
774
775 let required_cols = FixedBitSet::with_capacity(columns.len());
776 let plan_root = PlanRoot::new_with_logical_plan(
777 source_node,
778 RequiredDist::Any,
779 Order::any(),
780 required_cols,
781 vec![],
782 );
783
784 let has_non_ttl_watermark = watermark_descs.iter().any(|d| !d.with_ttl);
785
786 if !append_only && has_non_ttl_watermark {
787 return Err(ErrorCode::NotSupported(
788 "Defining watermarks on table requires the table to be append only.".to_owned(),
789 "Use the key words `APPEND ONLY`".to_owned(),
790 )
791 .into());
792 }
793
794 if !append_only && retention_seconds.is_some() {
795 if session
796 .config()
797 .unsafe_enable_storage_retention_for_non_append_only_tables()
798 {
799 tracing::warn!(
800 "Storage retention is enabled for non-append-only table {}. This may lead to stream inconsistency.",
801 table_name
802 );
803 const NOTICE: &str = "Storage retention is enabled for non-append-only table. \
804 This may lead to stream inconsistency and unrecoverable \
805 node failure if there is any row INSERT/UPDATE/DELETE operation \
806 corresponding to the TTLed primary keys";
807 session.notice_to_user(NOTICE);
808 } else {
809 return Err(ErrorCode::NotSupported(
810 "Defining retention seconds on table requires the table to be append only."
811 .to_owned(),
812 "Use the key words `APPEND ONLY`".to_owned(),
813 )
814 .into());
815 }
816 }
817
818 let materialize =
819 plan_root.gen_table_plan(context, table_name, database_id, schema_id, info, props)?;
820
821 let mut table = materialize.table().clone();
822 table.owner = session.user_id();
823
824 Ok((
825 ensure_sync_log_store_fragment_root(materialize.into()),
826 table,
827 ))
828}
829
830#[allow(clippy::too_many_arguments)]
834pub(crate) fn gen_create_table_plan_for_cdc_table(
835 context: OptimizerContextRef,
836 source: Arc<SourceCatalog>,
837 external_table_name: String,
838 column_defs: Vec<ColumnDef>,
839 source_watermarks: Vec<SourceWatermark>,
840 mut columns: Vec<ColumnCatalog>,
841 pk_names: Vec<String>,
842 pk_comparisons: Vec<CdcKeyComparison>,
843 cdc_with_options: WithOptionsSecResolved,
844 mut col_id_gen: ColumnIdGenerator,
845 on_conflict: Option<OnConflict>,
846 with_version_columns: Vec<String>,
847 include_column_options: IncludeOption,
848 table_name: ObjectName,
849 resolved_table_name: String, database_id: DatabaseId,
851 schema_id: SchemaId,
852 table_id: TableId,
853 engine: Engine,
854) -> Result<(PlanRef, TableCatalog)> {
855 let session = context.session_ctx().clone();
856
857 handle_addition_columns(
859 None,
860 &cdc_with_options,
861 include_column_options,
862 &mut columns,
863 true,
864 )?;
865
866 for c in &mut columns {
867 col_id_gen.generate(c)?;
868 }
869
870 let (mut columns, pk_column_ids, _row_id_index) =
871 bind_pk_and_row_id_on_relation(columns, pk_names, true)?;
872
873 let watermark_descs = bind_source_watermark(
874 context.session_ctx(),
875 table_name.real_value(),
876 source_watermarks,
877 &columns,
878 )?;
879
880 bind_sql_column_constraints(
882 context.session_ctx(),
883 table_name.real_value(),
884 &mut columns,
885 &column_defs,
886 &pk_column_ids,
887 )?;
888
889 let definition = context.normalized_sql().to_owned();
890
891 let pk_column_indices = {
892 let mut id_to_idx = HashMap::new();
893 columns.iter().enumerate().for_each(|(idx, c)| {
894 id_to_idx.insert(c.column_id(), idx);
895 });
896 pk_column_ids
898 .iter()
899 .map(|c| id_to_idx.get(c).copied().unwrap())
900 .collect_vec()
901 };
902 let table_pk = pk_column_indices
903 .iter()
904 .map(|idx| ColumnOrder::new(*idx, OrderType::ascending()))
905 .collect();
906
907 let (options, secret_refs) = cdc_with_options.into_parts();
908
909 let non_generated_column_descs = columns
910 .iter()
911 .filter(|&c| !c.is_generated())
912 .map(|c| c.column_desc.clone())
913 .collect_vec();
914 let non_generated_column_num = non_generated_column_descs.len();
915 let cdc_table_type = ExternalCdcTableType::from_properties(&options);
916 let canonical_name = dispatch_cdc_source_type!(
917 options.cdc_source_type(),
918 T,
919 T::canonical_name(&options),
920 unreachable!("CDC table without a CDC connector")
921 );
922 let cdc_table_desc = CdcTableDesc {
923 table_id,
924 source_id: source.id, external_table_name: external_table_name.clone(),
926 pk: table_pk,
927 pk_comparisons,
928 columns: non_generated_column_descs,
929 stream_key: pk_column_indices,
930 connect_properties: options,
931 secret_refs,
932 };
933
934 tracing::debug!(?cdc_table_desc, "create cdc table");
935 let options = build_cdc_scan_options_with_options(
936 context.with_options(),
937 cdc_table_desc.connect_properties.cdc_source_type(),
938 )?;
939
940 let logical_scan = LogicalCdcScan::create(
941 external_table_name,
942 Rc::new(cdc_table_desc),
943 context.clone(),
944 options,
945 );
946
947 let scan_node: LogicalPlanRef = logical_scan.into();
948 let required_cols = FixedBitSet::with_capacity(non_generated_column_num);
949 let plan_root = PlanRoot::new_with_logical_plan(
950 scan_node,
951 RequiredDist::Any,
952 Order::any(),
953 required_cols,
954 vec![],
955 );
956
957 let cdc_table_id = build_cdc_table_id(source.id, &canonical_name);
958 let materialize = plan_root.gen_table_plan(
959 context,
960 resolved_table_name,
961 database_id,
962 schema_id,
963 CreateTableInfo {
964 columns,
965 pk_column_ids,
966 row_id_index: None,
967 watermark_descs,
968 source_catalog: Some((*source).clone()),
969 version: col_id_gen.into_version(),
970 },
971 CreateTableProps {
972 definition,
973 append_only: false,
974 on_conflict: on_conflict.into(),
975 with_version_columns,
976 webhook_info: None,
977 engine,
978 },
979 )?;
980
981 let mut table = materialize.table().clone();
982 table.owner = session.user_id();
983 table.cdc_table_id = Some(cdc_table_id);
984 table.cdc_table_type = Some(cdc_table_type);
985 Ok((
986 ensure_sync_log_store_fragment_root(materialize.into()),
987 table,
988 ))
989}
990
991#[allow(clippy::too_many_arguments)]
992pub(super) async fn handle_create_table_plan(
993 handler_args: HandlerArgs,
994 explain_options: ExplainOptions,
995 format_encode: Option<FormatEncodeOptions>,
996 cdc_table_info: Option<CdcTableInfo>,
997 table_name: &ObjectName,
998 column_defs: Vec<ColumnDef>,
999 wildcard_idx: Option<usize>,
1000 constraints: Vec<TableConstraint>,
1001 source_watermarks: Vec<SourceWatermark>,
1002 append_only: bool,
1003 on_conflict: Option<OnConflict>,
1004 with_version_columns: Vec<String>,
1005 include_column_options: IncludeOption,
1006 webhook_info: Option<WebhookSourceInfo>,
1007 engine: Engine,
1008) -> Result<(
1009 PlanRef,
1010 Option<SourceCatalog>,
1011 TableCatalog,
1012 TableJobType,
1013 Option<SourceId>,
1014)> {
1015 let col_id_gen = ColumnIdGenerator::new_initial();
1016 let format_encode = check_create_table_with_source(
1017 &handler_args.with_options,
1018 format_encode,
1019 &include_column_options,
1020 &cdc_table_info,
1021 )?;
1022 let webhook_info = webhook_info
1023 .map(|info| bind_webhook_info(&handler_args.session, &column_defs, info))
1024 .transpose()?;
1025
1026 let props = CreateTableProps {
1027 definition: handler_args.normalized_sql.clone(),
1028 append_only,
1029 on_conflict: on_conflict.into(),
1030 with_version_columns: with_version_columns.clone(),
1031 webhook_info,
1032 engine,
1033 };
1034
1035 let ((plan, source, table), job_type, shared_shource_id) = match (
1036 format_encode,
1037 cdc_table_info.as_ref(),
1038 ) {
1039 (Some(format_encode), None) => (
1040 gen_create_table_plan_with_source(
1041 handler_args,
1042 explain_options,
1043 table_name.clone(),
1044 column_defs,
1045 wildcard_idx,
1046 constraints,
1047 format_encode,
1048 source_watermarks,
1049 col_id_gen,
1050 include_column_options,
1051 props,
1052 SqlColumnStrategy::FollowChecked,
1053 )
1054 .await?,
1055 TableJobType::General,
1056 None,
1057 ),
1058 (None, None) => {
1059 let context = OptimizerContext::new(handler_args, explain_options);
1060 let (plan, table) = gen_create_table_plan(
1061 context,
1062 table_name.clone(),
1063 column_defs,
1064 constraints,
1065 col_id_gen,
1066 source_watermarks,
1067 props,
1068 false,
1069 )?;
1070
1071 ((plan, None, table), TableJobType::General, None)
1072 }
1073
1074 (None, Some(cdc_table)) => {
1075 sanity_check_for_table_on_cdc_source(
1076 append_only,
1077 &column_defs,
1078 &wildcard_idx,
1079 &constraints,
1080 &source_watermarks,
1081 )?;
1082
1083 generated_columns_check_for_cdc_table(&column_defs)?;
1084 not_null_check_for_cdc_table(&wildcard_idx, &column_defs)?;
1085
1086 let session = &handler_args.session;
1087 let db_name = &session.database();
1088 let user_name = &session.user_name();
1089 let search_path = session.config().search_path();
1090 let (schema_name, resolved_table_name) =
1091 Binder::resolve_schema_qualified_name(db_name, table_name)?;
1092 let (database_id, schema_id) =
1093 session.get_database_and_schema_id_for_create(schema_name.clone())?;
1094
1095 let (source_schema, source_name) =
1097 Binder::resolve_schema_qualified_name(db_name, &cdc_table.source_name)?;
1098
1099 let source = {
1100 let catalog_reader = session.env().catalog_reader().read_guard();
1101 let schema_path =
1102 SchemaPath::new(source_schema.as_deref(), &search_path, user_name);
1103
1104 let (source, _) = catalog_reader.get_source_by_name(
1105 db_name,
1106 schema_path,
1107 source_name.as_str(),
1108 )?;
1109 source.clone()
1110 };
1111 check_cdc_source_select_privilege(session, &source)?;
1112 let (cdc_with_options, external_table_name) = derive_connect_properties_for_cdc_table(
1113 &source.with_properties,
1114 cdc_table.external_table_name.clone(),
1115 )?;
1116
1117 let (columns, pk_names, pk_comparisons) = match wildcard_idx {
1118 Some(_) => bind_cdc_table_schema_externally(cdc_with_options.clone()).await?,
1119 None => {
1120 for column_def in &column_defs {
1121 for option_def in &column_def.options {
1122 if let ColumnOption::DefaultValue(_)
1123 | ColumnOption::DefaultValueInternal { .. } = option_def.option
1124 {
1125 return Err(ErrorCode::NotSupported(
1126 "Default value for columns defined on the table created from a CDC source".into(),
1127 "Remove the default value expression in the column definitions".into(),
1128 )
1129 .into());
1130 }
1131 }
1132 }
1133
1134 let (columns, pk_names) =
1135 bind_cdc_table_schema(&column_defs, &constraints, false)?;
1136 let pk_comparisons = Box::pin(bind_cdc_pk_comparisons_externally(
1137 cdc_with_options.clone(),
1138 &pk_names,
1139 ))
1140 .await?;
1141
1142 (columns, pk_names, pk_comparisons)
1143 }
1144 };
1145
1146 reject_pk_filtered_by_debezium_column_filter(&pk_names, &cdc_with_options)?;
1151
1152 let context: OptimizerContextRef =
1153 OptimizerContext::new(handler_args, explain_options).into();
1154 let shared_source_id = source.id;
1155 let (plan, table) = gen_create_table_plan_for_cdc_table(
1156 context,
1157 source,
1158 external_table_name,
1159 column_defs,
1160 source_watermarks,
1161 columns,
1162 pk_names,
1163 pk_comparisons,
1164 cdc_with_options,
1165 col_id_gen,
1166 on_conflict,
1167 with_version_columns,
1168 include_column_options,
1169 table_name.clone(),
1170 resolved_table_name,
1171 database_id,
1172 schema_id,
1173 TableId::placeholder(),
1174 engine,
1175 )?;
1176
1177 (
1178 (plan, None, table),
1179 TableJobType::SharedCdcSource,
1180 Some(shared_source_id),
1181 )
1182 }
1183 (Some(_), Some(_)) => {
1184 return Err(ErrorCode::NotSupported(
1185 "Data format and encoding format doesn't apply to table created from a CDC source"
1186 .into(),
1187 "Remove the FORMAT and ENCODE specification".into(),
1188 )
1189 .into());
1190 }
1191 };
1192 Ok((plan, source, table, job_type, shared_shource_id))
1193}
1194
1195fn generated_columns_check_for_cdc_table(columns: &Vec<ColumnDef>) -> Result<()> {
1197 let mut found_generated_column = false;
1198 for column in columns {
1199 let mut is_generated = false;
1200
1201 for option_def in &column.options {
1202 if let ColumnOption::GeneratedColumns(_) = option_def.option {
1203 is_generated = true;
1204 break;
1205 }
1206 }
1207
1208 if is_generated {
1209 found_generated_column = true;
1210 } else if found_generated_column {
1211 return Err(ErrorCode::NotSupported(
1212 "Non-generated column found after a generated column.".into(),
1213 "Ensure that all generated columns appear at the end of the cdc table definition."
1214 .into(),
1215 )
1216 .into());
1217 }
1218 }
1219 Ok(())
1220}
1221
1222#[allow(clippy::too_many_arguments)]
1223pub async fn handle_create_table(
1224 handler_args: HandlerArgs,
1225 table_name: ObjectName,
1226 column_defs: Vec<ColumnDef>,
1227 wildcard_idx: Option<usize>,
1228 constraints: Vec<TableConstraint>,
1229 if_not_exists: bool,
1230 format_encode: Option<FormatEncodeOptions>,
1231 source_watermarks: Vec<SourceWatermark>,
1232 append_only: bool,
1233 on_conflict: Option<OnConflict>,
1234 with_version_columns: Vec<String>,
1235 cdc_table_info: Option<CdcTableInfo>,
1236 include_column_options: IncludeOption,
1237 webhook_info: Option<WebhookSourceInfo>,
1238 ast_engine: risingwave_sqlparser::ast::Engine,
1239) -> Result<RwPgResponse> {
1240 let session = handler_args.session.clone();
1241
1242 if append_only {
1243 session.notice_to_user("APPEND ONLY TABLE is currently an experimental feature.");
1244 }
1245
1246 session.check_cluster_limits().await?;
1247
1248 let engine = match ast_engine {
1249 risingwave_sqlparser::ast::Engine::Hummock => Engine::Hummock,
1250 risingwave_sqlparser::ast::Engine::Iceberg => Engine::Iceberg,
1251 };
1252
1253 if let Either::Right(resp) = session.check_relation_name_duplicated(
1254 table_name.clone(),
1255 StatementType::CREATE_TABLE,
1256 if_not_exists,
1257 )? {
1258 return Ok(resp);
1259 }
1260
1261 let (graph, source, hummock_table, job_type, shared_source_id) = {
1262 let (plan, source, table, job_type, shared_source_id) = Box::pin(handle_create_table_plan(
1263 handler_args.clone(),
1264 ExplainOptions::default(),
1265 format_encode,
1266 cdc_table_info,
1267 &table_name,
1268 column_defs.clone(),
1269 wildcard_idx,
1270 constraints.clone(),
1271 source_watermarks,
1272 append_only,
1273 on_conflict,
1274 with_version_columns,
1275 include_column_options,
1276 webhook_info,
1277 engine,
1278 ))
1279 .await?;
1280 tracing::trace!("table_plan: {:?}", plan.explain_to_string());
1281
1282 let graph = build_graph(plan, Some(GraphJobType::Table))?;
1283
1284 (graph, source, table, job_type, shared_source_id)
1285 };
1286
1287 tracing::trace!(
1288 "name={}, graph=\n{}",
1289 table_name,
1290 serde_json::to_string_pretty(&graph).unwrap()
1291 );
1292
1293 let dependencies = shared_source_id
1294 .map(|id| HashSet::from([id.as_object_id()]))
1295 .unwrap_or_default();
1296
1297 match engine {
1299 Engine::Hummock => {
1300 let catalog_writer = session.catalog_writer()?;
1301 let action = match job_type {
1302 TableJobType::SharedCdcSource => LongRunningNotificationAction::MonitorBackfillJob,
1303 _ => LongRunningNotificationAction::DiagnoseBarrierLatency,
1304 };
1305 execute_with_long_running_notification(
1306 catalog_writer.create_table(
1307 source.map(|s| s.to_prost()),
1308 hummock_table.to_prost(),
1309 graph,
1310 job_type,
1311 if_not_exists,
1312 dependencies,
1313 ),
1314 &session,
1315 "CREATE TABLE",
1316 action,
1317 )
1318 .await?;
1319 }
1320 Engine::Iceberg => {
1321 let hummock_table_name = hummock_table.name.clone();
1322 session.create_staging_table(hummock_table.clone());
1323 let res = Box::pin(create_iceberg_engine_table(
1324 session.clone(),
1325 handler_args,
1326 source.map(|s| s.to_prost()),
1327 hummock_table,
1328 graph,
1329 table_name,
1330 job_type,
1331 if_not_exists,
1332 ))
1333 .await;
1334 session.drop_staging_table(&hummock_table_name);
1335 res?
1336 }
1337 }
1338
1339 Ok(PgResponse::empty_result(StatementType::CREATE_TABLE))
1340}
1341
1342fn build_iceberg_engine_sink_options(
1343 mut sink_options: BTreeMap<String, String>,
1344 user_options: &WithOptions,
1345 table: &TableCatalog,
1346 primary_key: &[String],
1347) -> Result<BTreeMap<String, String>> {
1348 sink_options.extend(
1349 user_options
1350 .iter()
1351 .filter(|(key, _)| is_iceberg_engine_option(key))
1352 .map(|(key, value)| (key.clone(), value.clone())),
1353 );
1354
1355 sink_options
1356 .entry(ENABLE_COMPACTION.to_owned())
1357 .or_insert_with(|| "true".to_owned());
1358 sink_options.insert(
1359 "type".to_owned(),
1360 if table.append_only {
1361 "append-only"
1362 } else {
1363 "upsert"
1364 }
1365 .to_owned(),
1366 );
1367
1368 if !table.append_only {
1371 sink_options.insert("primary_key".to_owned(), primary_key.join(","));
1372 }
1373
1374 sink_options.insert("create_table_if_not_exists".to_owned(), "true".to_owned());
1375 sink_options.insert("is_exactly_once".to_owned(), "true".to_owned());
1376
1377 let config = IcebergConfig::from_btreemap(sink_options.clone())?;
1378
1379 if !table.append_only && !config.enable_pk_index {
1382 for pk in table.pk() {
1383 let column = &table.columns()[pk.column_index];
1384 let data_type = column.data_type();
1385 if data_type.is_composite()
1386 || matches!(data_type, DataType::Float32 | DataType::Float64)
1387 {
1388 return Err(ErrorCode::NotSupported(
1389 format!(
1390 "Iceberg engine table primary key column \"{}\" has unsupported type {} for equality deletes",
1391 column.name(), data_type
1392 ),
1393 "Use non-floating-point scalar primary key columns.".to_owned(),
1394 )
1395 .into());
1396 }
1397 }
1398 }
1399
1400 if config.table_format_version() < FormatVersion::V3 {
1403 sink_options
1404 .entry(ENABLE_MANIFEST_REWRITE.to_owned())
1405 .or_insert_with(|| "true".to_owned());
1406 }
1407
1408 if let Some(partition_by) = &config.partition_by {
1409 let mut partition_columns = vec![];
1410 for (column, _) in parse_partition_by_exprs(partition_by.clone())? {
1411 table
1412 .columns()
1413 .iter()
1414 .find(|col| col.name().eq_ignore_ascii_case(&column))
1415 .ok_or_else(|| {
1416 ErrorCode::InvalidInputSyntax(format!(
1417 "Partition source column does not exist in schema: {}",
1418 column
1419 ))
1420 })?;
1421
1422 partition_columns.push(column);
1423 }
1424
1425 ensure_partition_columns_are_prefix_of_primary_key(&partition_columns, primary_key)
1426 .map_err(|_| {
1427 ErrorCode::InvalidInputSyntax(
1428 "The partition columns should be the prefix of the primary key".to_owned(),
1429 )
1430 })?;
1431 }
1432
1433 if let Some(order_key) = &config.order_key {
1434 validate_order_key_columns(order_key, table.columns().iter().map(|col| col.name()))
1435 .map_err(|err| ErrorCode::InvalidInputSyntax(err.to_report_string()))?;
1436 }
1437
1438 if config.enable_pk_index {
1439 sink_options.remove("primary_key");
1440 } else {
1441 sink_options.insert(AUTO_SCHEMA_CHANGE_KEY.to_owned(), "true".to_owned());
1442 }
1443
1444 Ok(sink_options)
1445}
1446
1447#[allow(clippy::too_many_arguments)]
1456pub async fn create_iceberg_engine_table(
1457 session: Arc<SessionImpl>,
1458 handler_args: HandlerArgs,
1459 mut source: Option<PbSource>,
1460 table: TableCatalog,
1461 graph: StreamFragmentGraph,
1462 table_name: ObjectName,
1463 job_type: PbTableJobType,
1464 if_not_exists: bool,
1465) -> Result<()> {
1466 let rw_db_name = session
1467 .env()
1468 .catalog_reader()
1469 .read_guard()
1470 .get_database_by_id(table.database_id)?
1471 .name()
1472 .to_owned();
1473 let rw_schema_name = session
1474 .env()
1475 .catalog_reader()
1476 .read_guard()
1477 .get_schema_by_id(table.database_id, table.schema_id)?
1478 .name()
1479 .clone();
1480 let iceberg_catalog_name = rw_db_name.clone();
1481 let iceberg_database_name = rw_schema_name.clone();
1482 let iceberg_table_name = table_name.0.last().unwrap().real_value();
1483
1484 let iceberg_engine_connection: String = session.config().iceberg_engine_connection();
1485 let sink_decouple = session.config().sink_decouple();
1486 if matches!(sink_decouple, SinkDecouple::Disable) {
1487 bail!(
1488 "Iceberg engine table only supports with sink decouple, try `set sink_decouple = true` to resolve it"
1489 );
1490 }
1491
1492 let mut connection_ref = BTreeMap::new();
1493 let with_common = if iceberg_engine_connection.is_empty() {
1494 bail!("to use iceberg engine table, the variable `iceberg_engine_connection` must be set.");
1495 } else {
1496 let parts: Vec<&str> = iceberg_engine_connection.split('.').collect();
1497 assert_eq!(parts.len(), 2);
1498 let connection_catalog =
1499 session.get_connection_by_name(Some(parts[0].to_owned()), parts[1])?;
1500 if let ConnectionInfo::ConnectionParams(params) = &connection_catalog.info {
1501 if params.connection_type == ConnectionType::Iceberg as i32 {
1502 connection_ref.insert(
1504 "connection".to_owned(),
1505 ConnectionRefValue {
1506 connection_name: ObjectName::from(vec![
1507 Ident::from(parts[0]),
1508 Ident::from(parts[1]),
1509 ]),
1510 },
1511 );
1512
1513 let mut with_common = BTreeMap::new();
1514 with_common.insert("connector".to_owned(), "iceberg".to_owned());
1515 with_common.insert("database.name".to_owned(), iceberg_database_name);
1516 with_common.insert("table.name".to_owned(), iceberg_table_name);
1517
1518 let hosted_catalog = params
1519 .properties
1520 .get("hosted_catalog")
1521 .map(|s| s.eq_ignore_ascii_case("true"))
1522 .unwrap_or(false);
1523 if hosted_catalog {
1524 let meta_client = session.env().meta_client();
1525 let meta_store_endpoint = meta_client.get_meta_store_endpoint().await?;
1526
1527 let meta_store_endpoint =
1528 url::Url::parse(&meta_store_endpoint).map_err(|_| {
1529 ErrorCode::InternalError(
1530 "failed to parse the meta store endpoint".to_owned(),
1531 )
1532 })?;
1533 let meta_store_backend = meta_store_endpoint.scheme().to_owned();
1534 let meta_store_user = meta_store_endpoint.username().to_owned();
1535 let meta_store_password = match meta_store_endpoint.password() {
1536 Some(password) => percent_decode_str(password)
1537 .decode_utf8()
1538 .map_err(|_| {
1539 ErrorCode::InternalError(
1540 "failed to parse password from meta store endpoint".to_owned(),
1541 )
1542 })?
1543 .into_owned(),
1544 None => "".to_owned(),
1545 };
1546 let meta_store_host = meta_store_endpoint
1547 .host_str()
1548 .ok_or_else(|| {
1549 ErrorCode::InternalError(
1550 "failed to parse host from meta store endpoint".to_owned(),
1551 )
1552 })?
1553 .to_owned();
1554 let meta_store_port = meta_store_endpoint.port().ok_or_else(|| {
1555 ErrorCode::InternalError(
1556 "failed to parse port from meta store endpoint".to_owned(),
1557 )
1558 })?;
1559 let meta_store_database = meta_store_endpoint
1560 .path()
1561 .trim_start_matches('/')
1562 .to_owned();
1563
1564 let Ok(meta_backend) = MetaBackend::from_str(&meta_store_backend, true) else {
1565 bail!("failed to parse meta backend: {}", meta_store_backend);
1566 };
1567
1568 let catalog_uri = match meta_backend {
1569 MetaBackend::Postgres => {
1570 format!(
1571 "jdbc:postgresql://{}:{}/{}",
1572 meta_store_host, meta_store_port, meta_store_database
1573 )
1574 }
1575 MetaBackend::Mysql => {
1576 format!(
1577 "jdbc:mysql://{}:{}/{}",
1578 meta_store_host, meta_store_port, meta_store_database
1579 )
1580 }
1581 MetaBackend::Sqlite | MetaBackend::Sql | MetaBackend::Mem => {
1582 bail!(
1583 "Unsupported meta backend for iceberg engine table: {}",
1584 meta_store_backend
1585 );
1586 }
1587 };
1588
1589 with_common.insert("catalog.type".to_owned(), "jdbc".to_owned());
1590 with_common.insert("catalog.uri".to_owned(), catalog_uri);
1591 with_common.insert("catalog.jdbc.user".to_owned(), meta_store_user);
1592 with_common.insert("catalog.jdbc.password".to_owned(), meta_store_password);
1593 with_common.insert("catalog.name".to_owned(), iceberg_catalog_name);
1594 }
1595
1596 with_common
1597 } else {
1598 return Err(RwError::from(ErrorCode::InvalidParameterValue(
1599 "Only iceberg connection could be used in iceberg engine".to_owned(),
1600 )));
1601 }
1602 } else {
1603 return Err(RwError::from(ErrorCode::InvalidParameterValue(
1604 "Private Link Service has been deprecated. Please create a new connection instead."
1605 .to_owned(),
1606 )));
1607 }
1608 };
1609
1610 let mut pks = table
1613 .pk_column_names()
1614 .iter()
1615 .map(|c| c.to_string())
1616 .collect::<Vec<String>>();
1617
1618 if pks.len() == 1 && pks[0].eq(ROW_ID_COLUMN_NAME) {
1620 pks = vec![RISINGWAVE_ICEBERG_ROW_ID.to_owned()];
1621 }
1622
1623 let sink_from = CreateSink::From(table_name.clone());
1624
1625 let mut sink_name = table_name.clone();
1626 *sink_name.0.last_mut().unwrap() = Ident::from(
1627 (ICEBERG_SINK_PREFIX.to_owned() + &sink_name.0.last().unwrap().real_value()).as_str(),
1628 );
1629 let create_sink_stmt = CreateSinkStatement {
1630 or_replace: false,
1631 if_not_exists: false,
1632 sink_name,
1633 with_properties: WithProperties(vec![]),
1634 sink_from,
1635 columns: vec![],
1636 emit_mode: None,
1637 sink_schema: None,
1638 into_table_name: None,
1639 };
1640
1641 let mut sink_handler_args = handler_args.clone();
1642
1643 let sink_with = build_iceberg_engine_sink_options(
1644 with_common.clone(),
1645 &handler_args.with_options,
1646 &table,
1647 &pks,
1648 )?;
1649
1650 if let Some(source) = source.as_mut() {
1651 source
1652 .with_properties
1653 .retain(|key, _| !is_iceberg_engine_option(key));
1654 }
1655
1656 sink_handler_args.with_options =
1676 WithOptions::new(sink_with, Default::default(), connection_ref.clone());
1677 let SinkPlanContext {
1678 sink_plan,
1679 sink_catalog,
1680 ..
1681 } = gen_sink_plan(sink_handler_args, create_sink_stmt, None, true).await?;
1682 let sink_graph = build_graph(sink_plan, Some(GraphJobType::Sink))?;
1683
1684 let mut source_name = table_name.clone();
1685 *source_name.0.last_mut().unwrap() = Ident::from(
1686 (ICEBERG_SOURCE_PREFIX.to_owned() + &source_name.0.last().unwrap().real_value()).as_str(),
1687 );
1688 let create_source_stmt = CreateSourceStatement {
1689 temporary: false,
1690 if_not_exists: false,
1691 columns: vec![],
1692 source_name,
1693 wildcard_idx: Some(0),
1694 constraints: vec![],
1695 with_properties: WithProperties(vec![]),
1696 format_encode: CompatibleFormatEncode::V2(FormatEncodeOptions::none()),
1697 source_watermarks: vec![],
1698 include_column_options: vec![],
1699 cdc_table_info: None,
1700 };
1701
1702 let mut source_handler_args = handler_args.clone();
1703 let source_with = with_common;
1704 source_handler_args.with_options =
1705 WithOptions::new(source_with, Default::default(), connection_ref);
1706
1707 let overwrite_options = OverwriteOptions::new(&mut source_handler_args);
1708 let format_encode = create_source_stmt.format_encode.into_v2_with_warning();
1709 let (with_properties, refresh_mode) =
1710 bind_connector_props(&source_handler_args, &format_encode, true)?;
1711
1712 let (iceberg_catalog, table_identifier, table_created) = {
1715 let sink_param = SinkParam::try_from_sink_catalog(sink_catalog.clone())?;
1716 let iceberg_sink = IcebergSink::try_from(sink_param)?;
1717 let table_created = iceberg_sink.create_table_if_not_exists().await?;
1718
1719 let iceberg_catalog = iceberg_sink.config.create_catalog().await?;
1720 let table_identifier = iceberg_sink.config.full_table_name()?;
1721 (iceberg_catalog, table_identifier, table_created)
1722 };
1723
1724 let create_source_type = CreateSourceType::for_newly_created(&session, &*with_properties);
1725 let (columns_from_resolve_source, source_info) = bind_columns_from_source(
1726 &session,
1727 &format_encode,
1728 Either::Left(&with_properties),
1729 create_source_type,
1730 )
1731 .await?;
1732 let mut col_id_gen = ColumnIdGenerator::new_initial();
1733
1734 let iceberg_source_catalog = bind_create_source_or_table_with_connector(
1735 source_handler_args,
1736 create_source_stmt.source_name,
1737 format_encode,
1738 with_properties,
1739 &create_source_stmt.columns,
1740 create_source_stmt.constraints,
1741 create_source_stmt.wildcard_idx,
1742 create_source_stmt.source_watermarks,
1743 columns_from_resolve_source,
1744 source_info,
1745 create_source_stmt.include_column_options,
1746 &mut col_id_gen,
1747 create_source_type,
1748 overwrite_options.source_rate_limit,
1749 SqlColumnStrategy::FollowChecked,
1750 refresh_mode,
1751 )
1752 .await?;
1753
1754 let _ = Jvm::get_or_init()?;
1757
1758 let catalog_writer = session.catalog_writer()?;
1759 let action = match job_type {
1760 TableJobType::SharedCdcSource => LongRunningNotificationAction::MonitorBackfillJob,
1761 _ => LongRunningNotificationAction::DiagnoseBarrierLatency,
1762 };
1763 let res = execute_with_long_running_notification(
1764 catalog_writer.create_iceberg_table(
1765 PbTableJobInfo {
1766 source,
1767 table: Some(table.to_prost()),
1768 fragment_graph: Some(graph),
1769 job_type: job_type as _,
1770 },
1771 PbSinkJobInfo {
1772 sink: Some(sink_catalog.to_proto()),
1773 fragment_graph: Some(sink_graph),
1774 },
1775 iceberg_source_catalog.to_prost(),
1776 if_not_exists,
1777 ),
1778 &session,
1779 "CREATE TABLE",
1780 action,
1781 )
1782 .await;
1783
1784 if res.is_err() {
1785 if table_created {
1788 let _ = iceberg_catalog
1789 .drop_table(&table_identifier)
1790 .await
1791 .inspect_err(|err| {
1792 tracing::error!(
1793 "failed to drop iceberg table {} after create iceberg engine table failed: {}",
1794 table_identifier,
1795 err.as_report()
1796 );
1797 });
1798 }
1799 res?
1800 }
1801
1802 Ok(())
1803}
1804
1805pub fn check_create_table_with_source(
1806 with_options: &WithOptions,
1807 format_encode: Option<FormatEncodeOptions>,
1808 include_column_options: &IncludeOption,
1809 cdc_table_info: &Option<CdcTableInfo>,
1810) -> Result<Option<FormatEncodeOptions>> {
1811 if cdc_table_info.is_some() {
1813 return Ok(format_encode);
1814 }
1815 let defined_source = with_options.is_source_connector();
1816
1817 if !include_column_options.is_empty() && !defined_source {
1818 return Err(ErrorCode::InvalidInputSyntax(
1819 "INCLUDE should be used with a connector".to_owned(),
1820 )
1821 .into());
1822 }
1823 if defined_source {
1824 format_encode.as_ref().ok_or_else(|| {
1825 ErrorCode::InvalidInputSyntax("Please specify a source schema using FORMAT".to_owned())
1826 })?;
1827 }
1828 Ok(format_encode)
1829}
1830
1831fn ensure_partition_columns_are_prefix_of_primary_key(
1832 partition_columns: &[String],
1833 primary_key_columns: &[String],
1834) -> std::result::Result<(), String> {
1835 if partition_columns.len() > primary_key_columns.len() {
1836 return Err("Partition columns cannot be longer than primary key columns.".to_owned());
1837 }
1838
1839 for (i, partition_col) in partition_columns.iter().enumerate() {
1840 if primary_key_columns.get(i) != Some(partition_col) {
1841 return Err(format!(
1842 "Partition column '{}' is not a prefix of the primary key.",
1843 partition_col
1844 ));
1845 }
1846 }
1847
1848 Ok(())
1849}
1850
1851#[allow(clippy::too_many_arguments)]
1852pub async fn generate_stream_graph_for_replace_table(
1853 _session: &Arc<SessionImpl>,
1854 table_name: ObjectName,
1855 original_catalog: &Arc<TableCatalog>,
1856 handler_args: HandlerArgs,
1857 statement: Statement,
1858 col_id_gen: ColumnIdGenerator,
1859 sql_column_strategy: SqlColumnStrategy,
1860) -> Result<(
1861 StreamFragmentGraph,
1862 TableCatalog,
1863 Option<SourceCatalog>,
1864 TableJobType,
1865)> {
1866 let Statement::CreateTable {
1867 columns,
1868 constraints,
1869 source_watermarks,
1870 append_only,
1871 on_conflict,
1872 with_version_columns,
1873 wildcard_idx,
1874 cdc_table_info,
1875 format_encode,
1876 include_column_options,
1877 engine,
1878 with_options,
1879 ..
1880 } = statement
1881 else {
1882 panic!("unexpected statement type: {:?}", statement);
1883 };
1884
1885 let format_encode = format_encode
1886 .clone()
1887 .map(|format_encode| format_encode.into_v2_with_warning());
1888
1889 let engine = match engine {
1890 risingwave_sqlparser::ast::Engine::Hummock => Engine::Hummock,
1891 risingwave_sqlparser::ast::Engine::Iceberg => Engine::Iceberg,
1892 };
1893
1894 let is_drop_connector =
1895 original_catalog.associated_source_id().is_some() && format_encode.is_none();
1896 if is_drop_connector {
1897 debug_assert!(
1898 include_column_options.is_empty()
1899 && with_options
1900 .iter()
1901 .all(|opt| opt.name.real_value().to_lowercase() != "connector")
1902 );
1903 }
1904
1905 let props = CreateTableProps {
1906 definition: handler_args.normalized_sql.clone(),
1907 append_only,
1908 on_conflict: on_conflict.into(),
1909 with_version_columns: with_version_columns
1910 .iter()
1911 .map(|col| col.real_value())
1912 .collect(),
1913 webhook_info: original_catalog.webhook_info.clone(),
1914 engine,
1915 };
1916
1917 let ((plan, mut source, mut table), job_type) = match (format_encode, cdc_table_info.as_ref()) {
1918 (Some(format_encode), None) => (
1919 gen_create_table_plan_with_source(
1920 handler_args,
1921 ExplainOptions::default(),
1922 table_name,
1923 columns,
1924 wildcard_idx,
1925 constraints,
1926 format_encode,
1927 source_watermarks,
1928 col_id_gen,
1929 include_column_options,
1930 props,
1931 sql_column_strategy,
1932 )
1933 .await?,
1934 TableJobType::General,
1935 ),
1936 (None, None) => {
1937 let context = OptimizerContext::from_handler_args(handler_args);
1938 let (plan, table) = gen_create_table_plan(
1939 context,
1940 table_name,
1941 columns,
1942 constraints,
1943 col_id_gen,
1944 source_watermarks,
1945 props,
1946 true,
1947 )?;
1948 ((plan, None, table), TableJobType::General)
1949 }
1950 (None, Some(cdc_table)) => {
1951 sanity_check_for_table_on_cdc_source(
1952 append_only,
1953 &columns,
1954 &wildcard_idx,
1955 &constraints,
1956 &source_watermarks,
1957 )?;
1958
1959 let session = &handler_args.session;
1960 let (source, resolved_table_name) =
1961 get_source_and_resolved_table_name(session, cdc_table.clone(), table_name.clone())?;
1962
1963 let (cdc_with_options, external_table_name) = derive_connect_properties_for_cdc_table(
1964 &source.with_properties,
1965 cdc_table.external_table_name.clone(),
1966 )?;
1967
1968 let (column_catalogs, pk_names) = bind_cdc_table_schema(&columns, &constraints, true)?;
1969 let pk_comparisons = Box::pin(bind_cdc_pk_comparisons_externally(
1970 cdc_with_options.clone(),
1971 &pk_names,
1972 ))
1973 .await?;
1974
1975 reject_pk_filtered_by_debezium_column_filter(&pk_names, &cdc_with_options)?;
1978
1979 let context: OptimizerContextRef =
1980 OptimizerContext::new(handler_args, ExplainOptions::default()).into();
1981 let (plan, table) = gen_create_table_plan_for_cdc_table(
1982 context,
1983 source,
1984 external_table_name,
1985 columns,
1986 source_watermarks,
1987 column_catalogs,
1988 pk_names,
1989 pk_comparisons,
1990 cdc_with_options,
1991 col_id_gen,
1992 on_conflict,
1993 with_version_columns
1994 .iter()
1995 .map(|col| col.real_value())
1996 .collect(),
1997 include_column_options,
1998 table_name,
1999 resolved_table_name,
2000 original_catalog.database_id,
2001 original_catalog.schema_id,
2002 original_catalog.id(),
2003 engine,
2004 )?;
2005
2006 ((plan, None, table), TableJobType::SharedCdcSource)
2007 }
2008 (Some(_), Some(_)) => {
2009 return Err(ErrorCode::NotSupported(
2010 "Data format and encoding format doesn't apply to table created from a CDC source"
2011 .into(),
2012 "Remove the FORMAT and ENCODE specification".into(),
2013 )
2014 .into());
2015 }
2016 };
2017
2018 if table.pk_column_ids() != original_catalog.pk_column_ids() {
2019 Err(ErrorCode::InvalidInputSyntax(
2020 "alter primary key of table is not supported".to_owned(),
2021 ))?
2022 }
2023
2024 let graph = build_graph(plan, Some(GraphJobType::Table))?;
2025
2026 table.id = original_catalog.id();
2028 if !is_drop_connector && let Some(source_id) = original_catalog.associated_source_id() {
2029 table.associated_source_id = Some(source_id);
2030
2031 let source = source.as_mut().unwrap();
2032 source.id = source_id;
2033 source.associated_table_id = Some(table.id());
2034 }
2035
2036 Ok((graph, table, source, job_type))
2037}
2038
2039fn get_source_and_resolved_table_name(
2040 session: &Arc<SessionImpl>,
2041 cdc_table: CdcTableInfo,
2042 table_name: ObjectName,
2043) -> Result<(Arc<SourceCatalog>, String)> {
2044 let db_name = &session.database();
2045 let (_, resolved_table_name) = Binder::resolve_schema_qualified_name(db_name, &table_name)?;
2046
2047 let (source_schema, source_name) =
2048 Binder::resolve_schema_qualified_name(db_name, &cdc_table.source_name)?;
2049
2050 let source = {
2051 let catalog_reader = session.env().catalog_reader().read_guard();
2052 let schema_name = source_schema.unwrap_or(DEFAULT_SCHEMA_NAME.to_owned());
2053 let (source, _) = catalog_reader.get_source_by_name(
2054 db_name,
2055 SchemaPath::Name(schema_name.as_str()),
2056 source_name.as_str(),
2057 )?;
2058 source.clone()
2059 };
2060 check_cdc_source_select_privilege(session, &source)?;
2061
2062 Ok((source, resolved_table_name))
2063}
2064
2065pub(crate) fn check_cdc_source_select_privilege(
2066 session: &SessionImpl,
2067 source: &SourceCatalog,
2068) -> Result<()> {
2069 session.check_privileges(&[ObjectCheckItem::new(
2070 source.owner,
2071 AclMode::Select,
2072 source.name.clone(),
2073 source.id,
2074 )])
2075}
2076
2077fn bind_webhook_info(
2079 session: &Arc<SessionImpl>,
2080 column_defs: &[ColumnDef],
2081 webhook_info: WebhookSourceInfo,
2082) -> Result<PbWebhookSourceInfo> {
2083 let WebhookSourceInfo {
2084 secret_ref,
2085 signature_expr,
2086 wait_for_persistence,
2087 is_batched,
2088 } = webhook_info;
2089
2090 for column in column_defs {
2091 for option_def in &column.options {
2092 match option_def.option {
2093 ColumnOption::Null => {}
2094 ColumnOption::GeneratedColumns(_) => {
2095 return Err(ErrorCode::InvalidInputSyntax(
2096 "generated columns are not supported for webhook tables".to_owned(),
2097 )
2098 .into());
2099 }
2100 ColumnOption::DefaultValue(_) | ColumnOption::DefaultValueInternal { .. } => {
2101 return Err(ErrorCode::InvalidInputSyntax(
2102 "default values are not supported for webhook tables".to_owned(),
2103 )
2104 .into());
2105 }
2106 ColumnOption::NotNull
2107 | ColumnOption::Unique { .. }
2108 | ColumnOption::ForeignKey { .. }
2109 | ColumnOption::Check(_)
2110 | ColumnOption::DialectSpecific(_) => {
2111 return Err(ErrorCode::InvalidInputSyntax(
2112 "only NULL column option is supported for webhook tables".to_owned(),
2113 )
2114 .into());
2115 }
2116 }
2117 }
2118 }
2119
2120 let (pb_secret_ref, secret_name) = if let Some(secret_ref) = secret_ref {
2122 let db_name = &session.database();
2123 let (schema_name, secret_name) =
2124 Binder::resolve_schema_qualified_name(db_name, &secret_ref.secret_name)?;
2125 let secret_catalog = session.get_secret_by_name(schema_name, &secret_name)?;
2126 (
2127 Some(PbSecretRef {
2128 secret_id: secret_catalog.id,
2129 ref_as: match secret_ref.ref_as {
2130 SecretRefAsType::Text => PbRefAsType::Text,
2131 SecretRefAsType::File => PbRefAsType::File,
2132 }
2133 .into(),
2134 }),
2135 Some(secret_name),
2136 )
2137 } else {
2138 (None, None)
2139 };
2140
2141 let signature_expr = if let Some(signature_expr) = signature_expr {
2142 let payload_name = if column_defs.len() == 1
2143 && column_defs[0].data_type.as_ref() == Some(&AstDataType::Jsonb)
2144 {
2145 column_defs[0].name.real_value()
2146 } else {
2147 WEBHOOK_PAYLOAD_FIELD_NAME.to_owned()
2148 };
2149 let secure_compare_context = SecureCompareContext {
2150 payload_name,
2151 secret_name,
2152 };
2153 let mut binder = Binder::new_for_ddl(session).with_secure_compare(secure_compare_context);
2154 let expr = binder.bind_expr(&signature_expr)?;
2155
2156 if expr.as_function_call().is_none()
2158 || expr.as_function_call().unwrap().func_type()
2159 != crate::optimizer::plan_node::generic::ExprType::SecureCompare
2160 {
2161 return Err(ErrorCode::InvalidInputSyntax(
2162 "The signature verification function must be SECURE_COMPARE()".to_owned(),
2163 )
2164 .into());
2165 }
2166
2167 Some(expr.to_expr_proto())
2168 } else {
2169 session.notice_to_user(
2170 "VALIDATE clause is strongly recommended for safety or production usages",
2171 );
2172 None
2173 };
2174
2175 let pb_webhook_info = PbWebhookSourceInfo {
2176 secret_ref: pb_secret_ref,
2177 signature_expr,
2178 wait_for_persistence,
2179 is_batched,
2180 };
2181
2182 Ok(pb_webhook_info)
2183}
2184
2185#[cfg(test)]
2186mod tests {
2187 use risingwave_common::catalog::{
2188 DEFAULT_DATABASE_NAME, ROW_ID_COLUMN_NAME, RW_TIMESTAMP_COLUMN_NAME,
2189 };
2190 use risingwave_common::types::StructType;
2191
2192 use super::*;
2193 use crate::test_utils::{LocalFrontend, PROTO_FILE_DATA, create_proto_file};
2194
2195 #[tokio::test]
2196 async fn test_iceberg_engine_primary_key_types() {
2197 let frontend = LocalFrontend::new(Default::default()).await;
2198 for (primary_key, append_only, pk_index, supported) in [
2199 ("id, period", false, false, false),
2200 ("id, items", false, false, false),
2201 ("id, mapping", false, false, false),
2202 ("id, f", false, false, false),
2203 ("id, d", false, false, false),
2204 ("id", false, false, true),
2205 ("id, ts", false, false, true),
2206 ("", false, false, true),
2207 ("id, period", true, false, true),
2208 ("id, period", false, true, true),
2209 ] {
2210 let pk_clause = if primary_key.is_empty() {
2211 String::new()
2212 } else {
2213 format!(", PRIMARY KEY ({primary_key})")
2214 };
2215 frontend
2216 .run_sql(&format!(
2217 "CREATE TABLE t (id INT, period STRUCT<start_ts TIMESTAMPTZ, end_ts TIMESTAMPTZ>, \
2218 items INT[], mapping MAP(INT, INT), f REAL, d DOUBLE PRECISION, ts TIMESTAMPTZ \
2219 {pk_clause}) {}",
2220 if append_only { "APPEND ONLY" } else { "" }
2221 ))
2222 .await
2223 .unwrap();
2224 let session = frontend.session_ref();
2225 let table = session
2226 .env()
2227 .catalog_reader()
2228 .read_guard()
2229 .get_created_table_by_name(
2230 DEFAULT_DATABASE_NAME,
2231 SchemaPath::Name(DEFAULT_SCHEMA_NAME),
2232 "t",
2233 )
2234 .unwrap()
2235 .0
2236 .clone();
2237 let pks = table
2238 .pk_column_names()
2239 .into_iter()
2240 .map(str::to_owned)
2241 .collect_vec();
2242 for write_mode in ["merge-on-read", "copy-on-write"] {
2243 if write_mode == "copy-on-write" && (append_only || pk_index) {
2244 continue;
2245 }
2246 let options = BTreeMap::from([
2247 ("connector".to_owned(), "iceberg".to_owned()),
2248 ("catalog.type".to_owned(), "storage".to_owned()),
2249 (
2250 "warehouse.path".to_owned(),
2251 "s3://test/warehouse".to_owned(),
2252 ),
2253 ("database.name".to_owned(), "public".to_owned()),
2254 ("table.name".to_owned(), "t".to_owned()),
2255 ("write_mode".to_owned(), write_mode.to_owned()),
2256 ("enable_pk_index".to_owned(), pk_index.to_string()),
2257 ]);
2258 let result = build_iceberg_engine_sink_options(
2259 options,
2260 &WithOptions::default(),
2261 &table,
2262 &pks,
2263 );
2264 if supported {
2265 result.unwrap();
2266 } else {
2267 let err = result.unwrap_err().to_report_string();
2268 let column = primary_key.split(", ").last().unwrap();
2269 assert!(
2270 err.contains(&format!(
2271 "Iceberg engine table primary key column \"{column}\" has unsupported type"
2272 )),
2273 "{write_mode}: {err}"
2274 );
2275 }
2276 }
2277 frontend.run_sql("DROP TABLE t").await.unwrap();
2278 }
2279 }
2280
2281 #[tokio::test]
2282 async fn test_cdc_relations_require_select_privilege_on_source() {
2283 let frontend = LocalFrontend::new(Default::default()).await;
2284 frontend
2287 .run_sql(
2288 r#"
2289 CREATE SOURCE cdc_source WITH (
2290 connector = 'postgres-cdc',
2291 hostname = 'localhost',
2292 port = '5432',
2293 username = 'postgres',
2294 password = '',
2295 database.name = 'db'
2296 ) FORMAT PLAIN ENCODE JSON
2297 "#,
2298 )
2299 .await
2300 .unwrap();
2301 frontend.run_sql("CREATE USER cdc_user").await.unwrap();
2302 frontend
2303 .run_sql("GRANT CREATE ON SCHEMA public TO cdc_user")
2304 .await
2305 .unwrap();
2306
2307 let user_id = frontend
2308 .session_ref()
2309 .env()
2310 .user_info_reader()
2311 .read_guard()
2312 .get_user_by_name("cdc_user")
2313 .unwrap()
2314 .id;
2315 let user_session = frontend.session_user_ref(
2316 DEFAULT_DATABASE_NAME.to_owned(),
2317 "cdc_user".to_owned(),
2318 user_id,
2319 );
2320 let create_table =
2321 "CREATE TABLE cdc_table (id INT PRIMARY KEY) FROM cdc_source TABLE 'public.t'";
2322 let create_table_source = "CREATE SOURCE cdc_table_source (id INT PRIMARY KEY) \
2323 WITH (snapshot = 'false') FROM cdc_source TABLE 'public.t'";
2324
2325 for sql in [create_table, create_table_source] {
2326 let err = frontend
2327 .run_sql_with_session(user_session.clone(), sql)
2328 .await
2329 .unwrap_err();
2330 assert!(
2331 err.to_string()
2332 .contains("permission denied for source \"cdc_source\": \"SELECT\""),
2333 "{err:?}"
2334 );
2335 }
2336
2337 frontend
2338 .run_sql("GRANT SELECT ON SOURCE cdc_source TO cdc_user")
2339 .await
2340 .unwrap();
2341 frontend
2342 .run_sql_with_session(user_session.clone(), create_table)
2343 .await
2344 .unwrap();
2345 frontend
2346 .run_sql_with_session(user_session.clone(), create_table_source)
2347 .await
2348 .unwrap();
2349
2350 let table_source = {
2351 let catalog_reader = frontend.session_ref().env().catalog_reader().read_guard();
2352 catalog_reader
2353 .get_source_by_name(
2354 DEFAULT_DATABASE_NAME,
2355 SchemaPath::Name("public"),
2356 "cdc_table_source",
2357 )
2358 .unwrap()
2359 .0
2360 .clone()
2361 };
2362 assert_eq!(table_source.with_properties.len(), 2);
2363 assert_eq!(
2364 table_source.with_properties.get_connector().as_deref(),
2365 Some("postgres-cdc")
2366 );
2367 assert_eq!(
2368 table_source
2369 .with_properties
2370 .get("snapshot")
2371 .map(String::as_str),
2372 Some("false")
2373 );
2374 assert!(table_source.with_properties.as_secret().is_empty());
2375 let external_table = table_source.info.external_table.as_ref().unwrap();
2379 assert!(!external_table.connect_properties.is_empty());
2380
2381 let err = frontend
2382 .run_sql_with_session(
2383 user_session.clone(),
2384 "SELECT * FROM postgres_query('cdc_table_source', 'SELECT 1')",
2385 )
2386 .await
2387 .unwrap_err();
2388 assert!(
2389 err.to_report_string()
2390 .contains("only accept shared CDC sources, not CDC table sources"),
2391 "{err:?}"
2392 );
2393
2394 frontend
2395 .run_sql("REVOKE SELECT ON SOURCE cdc_source FROM cdc_user")
2396 .await
2397 .unwrap();
2398 let err = frontend
2399 .run_sql_with_session(user_session, "ALTER TABLE cdc_table ADD COLUMN value INT")
2400 .await
2401 .unwrap_err();
2402 assert!(
2403 err.to_string()
2404 .contains("permission denied for source \"cdc_source\": \"SELECT\""),
2405 "{err:?}"
2406 );
2407 }
2408
2409 #[tokio::test]
2410 async fn test_create_table_handler() {
2411 let sql =
2412 "create table t (v1 smallint, v2 struct<v3 bigint, v4 float, v5 double>) append only;";
2413 let frontend = LocalFrontend::new(Default::default()).await;
2414 frontend.run_sql(sql).await.unwrap();
2415
2416 let session = frontend.session_ref();
2417 let catalog_reader = session.env().catalog_reader().read_guard();
2418 let schema_path = SchemaPath::Name(DEFAULT_SCHEMA_NAME);
2419
2420 let (table, _) = catalog_reader
2422 .get_created_table_by_name(DEFAULT_DATABASE_NAME, schema_path, "t")
2423 .unwrap();
2424 assert_eq!(table.name(), "t");
2425
2426 let columns = table
2427 .columns
2428 .iter()
2429 .map(|col| (col.name(), col.data_type().clone()))
2430 .collect::<HashMap<&str, DataType>>();
2431
2432 let expected_columns = maplit::hashmap! {
2433 ROW_ID_COLUMN_NAME => DataType::Serial,
2434 "v1" => DataType::Int16,
2435 "v2" => StructType::new(
2436 vec![("v3", DataType::Int64),("v4", DataType::Float64),("v5", DataType::Float64)],
2437 )
2438 .with_ids([3, 4, 5].map(ColumnId::new))
2439 .into(),
2440 RW_TIMESTAMP_COLUMN_NAME => DataType::Timestamptz,
2441 };
2442
2443 assert_eq!(columns, expected_columns, "{columns:#?}");
2444 }
2445
2446 #[tokio::test]
2447 async fn test_create_webhook_table_with_arbitrary_columns() {
2448 let frontend = LocalFrontend::new(Default::default()).await;
2449 frontend
2450 .run_sql("create schema ingest_schema;")
2451 .await
2452 .unwrap();
2453 frontend
2454 .run_sql(
2455 r#"
2456 create table ingest_schema.orders (
2457 id int,
2458 customer_name varchar,
2459 amount double precision,
2460 primary key (id)
2461 ) with (
2462 connector = 'webhook'
2463 ) validate as secure_compare(
2464 headers->>'x-rw-signature',
2465 'sha256=' || encode(hmac('webhook-secret', payload, 'sha256'), 'hex')
2466 );
2467 "#,
2468 )
2469 .await
2470 .unwrap();
2471
2472 let session = frontend.session_ref();
2473 let catalog_reader = session.env().catalog_reader().read_guard();
2474 let (table, _) = catalog_reader
2475 .get_created_table_by_name(
2476 DEFAULT_DATABASE_NAME,
2477 SchemaPath::Name("ingest_schema"),
2478 "orders",
2479 )
2480 .unwrap();
2481
2482 assert!(table.webhook_info.is_some());
2483 assert_eq!(
2484 table
2485 .columns
2486 .iter()
2487 .filter(|column| column.can_dml())
2488 .count(),
2489 3
2490 );
2491 }
2492
2493 #[tokio::test]
2494 async fn test_create_webhook_table_uses_single_jsonb_column_name_in_validate() {
2495 let frontend = LocalFrontend::new(Default::default()).await;
2496 frontend
2497 .run_sql(
2498 r#"
2499 create table webhook_single_column (
2500 body jsonb
2501 ) with (
2502 connector = 'webhook'
2503 ) validate as secure_compare(
2504 headers->>'x-rw-signature',
2505 'sha256=' || encode(hmac('webhook-secret', body, 'sha256'), 'hex')
2506 );
2507 "#,
2508 )
2509 .await
2510 .unwrap();
2511 }
2512
2513 #[tokio::test]
2514 async fn test_create_webhook_table_with_generated_columns() {
2515 let frontend = LocalFrontend::new(Default::default()).await;
2516 let err = frontend
2517 .run_sql(
2518 r#"
2519 create table webhook_generated_columns (
2520 id int,
2521 amount double precision,
2522 amount_with_fee double precision as amount + 1.0
2523 ) with (
2524 connector = 'webhook'
2525 );
2526 "#,
2527 )
2528 .await
2529 .unwrap_err();
2530
2531 assert!(
2532 err.to_string()
2533 .contains("generated columns are not supported for webhook tables"),
2534 "{err:?}"
2535 );
2536 }
2537
2538 #[tokio::test]
2539 async fn test_create_webhook_table_with_default_value() {
2540 let frontend = LocalFrontend::new(Default::default()).await;
2541 let err = frontend
2542 .run_sql(
2543 r#"
2544 create table webhook_default_value (
2545 id int default 42,
2546 amount double precision
2547 ) with (
2548 connector = 'webhook'
2549 );
2550 "#,
2551 )
2552 .await
2553 .unwrap_err();
2554
2555 assert!(
2556 err.to_string()
2557 .contains("default values are not supported for webhook tables"),
2558 "{err:?}"
2559 );
2560 }
2561
2562 #[tokio::test]
2563 async fn test_create_webhook_table_with_not_null_option() {
2564 let frontend = LocalFrontend::new(Default::default()).await;
2565 let err = frontend
2566 .run_sql(
2567 r#"
2568 create table webhook_not_null (
2569 id int not null,
2570 amount double precision
2571 ) with (
2572 connector = 'webhook'
2573 );
2574 "#,
2575 )
2576 .await
2577 .unwrap_err();
2578
2579 assert!(
2580 err.to_string()
2581 .contains("only NULL column option is supported for webhook tables"),
2582 "{err:?}"
2583 );
2584 }
2585
2586 #[test]
2587 fn test_bind_primary_key() {
2588 for (sql, expected) in [
2591 ("create table t (v1 int, v2 int)", Ok(&[0] as &[_])),
2592 ("create table t (v1 int primary key, v2 int)", Ok(&[1])),
2593 ("create table t (v1 int, v2 int primary key)", Ok(&[2])),
2594 (
2595 "create table t (v1 int primary key, v2 int primary key)",
2596 Err("multiple primary keys are not allowed"),
2597 ),
2598 (
2599 "create table t (v1 int primary key primary key, v2 int)",
2600 Err("multiple primary keys are not allowed"),
2601 ),
2602 (
2603 "create table t (v1 int, v2 int, primary key (v1))",
2604 Ok(&[1]),
2605 ),
2606 (
2607 "create table t (v1 int, primary key (v2), v2 int)",
2608 Ok(&[2]),
2609 ),
2610 (
2611 "create table t (primary key (v2, v1), v1 int, v2 int)",
2612 Ok(&[2, 1]),
2613 ),
2614 (
2615 "create table t (v1 int, primary key (v1), v2 int, primary key (v1))",
2616 Err("multiple primary keys are not allowed"),
2617 ),
2618 (
2619 "create table t (v1 int primary key, primary key (v1), v2 int)",
2620 Err("multiple primary keys are not allowed"),
2621 ),
2622 (
2623 "create table t (v1 int, primary key (V3), v2 int)",
2624 Err("column \"v3\" named in key does not exist"),
2625 ),
2626 ] {
2627 let mut ast = risingwave_sqlparser::parser::Parser::parse_sql(sql).unwrap();
2628 let risingwave_sqlparser::ast::Statement::CreateTable {
2629 columns: column_defs,
2630 constraints,
2631 ..
2632 } = ast.remove(0)
2633 else {
2634 panic!("test case should be create table")
2635 };
2636 let actual: Result<_> = (|| {
2637 let mut columns = bind_sql_columns(&column_defs, false)?;
2638 let mut col_id_gen = ColumnIdGenerator::new_initial();
2639 for c in &mut columns {
2640 col_id_gen.generate(c)?;
2641 }
2642
2643 let pk_names =
2644 bind_sql_pk_names(&column_defs, bind_table_constraints(&constraints)?)?;
2645 let (_, pk_column_ids, _) =
2646 bind_pk_and_row_id_on_relation(columns, pk_names, true)?;
2647 Ok(pk_column_ids)
2648 })();
2649 match (expected, actual) {
2650 (Ok(expected), Ok(actual)) => assert_eq!(
2651 expected.iter().copied().map(ColumnId::new).collect_vec(),
2652 actual,
2653 "sql: {sql}"
2654 ),
2655 (Ok(_), Err(actual)) => panic!("sql: {sql}\nunexpected error: {actual:?}"),
2656 (Err(_), Ok(actual)) => panic!("sql: {sql}\nexpects error but got: {actual:?}"),
2657 (Err(expected), Err(actual)) => assert!(
2658 actual.to_string().contains(expected),
2659 "sql: {sql}\nexpected: {expected:?}\nactual: {actual:?}"
2660 ),
2661 }
2662 }
2663 }
2664
2665 #[tokio::test]
2666 async fn test_duplicate_props_options() {
2667 let proto_file = create_proto_file(PROTO_FILE_DATA);
2668 let sql = format!(
2669 r#"CREATE TABLE t
2670 WITH (
2671 connector = 'kinesis',
2672 aws.region='user_test_topic',
2673 endpoint='172.10.1.1:9090,172.10.1.2:9090',
2674 aws.credentials.access_key_id = 'your_access_key_1',
2675 aws.credentials.secret_access_key = 'your_secret_key_1'
2676 )
2677 FORMAT PLAIN ENCODE PROTOBUF (
2678 message = '.test.TestRecord',
2679 aws.credentials.access_key_id = 'your_access_key_2',
2680 aws.credentials.secret_access_key = 'your_secret_key_2',
2681 schema.location = 'file://{}',
2682 )"#,
2683 proto_file.path().to_str().unwrap()
2684 );
2685 let frontend = LocalFrontend::new(Default::default()).await;
2686 frontend.run_sql(sql).await.unwrap();
2687
2688 let session = frontend.session_ref();
2689 let catalog_reader = session.env().catalog_reader().read_guard();
2690 let schema_path = SchemaPath::Name(DEFAULT_SCHEMA_NAME);
2691
2692 let (source, _) = catalog_reader
2694 .get_source_by_name(DEFAULT_DATABASE_NAME, schema_path, "t")
2695 .unwrap();
2696 assert_eq!(source.name, "t");
2697
2698 assert_eq!(
2700 source
2701 .info
2702 .format_encode_options
2703 .get("aws.credentials.access_key_id")
2704 .unwrap(),
2705 "your_access_key_2"
2706 );
2707 assert_eq!(
2708 source
2709 .info
2710 .format_encode_options
2711 .get("aws.credentials.secret_access_key")
2712 .unwrap(),
2713 "your_secret_key_2"
2714 );
2715
2716 assert_eq!(
2718 source
2719 .with_properties
2720 .get("aws.credentials.access_key_id")
2721 .unwrap(),
2722 "your_access_key_1"
2723 );
2724 assert_eq!(
2725 source
2726 .with_properties
2727 .get("aws.credentials.secret_access_key")
2728 .unwrap(),
2729 "your_secret_key_1"
2730 );
2731
2732 assert!(!source.with_properties.contains_key("schema.location"));
2734 }
2735}