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