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

danny0405 pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/hudi.git


The following commit(s) were added to refs/heads/master by this push:
     new c2ebc23e787a test(flink): improve table planning and lookup coverage 
(#19394)
c2ebc23e787a is described below

commit c2ebc23e787ac15e85db52c8a45b19e7adf39acb
Author: Danny Chan <[email protected]>
AuthorDate: Wed Jul 29 17:35:29 2026 +0800

    test(flink): improve table planning and lookup coverage (#19394)
---
 .../org/apache/hudi/table/HoodieTableSink.java     |   2 +-
 .../apache/hudi/table/TestHoodieTablePlanning.java | 176 +++++++++++++++++++++
 .../org/apache/hudi/table/TestHoodieTableSink.java |  93 +++++++++++
 .../apache/hudi/table/TestHoodieTableSource.java   |  39 +++++
 .../lookup/TestAsyncLookupFunctionWrapper.java     |  99 ++++++++++++
 .../table/lookup/TestHoodieLookupFunction.java     |  47 ++++++
 .../hudi/table/lookup/TestRocksDBLookupCache.java  |  86 ++++++++++
 7 files changed, 541 insertions(+), 1 deletion(-)

diff --git 
a/hudi-flink-datasource/hudi-flink/src/main/java/org/apache/hudi/table/HoodieTableSink.java
 
b/hudi-flink-datasource/hudi-flink/src/main/java/org/apache/hudi/table/HoodieTableSink.java
index 45bfde998af6..f2bf465d642a 100644
--- 
a/hudi-flink-datasource/hudi-flink/src/main/java/org/apache/hudi/table/HoodieTableSink.java
+++ 
b/hudi-flink-datasource/hudi-flink/src/main/java/org/apache/hudi/table/HoodieTableSink.java
@@ -155,7 +155,7 @@ public class HoodieTableSink implements
 
   @Override
   public DynamicTableSink copy() {
-    return new HoodieTableSink(this.conf, this.schema, this.overwrite);
+    return new HoodieTableSink(new Configuration(this.conf), this.schema, 
this.overwrite);
   }
 
   @Override
diff --git 
a/hudi-flink-datasource/hudi-flink/src/test/java/org/apache/hudi/table/TestHoodieTablePlanning.java
 
b/hudi-flink-datasource/hudi-flink/src/test/java/org/apache/hudi/table/TestHoodieTablePlanning.java
new file mode 100644
index 000000000000..be05ae931f37
--- /dev/null
+++ 
b/hudi-flink-datasource/hudi-flink/src/test/java/org/apache/hudi/table/TestHoodieTablePlanning.java
@@ -0,0 +1,176 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.hudi.table;
+
+import org.apache.hudi.common.model.WriteOperationType;
+import org.apache.hudi.configuration.FlinkOptions;
+import org.apache.hudi.util.StreamerUtil;
+import org.apache.hudi.utils.TestConfigurations;
+import org.apache.hudi.utils.TestTableEnvs;
+
+import org.apache.flink.configuration.Configuration;
+import org.apache.flink.streaming.api.datastream.DataStream;
+import 
org.apache.flink.streaming.api.transformations.LegacySourceTransformation;
+import org.apache.flink.streaming.api.transformations.SourceTransformation;
+import org.apache.flink.table.api.ExplainDetail;
+import org.apache.flink.table.api.TableEnvironment;
+import org.apache.flink.table.api.bridge.java.StreamTableEnvironment;
+import org.apache.flink.types.Row;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
+
+import java.io.File;
+import java.util.List;
+import java.util.stream.Collectors;
+
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+/**
+ * Planning tests for the Hudi table source, sink, and factory.
+ *
+ * <p>The tests translate plans but deliberately do not execute a Flink job.
+ */
+class TestHoodieTablePlanning {
+
+  @TempDir
+  File tempFile;
+
+  @Test
+  void testSourcePushDownsForBothSourceImplementations() throws Exception {
+    StreamTableEnvironment tableEnv =
+        (StreamTableEnvironment) TestTableEnvs.getBatchTableEnv();
+    createTable(tableEnv, "source_v2", new File(tempFile, "source_v2"), true);
+    createTable(tableEnv, "legacy_source", new File(tempFile, 
"legacy_source"), false);
+
+    for (String tableName : new String[] {"source_v2", "legacy_source"}) {
+      String query = "SELECT name FROM " + tableName + " WHERE `partition` = 
'p1'";
+      String pushedPlan = tableEnv.explainSql(
+          query,
+          ExplainDetail.CHANGELOG_MODE,
+          ExplainDetail.JSON_EXECUTION_PLAN);
+      assertTrue(pushedPlan.contains("TableSourceScan"), pushedPlan);
+      assertTrue(pushedPlan.contains("filter=[=(partition"), pushedPlan);
+      assertTrue(pushedPlan.contains("project=[name, partition]"), pushedPlan);
+      assertSourceTransformation(
+          tableEnv,
+          query,
+          tableName.equals("source_v2")
+              ? SourceTransformation.class
+              : LegacySourceTransformation.class);
+
+      String limitedPlan = tableEnv.explainSql(
+          "SELECT name FROM " + tableName + " LIMIT 2",
+          ExplainDetail.CHANGELOG_MODE,
+          ExplainDetail.JSON_EXECUTION_PLAN);
+      assertTrue(limitedPlan.contains("limit=[2]"), limitedPlan);
+    }
+  }
+
+  private static void assertSourceTransformation(
+      StreamTableEnvironment tableEnv,
+      String query,
+      Class<?> expectedSourceTransformation) {
+    DataStream<Row> plannedStream = 
tableEnv.toDataStream(tableEnv.sqlQuery(query));
+    List<String> transformationNames = plannedStream.getTransformation()
+        .getTransitivePredecessors()
+        .stream()
+        .map(transformation -> transformation.getName()
+            + ":" + transformation.getClass().getName())
+        .collect(Collectors.toList());
+    assertTrue(
+        plannedStream.getTransformation()
+            .getTransitivePredecessors()
+            .stream()
+            .anyMatch(expectedSourceTransformation::isInstance),
+        "Expected " + expectedSourceTransformation.getSimpleName() + " in " + 
transformationNames);
+  }
+
+  @Test
+  void testSinkPlanning() throws Exception {
+    TableEnvironment tableEnv = TestTableEnvs.getBatchTableEnv();
+    createTable(tableEnv, "sink_table", new File(tempFile, "sink_table"), 
true);
+
+    String plan = tableEnv.explainSql(
+        "INSERT INTO sink_table VALUES "
+            + "('id1', 'Alice', 20, TIMESTAMP '2026-01-01 00:00:00', 'p1')",
+        ExplainDetail.CHANGELOG_MODE,
+        ExplainDetail.JSON_EXECUTION_PLAN);
+
+    
assertTrue(plan.contains("Sink(table=[default_catalog.default_database.sink_table]"),
 plan);
+    assertTrue(plan.contains("stream_write: default_database.sink_table"), 
plan);
+
+    createTable(
+        tableEnv,
+        "append_sink",
+        new File(tempFile, "append_sink"),
+        true,
+        WriteOperationType.INSERT.value());
+    String appendPlan = tableEnv.explainSql(
+        "INSERT INTO append_sink VALUES "
+            + "('id2', 'Bob', 30, TIMESTAMP '2026-01-02 00:00:00', 'p2')",
+        ExplainDetail.CHANGELOG_MODE,
+        ExplainDetail.JSON_EXECUTION_PLAN);
+    assertTrue(
+        appendPlan.contains("hoodie_append_write: 
default_database.append_sink"),
+        appendPlan);
+  }
+
+  private static void createTable(
+      TableEnvironment tableEnv,
+      String tableName,
+      File tablePath,
+      boolean sourceV2) throws Exception {
+    createTable(tableEnv, tableName, tablePath, sourceV2, null);
+  }
+
+  private static void createTable(
+      TableEnvironment tableEnv,
+      String tableName,
+      File tablePath,
+      boolean sourceV2,
+      String operation) throws Exception {
+    Configuration conf = 
TestConfigurations.getDefaultConf(tablePath.getAbsolutePath());
+    if (operation != null) {
+      conf.set(FlinkOptions.OPERATION, operation);
+    }
+    StreamerUtil.initTableIfNotExists(conf);
+
+    tableEnv.executeSql(
+        "CREATE TABLE " + tableName + " ("
+            + "uuid STRING,"
+            + "name STRING,"
+            + "age INT,"
+            + "ts TIMESTAMP(3),"
+            + "`partition` STRING,"
+            + "PRIMARY KEY (uuid) NOT ENFORCED"
+            + ") PARTITIONED BY (`partition`) WITH ("
+            + "'connector' = 'hudi',"
+            + "'path' = '" + sqlLiteral(tablePath.getAbsolutePath()) + "',"
+            + "'" + FlinkOptions.ORDERING_FIELDS.key() + "' = 'ts',"
+            + "'" + FlinkOptions.READ_SOURCE_V2_ENABLED.key() + "' = '" + 
sourceV2 + "'"
+            + (operation == null
+                ? ""
+                : ",'" + FlinkOptions.OPERATION.key() + "' = '" + operation + 
"'")
+            + ")");
+  }
+
+  private static String sqlLiteral(String value) {
+    return value.replace("'", "''");
+  }
+}
diff --git 
a/hudi-flink-datasource/hudi-flink/src/test/java/org/apache/hudi/table/TestHoodieTableSink.java
 
b/hudi-flink-datasource/hudi-flink/src/test/java/org/apache/hudi/table/TestHoodieTableSink.java
new file mode 100644
index 000000000000..d75ca711f565
--- /dev/null
+++ 
b/hudi-flink-datasource/hudi-flink/src/test/java/org/apache/hudi/table/TestHoodieTableSink.java
@@ -0,0 +1,93 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.hudi.table;
+
+import org.apache.hudi.common.model.WriteOperationType;
+import org.apache.hudi.configuration.FlinkOptions;
+import org.apache.hudi.util.ChangelogModes;
+import org.apache.hudi.util.DataModificationInfos;
+import org.apache.hudi.utils.TestConfigurations;
+
+import org.apache.flink.configuration.Configuration;
+import org.apache.flink.table.connector.ChangelogMode;
+import org.junit.jupiter.api.Test;
+
+import java.util.Collections;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotSame;
+import static org.junit.jupiter.api.Assertions.assertSame;
+
+/**
+ * Tests for {@link HoodieTableSink}.
+ */
+class TestHoodieTableSink {
+
+  @Test
+  void testChangelogModeAndCopy() {
+    Configuration conf = new Configuration();
+    HoodieTableSink sink = new HoodieTableSink(conf, 
TestConfigurations.TABLE_SCHEMA);
+
+    assertEquals(ChangelogModes.UPSERT, 
sink.getChangelogMode(ChangelogMode.all()));
+    conf.set(FlinkOptions.CHANGELOG_ENABLED, true);
+    assertEquals(ChangelogModes.FULL, 
sink.getChangelogMode(ChangelogMode.insertOnly()));
+    assertEquals("HoodieTableSink", sink.asSummaryString());
+
+    HoodieTableSink copied = (HoodieTableSink) sink.copy();
+    assertNotSame(sink, copied);
+    assertNotSame(conf, copied.getConf());
+    assertEquals(ChangelogModes.FULL, 
copied.getChangelogMode(ChangelogMode.insertOnly()));
+
+    copied.applyRowLevelDelete(null);
+    assertEquals(
+        WriteOperationType.DELETE.value(),
+        copied.getConf().get(FlinkOptions.OPERATION));
+    assertEquals(WriteOperationType.UPSERT.value(), 
conf.get(FlinkOptions.OPERATION));
+  }
+
+  @Test
+  void testOverwriteAndRowLevelOperations() {
+    Configuration conf = new Configuration();
+    HoodieTableSink sink = new HoodieTableSink(conf, 
TestConfigurations.TABLE_SCHEMA);
+
+    sink.applyOverwrite(true);
+    assertEquals(
+        WriteOperationType.INSERT_OVERWRITE_TABLE.value(),
+        conf.get(FlinkOptions.OPERATION));
+    sink.applyStaticPartition(Collections.singletonMap("partition", "p1"));
+    assertEquals(
+        WriteOperationType.INSERT_OVERWRITE.value(),
+        conf.get(FlinkOptions.OPERATION));
+
+    conf.set(FlinkOptions.WRITE_PARTITION_OVERWRITE_MODE, "DYNAMIC");
+    sink.applyOverwrite(true);
+    assertEquals(
+        WriteOperationType.INSERT_OVERWRITE.value(),
+        conf.get(FlinkOptions.OPERATION));
+
+    assertSame(
+        DataModificationInfos.DEFAULT_DELETE_INFO,
+        sink.applyRowLevelDelete(null));
+    assertEquals(WriteOperationType.DELETE.value(), 
conf.get(FlinkOptions.OPERATION));
+    assertSame(
+        DataModificationInfos.DEFAULT_UPDATE_INFO,
+        sink.applyRowLevelUpdate(Collections.emptyList(), null));
+    assertEquals(WriteOperationType.UPSERT.value(), 
conf.get(FlinkOptions.OPERATION));
+  }
+}
diff --git 
a/hudi-flink-datasource/hudi-flink/src/test/java/org/apache/hudi/table/TestHoodieTableSource.java
 
b/hudi-flink-datasource/hudi-flink/src/test/java/org/apache/hudi/table/TestHoodieTableSource.java
index ee04e2175421..26694f2178af 100644
--- 
a/hudi-flink-datasource/hudi-flink/src/test/java/org/apache/hudi/table/TestHoodieTableSource.java
+++ 
b/hudi-flink-datasource/hudi-flink/src/test/java/org/apache/hudi/table/TestHoodieTableSource.java
@@ -24,6 +24,7 @@ import 
org.apache.hudi.common.model.PartitionBucketIndexHashingConfig;
 import org.apache.hudi.common.schema.HoodieSchemaField;
 import org.apache.hudi.common.table.HoodieTableMetaClient;
 import org.apache.hudi.configuration.FlinkOptions;
+import org.apache.hudi.exception.HoodieException;
 import org.apache.hudi.source.ExpressionPredicates;
 import org.apache.hudi.source.prune.ColumnStatsProbe;
 import org.apache.hudi.storage.StoragePath;
@@ -40,6 +41,9 @@ import org.apache.flink.api.common.io.InputFormat;
 import org.apache.flink.configuration.Configuration;
 import org.apache.flink.table.api.DataTypes;
 import org.apache.flink.table.catalog.ResolvedSchema;
+import org.apache.flink.table.connector.source.LookupTableSource;
+import 
org.apache.flink.table.connector.source.lookup.AsyncLookupFunctionProvider;
+import org.apache.flink.table.connector.source.lookup.LookupFunctionProvider;
 import org.apache.flink.table.data.RowData;
 import org.apache.flink.table.expressions.CallExpression;
 import org.apache.flink.table.expressions.FieldReferenceExpression;
@@ -78,7 +82,10 @@ import static 
org.junit.jupiter.api.Assertions.assertDoesNotThrow;
 import static org.junit.jupiter.api.Assertions.assertEquals;
 import static org.junit.jupiter.api.Assertions.assertNotNull;
 import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
 import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
 
 /**
  * Test cases for HoodieTableSource.
@@ -143,6 +150,16 @@ public class TestHoodieTableSource {
         "Query type: 'incremental' should be supported");
   }
 
+  @Test
+  void testStreamingInputFormatPlanning() {
+    HoodieTableSource tableSource = getEmptyStreamingSource(false);
+
+    assertThat(tableSource.getInputFormat(true), 
is(instanceOf(MergeOnReadInputFormat.class)));
+
+    tableSource.getConf().set(FlinkOptions.QUERY_TYPE, 
FlinkOptions.QUERY_TYPE_READ_OPTIMIZED);
+    assertThrows(HoodieException.class, () -> 
tableSource.getInputFormat(true));
+  }
+
   @ParameterizedTest
   @ValueSource(booleans = {true, false})
   void testGetTableAvroSchema(boolean isSourceV2) {
@@ -160,6 +177,28 @@ public class TestHoodieTableSource {
     assertThat(schemaFields, is(expected));
   }
 
+  @Test
+  void testLookupRuntimeProvider() {
+    HoodieTableSource tableSource = getEmptyStreamingSource(true);
+    LookupTableSource.LookupContext lookupContext = 
mock(LookupTableSource.LookupContext.class);
+    when(lookupContext.getKeys()).thenReturn(new int[][] {{0}});
+
+    assertThat(
+        tableSource.getLookupRuntimeProvider(lookupContext),
+        is(instanceOf(LookupFunctionProvider.class)));
+
+    tableSource.getConf().set(FlinkOptions.LOOKUP_ASYNC, true);
+    assertThat(
+        tableSource.getLookupRuntimeProvider(lookupContext),
+        is(instanceOf(AsyncLookupFunctionProvider.class)));
+
+    LookupTableSource.LookupContext nestedLookupContext = 
mock(LookupTableSource.LookupContext.class);
+    when(nestedLookupContext.getKeys()).thenReturn(new int[][] {{0, 1}});
+    assertThrows(
+        UnsupportedOperationException.class,
+        () -> tableSource.getLookupRuntimeProvider(nestedLookupContext));
+  }
+
   @ParameterizedTest
   @ValueSource(booleans = {true, false})
   void testDataSkippingFilterShouldBeNotNullWhenTableSourceIsCopied(boolean 
isSourceV2) {
diff --git 
a/hudi-flink-datasource/hudi-flink/src/test/java/org/apache/hudi/table/lookup/TestAsyncLookupFunctionWrapper.java
 
b/hudi-flink-datasource/hudi-flink/src/test/java/org/apache/hudi/table/lookup/TestAsyncLookupFunctionWrapper.java
new file mode 100644
index 000000000000..4fcdd738f04d
--- /dev/null
+++ 
b/hudi-flink-datasource/hudi-flink/src/test/java/org/apache/hudi/table/lookup/TestAsyncLookupFunctionWrapper.java
@@ -0,0 +1,99 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.hudi.table.lookup;
+
+import org.apache.flink.table.data.GenericRowData;
+import org.apache.flink.table.data.RowData;
+import org.apache.flink.table.functions.FunctionContext;
+import org.apache.flink.table.functions.LookupFunction;
+import org.junit.jupiter.api.Test;
+
+import java.io.IOException;
+import java.io.UncheckedIOException;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.concurrent.CompletionException;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertInstanceOf;
+import static org.junit.jupiter.api.Assertions.assertSame;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+/**
+ * Tests for {@link AsyncLookupFunctionWrapper}.
+ */
+class TestAsyncLookupFunctionWrapper {
+
+  @Test
+  void testLifecycleAndAsyncLookup() throws Exception {
+    TestingLookupFunction function = new TestingLookupFunction();
+    AsyncLookupFunctionWrapper wrapper = new 
AsyncLookupFunctionWrapper(function, 2);
+    RowData key = GenericRowData.of(1);
+
+    wrapper.open(null);
+    Collection<RowData> result = wrapper.asyncLookup(key).join();
+
+    assertEquals(1, result.size());
+    assertSame(key, result.iterator().next());
+    assertTrue(function.opened);
+    wrapper.close();
+    assertTrue(function.closed);
+  }
+
+  @Test
+  void testIOExceptionIsPropagatedAsUncheckedIOException() throws Exception {
+    AsyncLookupFunctionWrapper wrapper =
+        new AsyncLookupFunctionWrapper(new FailingLookupFunction(), 1);
+
+    CompletionException exception = assertThrows(
+        CompletionException.class,
+        () -> wrapper.asyncLookup(GenericRowData.of(1)).join());
+
+    assertInstanceOf(UncheckedIOException.class, exception.getCause());
+    wrapper.close();
+  }
+
+  private static class TestingLookupFunction extends LookupFunction {
+    private boolean opened;
+    private boolean closed;
+
+    @Override
+    public void open(FunctionContext context) {
+      opened = true;
+    }
+
+    @Override
+    public Collection<RowData> lookup(RowData keyRow) {
+      return Collections.singletonList(keyRow);
+    }
+
+    @Override
+    public void close() {
+      closed = true;
+    }
+  }
+
+  private static class FailingLookupFunction extends LookupFunction {
+    @Override
+    public Collection<RowData> lookup(RowData keyRow) throws IOException {
+      throw new IOException("expected");
+    }
+  }
+}
diff --git 
a/hudi-flink-datasource/hudi-flink/src/test/java/org/apache/hudi/table/lookup/TestHoodieLookupFunction.java
 
b/hudi-flink-datasource/hudi-flink/src/test/java/org/apache/hudi/table/lookup/TestHoodieLookupFunction.java
index 8d336ede565e..750f11f7956c 100644
--- 
a/hudi-flink-datasource/hudi-flink/src/test/java/org/apache/hudi/table/lookup/TestHoodieLookupFunction.java
+++ 
b/hudi-flink-datasource/hudi-flink/src/test/java/org/apache/hudi/table/lookup/TestHoodieLookupFunction.java
@@ -39,8 +39,14 @@ import java.util.Collections;
 import java.util.List;
 
 import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertInstanceOf;
 import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
 import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
 
 /**
  * Tests for {@link HoodieLookupFunction}.
@@ -98,6 +104,32 @@ class TestHoodieLookupFunction {
     }
   }
 
+  @Test
+  void testRocksDBCacheLifecycleAndLookupFailure() throws Exception {
+    Configuration conf = getConf();
+    conf.set(FlinkOptions.LOOKUP_JOIN_CACHE_TYPE, "rocksdb");
+    conf.set(FlinkOptions.LOOKUP_JOIN_ROCKSDB_PATH, new File(tempFile, 
"rocksdb").getAbsolutePath());
+    StreamerUtil.initTableIfNotExists(conf);
+
+    HoodieLookupFunction function =
+        newLookupFunction(new 
CountingLookupTableReader(Collections.emptyList(), conf), conf);
+    function.open(null);
+
+    assertEquals(Duration.ofDays(1), function.getReloadInterval());
+    assertInstanceOf(RocksDBLookupCache.class, getCache(function));
+    assertNull(function.lookup(lookupKey()));
+    function.close();
+
+    LookupCache failingCache = mock(LookupCache.class);
+    when(failingCache.getRows(any())).thenThrow(new IOException("expected"));
+    setCache(function, failingCache);
+    setNextLoadTime(function, Long.MAX_VALUE);
+    RuntimeException exception =
+        assertThrows(RuntimeException.class, () -> 
function.lookup(lookupKey()));
+    assertInstanceOf(IOException.class, exception.getCause());
+    function.close();
+  }
+
   private HoodieLookupFunction newLookupFunction(CountingLookupTableReader 
reader, Configuration conf) {
     return new HoodieLookupFunction(
         reader,
@@ -129,6 +161,21 @@ class TestHoodieLookupFunction {
     field.setLong(function, nextLoadTime);
   }
 
+  private static void setCache(HoodieLookupFunction function, LookupCache 
cache) throws Exception {
+    Field field = cacheField();
+    field.set(function, cache);
+  }
+
+  private static LookupCache getCache(HoodieLookupFunction function) throws 
Exception {
+    return (LookupCache) cacheField().get(function);
+  }
+
+  private static Field cacheField() throws NoSuchFieldException {
+    Field field = HoodieLookupFunction.class.getDeclaredField("cache");
+    field.setAccessible(true);
+    return field;
+  }
+
   private static class CountingLookupTableReader extends 
HoodieLookupTableReader {
     private final List<RowData> rows;
     private int openCount;
diff --git 
a/hudi-flink-datasource/hudi-flink/src/test/java/org/apache/hudi/table/lookup/TestRocksDBLookupCache.java
 
b/hudi-flink-datasource/hudi-flink/src/test/java/org/apache/hudi/table/lookup/TestRocksDBLookupCache.java
new file mode 100644
index 000000000000..95b805f0809d
--- /dev/null
+++ 
b/hudi-flink-datasource/hudi-flink/src/test/java/org/apache/hudi/table/lookup/TestRocksDBLookupCache.java
@@ -0,0 +1,86 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.hudi.table.lookup;
+
+import org.apache.flink.api.common.typeutils.TypeSerializer;
+import org.apache.flink.table.data.GenericRowData;
+import org.apache.flink.table.data.RowData;
+import org.apache.flink.table.data.StringData;
+import org.apache.flink.table.runtime.typeutils.InternalSerializers;
+import org.apache.flink.table.types.logical.IntType;
+import org.apache.flink.table.types.logical.RowType;
+import org.apache.flink.table.types.logical.VarCharType;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
+
+import java.io.File;
+import java.util.List;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNull;
+
+/**
+ * Tests for {@link RocksDBLookupCache}.
+ */
+class TestRocksDBLookupCache {
+
+  @TempDir
+  File tempFile;
+
+  @Test
+  void testPutGetClearAndClose() throws Exception {
+    RowType keyType = RowType.of(
+        new VarCharType(VarCharType.MAX_LENGTH));
+    RowType rowType = RowType.of(
+        new VarCharType(VarCharType.MAX_LENGTH),
+        new IntType());
+    TypeSerializer<RowData> keySerializer = 
InternalSerializers.create(keyType);
+    TypeSerializer<RowData> rowSerializer = 
InternalSerializers.create(rowType);
+    RocksDBLookupCache cache =
+        new RocksDBLookupCache(keySerializer, rowSerializer, 
tempFile.getAbsolutePath());
+
+    RowData key1 = key("id1");
+    cache.addRow(key1, row("id1", 10));
+    cache.addRow(key1, row("id1", 20));
+    cache.addRow(key("id2"), row("id2", 30));
+
+    List<RowData> rows = cache.getRows(key1);
+    assertEquals(2, rows.size());
+    assertEquals("id1", rows.get(0).getString(0).toString());
+    assertEquals(10, rows.get(0).getInt(1));
+    assertEquals(20, rows.get(1).getInt(1));
+    assertNull(cache.getRows(key("missing")));
+
+    cache.clear();
+    assertNull(cache.getRows(key1));
+    cache.addRow(key1, row("id1", 40));
+    assertEquals(40, cache.getRows(key1).get(0).getInt(1));
+
+    cache.close();
+    cache.close();
+  }
+
+  private static RowData key(String key) {
+    return GenericRowData.of(StringData.fromString(key));
+  }
+
+  private static RowData row(String key, int value) {
+    return GenericRowData.of(StringData.fromString(key), value);
+  }
+}

Reply via email to