1use std::collections::HashMap;
16use std::str::FromStr;
17use std::sync::LazyLock;
18
19use anyhow::{Context, anyhow};
20use iceberg::arrow::schema_to_arrow_schema;
21use iceberg::spec::{
22 FormatVersion, NullOrder, SortDirection, SortField, SortOrder, TableProperties, Transform,
23 UnboundPartitionField, UnboundPartitionSpec,
24};
25use iceberg::table::Table;
26use iceberg::{Catalog, NamespaceIdent, TableCreation};
27use itertools::Itertools;
28use regex::Regex;
29use risingwave_common::array::arrow::arrow_schema_iceberg::{
30 self, DataType as ArrowDataType, Field as ArrowField, Fields as ArrowFields,
31 Schema as ArrowSchema,
32};
33use risingwave_common::array::arrow::{IcebergArrowConvert, IcebergCreateTableArrowConvert};
34use risingwave_common::bail;
35use risingwave_common::catalog::Schema;
36use risingwave_common::util::iter_util::ZipEqFast;
37use url::Url;
38
39use super::{IcebergConfig, PARTITION_DATA_ID_START, SinkError};
40use crate::connector_common::{IcebergCatalogKind, IcebergCatalogRuntime};
41use crate::sink::{Result, SinkParam};
42
43static ORDER_KEY_COLUMN_RE: LazyLock<Regex> =
44 LazyLock::new(|| Regex::new(r"^[A-Za-z_][A-Za-z0-9_]*$").expect("valid order key regex"));
45
46#[derive(Debug, Clone, PartialEq, Eq)]
47pub struct IcebergOrderKeyField {
48 pub column: String,
49 pub direction: SortDirection,
50 pub null_order: NullOrder,
51}
52
53impl IcebergOrderKeyField {
54 fn default_null_order(direction: SortDirection) -> NullOrder {
55 match direction {
56 SortDirection::Ascending => NullOrder::First,
57 SortDirection::Descending => NullOrder::Last,
58 }
59 }
60}
61
62pub async fn create_and_validate_table_impl(
63 config: &IcebergConfig,
64 param: &SinkParam,
65) -> Result<Table> {
66 if config.create_table_if_not_exists {
67 create_table_if_not_exists_impl(config, param).await?;
68 }
69
70 let table = config
71 .load_table()
72 .await
73 .map_err(|err| SinkError::Iceberg(anyhow!(err)))?;
74
75 if config.enable_pk_index {
76 let table_format_version = table.metadata().format_version();
77 if table_format_version < FormatVersion::V2 {
78 return Err(SinkError::Config(anyhow!(
79 "`enable_pk_index` requires an Iceberg table with format version >= 2, \
80 but the target table is format version {}",
81 table_format_version
82 )));
83 }
84 }
85
86 let sink_schema = param.schema();
87 let iceberg_arrow_schema = schema_to_arrow_schema(table.metadata().current_schema())
88 .map_err(|err| SinkError::Iceberg(anyhow!(err)))?;
89
90 try_matches_arrow_schema(&sink_schema, &iceberg_arrow_schema)
91 .map_err(|err| SinkError::Iceberg(anyhow!(err)))?;
92
93 Ok(table)
94}
95
96pub(super) async fn create_table_if_not_exists_impl(
98 config: &IcebergConfig,
99 param: &SinkParam,
100) -> Result<bool> {
101 let catalog = config.create_catalog().await?;
102 let table_id = config
103 .full_table_name()
104 .context("Unable to parse table name")?;
105 let namespace = table_id.namespace().clone();
106 let table_name = table_id.name().to_owned();
107 create_namespace_if_not_exists(catalog.as_ref(), &namespace).await?;
108
109 if catalog
110 .table_exists(&table_id)
111 .await
112 .map_err(|e| SinkError::Iceberg(anyhow!(e)))?
113 {
114 return Ok(false);
115 }
116
117 if config.table_format_version() < FormatVersion::V3
118 && let Some(column) = param
119 .columns
120 .iter()
121 .find(|column| column.data_type.contains_variant())
122 {
123 return Err(SinkError::Config(anyhow!(
124 "creating an Iceberg table with VARIANT column `{}` requires `format_version = '3'`",
125 column.name
126 )));
127 }
128
129 let iceberg_create_table_arrow_convert = IcebergCreateTableArrowConvert::default();
130 let arrow_fields = param
132 .columns
133 .iter()
134 .map(|column| {
135 Ok(iceberg_create_table_arrow_convert
136 .to_arrow_field(&column.name, &column.data_type)
137 .map_err(|e| SinkError::Iceberg(anyhow!(e)))
138 .context(format!(
139 "failed to convert {}: {} to arrow type",
140 column.name, column.data_type
141 ))?)
142 })
143 .collect::<Result<Vec<ArrowField>>>()?;
144 let arrow_schema = arrow_schema_iceberg::Schema::new(arrow_fields);
145 let iceberg_schema = iceberg::arrow::arrow_schema_to_schema(&arrow_schema)
146 .map_err(|e| SinkError::Iceberg(anyhow!(e)))
147 .context("failed to convert arrow schema to iceberg schema")?;
148
149 let location = {
150 let mut names = namespace.clone().inner();
151 names.push(table_name.clone());
152 match &config.common.warehouse_path {
153 Some(warehouse_path) => {
154 let is_s3_tables = warehouse_path.starts_with("arn:aws:s3tables");
155 let is_bq_catalog_federation = warehouse_path.starts_with("bq://");
157 let url = Url::parse(warehouse_path);
158 if url.is_err() || is_s3_tables || is_bq_catalog_federation {
159 if config
162 .common
163 .is_rest_catalog()
164 .map_err(|err| SinkError::Config(anyhow!(err)))?
165 {
166 None
167 } else {
168 bail!(format!("Invalid warehouse path: {}", warehouse_path))
169 }
170 } else if warehouse_path.ends_with('/') {
171 Some(format!("{}{}", warehouse_path, names.join("/")))
172 } else {
173 Some(format!("{}/{}", warehouse_path, names.join("/")))
174 }
175 }
176 None => None,
177 }
178 };
179
180 let partition_spec = match &config.partition_by {
181 Some(partition_by) => {
182 let mut partition_fields = Vec::<UnboundPartitionField>::new();
183 for (i, (column, transform)) in parse_partition_by_exprs(partition_by.clone())?
184 .into_iter()
185 .enumerate()
186 {
187 match iceberg_schema.field_id_by_name(&column) {
188 Some(id) => partition_fields.push(
189 UnboundPartitionField::builder()
190 .source_id(id)
191 .transform(transform)
192 .name(format!("_p_{}", column))
193 .field_id(PARTITION_DATA_ID_START + i as i32)
194 .build(),
195 ),
196 None => bail!(format!(
197 "Partition source column does not exist in schema: {}",
198 column
199 )),
200 };
201 }
202 Some(
203 UnboundPartitionSpec::builder()
204 .with_spec_id(0)
205 .add_partition_fields(partition_fields)
206 .map_err(|e| SinkError::Iceberg(anyhow!(e)))
207 .context("failed to add partition columns")?
208 .build(),
209 )
210 }
211 None => None,
212 };
213
214 let sort_order = match &config.order_key {
215 Some(order_key) => Some(build_sort_order(order_key, &iceberg_schema)?),
216 None => None,
217 };
218
219 let properties = if matches!(
222 config.catalog_kind()?,
223 IcebergCatalogKind::Glue(IcebergCatalogRuntime::NativeRust)
224 ) {
225 HashMap::new()
226 } else {
227 HashMap::from([(
228 TableProperties::PROPERTY_FORMAT_VERSION.to_owned(),
229 (config.format_version as u8).to_string(),
230 )])
231 };
232
233 let table_creation_builder = TableCreation::builder()
234 .name(table_name)
235 .schema(iceberg_schema)
236 .format_version(config.table_format_version())
237 .properties(properties);
238
239 let table_creation = match (location, partition_spec, sort_order) {
240 (Some(location), Some(partition_spec), Some(sort_order)) => table_creation_builder
241 .location(location)
242 .partition_spec(partition_spec)
243 .sort_order(sort_order)
244 .build(),
245 (Some(location), Some(partition_spec), None) => table_creation_builder
246 .location(location)
247 .partition_spec(partition_spec)
248 .build(),
249 (Some(location), None, Some(sort_order)) => table_creation_builder
250 .location(location)
251 .sort_order(sort_order)
252 .build(),
253 (Some(location), None, None) => table_creation_builder.location(location).build(),
254 (None, Some(partition_spec), Some(sort_order)) => table_creation_builder
255 .partition_spec(partition_spec)
256 .sort_order(sort_order)
257 .build(),
258 (None, Some(partition_spec), None) => table_creation_builder
259 .partition_spec(partition_spec)
260 .build(),
261 (None, None, Some(sort_order)) => table_creation_builder.sort_order(sort_order).build(),
262 (None, None, None) => table_creation_builder.build(),
263 };
264
265 catalog
266 .create_table(&namespace, table_creation)
267 .await
268 .map_err(|e| SinkError::Iceberg(anyhow!(e)))
269 .context("failed to create iceberg table")?;
270 Ok(true)
271}
272
273async fn create_namespace_if_not_exists(
274 catalog: &dyn Catalog,
275 namespace: &NamespaceIdent,
276) -> Result<()> {
277 let mut namespaces = vec![namespace.clone()];
278 let mut parent = namespace.parent();
279 while let Some(parent_namespace) = parent {
280 parent = parent_namespace.parent();
281 namespaces.push(parent_namespace);
282 }
283
284 for namespace in namespaces.into_iter().rev() {
285 if !catalog
286 .namespace_exists(&namespace)
287 .await
288 .map_err(|e| SinkError::Iceberg(anyhow!(e)))?
289 {
290 catalog
291 .create_namespace(&namespace, HashMap::default())
292 .await
293 .map_err(|e| SinkError::Iceberg(anyhow!(e)))
294 .with_context(|| format!("failed to create iceberg namespace: {namespace}"))?;
295 }
296 }
297
298 Ok(())
299}
300
301const MAP_KEY: &str = "key";
302const MAP_VALUE: &str = "value";
303
304fn field_is_compatible(
305 rw_type: &risingwave_common::types::DataType,
306 arrow_field: &ArrowField,
307) -> anyhow::Result<bool> {
308 use risingwave_common::types::DataType as RwDataType;
309
310 let arrow_is_variant = matches!(
314 IcebergArrowConvert.type_from_field(arrow_field),
315 Ok(RwDataType::Variant)
316 );
317 if matches!(rw_type, RwDataType::Variant) || arrow_is_variant {
318 return Ok(matches!(rw_type, RwDataType::Variant) && arrow_is_variant);
319 }
320
321 let converted_arrow_data_type = IcebergArrowConvert
322 .to_arrow_field("", rw_type)
323 .map_err(|e| anyhow!(e))?
324 .data_type()
325 .clone();
326
327 match (rw_type, &converted_arrow_data_type, arrow_field.data_type()) {
328 (_, ArrowDataType::Decimal128(_, _), ArrowDataType::Decimal128(_, _)) => Ok(true),
329 (_, ArrowDataType::Binary, ArrowDataType::LargeBinary)
330 | (_, ArrowDataType::LargeBinary, ArrowDataType::Binary) => Ok(true),
331 (RwDataType::List(list), ArrowDataType::List(_), ArrowDataType::List(element)) => {
332 field_is_compatible(list.elem(), element)
333 }
334 (RwDataType::Map(map), ArrowDataType::Map(_, _), ArrowDataType::Map(entries, _)) => {
335 let ArrowDataType::Struct(fields) = entries.data_type() else {
336 return Ok(false);
337 };
338 let key = fields.iter().find(|field| field.name() == MAP_KEY);
339 let value = fields.iter().find(|field| field.name() == MAP_VALUE);
340 match (key, value) {
341 (Some(key), Some(value)) => Ok(field_is_compatible(map.key(), key)?
342 && field_is_compatible(map.value(), value)?),
343 _ => Ok(false),
344 }
345 }
346 (
347 RwDataType::Struct(rw_fields),
348 ArrowDataType::Struct(_),
349 ArrowDataType::Struct(arrow_fields),
350 ) => {
351 if rw_fields.len() != arrow_fields.len() {
352 return Ok(false);
353 }
354 for ((rw_name, rw_type), arrow_field) in rw_fields.iter().zip_eq_fast(arrow_fields) {
357 if rw_name != arrow_field.name().as_str() {
358 return Ok(false);
359 }
360 if !field_is_compatible(rw_type, arrow_field)? {
361 return Ok(false);
362 }
363 }
364 Ok(true)
365 }
366 (_, left, right) => Ok(left.equals_datatype(right)),
367 }
368}
369
370fn check_compatibility(
371 schema_fields: HashMap<&str, &risingwave_common::types::DataType>,
372 fields: &ArrowFields,
373) -> anyhow::Result<bool> {
374 for arrow_field in fields {
375 let our_field_type = schema_fields
376 .get(arrow_field.name().as_str())
377 .ok_or_else(|| anyhow!("Field {} not found in our schema", arrow_field.name()))?;
378
379 if !field_is_compatible(our_field_type, arrow_field)? {
380 let converted_arrow_data_type = IcebergArrowConvert
381 .to_arrow_field("", our_field_type)
382 .map_err(|e| anyhow!(e))?
383 .data_type()
384 .clone();
385 bail!(
386 "field {}'s type is incompatible\nRisingWave converted data type: {}\niceberg's data type: {}",
387 arrow_field.name(),
388 converted_arrow_data_type,
389 arrow_field.data_type()
390 );
391 }
392 }
393 Ok(true)
394}
395
396pub fn try_matches_arrow_schema(rw_schema: &Schema, arrow_schema: &ArrowSchema) -> Result<()> {
398 if rw_schema.fields.len() != arrow_schema.fields().len() {
399 bail!(
400 "Schema length mismatch, risingwave is {}, and iceberg is {}",
401 rw_schema.fields.len(),
402 arrow_schema.fields.len()
403 );
404 }
405
406 let mut schema_fields = HashMap::new();
407 rw_schema.fields.iter().for_each(|field| {
408 let res = schema_fields.insert(field.name.as_str(), &field.data_type);
409 assert!(res.is_none())
411 });
412
413 check_compatibility(schema_fields, &arrow_schema.fields)?;
414
415 for (idx, (rw_field, arrow_field)) in rw_schema
418 .fields
419 .iter()
420 .zip_eq_fast(arrow_schema.fields().iter())
421 .enumerate()
422 {
423 if rw_field.name.as_str() != arrow_field.name().as_str() {
424 bail!(
425 "Column order mismatch at position {}: the sink has column `{}` but the \
426 Iceberg table has column `{}`. The Iceberg sink maps columns to the table \
427 by position, so the sink's column order must match the Iceberg table \
428 columns [{}].",
429 idx,
430 rw_field.name,
431 arrow_field.name(),
432 arrow_schema.fields().iter().map(|f| f.name()).join(", "),
433 );
434 }
435 }
436
437 Ok(())
438}
439
440pub fn parse_partition_by_exprs(
441 expr: String,
442) -> std::result::Result<Vec<(String, Transform)>, anyhow::Error> {
443 let re = Regex::new(r"(?<transform>\w+)(\(((?<n>\d+)?(?:,|(,\s)))?(?<field>\w+)\))?").unwrap();
445 if !re.is_match(&expr) {
446 bail!(format!(
447 "Invalid partition fields: {}\nHINT: Supported formats are column, transform(column), transform(n,column), transform(n, column)",
448 expr
449 ))
450 }
451 let caps = re.captures_iter(&expr);
452
453 let mut partition_columns = vec![];
454
455 for mat in caps {
456 let (column, transform) = if mat.name("n").is_none() && mat.name("field").is_none() {
457 (&mat["transform"], Transform::Identity)
458 } else {
459 let mut func = mat["transform"].to_owned();
460 if func == "bucket" || func == "truncate" {
461 let n = &mat
462 .name("n")
463 .ok_or_else(|| anyhow!("The `n` must be set with `bucket` and `truncate`"))?
464 .as_str();
465 func = format!("{func}[{n}]");
466 }
467 (
468 &mat["field"],
469 Transform::from_str(&func)
470 .with_context(|| format!("invalid transform function {}", func))?,
471 )
472 };
473 partition_columns.push((column.to_owned(), transform));
474 }
475 Ok(partition_columns)
476}
477
478pub fn parse_order_key_exprs(
479 expr: String,
480) -> std::result::Result<Vec<IcebergOrderKeyField>, anyhow::Error> {
481 let mut order_keys = Vec::new();
482 let mut seen_columns = std::collections::HashSet::new();
483
484 for raw_item in expr.split(',') {
485 let item = raw_item.trim();
486 if item.is_empty() {
487 bail!("Invalid order key: empty item in `{expr}`");
488 }
489
490 let tokens = item.split_whitespace().collect_vec();
491 if tokens.is_empty() {
492 bail!("Invalid order key item `{item}`");
493 }
494 if tokens.len() > 4 {
495 bail!(
496 "Invalid order key item `{item}`\nHINT: Supported format is `column [asc|desc] [nulls first|last]`"
497 );
498 }
499
500 let column = tokens[0];
501 if !ORDER_KEY_COLUMN_RE.is_match(column) {
502 bail!(
503 "Invalid order key column `{column}`\nHINT: Only plain column names are supported in order_key"
504 );
505 }
506 if !seen_columns.insert(column.to_ascii_lowercase()) {
507 bail!("Duplicate column `{column}` in order_key");
508 }
509
510 let mut direction = SortDirection::Ascending;
511 let mut null_order = None;
512 let mut idx = 1;
513 while idx < tokens.len() {
514 match tokens[idx].to_ascii_lowercase().as_str() {
515 "asc" => {
516 direction = SortDirection::Ascending;
517 idx += 1;
518 }
519 "desc" => {
520 direction = SortDirection::Descending;
521 idx += 1;
522 }
523 "nulls" => {
524 let order = tokens.get(idx + 1).ok_or_else(|| {
525 anyhow!(
526 "Invalid order key item `{item}`: `NULLS` must be followed by `FIRST` or `LAST`"
527 )
528 })?;
529 null_order = Some(match order.to_ascii_lowercase().as_str() {
530 "first" => NullOrder::First,
531 "last" => NullOrder::Last,
532 _ => bail!(
533 "Invalid order key item `{item}`\nHINT: `NULLS` must be followed by `FIRST` or `LAST`"
534 ),
535 });
536 idx += 2;
537 }
538 token => {
539 bail!(
540 "Invalid order key token `{token}` in `{item}`\nHINT: Supported format is `column [asc|desc] [nulls first|last]`"
541 );
542 }
543 }
544 }
545
546 order_keys.push(IcebergOrderKeyField {
547 column: column.to_owned(),
548 direction,
549 null_order: null_order
550 .unwrap_or_else(|| IcebergOrderKeyField::default_null_order(direction)),
551 });
552 }
553
554 if order_keys.is_empty() {
555 bail!("order_key must not be empty");
556 }
557
558 Ok(order_keys)
559}
560
561pub fn validate_order_key_columns<'a>(
562 order_key: &str,
563 columns: impl IntoIterator<Item = &'a str>,
564) -> std::result::Result<Vec<IcebergOrderKeyField>, anyhow::Error> {
565 let parsed = parse_order_key_exprs(order_key.to_owned())?;
566 let columns = columns
567 .into_iter()
568 .map(|column| column.to_ascii_lowercase())
569 .collect::<std::collections::HashSet<_>>();
570 for item in &parsed {
571 if item.column.starts_with('_') {
572 bail!(
573 "System column `{}` is not allowed in order_key",
574 item.column
575 );
576 }
577 if !columns.contains(&item.column.to_ascii_lowercase()) {
578 bail!("Order key column does not exist in schema: {}", item.column);
579 }
580 }
581 Ok(parsed)
582}
583
584fn build_sort_order(order_key: &str, schema: &iceberg::spec::Schema) -> Result<SortOrder> {
585 let order_fields = validate_order_key_columns(
586 order_key,
587 schema
588 .as_struct()
589 .fields()
590 .iter()
591 .map(|field| field.name.as_str()),
592 )?;
593 let mut builder = SortOrder::builder();
594 for field in order_fields {
595 let source_id = schema.field_id_by_name(&field.column).ok_or_else(|| {
596 anyhow!(
597 "Order key column does not exist in schema: {}",
598 field.column
599 )
600 })?;
601 builder.with_sort_field(
602 SortField::builder()
603 .source_id(source_id)
604 .transform(Transform::Identity)
605 .direction(field.direction)
606 .null_order(field.null_order)
607 .build(),
608 );
609 }
610 builder
611 .build(schema)
612 .map_err(|e| SinkError::Iceberg(anyhow!(e)))
613}