This is an automated email from the ASF dual-hosted git repository.

laskoviymishka pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/iceberg.git


The following commit(s) were added to refs/heads/main by this push:
     new 561a4e326f Kafka Connect: Split route field path once instead of per 
record (#16655)
561a4e326f is described below

commit 561a4e326fe77704e1e3e1d09507f121491a6b54
Author: Vova Kolmakov <[email protected]>
AuthorDate: Mon Oct 12 16:55:53 2026 +0700

    Kafka Connect: Split route field path once instead of per record (#16655)
    
    Co-authored-by: Vova Kolmakov <[email protected]>
    Co-authored-by: Claude Opus 4.8 (1M context) <[email protected]>
---
 .../apache/iceberg/connect/data/RecordUtils.java   |  4 +--
 .../apache/iceberg/connect/data/SinkWriter.java    | 19 +++++++------
 .../iceberg/connect/data/TestRecordUtils.java      | 32 +++++++++++++++-------
 .../iceberg/connect/data/TestSinkWriter.java       | 16 +++++++++++
 4 files changed, 49 insertions(+), 22 deletions(-)

diff --git 
a/kafka-connect/kafka-connect/src/main/java/org/apache/iceberg/connect/data/RecordUtils.java
 
b/kafka-connect/kafka-connect/src/main/java/org/apache/iceberg/connect/data/RecordUtils.java
index 3d8f2adf7b..4ad08dd672 100644
--- 
a/kafka-connect/kafka-connect/src/main/java/org/apache/iceberg/connect/data/RecordUtils.java
+++ 
b/kafka-connect/kafka-connect/src/main/java/org/apache/iceberg/connect/data/RecordUtils.java
@@ -35,7 +35,6 @@ import org.apache.iceberg.io.OutputFileFactory;
 import org.apache.iceberg.io.TaskWriter;
 import org.apache.iceberg.io.UnpartitionedWriter;
 import org.apache.iceberg.relocated.com.google.common.base.Preconditions;
-import org.apache.iceberg.relocated.com.google.common.base.Splitter;
 import org.apache.iceberg.relocated.com.google.common.collect.Maps;
 import org.apache.iceberg.relocated.com.google.common.collect.Sets;
 import org.apache.iceberg.relocated.com.google.common.primitives.Ints;
@@ -49,8 +48,7 @@ class RecordUtils {
 
   @SuppressWarnings("unchecked")
   static Object extractFromRecordValue(
-      Object recordValue, String fieldName, IcebergSinkConfig config) {
-    List<String> fields = Splitter.on('.').splitToList(fieldName);
+      Object recordValue, List<String> fields, IcebergSinkConfig config) {
     if (recordValue instanceof Struct) {
       return valueFromStruct((Struct) recordValue, fields, 
config.replaceNullWithDefault());
     } else if (recordValue instanceof Map) {
diff --git 
a/kafka-connect/kafka-connect/src/main/java/org/apache/iceberg/connect/data/SinkWriter.java
 
b/kafka-connect/kafka-connect/src/main/java/org/apache/iceberg/connect/data/SinkWriter.java
index 97e14f8b7e..52ead20f8d 100644
--- 
a/kafka-connect/kafka-connect/src/main/java/org/apache/iceberg/connect/data/SinkWriter.java
+++ 
b/kafka-connect/kafka-connect/src/main/java/org/apache/iceberg/connect/data/SinkWriter.java
@@ -30,6 +30,7 @@ import java.util.stream.Collectors;
 import org.apache.iceberg.catalog.Catalog;
 import org.apache.iceberg.connect.IcebergSinkConfig;
 import org.apache.iceberg.relocated.com.google.common.base.Preconditions;
+import org.apache.iceberg.relocated.com.google.common.base.Splitter;
 import org.apache.iceberg.relocated.com.google.common.collect.Maps;
 import org.apache.kafka.common.TopicPartition;
 import org.apache.kafka.connect.sink.SinkRecord;
@@ -39,12 +40,15 @@ public class SinkWriter {
   private final IcebergWriterFactory writerFactory;
   private final Map<String, RecordWriter> writers;
   private final Map<TopicPartition, Offset> sourceOffsets;
+  private final List<String> routeFieldPath;
 
   public SinkWriter(Catalog catalog, IcebergSinkConfig config) {
     this.config = config;
     this.writerFactory = new IcebergWriterFactory(catalog, config);
     this.writers = Maps.newHashMap();
     this.sourceOffsets = Maps.newHashMap();
+    String routeField = config.tablesRouteField();
+    this.routeFieldPath = routeField == null ? null : 
Splitter.on('.').splitToList(routeField);
   }
 
   public void close() {
@@ -90,9 +94,7 @@ public class SinkWriter {
   }
 
   private void routeRecordStatically(SinkRecord record) {
-    String routeField = config.tablesRouteField();
-
-    if (routeField == null) {
+    if (routeFieldPath == null) {
       // route to all tables
       config
           .tables()
@@ -102,7 +104,7 @@ public class SinkWriter {
               });
 
     } else {
-      String routeValue = extractRouteValue(record.value(), routeField);
+      String routeValue = extractRouteValue(record.value());
       if (routeValue != null) {
         config
             .tables()
@@ -118,21 +120,20 @@ public class SinkWriter {
   }
 
   private void routeRecordDynamically(SinkRecord record) {
-    String routeField = config.tablesRouteField();
-    Preconditions.checkNotNull(routeField, "Route field cannot be null with 
dynamic routing");
+    Preconditions.checkNotNull(routeFieldPath, "Route field cannot be null 
with dynamic routing");
 
-    String routeValue = extractRouteValue(record.value(), routeField);
+    String routeValue = extractRouteValue(record.value());
     if (routeValue != null) {
       String tableName = routeValue.toLowerCase(Locale.ROOT);
       writerForTable(tableName, record, true).write(record);
     }
   }
 
-  private String extractRouteValue(Object recordValue, String routeField) {
+  private String extractRouteValue(Object recordValue) {
     if (recordValue == null) {
       return null;
     }
-    Object routeValue = RecordUtils.extractFromRecordValue(recordValue, 
routeField, config);
+    Object routeValue = RecordUtils.extractFromRecordValue(recordValue, 
routeFieldPath, config);
     return routeValue == null ? null : routeValue.toString();
   }
 
diff --git 
a/kafka-connect/kafka-connect/src/test/java/org/apache/iceberg/connect/data/TestRecordUtils.java
 
b/kafka-connect/kafka-connect/src/test/java/org/apache/iceberg/connect/data/TestRecordUtils.java
index 413b90dde4..b8414e1afe 100644
--- 
a/kafka-connect/kafka-connect/src/test/java/org/apache/iceberg/connect/data/TestRecordUtils.java
+++ 
b/kafka-connect/kafka-connect/src/test/java/org/apache/iceberg/connect/data/TestRecordUtils.java
@@ -28,6 +28,7 @@ import java.util.UUID;
 import org.apache.iceberg.catalog.TableIdentifier;
 import org.apache.iceberg.connect.IcebergSinkConfig;
 import org.apache.iceberg.connect.events.TableReference;
+import org.apache.iceberg.relocated.com.google.common.base.Splitter;
 import org.apache.iceberg.relocated.com.google.common.collect.ImmutableMap;
 import org.apache.kafka.connect.data.Schema;
 import org.apache.kafka.connect.data.SchemaBuilder;
@@ -51,7 +52,8 @@ public class TestRecordUtils extends WriterTestBase {
   public void testExtractFromRecordValueStruct() {
     Schema valSchema = SchemaBuilder.struct().field("key", 
Schema.INT64_SCHEMA).build();
     Struct val = new Struct(valSchema).put("key", 123L);
-    Object result = RecordUtils.extractFromRecordValue(val, "key", config);
+    Object result =
+        RecordUtils.extractFromRecordValue(val, 
Splitter.on('.').splitToList("key"), config);
     assertThat(result).isEqualTo(123L);
   }
 
@@ -65,7 +67,9 @@ public class TestRecordUtils extends WriterTestBase {
     Struct data = new Struct(dataSchema).put("id", id);
     Struct val = new Struct(valSchema).put("data", data);
 
-    Object result = RecordUtils.extractFromRecordValue(val, "data.id.key", 
config);
+    Object result =
+        RecordUtils.extractFromRecordValue(
+            val, Splitter.on('.').splitToList("data.id.key"), config);
     assertThat(result).isEqualTo(123L);
   }
 
@@ -74,10 +78,11 @@ public class TestRecordUtils extends WriterTestBase {
     Schema valSchema = SchemaBuilder.struct().field("key", 
Schema.INT64_SCHEMA).build();
     Struct val = new Struct(valSchema).put("key", 123L);
 
-    Object result = RecordUtils.extractFromRecordValue(val, "", config);
+    Object result =
+        RecordUtils.extractFromRecordValue(val, 
Splitter.on('.').splitToList(""), config);
     assertThat(result).isNull();
 
-    result = RecordUtils.extractFromRecordValue(val, "xkey", config);
+    result = RecordUtils.extractFromRecordValue(val, 
Splitter.on('.').splitToList("xkey"), config);
     assertThat(result).isNull();
   }
 
@@ -93,7 +98,8 @@ public class TestRecordUtils extends WriterTestBase {
             .build();
     Struct val = new Struct(valSchema).put("key", null);
 
-    Object result = RecordUtils.extractFromRecordValue(val, "key", config);
+    Object result =
+        RecordUtils.extractFromRecordValue(val, 
Splitter.on('.').splitToList("key"), config);
     if (replaceNullWithDefault) {
       assertThat(result).isEqualTo("default");
     } else {
@@ -118,7 +124,9 @@ public class TestRecordUtils extends WriterTestBase {
     Struct data = new Struct(dataSchema).put("id", id);
     Struct val = new Struct(valSchema).put("data", data);
 
-    Object result = RecordUtils.extractFromRecordValue(val, "data.id.key", 
config);
+    Object result =
+        RecordUtils.extractFromRecordValue(
+            val, Splitter.on('.').splitToList("data.id.key"), config);
     if (replaceNullWithDefault) {
       assertThat(result).isEqualTo(123L);
     } else {
@@ -129,7 +137,8 @@ public class TestRecordUtils extends WriterTestBase {
   @Test
   public void testExtractFromRecordValueMap() {
     Map<String, Object> val = ImmutableMap.of("key", 123L);
-    Object result = RecordUtils.extractFromRecordValue(val, "key", config);
+    Object result =
+        RecordUtils.extractFromRecordValue(val, 
Splitter.on('.').splitToList("key"), config);
     assertThat(result).isEqualTo(123L);
   }
 
@@ -139,7 +148,9 @@ public class TestRecordUtils extends WriterTestBase {
     Map<String, Object> data = ImmutableMap.of("id", id);
     Map<String, Object> val = ImmutableMap.of("data", data);
 
-    Object result = RecordUtils.extractFromRecordValue(val, "data.id.key", 
config);
+    Object result =
+        RecordUtils.extractFromRecordValue(
+            val, Splitter.on('.').splitToList("data.id.key"), config);
     assertThat(result).isEqualTo(123L);
   }
 
@@ -147,10 +158,11 @@ public class TestRecordUtils extends WriterTestBase {
   public void testExtractFromRecordValueMapNull() {
     Map<String, Object> val = ImmutableMap.of("key", 123L);
 
-    Object result = RecordUtils.extractFromRecordValue(val, "", config);
+    Object result =
+        RecordUtils.extractFromRecordValue(val, 
Splitter.on('.').splitToList(""), config);
     assertThat(result).isNull();
 
-    result = RecordUtils.extractFromRecordValue(val, "xkey", config);
+    result = RecordUtils.extractFromRecordValue(val, 
Splitter.on('.').splitToList("xkey"), config);
     assertThat(result).isNull();
   }
 
diff --git 
a/kafka-connect/kafka-connect/src/test/java/org/apache/iceberg/connect/data/TestSinkWriter.java
 
b/kafka-connect/kafka-connect/src/test/java/org/apache/iceberg/connect/data/TestSinkWriter.java
index 40a0673517..af4ec58e4d 100644
--- 
a/kafka-connect/kafka-connect/src/test/java/org/apache/iceberg/connect/data/TestSinkWriter.java
+++ 
b/kafka-connect/kafka-connect/src/test/java/org/apache/iceberg/connect/data/TestSinkWriter.java
@@ -204,6 +204,22 @@ public class TestSinkWriter {
     
assertThat(writerResult.tableReference().identifier()).isEqualTo(TABLE_IDENTIFIER);
   }
 
+  @Test
+  void dynamicRouteNestedField() {
+    IcebergSinkConfig config = mock(IcebergSinkConfig.class);
+    
when(config.tables()).thenReturn(ImmutableList.of(TABLE_IDENTIFIER.toString()));
+    when(config.tableConfig(any())).thenReturn(mock(TableSinkConfig.class));
+    when(config.dynamicTablesEnabled()).thenReturn(true);
+    when(config.tablesRouteField()).thenReturn("route.table");
+
+    Map<String, Object> value =
+        ImmutableMap.of("route", ImmutableMap.of("table", 
TABLE_IDENTIFIER.toString()));
+
+    List<IcebergWriterResult> writerResults = sinkWriterTest(value, config);
+    assertThat(writerResults).hasSize(1);
+    
assertThat(writerResults.get(0).tableReference().identifier()).isEqualTo(TABLE_IDENTIFIER);
+  }
+
   @ParameterizedTest
   @ValueSource(booleans = {true, false})
   public void testDynamicRouteReplaceNullWithDefault(boolean 
replaceNullWithDefault) {

Reply via email to