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 98462b48e15b fix(metadata): correct index definition lookup and
improve mdt read coverage (#19359)
98462b48e15b is described below
commit 98462b48e15b52fcbd47c4bf8bf3a3dda79020ae
Author: Shuo Cheng <[email protected]>
AuthorDate: Tue Jul 28 17:59:12 2026 +0800
fix(metadata): correct index definition lookup and improve mdt read
coverage (#19359)
---
.../hudi/client/TestJavaHoodieBackedMetadata.java | 116 +++++
.../hudi/metadata/MetadataPartitionType.java | 9 +-
.../metadata/TestBaseFileRecordParsingUtils.java | 90 ++++
.../TestHoodieBackedTableMetadataDataCleanup.java | 389 +++++++++++++-
.../hudi/metadata/TestHoodieTableMetadataUtil.java | 570 ++++++++++++++++++++-
.../hudi/metadata/TestMetadataPartitionType.java | 77 +++
.../hudi/metadata/TestBaseTableMetadata.java | 299 +++++++++++
.../TestFileSystemBackedTableMetadata.java | 36 ++
.../hudi/metadata/TestHoodieMetadataPayload.java | 63 +++
9 files changed, 1639 insertions(+), 10 deletions(-)
diff --git
a/hudi-client/hudi-java-client/src/test/java/org/apache/hudi/client/TestJavaHoodieBackedMetadata.java
b/hudi-client/hudi-java-client/src/test/java/org/apache/hudi/client/TestJavaHoodieBackedMetadata.java
index 7bff7b8f2cbe..776816420352 100644
---
a/hudi-client/hudi-java-client/src/test/java/org/apache/hudi/client/TestJavaHoodieBackedMetadata.java
+++
b/hudi-client/hudi-java-client/src/test/java/org/apache/hudi/client/TestJavaHoodieBackedMetadata.java
@@ -44,6 +44,7 @@ import org.apache.hudi.common.model.HoodieKey;
import org.apache.hudi.common.model.HoodieLogFile;
import org.apache.hudi.common.model.HoodieRecord;
import org.apache.hudi.common.model.HoodieRecord.HoodieRecordType;
+import org.apache.hudi.common.model.HoodieRecordGlobalLocation;
import org.apache.hudi.common.model.HoodieTableType;
import org.apache.hudi.common.model.HoodieWriteStat;
import org.apache.hudi.common.model.TableServiceType;
@@ -76,6 +77,7 @@ import org.apache.hudi.common.util.HoodieTimer;
import org.apache.hudi.common.util.JsonUtils;
import org.apache.hudi.common.util.Option;
import org.apache.hudi.common.util.collection.ClosableIterator;
+import org.apache.hudi.common.util.collection.Pair;
import org.apache.hudi.common.util.hash.PartitionIndexID;
import org.apache.hudi.config.HoodieArchivalConfig;
import org.apache.hudi.config.HoodieCleanConfig;
@@ -99,6 +101,7 @@ import org.apache.hudi.metadata.HoodieTableMetadata;
import org.apache.hudi.metadata.HoodieTableMetadataUtil;
import org.apache.hudi.metadata.JavaHoodieBackedTableMetadataWriter;
import org.apache.hudi.metadata.MetadataPartitionType;
+import org.apache.hudi.metadata.RawKey;
import org.apache.hudi.metrics.Metrics;
import org.apache.hudi.storage.StoragePath;
import org.apache.hudi.storage.StoragePathInfo;
@@ -164,6 +167,8 @@ import static
org.apache.hudi.metadata.HoodieTableMetadata.getMetadataTableBaseP
import static
org.apache.hudi.metadata.HoodieTableMetadataUtil.deleteMetadataTable;
import static org.apache.hudi.metadata.MetadataPartitionType.COLUMN_STATS;
import static org.apache.hudi.metadata.MetadataPartitionType.FILES;
+import static org.apache.hudi.metadata.MetadataPartitionType.PARTITION_STATS;
+import static org.apache.hudi.metadata.MetadataPartitionType.RECORD_INDEX;
import static org.apache.hudi.testutils.Assertions.assertNoWriteErrors;
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
import static org.junit.jupiter.api.Assertions.assertEquals;
@@ -1322,6 +1327,117 @@ public class TestJavaHoodieBackedMetadata extends
TestHoodieMetadataBase {
}
}
+ @Test
+ public void testMetadataReadRoundTrip() throws Exception {
+ this.tableType = COPY_ON_WRITE;
+ initPath();
+ initFileSystem(basePath, storageConf);
+ storage.createDirectory(new StoragePath(basePath));
+ initMetaClient(tableType);
+ initTestDataGenerator();
+ metadataTableBasePath = getMetadataTableBasePath(basePath);
+
+ HoodieJavaEngineContext engineContext = new
HoodieJavaEngineContext(storageConf);
+ HoodieWriteConfig writeConfig = getWriteConfigBuilder(true, true, false)
+ .withMetadataConfig(HoodieMetadataConfig.newBuilder()
+ .enable(true)
+ .withMetadataIndexColumnStats(true)
+ .withMetadataIndexPartitionStats(true)
+
.withColumnStatsIndexForColumns(HoodieRecord.RECORD_KEY_METADATA_FIELD)
+ .withEnableGlobalRecordLevelIndex(true)
+ .withRecordIndexFileGroupCount(3, 3)
+ .build())
+ .build();
+
+ try (HoodieJavaWriteClient client = new
HoodieJavaWriteClient(engineContext, writeConfig)) {
+ String instantTime = client.startCommit();
+ List<HoodieRecord> records = dataGen.generateInserts(instantTime, 30);
+ Map<String, String> recordKeyToPartition = records.stream()
+ .collect(Collectors.toMap(HoodieRecord::getRecordKey,
HoodieRecord::getPartitionPath));
+ List<WriteStatus> writeStatuses = client.insert(records, instantTime);
+ client.commit(instantTime, writeStatuses);
+ assertNoWriteErrors(writeStatuses);
+
+ metaClient = HoodieTableMetaClient.reload(metaClient);
+
assertTrue(metaClient.getTableConfig().isMetadataPartitionAvailable(FILES));
+
assertTrue(metaClient.getTableConfig().isMetadataPartitionAvailable(COLUMN_STATS));
+
assertTrue(metaClient.getTableConfig().isMetadataPartitionAvailable(PARTITION_STATS));
+
assertTrue(metaClient.getTableConfig().isMetadataPartitionAvailable(RECORD_INDEX));
+
+ try (HoodieTableMetadata tableMetadata = metadata(client);
+ HoodieTableMetadata fileSystemMetadata = new
FileSystemBackedTableMetadata(
+ engineContext, metaClient.getTableConfig(),
metaClient.getStorage(), basePath)) {
+ List<String> expectedPartitions =
fileSystemMetadata.getAllPartitionPaths();
+ List<String> actualPartitions = tableMetadata.getAllPartitionPaths();
+ Collections.sort(expectedPartitions);
+ Collections.sort(actualPartitions);
+ assertEquals(expectedPartitions, actualPartitions);
+ assertFalse(actualPartitions.isEmpty());
+
+ List<Pair<String, String>> partitionAndFileNames = new ArrayList<>();
+ for (String partition : actualPartitions) {
+ StoragePath partitionPath = partition.isEmpty()
+ ? new StoragePath(basePath)
+ : new StoragePath(basePath, partition);
+ List<String> expectedFileNames =
fileSystemMetadata.getAllFilesInPartition(partitionPath).stream()
+ .map(pathInfo -> pathInfo.getPath().getName())
+ .sorted()
+ .collect(Collectors.toList());
+ List<String> actualFileNames =
tableMetadata.getAllFilesInPartition(partitionPath).stream()
+ .map(pathInfo -> pathInfo.getPath().getName())
+ .sorted()
+ .collect(Collectors.toList());
+ assertEquals(expectedFileNames, actualFileNames);
+ assertFalse(actualFileNames.isEmpty());
+ actualFileNames.forEach(fileName ->
partitionAndFileNames.add(Pair.of(partition, fileName)));
+ }
+
+ Map<Pair<String, String>, HoodieMetadataColumnStats> columnStats =
+ tableMetadata.getColumnStats(partitionAndFileNames,
HoodieRecord.RECORD_KEY_METADATA_FIELD);
+ assertEquals(partitionAndFileNames.size(), columnStats.size());
+ columnStats.values().forEach(stats -> {
+ assertEquals(HoodieRecord.RECORD_KEY_METADATA_FIELD,
stats.getColumnName().toString());
+ assertFalse(stats.getIsDeleted());
+ assertNotNull(stats.getMinValue());
+ assertNotNull(stats.getMaxValue());
+ });
+
+ Map<String, String> partitionStatsKeyToPartition =
actualPartitions.stream()
+ .collect(Collectors.toMap(
+ partition -> HoodieTableMetadataUtil.getPartitionStatsIndexKey(
+ partition, HoodieRecord.RECORD_KEY_METADATA_FIELD),
+ partition -> partition));
+ List<RawKey> partitionStatsKeys =
partitionStatsKeyToPartition.keySet().stream()
+ .map(key -> (RawKey) () -> key)
+ .collect(Collectors.toList());
+ List<HoodieRecord<HoodieMetadataPayload>> partitionStats =
tableMetadata.getRecordsByKeyPrefixes(
+ HoodieListData.eager(partitionStatsKeys),
PARTITION_STATS.getPartitionPath(), true).collectAsList();
+ assertEquals(actualPartitions.size(), partitionStats.size());
+ partitionStats.forEach(record -> {
+
assertTrue(partitionStatsKeyToPartition.containsKey(record.getRecordKey()));
+ assertTrue(record.getData().getColumnStatMetadata().isPresent());
+ HoodieMetadataColumnStats stats =
record.getData().getColumnStatMetadata().get();
+
assertEquals(partitionStatsKeyToPartition.get(record.getRecordKey()),
stats.getFileName().toString());
+ assertEquals(HoodieRecord.RECORD_KEY_METADATA_FIELD,
stats.getColumnName().toString());
+ assertFalse(stats.getIsDeleted());
+ assertNotNull(stats.getMinValue());
+ assertNotNull(stats.getMaxValue());
+ });
+
+ List<Pair<String, HoodieRecordGlobalLocation>> recordLocations =
tableMetadata
+ .readRecordIndexLocationsWithKeys(HoodieListData.eager(new
ArrayList<>(recordKeyToPartition.keySet())))
+ .collectAsList();
+ assertEquals(records.size(), recordLocations.size());
+ assertEquals(recordKeyToPartition.keySet(),
+
recordLocations.stream().map(Pair::getLeft).collect(Collectors.toSet()));
+ recordLocations.forEach(entry -> {
+ assertEquals(recordKeyToPartition.get(entry.getLeft()),
entry.getRight().getPartitionPath());
+ assertNotNull(entry.getRight().getFileId());
+ });
+ }
+ }
+ }
+
@Test
public void
testReadRecordIndexLocationsByBucketIdFailsWhenRecordIndexDisabled() throws
Exception {
init(HoodieTableType.COPY_ON_WRITE);
diff --git
a/hudi-common/src/main/java/org/apache/hudi/metadata/MetadataPartitionType.java
b/hudi-common/src/main/java/org/apache/hudi/metadata/MetadataPartitionType.java
index 179bcc5f648f..6311a7cf1580 100644
---
a/hudi-common/src/main/java/org/apache/hudi/metadata/MetadataPartitionType.java
+++
b/hudi-common/src/main/java/org/apache/hudi/metadata/MetadataPartitionType.java
@@ -510,7 +510,7 @@ public enum MetadataPartitionType {
return false;
}
// check the index definition already exists or not for this column
- List<HoodieIndexDefinition> indexDefinitions =
getIndexDefinitions(secondaryIndexColumn, PARTITION_NAME_SECONDARY_INDEX,
dataMetaClient);
+ List<HoodieIndexDefinition> indexDefinitions =
getIndexDefinitions(PARTITION_NAME_SECONDARY_INDEX, secondaryIndexColumn,
dataMetaClient);
return indexDefinitions.isEmpty();
}
@@ -531,7 +531,7 @@ public enum MetadataPartitionType {
// get all index definitions for this column and index type
// check if none of the index definitions has index function matching the
expression
- List<HoodieIndexDefinition> indexDefinitions =
getIndexDefinitions(expressionIndexColumn, PARTITION_NAME_EXPRESSION_INDEX,
dataMetaClient);
+ List<HoodieIndexDefinition> indexDefinitions =
getIndexDefinitions(PARTITION_NAME_EXPRESSION_INDEX, expressionIndexColumn,
dataMetaClient);
return indexDefinitions.isEmpty()
|| indexDefinitions.stream().noneMatch(indexDefinition ->
indexDefinition.getIndexFunction().equals(expressionIndexOptions.get(HoodieExpressionIndex.EXPRESSION_OPTION)));
}
@@ -549,11 +549,6 @@ public enum MetadataPartitionType {
return indexDefinitions;
}
- private static boolean isIndexDefinitionPresentForColumn(String
indexedColumn, String indexType, HoodieTableMetaClient dataMetaClient) {
- return dataMetaClient.getIndexMetadata().isPresent() &&
dataMetaClient.getIndexMetadata().get().getIndexDefinitions().values().stream()
- .anyMatch(indexDefinition ->
indexDefinition.getSourceFields().contains(indexedColumn) &&
indexDefinition.getIndexType().equals(indexType));
- }
-
@Override
public String toString() {
return "Metadata partition {"
diff --git
a/hudi-common/src/test/java/org/apache/hudi/metadata/TestBaseFileRecordParsingUtils.java
b/hudi-common/src/test/java/org/apache/hudi/metadata/TestBaseFileRecordParsingUtils.java
new file mode 100644
index 000000000000..4470245ade36
--- /dev/null
+++
b/hudi-common/src/test/java/org/apache/hudi/metadata/TestBaseFileRecordParsingUtils.java
@@ -0,0 +1,90 @@
+/*
+ * 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.metadata;
+
+import org.apache.hudi.common.model.HoodieFileFormat;
+import org.apache.hudi.common.model.HoodieWriteStat;
+import org.apache.hudi.common.util.FileFormatUtils;
+import org.apache.hudi.core.io.storage.HoodieIOFactory;
+import org.apache.hudi.storage.HoodieStorage;
+import org.apache.hudi.storage.StoragePath;
+
+import org.junit.jupiter.api.Test;
+import org.mockito.MockedStatic;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.EnumSet;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+
+import static
org.apache.hudi.metadata.BaseFileRecordParsingUtils.RecordStatus.DELETE;
+import static
org.apache.hudi.metadata.BaseFileRecordParsingUtils.RecordStatus.INSERT;
+import static
org.apache.hudi.metadata.BaseFileRecordParsingUtils.RecordStatus.UPDATE;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+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.mockStatic;
+import static org.mockito.Mockito.when;
+
+class TestBaseFileRecordParsingUtils {
+
+ @Test
+ void testRecordKeyStatusClassificationAndSecondaryIndexKeys() {
+ HoodieStorage storage = mock(HoodieStorage.class);
+ HoodieIOFactory ioFactory = mock(HoodieIOFactory.class);
+ FileFormatUtils fileFormatUtils = mock(FileFormatUtils.class);
+
when(ioFactory.getFileFormatUtils(HoodieFileFormat.PARQUET)).thenReturn(fileFormatUtils);
+ when(fileFormatUtils.readRowKeys(any(),
any(StoragePath.class))).thenAnswer(invocation -> {
+ StoragePath path = invocation.getArgument(1);
+ return path.getName().equals("latest.parquet")
+ ? new HashSet<>(Arrays.asList("inserted", "updated"))
+ : new HashSet<>(Arrays.asList("updated", "deleted"));
+ });
+
+ try (MockedStatic<HoodieIOFactory> ioFactoryMock =
mockStatic(HoodieIOFactory.class)) {
+ ioFactoryMock.when(() ->
HoodieIOFactory.getIOFactory(storage)).thenReturn(ioFactory);
+
+ Map<BaseFileRecordParsingUtils.RecordStatus, List<String>> statuses =
+ BaseFileRecordParsingUtils.getRecordKeyStatuses(
+ "/table", "partition", "latest.parquet", "previous.parquet",
storage,
+ EnumSet.allOf(BaseFileRecordParsingUtils.RecordStatus.class));
+ assertEquals(Collections.singletonList("inserted"),
statuses.get(INSERT));
+ assertEquals(Collections.singletonList("updated"), statuses.get(UPDATE));
+ assertEquals(Collections.singletonList("deleted"), statuses.get(DELETE));
+
+ assertTrue(BaseFileRecordParsingUtils.getRecordKeyStatuses(
+ "/table", "partition", "latest.parquet", null, storage,
EnumSet.of(UPDATE, DELETE)).isEmpty());
+ assertEquals(
+ new HashSet<>(Arrays.asList("inserted", "updated")),
+ new HashSet<>(BaseFileRecordParsingUtils.getRecordKeyStatuses(
+ "/table", "partition", "latest.parquet", null, storage,
EnumSet.of(INSERT)).get(INSERT)));
+
+ HoodieWriteStat writeStat = mock(HoodieWriteStat.class);
+ when(writeStat.getPath()).thenReturn("partition/latest.parquet");
+ when(writeStat.getPartitionPath()).thenReturn("partition");
+ when(writeStat.getPrevBaseFile()).thenReturn("previous.parquet");
+ List<String> changedKeys =
+ BaseFileRecordParsingUtils.getRecordKeysDeletedOrUpdated("/table",
writeStat, storage);
+ assertEquals(new HashSet<>(Arrays.asList("updated", "deleted")), new
HashSet<>(changedKeys));
+ }
+ }
+}
diff --git
a/hudi-common/src/test/java/org/apache/hudi/metadata/TestHoodieBackedTableMetadataDataCleanup.java
b/hudi-common/src/test/java/org/apache/hudi/metadata/TestHoodieBackedTableMetadataDataCleanup.java
index 03d013f4c586..c6c97c66bb7e 100644
---
a/hudi-common/src/test/java/org/apache/hudi/metadata/TestHoodieBackedTableMetadataDataCleanup.java
+++
b/hudi-common/src/test/java/org/apache/hudi/metadata/TestHoodieBackedTableMetadataDataCleanup.java
@@ -18,23 +18,56 @@
package org.apache.hudi.metadata;
+import org.apache.hudi.avro.model.HoodieMetadataRecord;
+import org.apache.hudi.common.config.HoodieConfig;
+import org.apache.hudi.common.config.HoodieMetadataConfig;
import org.apache.hudi.common.data.HoodieData;
import org.apache.hudi.common.data.HoodieListData;
import org.apache.hudi.common.data.HoodiePairData;
+import org.apache.hudi.common.engine.HoodieEngineContext;
+import org.apache.hudi.common.expression.Expression;
+import org.apache.hudi.common.function.SerializableFunction;
+import org.apache.hudi.common.function.SerializableFunctionUnchecked;
+import org.apache.hudi.common.model.FileSlice;
+import org.apache.hudi.common.model.HoodieBaseFile;
+import org.apache.hudi.common.model.HoodieFileFormat;
+import org.apache.hudi.common.model.HoodieRecord;
+import org.apache.hudi.common.schema.internal.Types;
import org.apache.hudi.common.table.HoodieTableConfig;
import org.apache.hudi.common.table.HoodieTableMetaClient;
+import org.apache.hudi.common.table.read.HoodieFileGroupReader;
+import org.apache.hudi.common.table.timeline.HoodieActiveTimeline;
import org.apache.hudi.common.util.Option;
+import org.apache.hudi.common.util.collection.ClosableIterator;
+import org.apache.hudi.common.util.collection.Pair;
+import org.apache.hudi.core.io.storage.HoodieFileReaderFactory;
+import org.apache.hudi.core.io.storage.HoodieIOFactory;
import org.apache.hudi.exception.HoodieException;
+import org.apache.hudi.storage.HoodieStorage;
+import org.apache.hudi.storage.StorageConfiguration;
+import org.apache.hudi.storage.StoragePathInfo;
+import org.apache.avro.generic.IndexedRecord;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.MockedStatic;
+import java.io.IOException;
import java.lang.reflect.Field;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Stream;
import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.Assertions.fail;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyString;
@@ -239,4 +272,358 @@ public class TestHoodieBackedTableMetadataDataCleanup {
// Verify cleanup manager was called
verify(mockCleanupManager).ensureDataCleanupOnException(any());
}
-}
\ No newline at end of file
+
+ @Test
+ public void testSecondaryIndexUnsupportedVersion() {
+ HoodieData<String> keys =
HoodieListData.eager(Collections.singletonList("key"));
+ HoodieIndexVersion unsupportedVersion = mock(HoodieIndexVersion.class);
+ try (MockedStatic<HoodieTableMetadataUtil> mockedUtil =
mockStatic(HoodieTableMetadataUtil.class)) {
+ mockedUtil.when(() ->
HoodieTableMetadataUtil.existingIndexVersionOrDefault(anyString(), any()))
+ .thenReturn(unsupportedVersion);
+
+ when(mockMetadata.readSecondaryIndexLocationsWithKeys(keys,
"secondary_index_test")).thenCallRealMethod();
+ when(mockMetadata.readSecondaryIndexLocations(keys,
"secondary_index_test")).thenCallRealMethod();
+ when(mockMetadata.readSecondaryIndexDataTableRecordKeysWithKeys(keys,
"secondary_index_test")).thenCallRealMethod();
+
+ assertThrows(IllegalArgumentException.class,
+ () -> mockMetadata.readSecondaryIndexLocationsWithKeys(keys,
"secondary_index_test"));
+ assertThrows(IllegalArgumentException.class,
+ () -> mockMetadata.readSecondaryIndexLocations(keys,
"secondary_index_test"));
+ assertThrows(IllegalArgumentException.class,
+ () ->
mockMetadata.readSecondaryIndexDataTableRecordKeysWithKeys(keys,
"secondary_index_test"));
+ }
+ }
+
+ @Test
+ public void testSecondaryIndexEmptyAndMissingPartition() {
+ HoodieData<String> emptyKeys =
HoodieListData.eager(Collections.emptyList());
+ String partitionName = "secondary_index_test";
+ when(mockMetadata.readSecondaryIndexDataTableRecordKeysWithKeys(emptyKeys,
partitionName)).thenCallRealMethod();
+
+ try (MockedStatic<HoodieTableMetadataUtil> mockedUtil =
mockStatic(HoodieTableMetadataUtil.class)) {
+ mockedUtil.when(() ->
HoodieTableMetadataUtil.existingIndexVersionOrDefault(anyString(), any()))
+ .thenReturn(HoodieIndexVersion.V1);
+
assertTrue(mockMetadata.readSecondaryIndexDataTableRecordKeysWithKeys(emptyKeys,
partitionName)
+ .collectAsList().isEmpty());
+
+ mockedUtil.when(() ->
HoodieTableMetadataUtil.existingIndexVersionOrDefault(anyString(), any()))
+ .thenReturn(HoodieIndexVersion.V2);
+
assertTrue(mockMetadata.readSecondaryIndexDataTableRecordKeysWithKeys(emptyKeys,
partitionName)
+ .collectAsList().isEmpty());
+
+ HoodieData<String> keys =
HoodieListData.eager(Collections.singletonList("key"));
+ mockedUtil.when(() ->
HoodieTableMetadataUtil.existingIndexVersionOrDefault(anyString(), any()))
+ .thenReturn(HoodieIndexVersion.V1);
+
when(mockTableConfig.getMetadataPartitions()).thenReturn(Collections.emptySet());
+ when(mockMetadata.readSecondaryIndexLocationsWithKeys(keys,
partitionName)).thenCallRealMethod();
+ assertThrows(IllegalStateException.class,
+ () -> mockMetadata.readSecondaryIndexLocationsWithKeys(keys,
partitionName));
+ }
+ }
+
+ @Test
+ public void testEmptyRecordIndexAndMetadataTimeline() throws Exception {
+ Field fileSliceMapField =
HoodieBackedTableMetadata.class.getDeclaredField("partitionFileSliceMap");
+ fileSliceMapField.setAccessible(true);
+ Map<String, List<FileSlice>> fileSliceMap = new HashMap<>();
+ fileSliceMap.put(MetadataPartitionType.RECORD_INDEX.getPartitionPath(),
Collections.emptyList());
+ fileSliceMapField.set(mockMetadata, fileSliceMap);
+
+ when(mockMetadata.readRecordIndexLocations(
+
org.mockito.ArgumentMatchers.<SerializableFunctionUnchecked<List<FileSlice>,
List<FileSlice>>>any()))
+ .thenCallRealMethod();
+ assertTrue(mockMetadata.readRecordIndexLocations(slices ->
slices).collectAsList().isEmpty());
+
+ when(mockMetadata.getSyncedInstantTime()).thenCallRealMethod();
+ when(mockMetadata.getLatestCompactionTime()).thenCallRealMethod();
+ assertFalse(mockMetadata.getSyncedInstantTime().isPresent());
+ assertFalse(mockMetadata.getLatestCompactionTime().isPresent());
+ }
+
+ @Test
+ public void testPartitionFilterFallbackAndBucketValidation() throws
Exception {
+ List<String> selectedPartitions = Arrays.asList("year=2025", "year=2026");
+ Expression expression = mock(Expression.class);
+ when(expression.accept(any())).thenReturn(expression);
+
when(mockMetadata.getPartitionPathWithPathPrefixes(any())).thenReturn(selectedPartitions);
+
when(mockMetadata.getPartitionPathWithPathPrefixUsingFilterExpression(any(),
any(), any()))
+ .thenCallRealMethod();
+
+ assertEquals(selectedPartitions,
mockMetadata.getPartitionPathWithPathPrefixUsingFilterExpression(
+ Collections.singletonList("year="), mock(Types.RecordType.class),
expression));
+
+ Field partitionedMapField =
+
HoodieBackedTableMetadata.class.getDeclaredField("partitionedRLIFileSliceMap");
+ partitionedMapField.setAccessible(true);
+ partitionedMapField.set(mockMetadata, new HashMap<>());
+
when(mockMetadata.getBucketizedFileGroupsForPartitionedRLI(any())).thenCallRealMethod();
+ assertThrows(IllegalArgumentException.class,
+ () ->
mockMetadata.getBucketizedFileGroupsForPartitionedRLI(MetadataPartitionType.FILES));
+
+
when(mockMetadata.getFilegroupsForPartition(MetadataPartitionType.RECORD_INDEX))
+ .thenReturn(Collections.emptyList());
+ assertTrue(mockMetadata.getBucketizedFileGroupsForPartitionedRLI(
+ MetadataPartitionType.RECORD_INDEX).isEmpty());
+
+ FileSlice nonPartitionedSlice = mock(FileSlice.class);
+ when(nonPartitionedSlice.getFileId()).thenReturn("record-index-0000");
+
when(mockMetadata.getFilegroupsForPartition(MetadataPartitionType.RECORD_INDEX))
+ .thenReturn(Collections.singletonList(nonPartitionedSlice));
+ assertThrows(IllegalArgumentException.class,
+ () -> mockMetadata.getBucketizedFileGroupsForPartitionedRLI(
+ MetadataPartitionType.RECORD_INDEX));
+ }
+
+ @Test
+ public void testPartitionedRecordIndexLookupGuards() throws Exception {
+ Method lookupMethod = HoodieBackedTableMetadata.class.getDeclaredMethod(
+ "lookupIndexRecords", HoodieData.class, String.class, List.class,
Option.class);
+ lookupMethod.setAccessible(true);
+ FileSlice partitionedSlice = mock(FileSlice.class);
+
when(partitionedSlice.getFileId()).thenReturn("record-index-partition-x-0000");
+ List<FileSlice> slices = Collections.singletonList(partitionedSlice);
+
+ HoodieData<?> emptyResult = (HoodieData<?>) lookupMethod.invoke(
+ mockMetadata,
+ HoodieListData.eager(Collections.emptyList()),
+ MetadataPartitionType.RECORD_INDEX.getPartitionPath(),
+ slices,
+ Option.empty());
+ assertTrue(emptyResult.collectAsList().isEmpty());
+
+ InvocationTargetException exception = assertThrows(
+ InvocationTargetException.class,
+ () -> lookupMethod.invoke(
+ mockMetadata,
+ HoodieListData.eager(Collections.singletonList("key")),
+ MetadataPartitionType.RECORD_INDEX.getPartitionPath(),
+ slices,
+ Option.empty()));
+ assertTrue(exception.getCause() instanceof IllegalArgumentException);
+ }
+
+ @Test
+ public void testSecondaryIndexEmptyIteratorPath() throws Exception {
+ Method method = HoodieBackedTableMetadata.class.getDeclaredMethod(
+ "readSliceAndFilterByKeys", String.class, List.class, FileSlice.class);
+ method.setAccessible(true);
+ Object iterator = method.invoke(
+ mockMetadata,
+ MetadataPartitionType.SECONDARY_INDEX.getPartitionPath() + "test",
+ Collections.emptyList(),
+ mock(FileSlice.class));
+ assertFalse(((org.apache.hudi.common.util.collection.ClosableIterator<?>)
iterator).hasNext());
+ }
+
+ @Test
+ public void testInitializationFailureDisablesMetadata() throws Exception {
+ Field initializedField =
BaseTableMetadata.class.getDeclaredField("isMetadataTableInitialized");
+ initializedField.setAccessible(true);
+ initializedField.set(mockMetadata, true);
+ Field metadataBasePathField =
+ HoodieBackedTableMetadata.class.getDeclaredField("metadataBasePath");
+ metadataBasePathField.setAccessible(true);
+ metadataBasePathField.set(mockMetadata, "/table/.hoodie/metadata");
+ when(mockMetadata.getStorage()).thenReturn(mock(HoodieStorage.class));
+
+ HoodieTableMetaClient.Builder builder =
mock(HoodieTableMetaClient.Builder.class);
+ when(builder.setStorage(any())).thenReturn(builder);
+ when(builder.setBasePath(anyString())).thenReturn(builder);
+ when(builder.build()).thenThrow(new HoodieException("initialization
failed"));
+
+ try (MockedStatic<HoodieTableMetaClient> metaClientStatic =
+ mockStatic(HoodieTableMetaClient.class)) {
+
metaClientStatic.when(HoodieTableMetaClient::builder).thenReturn(builder);
+ Method initMethod =
HoodieBackedTableMetadata.class.getDeclaredMethod("initIfNeeded");
+ initMethod.setAccessible(true);
+ initMethod.invoke(mockMetadata);
+ }
+
+ assertFalse(mockMetadata.isMetadataTableInitialized());
+ }
+
+ @Test
+ @SuppressWarnings({"rawtypes", "unchecked"})
+ public void testSecondaryIndexRecordMapping() throws Exception {
+ prepareFileSliceRead(false);
+ HoodieRecord<HoodieMetadataPayload> metadataRecord =
+ HoodieMetadataPayload.createPartitionFilesRecord(
+ "key", Collections.emptyMap(), Collections.emptyList());
+ IndexedRecord indexedRecord =
+ (IndexedRecord) metadataRecord.getData().getInsertValue(
+ HoodieMetadataRecord.getClassSchema()).get();
+
+ HoodieFileGroupReader.HoodieFileGroupReaderBuilder builder =
+ mock(HoodieFileGroupReader.HoodieFileGroupReaderBuilder.class);
+ HoodieFileGroupReader fileGroupReader = mock(HoodieFileGroupReader.class);
+ when(builder.withReaderContext(any())).thenReturn(builder);
+ when(builder.withHoodieTableMetaClient(any())).thenReturn(builder);
+ when(builder.withLatestCommitTime(anyString())).thenReturn(builder);
+ when(builder.withBaseFileOption(any())).thenReturn(builder);
+ when(builder.withLogFiles(any())).thenReturn(builder);
+ when(builder.withPartitionPath(anyString())).thenReturn(builder);
+ when(builder.withDataSchema(any())).thenReturn(builder);
+ when(builder.withRequestedSchema(any())).thenReturn(builder);
+ when(builder.withProps(any())).thenReturn(builder);
+ when(builder.withRecordBufferLoader(any())).thenReturn(builder);
+ when(builder.build()).thenReturn(fileGroupReader);
+ when(fileGroupReader.getClosableIterator()).thenReturn(
+
ClosableIterator.wrap(Collections.singletonList(indexedRecord).iterator()));
+
+ FileSlice fileSlice = mock(FileSlice.class);
+ when(fileSlice.getPartitionPath()).thenReturn(
+ MetadataPartitionType.SECONDARY_INDEX.getPartitionPath() + "test");
+ when(fileSlice.getBaseFile()).thenReturn(Option.empty());
+ when(fileSlice.getLogFiles()).thenReturn(Stream.empty());
+
+ try (MockedStatic<HoodieFileGroupReader> readerStatic =
+ mockStatic(HoodieFileGroupReader.class)) {
+ readerStatic.when(HoodieFileGroupReader::builder).thenReturn(builder);
+ Method method = HoodieBackedTableMetadata.class.getDeclaredMethod(
+ "readSliceAndFilterByKeys", String.class, List.class,
FileSlice.class);
+ method.setAccessible(true);
+ ClosableIterator<Pair<String, HoodieRecord<HoodieMetadataPayload>>>
iterator =
+ (ClosableIterator<Pair<String,
HoodieRecord<HoodieMetadataPayload>>>) method.invoke(
+ mockMetadata,
+ MetadataPartitionType.SECONDARY_INDEX.getPartitionPath() +
"test",
+ Collections.singletonList("key"),
+ fileSlice);
+
+ assertTrue(iterator.hasNext());
+ assertEquals("key", iterator.next().getLeft());
+ assertFalse(iterator.hasNext());
+ iterator.close();
+
+ when(fileGroupReader.getClosableIterator())
+ .thenThrow(new IOException("iterator failed"));
+ InvocationTargetException exception = assertThrows(
+ InvocationTargetException.class,
+ () -> method.invoke(
+ mockMetadata,
+ MetadataPartitionType.SECONDARY_INDEX.getPartitionPath() +
"test",
+ Collections.singletonList("key"),
+ fileSlice));
+ assertTrue(exception.getCause() instanceof
org.apache.hudi.exception.HoodieIOException);
+
+ Method scanMethod = HoodieBackedTableMetadata.class.getDeclaredMethod(
+ "scanRecordsItr", FileSlice.class,
SerializableFunctionUnchecked.class);
+ scanMethod.setAccessible(true);
+ InvocationTargetException scanException = assertThrows(
+ InvocationTargetException.class,
+ () -> scanMethod.invoke(
+ mockMetadata,
+ fileSlice,
+
(SerializableFunctionUnchecked<org.apache.avro.generic.GenericRecord,
+ HoodieRecord<HoodieMetadataPayload>>) record -> null));
+ assertTrue(scanException.getCause() instanceof
org.apache.hudi.exception.HoodieIOException);
+ }
+ }
+
+ @Test
+ public void testReusableReaderIOExceptionIsWrapped() throws Exception {
+ prepareFileSliceRead(true);
+ HoodieStorage storage = mock(HoodieStorage.class);
+ when(mockMetadata.getStorage()).thenReturn(storage);
+
+ HoodieBaseFile baseFile = mock(HoodieBaseFile.class);
+ when(baseFile.getPathInfo()).thenReturn(mock(StoragePathInfo.class));
+ FileSlice fileSlice = mock(FileSlice.class);
+
when(fileSlice.getPartitionPath()).thenReturn(MetadataPartitionType.FILES.getPartitionPath());
+ when(fileSlice.getFileGroupId()).thenReturn(
+ new org.apache.hudi.common.model.HoodieFileGroupId(
+ MetadataPartitionType.FILES.getPartitionPath(), "file-id"));
+ when(fileSlice.getBaseFile()).thenReturn(Option.of(baseFile));
+
+ HoodieIOFactory ioFactory = mock(HoodieIOFactory.class);
+ HoodieFileReaderFactory readerFactory =
mock(HoodieFileReaderFactory.class);
+ when(ioFactory.getReaderFactory(HoodieRecord.HoodieRecordType.AVRO))
+ .thenReturn(readerFactory);
+ when(readerFactory.getFileReader(
+ any(HoodieConfig.class),
+ any(StoragePathInfo.class),
+ any(HoodieFileFormat.class),
+ any(Option.class)))
+ .thenThrow(new IOException("reader failed"));
+
+ try (MockedStatic<HoodieIOFactory> ioFactoryStatic =
+ mockStatic(HoodieIOFactory.class)) {
+ ioFactoryStatic.when(() ->
HoodieIOFactory.getIOFactory(storage)).thenReturn(ioFactory);
+ Method method = HoodieBackedTableMetadata.class.getDeclaredMethod(
+ "readSliceWithFilter",
+ org.apache.hudi.common.expression.Predicate.class,
+ FileSlice.class);
+ method.setAccessible(true);
+ InvocationTargetException exception = assertThrows(
+ InvocationTargetException.class,
+ () -> method.invoke(
+ mockMetadata,
+ mock(org.apache.hudi.common.expression.Predicate.class),
+ fileSlice));
+ assertTrue(exception.getCause() instanceof
org.apache.hudi.exception.HoodieIOException);
+ }
+ }
+
+ @Test
+ public void testEmptyShardReturnsEmptyIterator() throws Exception {
+ HoodieEngineContext engineContext = mock(HoodieEngineContext.class);
+ HoodieData<String> emptyKeys =
HoodieListData.eager(Collections.emptyList());
+ HoodieData<HoodieRecord<HoodieMetadataPayload>> emptyResult =
+ HoodieListData.eager(Collections.emptyList());
+ when(mockMetadata.getEngineContext()).thenReturn(engineContext);
+ when(engineContext.parallelize(
+ any(List.class),
org.mockito.ArgumentMatchers.eq(1))).thenReturn(emptyKeys);
+ when(engineContext.mapGroupsByKey(any(), any(), any(),
org.mockito.ArgumentMatchers.eq(true)))
+ .thenAnswer(invocation -> {
+ SerializableFunction<java.util.Iterator<String>,
+ java.util.Iterator<HoodieRecord<HoodieMetadataPayload>>>
processFunction =
+ invocation.getArgument(1);
+
assertFalse(processFunction.apply(Collections.emptyIterator()).hasNext());
+ return emptyResult;
+ });
+
+ Method method = HoodieBackedTableMetadata.class.getDeclaredMethod(
+ "lookupIndexRecords", HoodieData.class, String.class, List.class,
+ Option.class);
+ method.setAccessible(true);
+ Object result = method.invoke(
+ mockMetadata,
+ emptyKeys,
+ MetadataPartitionType.FILES.getPartitionPath(),
+ Arrays.asList(mock(FileSlice.class), mock(FileSlice.class)),
+ Option.empty());
+
+ assertEquals(emptyResult, result);
+ }
+
+ private void prepareFileSliceRead(boolean reuse) throws Exception {
+ HoodieTableMetaClient metadataMetaClient =
mock(HoodieTableMetaClient.class);
+ HoodieActiveTimeline timeline = mock(HoodieActiveTimeline.class);
+ when(metadataMetaClient.getActiveTimeline()).thenReturn(timeline);
+ when(timeline.filterCompletedInstants()).thenReturn(timeline);
+ when(timeline.lastInstant()).thenReturn(Option.empty());
+ HoodieTableConfig tableConfig = mock(HoodieTableConfig.class);
+ when(tableConfig.populateMetaFields()).thenReturn(true);
+ when(tableConfig.getBaseFileFormat()).thenReturn(HoodieFileFormat.PARQUET);
+ when(metadataMetaClient.getTableConfig()).thenReturn(tableConfig);
+
+ Field metadataMetaClientField =
+ HoodieBackedTableMetadata.class.getDeclaredField("metadataMetaClient");
+ metadataMetaClientField.setAccessible(true);
+ metadataMetaClientField.set(mockMetadata, metadataMetaClient);
+ Field validInstantsField =
+
HoodieBackedTableMetadata.class.getDeclaredField("validInstantTimestamps");
+ validInstantsField.setAccessible(true);
+ validInstantsField.set(mockMetadata, Collections.singleton("001"));
+ Field reuseField =
HoodieBackedTableMetadata.class.getDeclaredField("reuse");
+ reuseField.setAccessible(true);
+ reuseField.set(mockMetadata, reuse);
+ Field metadataConfigField =
BaseTableMetadata.class.getDeclaredField("metadataConfig");
+ metadataConfigField.setAccessible(true);
+ metadataConfigField.set(
+ mockMetadata, HoodieMetadataConfig.newBuilder().enable(true).build());
+ Field storageConfField =
+ AbstractHoodieTableMetadata.class.getDeclaredField("storageConf");
+ storageConfField.setAccessible(true);
+ storageConfField.set(mockMetadata, mock(StorageConfiguration.class));
+ }
+}
diff --git
a/hudi-common/src/test/java/org/apache/hudi/metadata/TestHoodieTableMetadataUtil.java
b/hudi-common/src/test/java/org/apache/hudi/metadata/TestHoodieTableMetadataUtil.java
index 77bc52a18d58..5b4f921936da 100644
---
a/hudi-common/src/test/java/org/apache/hudi/metadata/TestHoodieTableMetadataUtil.java
+++
b/hudi-common/src/test/java/org/apache/hudi/metadata/TestHoodieTableMetadataUtil.java
@@ -18,26 +18,67 @@
package org.apache.hudi.metadata;
+import org.apache.hudi.avro.model.HoodieInstantInfo;
+import org.apache.hudi.avro.model.HoodieMetadataColumnStats;
import org.apache.hudi.avro.model.HoodieMetadataRecord;
+import org.apache.hudi.avro.model.HoodieRollbackPlan;
import org.apache.hudi.common.avro.HoodieAvroUtils;
+import org.apache.hudi.common.config.HoodieMetadataConfig;
+import org.apache.hudi.common.data.HoodieData;
+import org.apache.hudi.common.data.HoodieListData;
+import org.apache.hudi.common.data.HoodiePairData;
+import org.apache.hudi.common.engine.HoodieEngineContext;
+import org.apache.hudi.common.engine.HoodieLocalEngineContext;
+import org.apache.hudi.common.engine.HoodieReaderContext;
+import org.apache.hudi.common.engine.ReaderContextFactory;
import org.apache.hudi.common.function.SerializableBiFunction;
+import org.apache.hudi.common.model.FileSlice;
+import org.apache.hudi.common.model.HoodieCommitMetadata;
import org.apache.hudi.common.model.HoodieIndexDefinition;
import org.apache.hudi.common.model.HoodieIndexMetadata;
import org.apache.hudi.common.model.HoodieRecord;
import org.apache.hudi.common.model.HoodieRecord.HoodieRecordType;
import org.apache.hudi.common.model.HoodieRecordGlobalLocation;
+import org.apache.hudi.common.model.HoodieWriteStat;
import org.apache.hudi.common.schema.HoodieSchema;
import org.apache.hudi.common.schema.HoodieSchemaField;
import org.apache.hudi.common.schema.HoodieSchemaType;
import org.apache.hudi.common.table.HoodieTableConfig;
import org.apache.hudi.common.table.HoodieTableMetaClient;
import org.apache.hudi.common.table.HoodieTableVersion;
+import org.apache.hudi.common.table.TableSchemaResolver;
+import org.apache.hudi.common.table.read.HoodieFileGroupReader;
+import org.apache.hudi.common.table.timeline.HoodieActiveTimeline;
+import org.apache.hudi.common.table.timeline.HoodieInstant;
import org.apache.hudi.common.table.timeline.HoodieInstantTimeGenerator;
+import org.apache.hudi.common.table.timeline.HoodieTimeline;
+import org.apache.hudi.common.table.timeline.InstantGenerator;
+import org.apache.hudi.common.table.view.HoodieTableFileSystemView;
+import org.apache.hudi.common.util.HoodieStorageUtils;
import org.apache.hudi.common.util.Option;
-
+import org.apache.hudi.common.util.collection.ClosableIterator;
+import org.apache.hudi.common.util.collection.ExternalSpillableMap;
+import org.apache.hudi.common.util.collection.Pair;
+import org.apache.hudi.exception.HoodieMetadataException;
+import org.apache.hudi.exception.HoodieNotSupportedException;
+import org.apache.hudi.metadata.model.FileInfoAndPartition;
+import org.apache.hudi.metadata.stats.HoodieColumnRangeMetadata;
+import org.apache.hudi.storage.HoodieStorage;
+import org.apache.hudi.storage.StorageConfiguration;
+import org.apache.hudi.storage.StoragePath;
+
+import org.apache.avro.AvroTypeException;
+import org.apache.avro.LogicalTypes;
import org.apache.avro.generic.GenericRecord;
import org.junit.jupiter.api.Test;
-
+import org.mockito.MockedConstruction;
+import org.mockito.MockedStatic;
+
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.math.BigDecimal;
import java.util.Arrays;
import java.util.Collections;
import java.util.Date;
@@ -45,6 +86,7 @@ import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.TimeZone;
+import java.util.stream.Stream;
import static
org.apache.hudi.metadata.HoodieTableMetadataUtil.PARTITION_NAME_COLUMN_STATS;
import static
org.apache.hudi.metadata.HoodieTableMetadataUtil.PARTITION_NAME_PARTITION_STATS;
@@ -52,8 +94,17 @@ import static
org.apache.hudi.metadata.HoodieTableMetadataUtil.getIndexVersionOp
import static
org.apache.hudi.metadata.SecondaryIndexKeyUtils.constructSecondaryIndexKey;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
+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.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.anyList;
import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.mockConstruction;
+import static org.mockito.Mockito.mockStatic;
+import static org.mockito.Mockito.reset;
+import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
class TestHoodieTableMetadataUtil {
@@ -432,4 +483,519 @@ class TestHoodieTableMetadataUtil {
TimeZone.setDefault(originalTimeZone);
}
}
+
+ @Test
+ void testColumnStatsValueValidation() {
+
assertFalse(HoodieTableMetadataUtil.getColumnStatsValueAsString(null).isPresent());
+ assertThrows(HoodieNotSupportedException.class,
+ () -> HoodieTableMetadataUtil.getColumnStatsValueAsString(new
Object()));
+ }
+
+ @Test
+ void testWritePartitionPathsIncludeNonPartitionedTableIdentifier() {
+ HoodieCommitMetadata commitMetadata = new HoodieCommitMetadata();
+ commitMetadata.addWriteStat("", new HoodieWriteStat());
+ commitMetadata.addWriteStat("year=2026", new HoodieWriteStat());
+
+ assertEquals(
+ new java.util.HashSet<>(Arrays.asList("", "year=2026")),
+
HoodieTableMetadataUtil.getWritePartitionPaths(Collections.singletonList(commitMetadata)));
+ }
+
+ @Test
+ void testDecimalUpcastValidation() {
+ assertThrows(AvroTypeException.class,
+ () -> HoodieTableMetadataUtil.tryUpcastDecimal(
+ new BigDecimal("1.23"), LogicalTypes.decimal(5, 1)));
+ assertThrows(AvroTypeException.class,
+ () -> HoodieTableMetadataUtil.tryUpcastDecimal(
+ new BigDecimal("123"), LogicalTypes.decimal(3, 1)));
+ assertThrows(AvroTypeException.class,
+ () -> HoodieTableMetadataUtil.tryUpcastDecimal(
+ new BigDecimal("1234"), LogicalTypes.decimal(3, 0)));
+ }
+
+ @Test
+ void testComparableCoercion() {
+ assertNull(HoodieTableMetadataUtil.coerceToComparable(
+ HoodieSchema.create(HoodieSchemaType.INT), null));
+ assertEquals(1, HoodieTableMetadataUtil.coerceToComparable(
+ HoodieSchema.create(HoodieSchemaType.INT), true));
+ assertEquals(0L, HoodieTableMetadataUtil.coerceToComparable(
+ HoodieSchema.create(HoodieSchemaType.LONG), false));
+ assertEquals(1.5f, HoodieTableMetadataUtil.coerceToComparable(
+ HoodieSchema.create(HoodieSchemaType.FLOAT), 1.5d));
+ assertEquals(2.5d, HoodieTableMetadataUtil.coerceToComparable(
+ HoodieSchema.create(HoodieSchemaType.DOUBLE), 2.5f));
+ assertEquals(1.0f, HoodieTableMetadataUtil.coerceToComparable(
+ HoodieSchema.create(HoodieSchemaType.FLOAT), true));
+ assertEquals(0.0d, HoodieTableMetadataUtil.coerceToComparable(
+ HoodieSchema.create(HoodieSchemaType.DOUBLE), false));
+ assertNull(HoodieTableMetadataUtil.coerceToComparable(
+ HoodieSchema.create(HoodieSchemaType.NULL), "ignored"));
+ }
+
+ @Test
+ void testFileGroupCountBoundsAndInflightWriteStatusTracking() {
+ assertEquals(10, HoodieTableMetadataUtil.estimateFileGroupCount(
+ MetadataPartitionType.RECORD_INDEX, () -> 10_000L, 1, 1, 10, 1.0f,
100));
+ assertEquals(5, HoodieTableMetadataUtil.estimateFileGroupCount(
+ MetadataPartitionType.RECORD_INDEX, () -> 500L, 1, 2, 10, 1.0f, 100));
+
+ HoodieTableMetaClient metaClient = mock(HoodieTableMetaClient.class);
+ HoodieTableConfig tableConfig = mock(HoodieTableConfig.class);
+ when(metaClient.getTableConfig()).thenReturn(tableConfig);
+
when(tableConfig.isMetadataPartitionAvailable(MetadataPartitionType.RECORD_INDEX)).thenReturn(false);
+ when(tableConfig.getMetadataPartitionsInflight())
+
.thenReturn(Collections.singleton(MetadataPartitionType.RECORD_INDEX.getPartitionPath()));
+
+
assertTrue(HoodieTableMetadataUtil.getMetadataPartitionsNeedingWriteStatusTracking(
+ HoodieMetadataConfig.newBuilder().enable(false).build(), metaClient));
+ }
+
+ @Test
+ @SuppressWarnings("deprecation")
+ void testGenerateKeyPrefixesMatchesRawKeyEncoding() {
+ List<String> columns = Arrays.asList("c1", "c2");
+ assertEquals(
+ HoodieTableMetadataUtil.generateColumnStatsKeys(columns,
"partition").stream()
+ .map(ColumnStatsIndexPrefixRawKey::encode)
+ .collect(java.util.stream.Collectors.toList()),
+ HoodieTableMetadataUtil.generateKeyPrefixes(columns, "partition"));
+ }
+
+ @Test
+ void testCollectColumnRangeMetadata() {
+ HoodieSchema recordSchema = mock(HoodieSchema.class);
+ StorageConfiguration<?> storageConfig = mock(StorageConfiguration.class);
+ when(storageConfig.getString(
+
org.apache.hudi.common.config.HoodieStorageConfig.WRITE_UTC_TIMEZONE.key(),
+
org.apache.hudi.common.config.HoodieStorageConfig.WRITE_UTC_TIMEZONE.defaultValue().toString()))
+ .thenReturn("UTC");
+
+ HoodieRecord<?> record = mock(HoodieRecord.class);
+ when(record.getRecordType()).thenReturn(HoodieRecordType.FLINK);
+ when(record.getColumnValueAsJava(
+ org.mockito.ArgumentMatchers.eq(recordSchema),
+ org.mockito.ArgumentMatchers.eq("id"),
+ org.mockito.ArgumentMatchers.any()))
+ .thenReturn(7);
+
+ HoodieSchemaField idField = HoodieSchemaField.of(
+ "id", HoodieSchema.create(HoodieSchemaType.INT), null, null);
+ HoodieSchemaField unsupportedField = HoodieSchemaField.of(
+ "attributes",
+ HoodieSchema.createMap(HoodieSchema.create(HoodieSchemaType.STRING)),
+ null,
+ null);
+ Map<String, HoodieColumnRangeMetadata<Comparable>> stats =
+ HoodieTableMetadataUtil.collectColumnRangeMetadata(
+ Collections.<HoodieRecord>singletonList(record).iterator(),
+ Arrays.asList(Pair.of("id", idField), Pair.of("attributes",
unsupportedField)),
+ "file.parquet",
+ recordSchema,
+ storageConfig,
+ HoodieIndexVersion.V1);
+
+ assertEquals(7, stats.get("id").getMinValue());
+ assertEquals(7, stats.get("id").getMaxValue());
+ assertNull(stats.get("attributes").getMinValue());
+ }
+
+ @Test
+ void testFilePartitionRecordConversionHandlesDeletesAndAppends() {
+ Map<String, List<String>> deletedFiles = new HashMap<>();
+ deletedFiles.put("p1", Collections.singletonList("old.parquet"));
+ Map<String, List<FileInfoAndPartition>> appendedFiles = new HashMap<>();
+ appendedFiles.put("p1", Collections.singletonList(
+ FileInfoAndPartition.of("p1", "new.parquet", 10L)));
+ appendedFiles.put("p2", Collections.singletonList(
+ FileInfoAndPartition.of("p2", "other.parquet", 20L)));
+
+ List<HoodieRecord> records =
HoodieTableMetadataUtil.convertFilesToFilesPartitionRecords(
+ deletedFiles, appendedFiles, "001", "test");
+
+ assertEquals(2, records.size());
+ assertFalse(appendedFiles.containsKey("p1"));
+ }
+
+ @Test
+ void testBloomAndColumnStatsConversionFast() {
+ HoodieLocalEngineContext engineContext =
+ new HoodieLocalEngineContext(mock(StorageConfiguration.class));
+ Map<String, List<String>> deletedFiles = new HashMap<>();
+ deletedFiles.put("p1", Arrays.asList("file.log.1",
"file_1-0-1_001.parquet"));
+
+ HoodieData<HoodieRecord> records =
HoodieTableMetadataUtil.convertFilesToBloomFilterRecords(
+ engineContext,
+ deletedFiles,
+ Collections.emptyMap(),
+ "001",
+ mock(HoodieTableMetaClient.class),
+ 2,
+ "SIMPLE");
+ assertEquals(1, records.collectAsList().size());
+
+ assertTrue(HoodieTableMetadataUtil.convertFilesToColumnStatsRecords(
+ engineContext,
+ Collections.emptyMap(),
+ Collections.emptyMap(),
+ mock(HoodieTableMetaClient.class),
+ 1,
+ 1024,
+ Collections.singletonList("id")).collectAsList().isEmpty());
+ }
+
+ @Test
+ void testFilesPartitionAvailability() {
+ HoodieTableMetaClient metaClient = mock(HoodieTableMetaClient.class);
+ HoodieTableConfig tableConfig = mock(HoodieTableConfig.class);
+ when(metaClient.getTableConfig()).thenReturn(tableConfig);
+ when(tableConfig.getMetadataPartitions())
+
.thenReturn(Collections.singleton(HoodieTableMetadataUtil.PARTITION_NAME_FILES));
+
+ assertTrue(HoodieTableMetadataUtil.isFilesPartitionAvailable(metaClient));
+ }
+
+ @Test
+ void testMetadataTableDeletionOutcomes() throws Exception {
+ HoodieTableMetaClient metaClient = mock(HoodieTableMetaClient.class);
+ HoodieTableConfig tableConfig = mock(HoodieTableConfig.class);
+ HoodieStorage storage = mock(HoodieStorage.class);
+ when(metaClient.getBasePath()).thenReturn(new StoragePath("/table"));
+ when(metaClient.getTableConfig()).thenReturn(tableConfig);
+ when(metaClient.getStorage()).thenReturn(storage);
+
+ when(storage.exists(any(StoragePath.class))).thenReturn(false);
+ assertNull(HoodieTableMetadataUtil.deleteMetadataTable(metaClient, null,
false));
+
+ reset(storage);
+ when(storage.exists(any(StoragePath.class))).thenThrow(new
FileNotFoundException("missing"));
+ assertNull(HoodieTableMetadataUtil.deleteMetadataTable(metaClient, null,
false));
+
+ reset(storage);
+ when(storage.exists(any(StoragePath.class))).thenThrow(new
IOException("check failed"));
+ assertThrows(HoodieMetadataException.class,
+ () -> HoodieTableMetadataUtil.deleteMetadataTable(metaClient, null,
false));
+
+ reset(storage);
+ when(storage.exists(any(StoragePath.class))).thenReturn(true);
+ when(storage.rename(any(StoragePath.class),
any(StoragePath.class))).thenReturn(true);
+ assertTrue(HoodieTableMetadataUtil.deleteMetadataTable(metaClient, null,
true)
+ .contains(".metadata_"));
+
+ reset(storage);
+ when(storage.exists(any(StoragePath.class))).thenReturn(true);
+ when(storage.rename(any(StoragePath.class), any(StoragePath.class)))
+ .thenThrow(new IOException("rename failed"));
+ assertNull(HoodieTableMetadataUtil.deleteMetadataTable(metaClient, null,
true));
+
+ reset(storage);
+ when(storage.exists(any(StoragePath.class))).thenReturn(true);
+ org.mockito.Mockito.doThrow(new IOException("delete failed"))
+ .when(storage).deleteDirectory(any(StoragePath.class));
+ assertThrows(HoodieMetadataException.class,
+ () -> HoodieTableMetadataUtil.deleteMetadataTable(metaClient, null,
false));
+ }
+
+ @Test
+ void testMetadataPartitionDeletionOutcomes() throws Exception {
+ HoodieTableMetaClient metaClient = mock(HoodieTableMetaClient.class);
+ HoodieTableConfig tableConfig = mock(HoodieTableConfig.class);
+ HoodieStorage storage = mock(HoodieStorage.class);
+ when(metaClient.getBasePath()).thenReturn(new StoragePath("/table"));
+ when(metaClient.getTableConfig()).thenReturn(tableConfig);
+ when(metaClient.getStorage()).thenReturn(storage);
+ String partition = MetadataPartitionType.COLUMN_STATS.getPartitionPath();
+
+ when(storage.exists(any(StoragePath.class))).thenReturn(false);
+ assertNull(HoodieTableMetadataUtil.deleteMetadataTablePartition(
+ metaClient, null, MetadataPartitionType.FILES.getPartitionPath(),
false));
+
+ reset(storage);
+ when(storage.exists(any(StoragePath.class))).thenThrow(new
FileNotFoundException("missing"));
+ assertNull(HoodieTableMetadataUtil.deleteMetadataTablePartition(
+ metaClient, null, partition, false));
+
+ reset(storage);
+ when(storage.exists(any(StoragePath.class))).thenThrow(new
IOException("check failed"));
+ assertThrows(HoodieMetadataException.class,
+ () -> HoodieTableMetadataUtil.deleteMetadataTablePartition(
+ metaClient, null, partition, false));
+
+ reset(storage);
+ when(storage.exists(any(StoragePath.class))).thenReturn(true);
+ when(storage.rename(any(StoragePath.class),
any(StoragePath.class))).thenReturn(true);
+ assertTrue(HoodieTableMetadataUtil.deleteMetadataTablePartition(
+ metaClient, null, partition, true).contains(".metadata_"));
+
+ reset(storage);
+ when(storage.exists(any(StoragePath.class))).thenReturn(true);
+ when(storage.rename(any(StoragePath.class), any(StoragePath.class)))
+ .thenThrow(new IOException("rename failed"));
+ assertNull(HoodieTableMetadataUtil.deleteMetadataTablePartition(
+ metaClient, null, partition, true));
+
+ reset(storage);
+ when(storage.exists(any(StoragePath.class))).thenReturn(true);
+ org.mockito.Mockito.doThrow(new IOException("delete failed"))
+ .when(storage).deleteDirectory(any(StoragePath.class));
+ assertThrows(HoodieMetadataException.class,
+ () -> HoodieTableMetadataUtil.deleteMetadataTablePartition(
+ metaClient, null, partition, false));
+ }
+
+ @Test
+ void testRecordKeyReadSchemaFailureIsWrapped() {
+ HoodieEngineContext engineContext = mock(HoodieEngineContext.class);
+ HoodieTableMetaClient metaClient = mock(HoodieTableMetaClient.class);
+ when(metaClient.getBasePath()).thenReturn(new StoragePath("/table"));
+
when(metaClient.getStorageConf()).thenReturn(mock(StorageConfiguration.class));
+
+ assertThrows(org.apache.hudi.exception.HoodieException.class,
+ () -> HoodieTableMetadataUtil.readRecordKeysFromFileSlices(
+ engineContext,
+ Collections.singletonList(Pair.of("partition",
mock(FileSlice.class))),
+ 1,
+ "test",
+ metaClient,
+ false));
+ }
+
+ @Test
+ @SuppressWarnings("unchecked")
+ void testPartitionStatsConversionFailureIsWrapped() {
+ HoodiePairData<String, List<HoodieColumnRangeMetadata<Comparable>>>
pairData =
+ mock(HoodiePairData.class);
+ when(pairData.flatMapValues(any())).thenThrow(new
RuntimeException("conversion failed"));
+
+ assertThrows(org.apache.hudi.exception.HoodieException.class,
+ () -> HoodieTableMetadataUtil.convertMetadataToPartitionStatsRecords(
+ pairData,
+ mock(HoodieTableMetaClient.class),
+ Collections.emptyMap(),
+ HoodieIndexVersion.V1));
+ }
+
+ @Test
+ void testMergeColumnStatsTombstoneWins() {
+ HoodieMetadataColumnStats previous = HoodieMetadataColumnStats.newBuilder()
+ .setColumnName("column")
+ .setIsDeleted(false)
+ .build();
+ HoodieMetadataColumnStats tombstone =
HoodieMetadataColumnStats.newBuilder()
+ .setColumnName("column")
+ .setIsDeleted(true)
+ .build();
+
+ assertEquals(tombstone,
HoodieTableMetadataUtil.mergeColumnStatsRecords(previous, tombstone));
+ assertEquals(previous,
HoodieTableMetadataUtil.mergeColumnStatsRecords(tombstone, previous));
+ }
+
+ @Test
+ void testFileSliceAndSchemaResolutionEdge() {
+ HoodieTableMetaClient metaClient = mock(HoodieTableMetaClient.class);
+ HoodieTimeline timeline = mock(HoodieTimeline.class);
+ when(metaClient.getCommitsTimeline()).thenReturn(timeline);
+ when(timeline.filterCompletedInstants()).thenReturn(timeline);
+ when(timeline.countInstants()).thenReturn(1);
+ when(metaClient.getBasePath()).thenReturn(new StoragePath("/table"));
+ assertThrows(org.apache.hudi.exception.HoodieException.class,
+ () -> HoodieTableMetadataUtil.tryResolveSchemaForTable(metaClient));
+
+ HoodieTableFileSystemView fileSystemView =
mock(HoodieTableFileSystemView.class);
+ HoodieActiveTimeline activeTimeline = mock(HoodieActiveTimeline.class);
+ when(metaClient.getActiveTimeline()).thenReturn(activeTimeline);
+ when(activeTimeline.filterCompletedInstants()).thenReturn(activeTimeline);
+ when(activeTimeline.lastInstant()).thenReturn(Option.empty());
+ assertTrue(HoodieTableMetadataUtil.getPartitionLatestMergedFileSlices(
+ metaClient, fileSystemView, "files").isEmpty());
+ }
+
+ @Test
+ void testEmptyLogInputsAvoidReaderConstruction() {
+ Pair<java.util.Set<String>, java.util.Set<String>> changes =
+ HoodieTableMetadataUtil.getRevivedAndDeletedKeysFromMergedLogs(
+ mock(HoodieTableMetaClient.class),
+ "001",
+ Collections.singletonList("previous.log"),
+ Option.empty(),
+ Collections.singletonList("current.log"),
+ "partition",
+ mock(org.apache.hudi.common.engine.HoodieReaderContext.class));
+
+ assertTrue(changes.getLeft().isEmpty());
+ assertTrue(changes.getRight().isEmpty());
+ }
+
+ @Test
+ void testRollbackPlanFallbackAndReadFailure() throws Exception {
+ Method method = HoodieTableMetadataUtil.class.getDeclaredMethod(
+ "getRollbackedCommits",
+ HoodieInstant.class,
+ HoodieActiveTimeline.class,
+ InstantGenerator.class);
+ method.setAccessible(true);
+
+ HoodieInstant completed = mock(HoodieInstant.class);
+ HoodieInstant requested = mock(HoodieInstant.class);
+ HoodieActiveTimeline timeline = mock(HoodieActiveTimeline.class);
+ InstantGenerator instantGenerator = mock(InstantGenerator.class);
+ HoodieRollbackPlan rollbackPlan = mock(HoodieRollbackPlan.class);
+ HoodieInstantInfo instantInfo = mock(HoodieInstantInfo.class);
+ when(completed.getAction()).thenReturn(HoodieTimeline.ROLLBACK_ACTION);
+ when(completed.requestedTime()).thenReturn("002");
+ when(timeline.readRollbackMetadata(completed)).thenThrow(new
IOException("empty rollback"));
+ when(instantGenerator.createNewInstant(
+ HoodieInstant.State.REQUESTED, HoodieTimeline.ROLLBACK_ACTION, "002"))
+ .thenReturn(requested);
+ when(timeline.readRollbackPlan(requested)).thenReturn(rollbackPlan);
+ when(rollbackPlan.getInstantToRollback()).thenReturn(instantInfo);
+ when(instantInfo.getCommitTime()).thenReturn("001");
+ assertEquals(Collections.singletonList("001"), method.invoke(
+ null, completed, timeline, instantGenerator));
+
+ when(completed.getAction()).thenReturn(HoodieTimeline.RESTORE_ACTION);
+ when(timeline.readRestoreMetadata(completed)).thenThrow(new
IOException("broken restore"));
+ InvocationTargetException exception = assertThrows(
+ InvocationTargetException.class,
+ () -> method.invoke(null, completed, timeline, instantGenerator));
+ assertTrue(exception.getCause() instanceof HoodieMetadataException);
+ }
+
+ @Test
+ void testMetadataPartitionExistenceFailureIsWrapped() throws Exception {
+ HoodieEngineContext context = mock(HoodieEngineContext.class);
+ StorageConfiguration<?> storageConfiguration =
mock(StorageConfiguration.class);
+ HoodieStorage storage = mock(HoodieStorage.class);
+
org.mockito.Mockito.doReturn(storageConfiguration).when(context).getStorageConf();
+ when(storage.exists(any(StoragePath.class))).thenThrow(new
IOException("failed"));
+
+ try (MockedStatic<HoodieStorageUtils> storageUtils =
mockStatic(HoodieStorageUtils.class)) {
+ storageUtils.when(() -> HoodieStorageUtils.getStorage(any(String.class),
any()))
+ .thenReturn(storage);
+ assertThrows(org.apache.hudi.exception.HoodieIOException.class,
+ () -> HoodieTableMetadataUtil.metadataPartitionExists(
+ "/table", context,
MetadataPartitionType.FILES.getPartitionPath()));
+ }
+ }
+
+ @Test
+ @SuppressWarnings("unchecked")
+ void testDeletedFileStatsCreateStubs() throws Exception {
+ Method method = HoodieTableMetadataUtil.class.getDeclaredMethod(
+ "getFileStatsRangeMetadata",
+ String.class,
+ String.class,
+ HoodieTableMetaClient.class,
+ List.class,
+ boolean.class,
+ int.class,
+ HoodieIndexVersion.class);
+ method.setAccessible(true);
+
+ List<HoodieColumnRangeMetadata<Comparable>> stats =
+ (List<HoodieColumnRangeMetadata<Comparable>>) method.invoke(
+ null,
+ "partition",
+ "file.parquet",
+ mock(HoodieTableMetaClient.class),
+ Arrays.asList("c1", "c2"),
+ true,
+ 1024,
+ HoodieIndexVersion.V1);
+ assertEquals(2, stats.size());
+ }
+
+ @Test
+ void testCommitPartitionExtraction() throws Exception {
+ HoodieCommitMetadata commitMetadata = new HoodieCommitMetadata();
+ commitMetadata.addWriteStat("", new HoodieWriteStat());
+ commitMetadata.addWriteStat("partition", new HoodieWriteStat());
+ Method method = HoodieTableMetadataUtil.class.getDeclaredMethod(
+ "getPartitionsAdded", HoodieCommitMetadata.class);
+ method.setAccessible(true);
+
+ assertEquals(
+ new
java.util.HashSet<>(Arrays.asList(HoodieTableMetadata.NON_PARTITIONED_NAME,
"partition")),
+ new java.util.HashSet<>((List<String>) method.invoke(null,
commitMetadata)));
+ }
+
+ @Test
+ void testInflightFileSliceViewIsClosedWhenCreatedInternally() {
+ HoodieTableMetaClient metaClient = mock(HoodieTableMetaClient.class);
+ HoodieTableFileSystemView fileSystemView =
mock(HoodieTableFileSystemView.class);
+ when(fileSystemView.getLatestFileSlicesIncludingInflight("files"))
+ .thenReturn(Stream.empty());
+
+ try (MockedStatic<HoodieTableMetadataUtil> util =
+ mockStatic(HoodieTableMetadataUtil.class,
org.mockito.Answers.CALLS_REAL_METHODS)) {
+ util.when(() ->
HoodieTableMetadataUtil.getFileSystemViewForMetadataTable(metaClient))
+ .thenReturn(fileSystemView);
+
assertTrue(HoodieTableMetadataUtil.getPartitionLatestFileSlicesIncludingInflight(
+ metaClient, Option.empty(), "files").isEmpty());
+ }
+
+ verify(fileSystemView).close();
+ }
+
+ @Test
+ @SuppressWarnings({"rawtypes", "unchecked"})
+ void testLogOnlyFileSliceRecordKeys() throws Exception {
+ HoodieEngineContext engineContext = mock(HoodieEngineContext.class);
+ HoodieTableMetaClient metaClient = mock(HoodieTableMetaClient.class);
+ HoodieSchema schema = mock(HoodieSchema.class);
+ FileSlice fileSlice = mock(FileSlice.class);
+ List<Pair<String, FileSlice>> slices =
+ Collections.singletonList(Pair.of("partition", fileSlice));
+ when(metaClient.getBasePath()).thenReturn(new StoragePath("/table"));
+ StorageConfiguration<?> storageConfiguration =
mock(StorageConfiguration.class);
+ when(storageConfiguration.getEnum(
+ any(String.class), any(ExternalSpillableMap.DiskMapType.class)))
+ .thenAnswer(invocation -> invocation.getArgument(1));
+
org.mockito.Mockito.doReturn(storageConfiguration).when(metaClient).getStorageConf();
+ HoodieActiveTimeline timeline = mock(HoodieActiveTimeline.class);
+ when(metaClient.getActiveTimeline()).thenReturn(timeline);
+ when(timeline.filterCompletedInstants()).thenReturn(timeline);
+ when(timeline.lastInstant()).thenReturn(Option.empty());
+ when(fileSlice.getBaseFile()).thenReturn(Option.empty());
+ when(fileSlice.getLogFiles()).thenReturn(Stream.empty());
+ when(fileSlice.getPartitionPath()).thenReturn("partition");
+ when(fileSlice.getFileId()).thenReturn("file-id");
+ when(fileSlice.getBaseInstantTime()).thenReturn("20240101000000000");
+ org.mockito.Mockito.doReturn(HoodieListData.eager(slices))
+ .when(engineContext).parallelize(anyList(), anyInt());
+
+ ReaderContextFactory readerContextFactory =
mock(ReaderContextFactory.class);
+ HoodieReaderContext readerContext = mock(HoodieReaderContext.class);
+ org.mockito.Mockito.doReturn(readerContextFactory)
+ .when(engineContext).getReaderContextFactory(metaClient);
+ when(readerContextFactory.getContext()).thenReturn(readerContext);
+
+ HoodieFileGroupReader.HoodieFileGroupReaderBuilder builder =
+ mock(HoodieFileGroupReader.HoodieFileGroupReaderBuilder.class);
+ HoodieFileGroupReader fileGroupReader = mock(HoodieFileGroupReader.class);
+ when(builder.withReaderContext(any())).thenReturn(builder);
+ when(builder.withHoodieTableMetaClient(any())).thenReturn(builder);
+ when(builder.withBaseFileOption(any())).thenReturn(builder);
+ when(builder.withLogFiles(any())).thenReturn(builder);
+ when(builder.withPartitionPath(any())).thenReturn(builder);
+ when(builder.withDataSchema(any())).thenReturn(builder);
+ when(builder.withRequestedSchema(any())).thenReturn(builder);
+ when(builder.withLatestCommitTime(any())).thenReturn(builder);
+ when(builder.withProps(any())).thenReturn(builder);
+ when(builder.build()).thenReturn(fileGroupReader);
+ when(fileGroupReader.getClosableKeyIterator())
+ .thenReturn(ClosableIterator.wrap(Collections.emptyIterator()));
+
+ try (MockedConstruction<TableSchemaResolver> ignored =
+ mockConstruction(TableSchemaResolver.class,
+ (resolver, context) ->
when(resolver.getTableSchema()).thenReturn(schema));
+ MockedStatic<HoodieFileGroupReader> readerStatic =
+ mockStatic(HoodieFileGroupReader.class)) {
+ readerStatic.when(HoodieFileGroupReader::builder).thenReturn(builder);
+ assertTrue(HoodieTableMetadataUtil.readRecordKeysFromFileSlices(
+ engineContext, slices, 1, "test", metaClient,
false).collectAsList().isEmpty());
+ }
+ }
}
diff --git
a/hudi-common/src/test/java/org/apache/hudi/metadata/TestMetadataPartitionType.java
b/hudi-common/src/test/java/org/apache/hudi/metadata/TestMetadataPartitionType.java
index 92ce2d2693b7..8aa5879794c8 100644
---
a/hudi-common/src/test/java/org/apache/hudi/metadata/TestMetadataPartitionType.java
+++
b/hudi-common/src/test/java/org/apache/hudi/metadata/TestMetadataPartitionType.java
@@ -19,6 +19,8 @@
package org.apache.hudi.metadata;
+import org.apache.hudi.avro.model.HoodieMetadataBloomFilter;
+import org.apache.hudi.avro.model.HoodieMetadataRecord;
import org.apache.hudi.common.config.HoodieMetadataConfig;
import org.apache.hudi.common.model.HoodieIndexDefinition;
import org.apache.hudi.common.model.HoodieIndexMetadata;
@@ -28,6 +30,8 @@ import org.apache.hudi.common.table.HoodieTableVersion;
import org.apache.hudi.common.util.Option;
import org.apache.hudi.common.util.StringUtils;
+import org.apache.avro.Schema;
+import org.apache.avro.generic.GenericData;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
@@ -35,6 +39,7 @@ import org.junit.jupiter.params.provider.EnumSource;
import org.junit.jupiter.params.provider.MethodSource;
import org.mockito.Mockito;
+import java.nio.ByteBuffer;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
@@ -320,4 +325,76 @@ public class TestMetadataPartitionType {
}
}
}
+
+ @Test
+ public void testProjectedPayloadConstruction() {
+ HoodieMetadataPayload payload = new HoodieMetadataPayload(Option.empty());
+ Schema projectedSchema = Schema.createRecord("ProjectedMetadataRecord",
null, null, false);
+ projectedSchema.setFields(Collections.emptyList());
+ GenericData.Record projectedRecord = new
GenericData.Record(projectedSchema);
+
+ MetadataPartitionType.FILES.constructMetadataPayload(payload,
projectedRecord);
+ MetadataPartitionType.COLUMN_STATS.constructMetadataPayload(payload,
projectedRecord);
+ assertThrows(UnsupportedOperationException.class,
+ () ->
MetadataPartitionType.EXPRESSION_INDEX.constructMetadataPayload(payload,
projectedRecord));
+
+ GenericData.Record invalidBloomFilterRecord =
+ new GenericData.Record(HoodieMetadataRecord.getClassSchema());
+ assertThrows(IllegalArgumentException.class,
+ () ->
MetadataPartitionType.BLOOM_FILTERS.constructMetadataPayload(payload,
invalidBloomFilterRecord));
+ assertThrows(IllegalArgumentException.class, () ->
MetadataPartitionType.get(Integer.MAX_VALUE));
+ }
+
+ @Test
+ public void testBloomFilterCombinationAndAllPartitionsEnablement() {
+ HoodieMetadataPayload older = new HoodieMetadataPayload("key",
+ new HoodieMetadataBloomFilter("SIMPLE", "1", ByteBuffer.wrap(new
byte[] {1}), false));
+ HoodieMetadataPayload newer = new HoodieMetadataPayload("key",
+ new HoodieMetadataBloomFilter("SIMPLE", "2", ByteBuffer.wrap(new
byte[] {2}), false));
+
+ HoodieMetadataPayload combined =
MetadataPartitionType.BLOOM_FILTERS.combineMetadataPayloads(older, newer);
+
+ assertEquals(newer.getBloomFilterMetadata(),
combined.getBloomFilterMetadata());
+ assertTrue(MetadataPartitionType.ALL_PARTITIONS.isMetadataPartitionEnabled(
+ HoodieMetadataConfig.newBuilder().enable(true).build(),
mock(HoodieTableConfig.class)));
+ }
+
+ @Test
+ public void testNewIndexDefinitionChecks() {
+ HoodieTableMetaClient metaClient = mock(HoodieTableMetaClient.class);
+ HoodieIndexDefinition secondaryIndex = createIndexDefinition(
+ MetadataPartitionType.SECONDARY_INDEX, "existing",
HoodieTableMetadataUtil.PARTITION_NAME_SECONDARY_INDEX,
+ null, Collections.singletonList("secondary_col"), null);
+ HoodieIndexDefinition expressionIndex = createIndexDefinition(
+ MetadataPartitionType.EXPRESSION_INDEX, "existing",
HoodieTableMetadataUtil.PARTITION_NAME_EXPRESSION_INDEX,
+ "lower", Collections.singletonList("expression_col"), null);
+ HoodieIndexMetadata indexMetadata = new
HoodieIndexMetadata(createIndexDefinitions(secondaryIndex, expressionIndex));
+ when(metaClient.getIndexMetadata()).thenReturn(Option.of(indexMetadata));
+
+ HoodieMetadataConfig secondaryConfig = HoodieMetadataConfig.newBuilder()
+ .withSecondaryIndexForColumn("secondary_col")
+ .build();
+
assertFalse(MetadataPartitionType.isNewSecondaryIndexDefinitionRequired(secondaryConfig,
metaClient));
+
+ HoodieMetadataConfig expressionConfig = HoodieMetadataConfig.newBuilder()
+ .withExpressionIndexColumn("expression_col")
+ .withExpressionIndexOptions(Collections.singletonMap("expr", "lower"))
+ .build();
+
assertFalse(MetadataPartitionType.isNewExpressionIndexDefinitionRequired(expressionConfig,
metaClient));
+
+ HoodieMetadataConfig differentExpressionConfig =
HoodieMetadataConfig.newBuilder()
+ .withExpressionIndexColumn("expression_col")
+ .withExpressionIndexOptions(Collections.singletonMap("expr", "upper"))
+ .build();
+
assertTrue(MetadataPartitionType.isNewExpressionIndexDefinitionRequired(differentExpressionConfig,
metaClient));
+
+ HoodieMetadataConfig noExpressionConfig = HoodieMetadataConfig.newBuilder()
+ .withExpressionIndexColumn("expression_col")
+ .build();
+
assertFalse(MetadataPartitionType.isNewExpressionIndexDefinitionRequired(noExpressionConfig,
metaClient));
+ }
+
+ private static Map<String, HoodieIndexDefinition>
createIndexDefinitions(HoodieIndexDefinition... definitions) {
+ return
Arrays.stream(definitions).collect(Collectors.toMap(HoodieIndexDefinition::getIndexName,
definition -> definition));
+ }
}
diff --git
a/hudi-hadoop-common/src/test/java/org/apache/hudi/metadata/TestBaseTableMetadata.java
b/hudi-hadoop-common/src/test/java/org/apache/hudi/metadata/TestBaseTableMetadata.java
new file mode 100644
index 000000000000..f8115530e856
--- /dev/null
+++
b/hudi-hadoop-common/src/test/java/org/apache/hudi/metadata/TestBaseTableMetadata.java
@@ -0,0 +1,299 @@
+/*
+ * 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.metadata;
+
+import org.apache.hudi.common.config.HoodieMetadataConfig;
+import org.apache.hudi.common.data.HoodieData;
+import org.apache.hudi.common.data.HoodieListData;
+import org.apache.hudi.common.data.HoodieListPairData;
+import org.apache.hudi.common.data.HoodiePairData;
+import org.apache.hudi.common.engine.HoodieEngineContext;
+import org.apache.hudi.common.expression.Expression;
+import org.apache.hudi.common.model.FileSlice;
+import org.apache.hudi.common.model.HoodieRecord;
+import org.apache.hudi.common.model.HoodieRecordGlobalLocation;
+import org.apache.hudi.common.schema.internal.Types;
+import org.apache.hudi.common.table.HoodieTableMetaClient;
+import org.apache.hudi.common.testutils.HoodieTestUtils;
+import org.apache.hudi.common.util.Option;
+import org.apache.hudi.common.util.collection.Pair;
+import org.apache.hudi.exception.HoodieException;
+import org.apache.hudi.exception.HoodieMetadataException;
+import org.apache.hudi.storage.HoodieStorage;
+import org.apache.hudi.storage.StorageConfiguration;
+import org.apache.hudi.storage.StoragePath;
+import org.apache.hudi.storage.StoragePathInfo;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
+
+import java.nio.file.Path;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+class TestBaseTableMetadata {
+
+ @TempDir
+ Path tempDir;
+
+ private HoodieTableMetaClient metaClient;
+ private String basePath;
+
+ @BeforeEach
+ void setUp() throws Exception {
+ basePath = tempDir.toString();
+ metaClient = HoodieTestUtils.init(basePath);
+ }
+
+ @Test
+ void testReadFailuresAreWrappedWithMetadataContext() {
+ TestingTableMetadata metadata = newMetadata();
+ metadata.failSingleReads = true;
+ assertThrows(HoodieMetadataException.class,
metadata::getAllPartitionPaths);
+
+ metadata.failSingleReads = false;
+ metadata.failBulkReads = true;
+ assertThrows(HoodieMetadataException.class,
+ () ->
metadata.getAllFilesInPartitions(Collections.singletonList(basePath)));
+ }
+
+ @Test
+ void testDisabledIndexesAndEmptyBloomFilterLookup() {
+ TestingTableMetadata metadata = newMetadata();
+ assertFalse(metadata.getBloomFilter("", "file.parquet",
MetadataPartitionType.BLOOM_FILTERS.getPartitionPath()).isPresent());
+ assertTrue(metadata.getBloomFilters(
+ Collections.singletonList(Pair.of("", "file.parquet")),
+ MetadataPartitionType.BLOOM_FILTERS.getPartitionPath()).isEmpty());
+ assertTrue(metadata.getColumnStats(
+ Collections.singletonList(Pair.of("", "file.parquet")),
+ Collections.singletonList("column")).isEmpty());
+
+ metaClient.getTableConfig().setMetadataPartitionState(
+ metaClient, MetadataPartitionType.BLOOM_FILTERS.getPartitionPath(),
true);
+ metadata = newMetadata();
+ assertFalse(metadata.getBloomFilter(
+ "", "file.parquet",
MetadataPartitionType.BLOOM_FILTERS.getPartitionPath()).isPresent());
+ assertTrue(metadata.getBloomFilters(
+ Collections.emptyList(),
MetadataPartitionType.BLOOM_FILTERS.getPartitionPath()).isEmpty());
+ }
+
+ @Test
+ void testPayloadFailuresAndMissingColumnStats() {
+ HoodieMetadataPayload payload = new HoodieMetadataPayload(
+ "bad", MetadataPartitionType.FILES.getRecordType(),
Collections.emptyMap()) {
+ @Override
+ public List<StoragePathInfo> getFileList(
+ HoodieStorage storage, StoragePath partitionPath) {
+ throw new HoodieException("corrupt payload");
+ }
+ };
+
+ TestingTableMetadata firstMetadata = newMetadata();
+ firstMetadata.singleRecord = Option.of(payload);
+ assertThrows(HoodieException.class,
+ () -> firstMetadata.getAllFilesInPartition(new StoragePath(basePath)));
+
+ HoodieMetadataPayload inconsistentPayload =
+ HoodieMetadataPayload.createPartitionListRecord(
+ Collections.singletonList("ghost"), true).getData();
+ firstMetadata.singleRecord = Option.of(inconsistentPayload);
+ assertThrows(HoodieMetadataException.class,
firstMetadata::getAllPartitionPaths);
+
+ metaClient.getTableConfig().setMetadataPartitionState(
+ metaClient, MetadataPartitionType.COLUMN_STATS.getPartitionPath(),
true);
+ TestingTableMetadata metadata = newMetadata();
+ HoodieMetadataPayload missingColumnStats =
+ HoodieMetadataPayload.createPartitionFilesRecord(
+ "", Collections.singletonMap("file.parquet", 1L),
+ Collections.emptyList()).getData();
+ metadata.pairRecords = HoodieListPairData.eager(
+ Collections.singletonList(Pair.of("missing-key", missingColumnStats)));
+ assertTrue(metadata.getColumnStats(
+ Collections.singletonList(Pair.of("", "file.parquet")),
+ Collections.singletonList("column")).isEmpty());
+ }
+
+ @Test
+ void testProtectedReadContextAccessors() {
+ TestingTableMetadata metadata = newMetadata();
+ assertNotNull(metadata.storageConfiguration());
+ assertEquals("00000000000000", metadata.latestDataInstant());
+ }
+
+ @Test
+ void testHoodieBackedMetadataStaysDisabledWithoutMetadataTable() {
+ HoodieBackedTableMetadata metadata = new HoodieBackedTableMetadata(
+ null,
+ metaClient.getStorage(),
+ HoodieMetadataConfig.newBuilder().enable(false).build(),
+ basePath);
+
+ assertFalse(metadata.isMetadataTableInitialized());
+ assertFalse(metadata.getSyncedInstantTime().isPresent());
+ assertFalse(metadata.getLatestCompactionTime().isPresent());
+ metadata.close();
+ }
+
+ private TestingTableMetadata newMetadata() {
+ return new TestingTableMetadata(
+ null, metaClient.getStorage(),
+ HoodieMetadataConfig.newBuilder()
+ .enable(true)
+ .ignoreSpuriousDeletes(false)
+ .build(),
+ basePath);
+ }
+
+ private static class TestingTableMetadata extends BaseTableMetadata {
+ private boolean failSingleReads;
+ private boolean failBulkReads;
+ private Option<HoodieMetadataPayload> singleRecord = Option.empty();
+ private HoodiePairData<String, HoodieMetadataPayload> pairRecords =
+ HoodieListPairData.eager(Collections.emptyList());
+
+ TestingTableMetadata(HoodieEngineContext engineContext,
+ HoodieStorage storage,
+ HoodieMetadataConfig metadataConfig,
+ String dataBasePath) {
+ super(engineContext, storage, metadataConfig, dataBasePath);
+ isMetadataTableInitialized = true;
+ }
+
+ @Override
+ protected Option<HoodieMetadataPayload> readFilesIndexRecords(String key,
String partitionName) {
+ if (failSingleReads) {
+ throw new HoodieException("single read failed");
+ }
+ return singleRecord;
+ }
+
+ @Override
+ public List<String> getPartitionPathWithPathPrefixUsingFilterExpression(
+ List<String> relativePathPrefixes,
+ Types.RecordType partitionFields,
+ Expression expression) {
+ return Collections.emptyList();
+ }
+
+ @Override
+ public List<String> getPartitionPathWithPathPrefixes(List<String>
relativePathPrefixes) {
+ return Collections.emptyList();
+ }
+
+ @Override
+ public HoodiePairData<String, HoodieMetadataPayload>
readIndexRecordsWithKeys(
+ HoodieData<? extends RawKey> rawKeys, String partitionName) {
+ if (failBulkReads) {
+ throw new HoodieException("bulk read failed");
+ }
+ return pairRecords;
+ }
+
+ @Override
+ protected HoodiePairData<String, HoodieMetadataPayload>
readIndexRecordsWithKeys(
+ HoodieData<? extends RawKey> rawKeys,
+ String partitionName,
+ Option<String> dataTablePartition) {
+ return readIndexRecordsWithKeys(rawKeys, partitionName);
+ }
+
+ @Override
+ public HoodiePairData<String, String>
readSecondaryIndexDataTableRecordKeysWithKeys(
+ HoodieData<String> keys, String partitionName) {
+ return HoodieListPairData.eager(Collections.emptyList());
+ }
+
+ @Override
+ public HoodiePairData<String, HoodieRecordGlobalLocation>
readSecondaryIndexLocationsWithKeys(
+ HoodieData<String> secondaryKeys, String partitionName) {
+ return HoodieListPairData.eager(Collections.emptyList());
+ }
+
+ @Override
+ public HoodiePairData<String, HoodieRecordGlobalLocation>
readRecordIndexLocationsWithKeys(
+ HoodieData<String> recordKeys) {
+ return HoodieListPairData.eager(Collections.emptyList());
+ }
+
+ @Override
+ public HoodiePairData<String, HoodieRecordGlobalLocation>
readRecordIndexLocationsWithKeys(
+ HoodieData<String> recordKeys, Option<String> dataTablePartition) {
+ return HoodieListPairData.eager(Collections.emptyList());
+ }
+
+ @Override
+ public HoodieData<HoodieRecord<HoodieMetadataPayload>>
getRecordsByKeyPrefixes(
+ HoodieData<? extends RawKey> rawKeys,
+ String partitionName,
+ boolean shouldLoadInMemory) {
+ return HoodieListData.eager(Collections.emptyList());
+ }
+
+ @Override
+ public Map<Pair<String, StoragePath>, List<StoragePathInfo>>
listPartitions(
+ List<Pair<String, StoragePath>> partitionPathList) {
+ return Collections.emptyMap();
+ }
+
+ @Override
+ public Option<String> getSyncedInstantTime() {
+ return Option.empty();
+ }
+
+ @Override
+ public Option<String> getLatestCompactionTime() {
+ return Option.empty();
+ }
+
+ @Override
+ public void reset() {
+ }
+
+ @Override
+ public void close() {
+ }
+
+ @Override
+ public int getNumFileGroupsForPartition(MetadataPartitionType partition) {
+ return 0;
+ }
+
+ @Override
+ public Map<String, List<FileSlice>>
getBucketizedFileGroupsForPartitionedRLI(
+ MetadataPartitionType partition) {
+ return Collections.emptyMap();
+ }
+
+ StorageConfiguration<?> storageConfiguration() {
+ return getStorageConf();
+ }
+
+ String latestDataInstant() {
+ return getLatestDataInstantTime();
+ }
+ }
+}
diff --git
a/hudi-hadoop-common/src/test/java/org/apache/hudi/metadata/TestFileSystemBackedTableMetadata.java
b/hudi-hadoop-common/src/test/java/org/apache/hudi/metadata/TestFileSystemBackedTableMetadata.java
index 48a2851c0924..30a26b65c9c9 100644
---
a/hudi-hadoop-common/src/test/java/org/apache/hudi/metadata/TestFileSystemBackedTableMetadata.java
+++
b/hudi-hadoop-common/src/test/java/org/apache/hudi/metadata/TestFileSystemBackedTableMetadata.java
@@ -18,10 +18,13 @@
package org.apache.hudi.metadata;
+import org.apache.hudi.common.data.HoodieListData;
import org.apache.hudi.common.engine.HoodieLocalEngineContext;
import org.apache.hudi.common.testutils.HoodieCommonTestHarness;
import org.apache.hudi.common.testutils.HoodieTestTable;
+import org.apache.hudi.common.util.Option;
import org.apache.hudi.common.util.collection.Pair;
+import org.apache.hudi.exception.HoodieMetadataException;
import org.apache.hudi.storage.StoragePath;
import org.apache.hudi.storage.StoragePathInfo;
@@ -256,4 +259,37 @@ public class TestFileSystemBackedTableMetadata extends
HoodieCommonTestHarness {
}
}
+ @Test
+ public void testMetadataIndexOperationsAreUnsupported() {
+ HoodieLocalEngineContext localEngineContext =
+ new HoodieLocalEngineContext(metaClient.getStorageConf());
+ FileSystemBackedTableMetadata metadata = new FileSystemBackedTableMetadata(
+ localEngineContext, metaClient.getTableConfig(),
metaClient.getStorage(), basePath);
+
+ Assertions.assertThrows(UnsupportedOperationException.class,
metadata::getSyncedInstantTime);
+ Assertions.assertThrows(UnsupportedOperationException.class,
metadata::getLatestCompactionTime);
+ Assertions.assertThrows(HoodieMetadataException.class,
+ () -> metadata.getBloomFilter("", "file.parquet",
MetadataPartitionType.BLOOM_FILTERS.getPartitionPath()));
+ Assertions.assertThrows(HoodieMetadataException.class,
+ () -> metadata.getBloomFilters(Collections.emptyList(),
MetadataPartitionType.BLOOM_FILTERS.getPartitionPath()));
+ Assertions.assertThrows(HoodieMetadataException.class,
+ () -> metadata.getColumnStats(Collections.emptyList(), "column"));
+ Assertions.assertThrows(HoodieMetadataException.class,
+ () -> metadata.getColumnStats(Collections.emptyList(),
Collections.singletonList("column")));
+ Assertions.assertThrows(HoodieMetadataException.class,
+ () -> metadata.getRecordsByKeyPrefixes(
+ HoodieListData.eager(Collections.emptyList()),
MetadataPartitionType.FILES.getPartitionPath(), false));
+ Assertions.assertThrows(HoodieMetadataException.class,
+ () ->
metadata.readRecordIndexLocationsWithKeys(HoodieListData.eager(Collections.emptyList())));
+ Assertions.assertThrows(HoodieMetadataException.class,
+ () ->
metadata.readRecordIndexLocationsWithKeys(HoodieListData.eager(Collections.emptyList()),
Option.empty()));
+ Assertions.assertThrows(HoodieMetadataException.class,
+ () -> metadata.readSecondaryIndexLocationsWithKeys(
+ HoodieListData.eager(Collections.emptyList()),
MetadataPartitionType.SECONDARY_INDEX.getPartitionPath()));
+ Assertions.assertThrows(HoodieMetadataException.class,
+ () ->
metadata.getNumFileGroupsForPartition(MetadataPartitionType.FILES));
+ Assertions.assertThrows(HoodieMetadataException.class,
+ () ->
metadata.getBucketizedFileGroupsForPartitionedRLI(MetadataPartitionType.RECORD_INDEX));
+ }
+
}
diff --git
a/hudi-hadoop-common/src/test/java/org/apache/hudi/metadata/TestHoodieMetadataPayload.java
b/hudi-hadoop-common/src/test/java/org/apache/hudi/metadata/TestHoodieMetadataPayload.java
index 21564b10b6df..45048d4e80b0 100644
---
a/hudi-hadoop-common/src/test/java/org/apache/hudi/metadata/TestHoodieMetadataPayload.java
+++
b/hudi-hadoop-common/src/test/java/org/apache/hudi/metadata/TestHoodieMetadataPayload.java
@@ -18,17 +18,24 @@
package org.apache.hudi.metadata;
+import org.apache.hudi.avro.model.HoodieMetadataRecord;
import org.apache.hudi.common.model.HoodieRecord;
+import org.apache.hudi.common.schema.HoodieSchema;
+import org.apache.hudi.common.schema.HoodieSchemaUtils;
import org.apache.hudi.common.testutils.HoodieCommonTestHarness;
import org.apache.hudi.common.util.Option;
import org.apache.hudi.common.util.collection.Pair;
+import org.apache.hudi.exception.HoodieMetadataException;
import org.apache.hudi.metadata.stats.HoodieColumnRangeMetadata;
import org.apache.hudi.metadata.stats.ValueMetadata;
+import org.apache.avro.Schema;
+import org.apache.avro.generic.GenericRecord;
import org.apache.avro.generic.IndexedRecord;
import org.junit.jupiter.api.Test;
import java.io.IOException;
+import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
@@ -325,6 +332,62 @@ public class TestHoodieMetadataPayload extends
HoodieCommonTestHarness {
assertEquals(newSecondaryIndexRecord.getData(),
combinedSecondaryIndexRecord.get().getData());
}
+ @Test
+ public void testPayloadAccessorsAndObjectMethods() {
+ HoodieMetadataPayload emptyPayload = new
HoodieMetadataPayload(Option.empty());
+ assertFalse(emptyPayload.getBloomFilterMetadata().isPresent());
+ assertFalse(emptyPayload.getColumnStatMetadata().isPresent());
+ assertFalse(emptyPayload.equals("not-a-payload"));
+ emptyPayload.hashCode();
+
+ HoodieMetadataPayload secondaryIndexPayload =
HoodieMetadataPayload.createSecondaryIndexRecord(
+ "record-key", "secondary-key",
MetadataPartitionType.SECONDARY_INDEX.getPartitionPath() + "test",
true).getData();
+ assertTrue(secondaryIndexPayload.isSecondaryIndexDeleted());
+ }
+
+ @Test
+ public void testInvalidRecordIndexInputs() {
+ assertThrows(HoodieMetadataException.class,
+ () ->
HoodieMetadataPayload.parseRecordIndexInstantTime("not-an-instant"));
+ assertThrows(HoodieMetadataException.class,
+ () -> HoodieMetadataPayload.createRecordIndexUpdate(
+ "record-key", PARTITION_NAME, "not-a-uuid", "20240101000000000",
0));
+ }
+
+ @Test
+ public void testProjectedInsertValueIncludesBloomFilter() throws IOException
{
+ HoodieMetadataPayload bloomFilterPayload =
HoodieMetadataPayload.createBloomFilterMetadataRecord(
+ PARTITION_NAME, "file-id_1-0-1_20240101000000000.parquet",
"20240101000000000", "SIMPLE",
+ ByteBuffer.wrap("bloom-data".getBytes()), false).getData();
+ Schema projectedSchema = HoodieSchemaUtils.addMetadataFields(
+
HoodieSchema.fromAvroSchema(HoodieMetadataRecord.getClassSchema())).toAvroSchema();
+
+ IndexedRecord projectedRecord =
bloomFilterPayload.getInsertValue(projectedSchema).get();
+
+ assertEquals(bloomFilterPayload.getBloomFilterMetadata().get(),
+ ((GenericRecord) projectedRecord).get("BloomFilterMetadata"));
+ }
+
+ @Test
+ public void testPayloadToStringForIndexedRecordTypes() {
+ HoodieMetadataPayload filesPayload =
HoodieMetadataPayload.createPartitionFilesRecord(
+ PARTITION_NAME, Collections.singletonMap("file.parquet", 10L),
Collections.singletonList("old.parquet")).getData();
+ assertTrue(filesPayload.toString().contains("creations=[file.parquet]"));
+ assertTrue(filesPayload.toString().contains("deletions=[old.parquet]"));
+
+ HoodieMetadataPayload bloomFilterPayload =
HoodieMetadataPayload.createBloomFilterMetadataRecord(
+ PARTITION_NAME, "file-id_1-0-1_20240101000000000.parquet",
"20240101000000000", "SIMPLE",
+ ByteBuffer.wrap("bloom-data".getBytes()), false).getData();
+ assertTrue(bloomFilterPayload.toString().contains("BloomFilter"));
+
+ HoodieColumnRangeMetadata<Comparable> columnRange =
HoodieColumnRangeMetadata.<Comparable>create(
+ "file.parquet", "column", 1, 2, 0, 2, 10, 10,
ValueMetadata.V1EmptyMetadata.get());
+ HoodieMetadataPayload columnStatsPayload =
+ (HoodieMetadataPayload) HoodieMetadataPayload.createColumnStatsRecords(
+ PARTITION_NAME, Collections.singletonList(columnRange),
false).findFirst().get().getData();
+ assertTrue(columnStatsPayload.toString().contains("ColStats"));
+ }
+
@Test
public void testConstructSecondaryIndexKey() {
// Simple case