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(
97 config: &IcebergConfig,
98 param: &SinkParam,
99) -> Result<()> {
100 let catalog = config.create_catalog().await?;
101 let table_id = config
102 .full_table_name()
103 .context("Unable to parse table name")?;
104 let namespace = table_id.namespace().clone();
105 let table_name = table_id.name().to_owned();
106 create_namespace_if_not_exists(catalog.as_ref(), &namespace).await?;
107
108 if !catalog
109 .table_exists(&table_id)
110 .await
111 .map_err(|e| SinkError::Iceberg(anyhow!(e)))?
112 {
113 let iceberg_create_table_arrow_convert = IcebergCreateTableArrowConvert::default();
114 let arrow_fields = param
116 .columns
117 .iter()
118 .map(|column| {
119 Ok(iceberg_create_table_arrow_convert
120 .to_arrow_field(&column.name, &column.data_type)
121 .map_err(|e| SinkError::Iceberg(anyhow!(e)))
122 .context(format!(
123 "failed to convert {}: {} to arrow type",
124 column.name, column.data_type
125 ))?)
126 })
127 .collect::<Result<Vec<ArrowField>>>()?;
128 let arrow_schema = arrow_schema_iceberg::Schema::new(arrow_fields);
129 let iceberg_schema = iceberg::arrow::arrow_schema_to_schema(&arrow_schema)
130 .map_err(|e| SinkError::Iceberg(anyhow!(e)))
131 .context("failed to convert arrow schema to iceberg schema")?;
132
133 let location = {
134 let mut names = namespace.clone().inner();
135 names.push(table_name.clone());
136 match &config.common.warehouse_path {
137 Some(warehouse_path) => {
138 let is_s3_tables = warehouse_path.starts_with("arn:aws:s3tables");
139 let is_bq_catalog_federation = warehouse_path.starts_with("bq://");
141 let url = Url::parse(warehouse_path);
142 if url.is_err() || is_s3_tables || is_bq_catalog_federation {
143 if config
146 .common
147 .is_rest_catalog()
148 .map_err(|err| SinkError::Config(anyhow!(err)))?
149 {
150 None
151 } else {
152 bail!(format!("Invalid warehouse path: {}", warehouse_path))
153 }
154 } else if warehouse_path.ends_with('/') {
155 Some(format!("{}{}", warehouse_path, names.join("/")))
156 } else {
157 Some(format!("{}/{}", warehouse_path, names.join("/")))
158 }
159 }
160 None => None,
161 }
162 };
163
164 let partition_spec = match &config.partition_by {
165 Some(partition_by) => {
166 let mut partition_fields = Vec::<UnboundPartitionField>::new();
167 for (i, (column, transform)) in parse_partition_by_exprs(partition_by.clone())?
168 .into_iter()
169 .enumerate()
170 {
171 match iceberg_schema.field_id_by_name(&column) {
172 Some(id) => partition_fields.push(
173 UnboundPartitionField::builder()
174 .source_id(id)
175 .transform(transform)
176 .name(format!("_p_{}", column))
177 .field_id(PARTITION_DATA_ID_START + i as i32)
178 .build(),
179 ),
180 None => bail!(format!(
181 "Partition source column does not exist in schema: {}",
182 column
183 )),
184 };
185 }
186 Some(
187 UnboundPartitionSpec::builder()
188 .with_spec_id(0)
189 .add_partition_fields(partition_fields)
190 .map_err(|e| SinkError::Iceberg(anyhow!(e)))
191 .context("failed to add partition columns")?
192 .build(),
193 )
194 }
195 None => None,
196 };
197
198 let sort_order = match &config.order_key {
199 Some(order_key) => Some(build_sort_order(order_key, &iceberg_schema)?),
200 None => None,
201 };
202
203 let properties = if matches!(
206 config.catalog_kind()?,
207 IcebergCatalogKind::Glue(IcebergCatalogRuntime::NativeRust)
208 ) {
209 HashMap::new()
210 } else {
211 HashMap::from([(
212 TableProperties::PROPERTY_FORMAT_VERSION.to_owned(),
213 (config.format_version as u8).to_string(),
214 )])
215 };
216
217 let table_creation_builder = TableCreation::builder()
218 .name(table_name)
219 .schema(iceberg_schema)
220 .format_version(config.table_format_version())
221 .properties(properties);
222
223 let table_creation = match (location, partition_spec, sort_order) {
224 (Some(location), Some(partition_spec), Some(sort_order)) => table_creation_builder
225 .location(location)
226 .partition_spec(partition_spec)
227 .sort_order(sort_order)
228 .build(),
229 (Some(location), Some(partition_spec), None) => table_creation_builder
230 .location(location)
231 .partition_spec(partition_spec)
232 .build(),
233 (Some(location), None, Some(sort_order)) => table_creation_builder
234 .location(location)
235 .sort_order(sort_order)
236 .build(),
237 (Some(location), None, None) => table_creation_builder.location(location).build(),
238 (None, Some(partition_spec), Some(sort_order)) => table_creation_builder
239 .partition_spec(partition_spec)
240 .sort_order(sort_order)
241 .build(),
242 (None, Some(partition_spec), None) => table_creation_builder
243 .partition_spec(partition_spec)
244 .build(),
245 (None, None, Some(sort_order)) => table_creation_builder.sort_order(sort_order).build(),
246 (None, None, None) => table_creation_builder.build(),
247 };
248
249 catalog
250 .create_table(&namespace, table_creation)
251 .await
252 .map_err(|e| SinkError::Iceberg(anyhow!(e)))
253 .context("failed to create iceberg table")?;
254 }
255 Ok(())
256}
257
258async fn create_namespace_if_not_exists(
259 catalog: &dyn Catalog,
260 namespace: &NamespaceIdent,
261) -> Result<()> {
262 let mut namespaces = vec![namespace.clone()];
263 let mut parent = namespace.parent();
264 while let Some(parent_namespace) = parent {
265 parent = parent_namespace.parent();
266 namespaces.push(parent_namespace);
267 }
268
269 for namespace in namespaces.into_iter().rev() {
270 if !catalog
271 .namespace_exists(&namespace)
272 .await
273 .map_err(|e| SinkError::Iceberg(anyhow!(e)))?
274 {
275 catalog
276 .create_namespace(&namespace, HashMap::default())
277 .await
278 .map_err(|e| SinkError::Iceberg(anyhow!(e)))
279 .with_context(|| format!("failed to create iceberg namespace: {namespace}"))?;
280 }
281 }
282
283 Ok(())
284}
285
286const MAP_KEY: &str = "key";
287const MAP_VALUE: &str = "value";
288
289fn get_fields<'a>(
290 our_field_type: &'a risingwave_common::types::DataType,
291 data_type: &ArrowDataType,
292 schema_fields: &mut HashMap<&'a str, &'a risingwave_common::types::DataType>,
293) -> Option<ArrowFields> {
294 match data_type {
295 ArrowDataType::Struct(fields) => {
296 match our_field_type {
297 risingwave_common::types::DataType::Struct(struct_fields) => {
298 struct_fields.iter().for_each(|(name, data_type)| {
299 let res = schema_fields.insert(name, data_type);
300 assert!(res.is_none())
302 });
303 }
304 risingwave_common::types::DataType::Map(map_fields) => {
305 schema_fields.insert(MAP_KEY, map_fields.key());
306 schema_fields.insert(MAP_VALUE, map_fields.value());
307 }
308 risingwave_common::types::DataType::List(list) => {
309 list.elem()
310 .as_struct()
311 .iter()
312 .for_each(|(name, data_type)| {
313 let res = schema_fields.insert(name, data_type);
314 assert!(res.is_none())
316 });
317 }
318 _ => {}
319 };
320 Some(fields.clone())
321 }
322 ArrowDataType::List(field) | ArrowDataType::Map(field, _) => {
323 get_fields(our_field_type, field.data_type(), schema_fields)
324 }
325 _ => None, }
327}
328
329fn check_compatibility(
330 schema_fields: HashMap<&str, &risingwave_common::types::DataType>,
331 fields: &ArrowFields,
332) -> anyhow::Result<bool> {
333 for arrow_field in fields {
334 let our_field_type = schema_fields
335 .get(arrow_field.name().as_str())
336 .ok_or_else(|| anyhow!("Field {} not found in our schema", arrow_field.name()))?;
337
338 let converted_arrow_data_type = IcebergArrowConvert
340 .to_arrow_field("", our_field_type)
341 .map_err(|e| anyhow!(e))?
342 .data_type()
343 .clone();
344
345 let compatible = match (&converted_arrow_data_type, arrow_field.data_type()) {
346 (ArrowDataType::Decimal128(_, _), ArrowDataType::Decimal128(_, _)) => true,
347 (ArrowDataType::Binary, ArrowDataType::LargeBinary) => true,
348 (ArrowDataType::LargeBinary, ArrowDataType::Binary) => true,
349 (ArrowDataType::List(_), ArrowDataType::List(field))
350 | (ArrowDataType::Map(_, _), ArrowDataType::Map(field, _)) => {
351 let mut schema_fields = HashMap::new();
352 get_fields(our_field_type, field.data_type(), &mut schema_fields)
353 .is_none_or(|fields| check_compatibility(schema_fields, &fields).unwrap())
354 }
355 (ArrowDataType::Struct(_), ArrowDataType::Struct(fields)) => {
357 let mut schema_fields = HashMap::new();
358 our_field_type
359 .as_struct()
360 .iter()
361 .for_each(|(name, data_type)| {
362 let res = schema_fields.insert(name, data_type);
363 assert!(res.is_none())
365 });
366 check_compatibility(schema_fields, fields)?
367 }
368 (left, right) => left.equals_datatype(right),
376 };
377 if !compatible {
378 bail!(
379 "field {}'s type is incompatible\nRisingWave converted data type: {}\niceberg's data type: {}",
380 arrow_field.name(),
381 converted_arrow_data_type,
382 arrow_field.data_type()
383 );
384 }
385 }
386 Ok(true)
387}
388
389pub fn try_matches_arrow_schema(rw_schema: &Schema, arrow_schema: &ArrowSchema) -> Result<()> {
391 if rw_schema.fields.len() != arrow_schema.fields().len() {
392 bail!(
393 "Schema length mismatch, risingwave is {}, and iceberg is {}",
394 rw_schema.fields.len(),
395 arrow_schema.fields.len()
396 );
397 }
398
399 let mut schema_fields = HashMap::new();
400 rw_schema.fields.iter().for_each(|field| {
401 let res = schema_fields.insert(field.name.as_str(), &field.data_type);
402 assert!(res.is_none())
404 });
405
406 check_compatibility(schema_fields, &arrow_schema.fields)?;
407
408 for (idx, (rw_field, arrow_field)) in rw_schema
411 .fields
412 .iter()
413 .zip_eq_fast(arrow_schema.fields().iter())
414 .enumerate()
415 {
416 if rw_field.name.as_str() != arrow_field.name().as_str() {
417 bail!(
418 "Column order mismatch at position {}: the sink has column `{}` but the \
419 Iceberg table has column `{}`. The Iceberg sink maps columns to the table \
420 by position, so the sink's column order must match the Iceberg table \
421 columns [{}].",
422 idx,
423 rw_field.name,
424 arrow_field.name(),
425 arrow_schema.fields().iter().map(|f| f.name()).join(", "),
426 );
427 }
428 }
429
430 Ok(())
431}
432
433pub fn parse_partition_by_exprs(
434 expr: String,
435) -> std::result::Result<Vec<(String, Transform)>, anyhow::Error> {
436 let re = Regex::new(r"(?<transform>\w+)(\(((?<n>\d+)?(?:,|(,\s)))?(?<field>\w+)\))?").unwrap();
438 if !re.is_match(&expr) {
439 bail!(format!(
440 "Invalid partition fields: {}\nHINT: Supported formats are column, transform(column), transform(n,column), transform(n, column)",
441 expr
442 ))
443 }
444 let caps = re.captures_iter(&expr);
445
446 let mut partition_columns = vec![];
447
448 for mat in caps {
449 let (column, transform) = if mat.name("n").is_none() && mat.name("field").is_none() {
450 (&mat["transform"], Transform::Identity)
451 } else {
452 let mut func = mat["transform"].to_owned();
453 if func == "bucket" || func == "truncate" {
454 let n = &mat
455 .name("n")
456 .ok_or_else(|| anyhow!("The `n` must be set with `bucket` and `truncate`"))?
457 .as_str();
458 func = format!("{func}[{n}]");
459 }
460 (
461 &mat["field"],
462 Transform::from_str(&func)
463 .with_context(|| format!("invalid transform function {}", func))?,
464 )
465 };
466 partition_columns.push((column.to_owned(), transform));
467 }
468 Ok(partition_columns)
469}
470
471pub fn parse_order_key_exprs(
472 expr: String,
473) -> std::result::Result<Vec<IcebergOrderKeyField>, anyhow::Error> {
474 let mut order_keys = Vec::new();
475 let mut seen_columns = std::collections::HashSet::new();
476
477 for raw_item in expr.split(',') {
478 let item = raw_item.trim();
479 if item.is_empty() {
480 bail!("Invalid order key: empty item in `{expr}`");
481 }
482
483 let tokens = item.split_whitespace().collect_vec();
484 if tokens.is_empty() {
485 bail!("Invalid order key item `{item}`");
486 }
487 if tokens.len() > 4 {
488 bail!(
489 "Invalid order key item `{item}`\nHINT: Supported format is `column [asc|desc] [nulls first|last]`"
490 );
491 }
492
493 let column = tokens[0];
494 if !ORDER_KEY_COLUMN_RE.is_match(column) {
495 bail!(
496 "Invalid order key column `{column}`\nHINT: Only plain column names are supported in order_key"
497 );
498 }
499 if !seen_columns.insert(column.to_ascii_lowercase()) {
500 bail!("Duplicate column `{column}` in order_key");
501 }
502
503 let mut direction = SortDirection::Ascending;
504 let mut null_order = None;
505 let mut idx = 1;
506 while idx < tokens.len() {
507 match tokens[idx].to_ascii_lowercase().as_str() {
508 "asc" => {
509 direction = SortDirection::Ascending;
510 idx += 1;
511 }
512 "desc" => {
513 direction = SortDirection::Descending;
514 idx += 1;
515 }
516 "nulls" => {
517 let order = tokens.get(idx + 1).ok_or_else(|| {
518 anyhow!(
519 "Invalid order key item `{item}`: `NULLS` must be followed by `FIRST` or `LAST`"
520 )
521 })?;
522 null_order = Some(match order.to_ascii_lowercase().as_str() {
523 "first" => NullOrder::First,
524 "last" => NullOrder::Last,
525 _ => bail!(
526 "Invalid order key item `{item}`\nHINT: `NULLS` must be followed by `FIRST` or `LAST`"
527 ),
528 });
529 idx += 2;
530 }
531 token => {
532 bail!(
533 "Invalid order key token `{token}` in `{item}`\nHINT: Supported format is `column [asc|desc] [nulls first|last]`"
534 );
535 }
536 }
537 }
538
539 order_keys.push(IcebergOrderKeyField {
540 column: column.to_owned(),
541 direction,
542 null_order: null_order
543 .unwrap_or_else(|| IcebergOrderKeyField::default_null_order(direction)),
544 });
545 }
546
547 if order_keys.is_empty() {
548 bail!("order_key must not be empty");
549 }
550
551 Ok(order_keys)
552}
553
554pub fn validate_order_key_columns<'a>(
555 order_key: &str,
556 columns: impl IntoIterator<Item = &'a str>,
557) -> std::result::Result<Vec<IcebergOrderKeyField>, anyhow::Error> {
558 let parsed = parse_order_key_exprs(order_key.to_owned())?;
559 let columns = columns
560 .into_iter()
561 .map(|column| column.to_ascii_lowercase())
562 .collect::<std::collections::HashSet<_>>();
563 for item in &parsed {
564 if item.column.starts_with('_') {
565 bail!(
566 "System column `{}` is not allowed in order_key",
567 item.column
568 );
569 }
570 if !columns.contains(&item.column.to_ascii_lowercase()) {
571 bail!("Order key column does not exist in schema: {}", item.column);
572 }
573 }
574 Ok(parsed)
575}
576
577fn build_sort_order(order_key: &str, schema: &iceberg::spec::Schema) -> Result<SortOrder> {
578 let order_fields = validate_order_key_columns(
579 order_key,
580 schema
581 .as_struct()
582 .fields()
583 .iter()
584 .map(|field| field.name.as_str()),
585 )?;
586 let mut builder = SortOrder::builder();
587 for field in order_fields {
588 let source_id = schema.field_id_by_name(&field.column).ok_or_else(|| {
589 anyhow!(
590 "Order key column does not exist in schema: {}",
591 field.column
592 )
593 })?;
594 builder.with_sort_field(
595 SortField::builder()
596 .source_id(source_id)
597 .transform(Transform::Identity)
598 .direction(field.direction)
599 .null_order(field.null_order)
600 .build(),
601 );
602 }
603 builder
604 .build(schema)
605 .map_err(|e| SinkError::Iceberg(anyhow!(e)))
606}