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 260ee6be950c test(common): improve ORC and filesystem utility coverage
(#19542)
260ee6be950c is described below
commit 260ee6be950ccea7107ea82ed2983a5d060972f0
Author: Shuo Cheng <[email protected]>
AuthorDate: Tue Aug 11 10:44:08 2026 +0800
test(common): improve ORC and filesystem utility coverage (#19542)
* test(common): improve ORC and filesystem utility coverage
---
.../apache/hudi/common/util/TestAvroOrcUtils.java | 246 +++++++++++
.../org/apache/hudi/common/util/TestOrcUtils.java | 150 +++++++
.../org/apache/hudi/hadoop/fs/TestCachingPath.java | 105 +++++
.../apache/hudi/hadoop/fs/TestHadoopFSUtils.java | 130 ++++++
.../fs/TestHoodieRetryWrapperFileSystem.java | 158 +++++++
.../fs/TestHoodieWrapperFileSystemOperations.java | 476 +++++++++++++++++++++
.../hadoop/TestHoodieVariantReconstruction.java | 90 ++++
.../io/TestHoodieParquetBinaryCopyBasePaths.java | 76 ++++
8 files changed, 1431 insertions(+)
diff --git
a/hudi-hadoop-common/src/test/java/org/apache/hudi/common/util/TestAvroOrcUtils.java
b/hudi-hadoop-common/src/test/java/org/apache/hudi/common/util/TestAvroOrcUtils.java
index f2d4db28809d..8d5d79f82d53 100644
---
a/hudi-hadoop-common/src/test/java/org/apache/hudi/common/util/TestAvroOrcUtils.java
+++
b/hudi-hadoop-common/src/test/java/org/apache/hudi/common/util/TestAvroOrcUtils.java
@@ -19,21 +19,44 @@
package org.apache.hudi.common.util;
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.testutils.HoodieCommonTestHarness;
+import org.apache.avro.Conversions;
+import org.apache.avro.generic.GenericData;
+import org.apache.avro.generic.GenericRecord;
+import org.apache.avro.util.Utf8;
+import org.apache.hadoop.hive.ql.exec.vector.BytesColumnVector;
+import org.apache.hadoop.hive.ql.exec.vector.ColumnVector;
+import org.apache.hadoop.hive.ql.exec.vector.LongColumnVector;
+import org.apache.hadoop.hive.ql.exec.vector.UnionColumnVector;
+import org.apache.hadoop.hive.ql.exec.vector.VectorizedRowBatch;
import org.apache.orc.TypeDescription;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
+import java.math.BigDecimal;
+import java.math.BigInteger;
+import java.nio.ByteBuffer;
+import java.sql.Date;
import java.util.Arrays;
+import java.util.LinkedHashMap;
import java.util.List;
+import java.util.Map;
import static
org.apache.hudi.common.testutils.HoodieTestDataGenerator.HOODIE_SCHEMA;
import static
org.apache.hudi.common.testutils.HoodieTestDataGenerator.TRIP_EXAMPLE_SCHEMA;
import static
org.apache.hudi.common.testutils.HoodieTestDataGenerator.TRIP_SCHEMA;
+import static org.junit.jupiter.api.Assertions.assertArrayEquals;
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.assertSame;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
/**
* Tests {@link AvroOrcUtils}.
@@ -113,4 +136,227 @@ public class TestAvroOrcUtils extends
HoodieCommonTestHarness {
assertEquals(TypeDescription.Category.LONG,
orcLocalTimestampMicros.getCategory(),
"LocalTimestampMicros should convert to ORC Long (preserving old
behavior)");
}
+
+ @Test
+ public void testPrimitiveValuesRoundTripThroughColumnVectors() {
+ assertTrue((Boolean)
roundTrip(HoodieSchema.create(HoodieSchemaType.BOOLEAN), true));
+ assertEquals(12, roundTrip(HoodieSchema.create(HoodieSchemaType.INT), 12));
+ assertEquals(34L, roundTrip(HoodieSchema.create(HoodieSchemaType.LONG),
34L));
+ assertEquals(1.25f, roundTrip(HoodieSchema.create(HoodieSchemaType.FLOAT),
1.25f));
+ assertEquals(2.5d, roundTrip(HoodieSchema.create(HoodieSchemaType.DOUBLE),
2.5d));
+ assertEquals("hoodie",
roundTrip(HoodieSchema.create(HoodieSchemaType.STRING), new
Utf8("hoodie")).toString());
+ assertEquals(42, roundTrip(HoodieSchema.createDate(), 42));
+ assertEquals(1_700_000_000_123L,
roundTrip(HoodieSchema.createTimestampMillis(), 1_700_000_000_123L));
+ assertEquals(1_700_000_000_123_456L,
+ roundTrip(HoodieSchema.createTimestampMicros(),
1_700_000_000_123_456L));
+
+ ByteBuffer binary = (ByteBuffer) roundTrip(
+ HoodieSchema.create(HoodieSchemaType.BYTES), ByteBuffer.wrap(new
byte[] {1, 2, 3}));
+ assertArrayEquals(new byte[] {1, 2, 3}, toByteArray(binary));
+ }
+
+ @Test
+ public void testComplexValuesRoundTripThroughColumnVectors() {
+ HoodieSchema arraySchema =
HoodieSchema.createArray(HoodieSchema.create(HoodieSchemaType.INT));
+ assertEquals(Arrays.asList(1, 2, 3), roundTrip(arraySchema,
Arrays.asList(1, 2, 3)));
+
+ HoodieSchema mapSchema =
HoodieSchema.createMap(HoodieSchema.create(HoodieSchemaType.LONG));
+ Map<String, Long> values = new LinkedHashMap<>();
+ values.put("first", 10L);
+ values.put("second", 20L);
+ assertEquals(values, roundTrip(mapSchema, values));
+
+ HoodieSchema recordSchema = HoodieSchema.createRecord("nested", null,
null, Arrays.asList(
+ HoodieSchemaField.of("name",
HoodieSchema.create(HoodieSchemaType.STRING), null, null),
+ HoodieSchemaField.of("count",
HoodieSchema.create(HoodieSchemaType.INT), null, null)));
+ GenericRecord record = new GenericData.Record(recordSchema.toAvroSchema());
+ record.put("name", "record-name");
+ record.put("count", 7);
+ GenericRecord converted = (GenericRecord) roundTrip(recordSchema, record);
+ assertEquals("record-name", converted.get("name").toString());
+ assertEquals(7, converted.get("count"));
+
+ HoodieSchema unionSchema = HoodieSchema.createUnion(Arrays.asList(
+ HoodieSchema.create(HoodieSchemaType.INT),
HoodieSchema.create(HoodieSchemaType.STRING)));
+ assertEquals(9, roundTrip(unionSchema, 9));
+ assertEquals("union-value", roundTrip(unionSchema,
"union-value").toString());
+ }
+
+ @Test
+ public void testDecimalEnumAndFixedValuesRoundTripThroughColumnVectors() {
+ HoodieSchema decimalSchema = HoodieSchema.createDecimal(10, 2);
+ ByteBuffer decimalBytes = (ByteBuffer) roundTrip(decimalSchema, new
BigDecimal("1234.50"));
+ assertEquals(new BigInteger("123450"), new
BigInteger(toByteArray(decimalBytes)));
+
+ HoodieSchema fixedDecimalSchema =
HoodieSchema.parse("{\"type\":\"fixed\",\"name\":\"amount\","
+ +
"\"size\":8,\"logicalType\":\"decimal\",\"precision\":12,\"scale\":2}");
+ BigDecimal fixedValue = new BigDecimal("9876.50");
+ GenericData.Fixed fixed = (GenericData.Fixed) new
Conversions.DecimalConversion().toFixed(
+ fixedValue, fixedDecimalSchema.toAvroSchema(),
fixedDecimalSchema.toAvroSchema().getLogicalType());
+ GenericData.Fixed convertedFixed = (GenericData.Fixed)
roundTrip(fixedDecimalSchema, fixed);
+ assertEquals(fixedValue, new Conversions.DecimalConversion().fromFixed(
+ convertedFixed, fixedDecimalSchema.toAvroSchema(),
fixedDecimalSchema.toAvroSchema().getLogicalType()));
+
+ HoodieSchema enumSchema = HoodieSchema.parse(
+
"{\"type\":\"enum\",\"name\":\"status\",\"symbols\":[\"OPEN\",\"CLOSED\"]}");
+ GenericData.EnumSymbol open = new
GenericData.EnumSymbol(enumSchema.toAvroSchema(), "OPEN");
+ assertEquals(open, roundTrip(enumSchema, open));
+ }
+
+ @Test
+ public void testNullResizeAndRepeatingVectors() {
+ HoodieSchema schema = HoodieSchema.create(HoodieSchemaType.LONG);
+ TypeDescription type = AvroOrcUtils.createOrcSchema(schema);
+ VectorizedRowBatch batch =
TypeDescription.createStruct().addField("value", type).createRowBatch();
+ ColumnVector vector = batch.cols[0];
+
+ int expandedPosition = vector.isNull.length;
+ AvroOrcUtils.addToVector(type, vector, schema, null, expandedPosition);
+ assertFalse(vector.noNulls);
+ assertTrue(vector.isNull[expandedPosition]);
+ assertNull(AvroOrcUtils.readFromVector(type, vector, schema,
expandedPosition));
+
+ ((LongColumnVector) vector).vector[0] = 99;
+ vector.isNull[0] = false;
+ vector.isRepeating = true;
+ assertEquals(99L, AvroOrcUtils.readFromVector(type, vector, schema, 17));
+ }
+
+ @Test
+ public void testCreateSchemaCoversEveryOrcCategory() {
+ TypeDescription orcSchema = TypeDescription.fromString("struct<"
+ +
"boolean_field:boolean,byte_field:tinyint,short_field:smallint,int_field:int,long_field:bigint,"
+ +
"float_field:float,double_field:double,string_field:string,char_field:char(5),varchar_field:varchar(8),"
+ +
"date_field:date,timestamp_field:timestamp,binary_field:binary,decimal_field:decimal(12,3),"
+ +
"list_field:array<int>,map_field:map<string,bigint>,union_field:uniontype<int,string>>");
+
+ HoodieSchema schema = AvroOrcUtils.createSchema(orcSchema);
+ assertEquals(HoodieSchemaType.RECORD, schema.getType());
+ assertEquals(17, schema.getFields().size());
+ assertEquals(HoodieSchemaType.BOOLEAN,
schema.getField("boolean_field").get().schema().getType());
+ assertEquals(HoodieSchemaType.BYTES,
schema.getField("binary_field").get().schema().getType());
+ assertEquals(HoodieSchemaType.ARRAY,
schema.getField("list_field").get().schema().getType());
+ assertEquals(HoodieSchemaType.MAP,
schema.getField("map_field").get().schema().getType());
+ assertEquals(HoodieSchemaType.UNION,
schema.getField("union_field").get().schema().getType());
+ }
+
+ @Test
+ public void testCreateSchemaWithDefaultsAndNestedNamespaces() {
+ TypeDescription orcSchema = TypeDescription.fromString(
+
"struct<id:bigint,nested:struct<name:string,active:boolean>,amount:decimal(8,2),tags:array<string>>");
+
+ HoodieSchema nullable = AvroOrcUtils.createSchemaWithDefaultValue(
+ orcSchema, "root_record", "org.apache.hudi.test", true);
+ assertEquals("root_record", nullable.getName());
+ assertTrue(nullable.getFields().stream().allMatch(field ->
field.schema().getType() == HoodieSchemaType.UNION));
+
+ HoodieSchema required = AvroOrcUtils.createSchemaWithDefaultValue(
+ orcSchema, "root_record", "org.apache.hudi.test", false);
+ assertTrue(required.getFields().stream().noneMatch(field ->
field.schema().getType() == HoodieSchemaType.UNION));
+ assertEquals("nested",
required.getField("nested").get().schema().getName());
+ }
+
+ @Test
+ public void testCachingOfByteReferencesDoesNotCopyInput() {
+ HoodieSchema schema = HoodieSchema.create(HoodieSchemaType.BYTES);
+ TypeDescription type = AvroOrcUtils.createOrcSchema(schema);
+ VectorizedRowBatch batch =
TypeDescription.createStruct().addField("value", type).createRowBatch();
+ byte[] input = new byte[] {4, 5, 6};
+
+ AvroOrcUtils.addToVector(type, batch.cols[0], schema, input, 0);
+
+ BytesColumnVector vector = (BytesColumnVector) batch.cols[0];
+ assertSame(input, vector.vector[0]);
+ }
+
+ @Test
+ public void testAdditionalOrcVectorRepresentationsAndFailures() {
+ assertEquals((byte) 7, roundTrip(TypeDescription.createByte(),
HoodieSchema.create(HoodieSchemaType.INT), (byte) 7));
+ assertEquals((short) 11, roundTrip(TypeDescription.createShort(),
HoodieSchema.create(HoodieSchemaType.INT), (short) 11));
+ assertEquals("char",
roundTrip(TypeDescription.createChar().withMaxLength(8),
+ HoodieSchema.create(HoodieSchemaType.STRING), "char"));
+ assertEquals("varchar",
roundTrip(TypeDescription.createVarchar().withMaxLength(8),
+ HoodieSchema.create(HoodieSchemaType.STRING), "varchar"));
+
+ HoodieSchema dateSchema = HoodieSchema.create(HoodieSchemaType.INT);
+ assertEquals(1, roundTrip(TypeDescription.createDate(), dateSchema,
Date.valueOf("1970-01-02")));
+ java.util.Date utilDate = new
java.util.Date(Date.valueOf("1970-01-03").getTime());
+ assertEquals(2, roundTrip(TypeDescription.createDate(), dateSchema,
utilDate));
+
+ HoodieSchema stringSchema = HoodieSchema.create(HoodieSchemaType.STRING);
+ assertThrows(IllegalStateException.class,
+ () -> addValue(TypeDescription.createString(), stringSchema, new
Object()));
+ assertThrows(IllegalStateException.class,
+ () -> addValue(TypeDescription.createBinary(),
HoodieSchema.create(HoodieSchemaType.BYTES), "not-binary"));
+ assertThrows(IllegalStateException.class,
+ () ->
addValue(TypeDescription.createDecimal().withScale(2).withPrecision(8),
+ HoodieSchema.createDecimal(8, 2), "not-decimal"));
+ assertThrows(org.apache.hudi.exception.HoodieIOException.class,
+ () -> roundTrip(TypeDescription.createVarchar().withMaxLength(3),
stringSchema, "too-long"));
+ }
+
+ @Test
+ public void testUnionMatchesPrimitiveAndBinaryRepresentations() {
+ HoodieSchema schema = HoodieSchema.createUnion(Arrays.asList(
+ HoodieSchema.create(HoodieSchemaType.BOOLEAN),
+ HoodieSchema.create(HoodieSchemaType.INT),
+ HoodieSchema.create(HoodieSchemaType.LONG),
+ HoodieSchema.create(HoodieSchemaType.FLOAT),
+ HoodieSchema.create(HoodieSchemaType.DOUBLE),
+ HoodieSchema.create(HoodieSchemaType.STRING),
+ HoodieSchema.create(HoodieSchemaType.BYTES)));
+ TypeDescription type = TypeDescription.createUnion()
+ .addUnionChild(TypeDescription.createBoolean())
+ .addUnionChild(TypeDescription.createInt())
+ .addUnionChild(TypeDescription.createLong())
+ .addUnionChild(TypeDescription.createFloat())
+ .addUnionChild(TypeDescription.createDouble())
+ .addUnionChild(TypeDescription.createString())
+ .addUnionChild(TypeDescription.createBinary());
+
+ assertTrue((Boolean) roundTrip(type, schema, true));
+ assertEquals(17, roundTrip(type, schema, 17));
+ assertEquals(23L, roundTrip(type, schema, 23L));
+ assertEquals(1.5f, roundTrip(type, schema, 1.5f));
+ assertEquals(2.5d, roundTrip(type, schema, 2.5d));
+ assertEquals("text", roundTrip(type, schema, new Utf8("text")).toString());
+ assertArrayEquals(new byte[] {9, 8}, toByteArray((ByteBuffer) roundTrip(
+ type, schema, new byte[] {9, 8})));
+
+ VectorizedRowBatch batch =
TypeDescription.createStruct().addField("value", type).createRowBatch();
+ assertFalse(AvroOrcUtils.addUnionValue((UnionColumnVector) batch.cols[0],
type.getChildren(), schema,
+ new Object(), 0));
+ }
+
+ @Test
+ public void testTimeAndNullUnionSchemaCreation() {
+ assertEquals(TypeDescription.createInt(),
AvroOrcUtils.createOrcSchema(HoodieSchema.createTimeMillis()));
+ assertEquals(TypeDescription.createLong(),
AvroOrcUtils.createOrcSchema(HoodieSchema.createTimeMicros()));
+ assertEquals(TypeDescription.createUnion(),
+ AvroOrcUtils.createOrcSchema(HoodieSchema.createUnion(Arrays.asList(
+ HoodieSchema.create(HoodieSchemaType.NULL)))));
+ }
+
+ private static Object roundTrip(HoodieSchema schema, Object value) {
+ TypeDescription type = AvroOrcUtils.createOrcSchema(schema);
+ return roundTrip(type, schema, value);
+ }
+
+ private static Object roundTrip(TypeDescription type, HoodieSchema schema,
Object value) {
+ VectorizedRowBatch batch =
TypeDescription.createStruct().addField("value", type).createRowBatch();
+ AvroOrcUtils.addToVector(type, batch.cols[0], schema, value, 0);
+ return AvroOrcUtils.readFromVector(type, batch.cols[0], schema, 0);
+ }
+
+ private static void addValue(TypeDescription type, HoodieSchema schema,
Object value) {
+ VectorizedRowBatch batch =
TypeDescription.createStruct().addField("value", type).createRowBatch();
+ AvroOrcUtils.addToVector(type, batch.cols[0], schema, value, 0);
+ }
+
+ private static byte[] toByteArray(ByteBuffer buffer) {
+ ByteBuffer duplicate = buffer.duplicate();
+ byte[] bytes = new byte[duplicate.remaining()];
+ duplicate.get(bytes);
+ return bytes;
+ }
}
diff --git
a/hudi-hadoop-common/src/test/java/org/apache/hudi/common/util/TestOrcUtils.java
b/hudi-hadoop-common/src/test/java/org/apache/hudi/common/util/TestOrcUtils.java
new file mode 100644
index 000000000000..6c42ce1216c9
--- /dev/null
+++
b/hudi-hadoop-common/src/test/java/org/apache/hudi/common/util/TestOrcUtils.java
@@ -0,0 +1,150 @@
+/*
+ * 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.common.util;
+
+import org.apache.hudi.common.model.HoodieFileFormat;
+import org.apache.hudi.common.model.HoodieKey;
+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.HoodieTestUtils;
+import org.apache.hudi.common.util.collection.ClosableIterator;
+import org.apache.hudi.common.util.collection.Pair;
+import org.apache.hudi.exception.MetadataNotFoundException;
+import org.apache.hudi.hadoop.fs.HadoopFSUtils;
+import org.apache.hudi.metadata.HoodieIndexVersion;
+import org.apache.hudi.storage.HoodieStorage;
+import org.apache.hudi.storage.StoragePath;
+
+import org.apache.hadoop.conf.Configuration;
+import org.apache.hadoop.fs.FileSystem;
+import org.apache.hadoop.hive.ql.exec.vector.BytesColumnVector;
+import org.apache.hadoop.hive.ql.exec.vector.VectorizedRowBatch;
+import org.apache.orc.OrcFile;
+import org.apache.orc.TypeDescription;
+import org.apache.orc.Writer;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
+
+import java.nio.ByteBuffer;
+import java.nio.file.Path;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.Properties;
+import java.util.Set;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+class TestOrcUtils {
+
+ @Test
+ void testMetadataAndFormatHelpers(@TempDir Path tempDir) throws Exception {
+ StoragePath file = new
StoragePath(tempDir.resolve("metadata.orc").toUri());
+ HoodieStorage storage = HoodieTestUtils.getStorage(file);
+ OrcUtils utils = new OrcUtils();
+ Properties properties = new Properties();
+ properties.setProperty("first", "one");
+ properties.setProperty("second", "two");
+
+ utils.writeMetaFile(storage, file, properties);
+
+ assertEquals(HoodieFileFormat.ORC, utils.getFormat());
+ assertEquals(0, utils.getRowCount(storage, file));
+ assertEquals(Collections.singletonMap("first", "one"),
+ utils.readFooter(storage, true, file, "first"));
+ assertTrue(utils.readFooter(storage, false, file, "missing").isEmpty());
+ assertThrows(MetadataNotFoundException.class,
+ () -> utils.readFooter(storage, true, file, "missing"));
+
assertEquals(HoodieSchemaUtils.getRecordKeySchema().getFields().get(0).name(),
+ utils.readSchema(storage, file).getFields().get(0).name());
+ assertThrows(UnsupportedOperationException.class,
+ () -> utils.readColumnStatsFromMetadata(storage, file,
Collections.emptyList(), HoodieIndexVersion.V1));
+ assertThrows(UnsupportedOperationException.class, () ->
utils.serializeRecordsToLogBlock(
+ storage, Collections.<HoodieRecord>emptyList(), null, null, null,
Collections.emptyMap()));
+ assertThrows(UnsupportedOperationException.class, () ->
utils.serializeRecordsToLogBlock(
+ storage, Collections.<HoodieRecord>emptyIterator(),
HoodieRecord.HoodieRecordType.AVRO,
+ null, null, null, Collections.emptyMap()));
+
+ StoragePath missing = new
StoragePath(tempDir.resolve("missing.orc").toUri());
+ try (ClosableIterator<Pair<HoodieKey, Long>> iterator =
utils.fetchRecordKeysWithPositions(
+ storage, missing, Option.empty(), Option.empty())) {
+ assertFalse(iterator.hasNext());
+ }
+ }
+
+ @Test
+ void testRecordReadingKeyFilteringAndPositions(@TempDir Path tempDir) throws
Exception {
+ StoragePath file = new StoragePath(tempDir.resolve("records.orc").toUri());
+ HoodieStorage storage = HoodieTestUtils.getStorage(file);
+ HoodieSchema schema = HoodieSchemaUtils.getRecordKeySchema();
+ TypeDescription orcSchema = AvroOrcUtils.createOrcSchema(schema);
+ Configuration conf = storage.getConf().unwrapAs(Configuration.class);
+ OrcFile.WriterOptions options = OrcFile.writerOptions(conf)
+ .fileSystem((FileSystem) storage.getFileSystem()).setSchema(orcSchema);
+ try (Writer writer =
OrcFile.createWriter(HadoopFSUtils.convertToHadoopPath(file), options)) {
+ VectorizedRowBatch batch = orcSchema.createRowBatch();
+ BytesColumnVector keys = (BytesColumnVector) batch.cols[0];
+ for (String key : Arrays.asList("key-1", "key-2", "key-3")) {
+ keys.setVal(batch.size++, StringUtils.getUTF8Bytes(key));
+ }
+ writer.addRowBatch(batch);
+ }
+
+ OrcUtils utils = new OrcUtils();
+ assertEquals(3, utils.getRowCount(storage, file));
+ assertEquals(3, utils.readAvroRecords(storage, file).size());
+ assertEquals(3, utils.readAvroRecords(storage, file, schema).size());
+
+ Set<Pair<String, Long>> all = utils.filterRowKeys(storage, file,
Collections.emptySet());
+ assertEquals(3, all.size());
+ Set<Pair<String, Long>> selected = utils.filterRowKeys(
+ storage, file, new HashSet<>(Collections.singletonList("key-2")));
+ assertEquals(Collections.singleton(Pair.of("key-2", 1L)), selected);
+
+ try (ClosableIterator<Pair<HoodieKey, Long>> iterator =
utils.fetchRecordKeysWithPositions(
+ storage, file, Option.empty(), Option.of("partition"))) {
+ assertTrue(iterator.hasNext());
+ Pair<HoodieKey, Long> first = iterator.next();
+ assertEquals("key-1", first.getLeft().getRecordKey());
+ assertEquals("partition", first.getLeft().getPartitionPath());
+ assertEquals(0L, first.getRight());
+ }
+ }
+
+ @Test
+ void testReadSchemaPrefersEmbeddedAvroSchema(@TempDir Path tempDir) throws
Exception {
+ StoragePath file = new StoragePath(tempDir.resolve("schema.orc").toUri());
+ HoodieStorage storage = HoodieTestUtils.getStorage(file);
+ HoodieSchema schema = HoodieSchemaUtils.getRecordKeySchema();
+ TypeDescription orcSchema = AvroOrcUtils.createOrcSchema(schema);
+ Configuration conf = storage.getConf().unwrapAs(Configuration.class);
+ OrcFile.WriterOptions options = OrcFile.writerOptions(conf)
+ .fileSystem((FileSystem) storage.getFileSystem()).setSchema(orcSchema);
+ try (Writer writer =
OrcFile.createWriter(HadoopFSUtils.convertToHadoopPath(file), options)) {
+ writer.addUserMetadata("orc.avro.schema",
ByteBuffer.wrap(StringUtils.getUTF8Bytes(schema.toString())));
+ }
+
+ assertEquals(schema, new OrcUtils().readSchema(storage, file));
+ }
+}
diff --git
a/hudi-hadoop-common/src/test/java/org/apache/hudi/hadoop/fs/TestCachingPath.java
b/hudi-hadoop-common/src/test/java/org/apache/hudi/hadoop/fs/TestCachingPath.java
new file mode 100644
index 000000000000..d17845900c24
--- /dev/null
+++
b/hudi-hadoop-common/src/test/java/org/apache/hudi/hadoop/fs/TestCachingPath.java
@@ -0,0 +1,105 @@
+/*
+ * 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.hadoop.fs;
+
+import org.apache.hadoop.fs.Path;
+import org.junit.jupiter.api.Test;
+
+import java.net.URI;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertSame;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+class TestCachingPath {
+
+ @Test
+ void testCachesDerivedValues() {
+ CachingPath path = new
CachingPath("s3://bucket/table/partition/file.parquet");
+
+ String name = path.getName();
+ Path parent = path.getParent();
+ String fullPath = path.toString();
+
+ assertSame(name, path.getName());
+ assertSame(parent, path.getParent());
+ assertSame(fullPath, path.toString());
+ assertEquals("file.parquet", name);
+ assertEquals("s3://bucket/table/partition", parent.toString());
+ }
+
+ @Test
+ void testEveryConstructorCreatesEquivalentPath() {
+ Path parent = new Path("file:///tmp/table");
+ Path child = new Path("partition/file.parquet");
+ URI expected = URI.create("file:/tmp/table/partition/file.parquet");
+
+ assertEquals(expected, new CachingPath(parent.toString(),
child.toString()).toUri());
+ assertEquals(expected, new CachingPath(parent, child.toString()).toUri());
+ assertEquals(expected, new CachingPath(parent.toString(), child).toUri());
+ assertEquals(expected, new CachingPath(parent, child).toUri());
+ assertEquals(expected, new CachingPath(expected).toUri());
+ }
+
+ @Test
+ void testWrapAndSubPath() {
+ CachingPath cached = new CachingPath("s3://bucket/table");
+ assertSame(cached, CachingPath.wrap(cached));
+
+ CachingPath wrapped = CachingPath.wrap(new Path("s3://bucket/table"));
+ assertEquals(cached, wrapped);
+ assertEquals("s3://bucket/table/partition/file.parquet",
+ wrapped.subPath("partition/file.parquet").toString());
+ }
+
+ @Test
+ void testUnsafeConcatenationHandlesSeparatorsAndPreservesUriParts() throws
Exception {
+ Path base = new Path(new URI("s3", "bucket", "/table", "version=1",
"fragment"));
+
+ CachingPath expected = CachingPath.concatPathUnsafe(base, "partition");
+ assertEquals("/table/partition", expected.toUri().getPath());
+ assertEquals("version=1", expected.toUri().getQuery());
+ assertEquals("fragment", expected.toUri().getFragment());
+
+ assertEquals("/table/partition", CachingPath.concatPathUnsafe(new
Path("s3://bucket/table/"), "/partition").toUri().getPath());
+ assertEquals("/table/partition", CachingPath.concatPathUnsafe(new
Path("s3://bucket/table/"), "partition").toUri().getPath());
+ assertEquals("/table/partition", CachingPath.concatPathUnsafe(new
Path("s3://bucket/table"), "/partition").toUri().getPath());
+ assertEquals("/table/partition", CachingPath.concatPathUnsafe(new
Path("s3://bucket/table"), new Path("partition")).toUri().getPath());
+ assertThrows(IllegalStateException.class,
+ () -> CachingPath.concatPathUnsafe(base, new
Path("s3://other/absolute")));
+ }
+
+ @Test
+ void testRelativePathAndSchemeRemoval() {
+ CachingPath relative =
CachingPath.createRelativePathUnsafe("partition%2Fvalue/file.parquet");
+ assertFalse(relative.toUri().isAbsolute());
+ assertEquals("partition%2Fvalue/file.parquet", relative.toString());
+
+ Path absolute = new Path("s3://bucket/table/file.parquet");
+ Path stripped = CachingPath.getPathWithoutSchemeAndAuthority(absolute);
+ assertTrue(stripped instanceof CachingPath);
+ assertEquals("/table/file.parquet", stripped.toString());
+
+ Path alreadyRelative = new Path("table/file.parquet");
+ assertSame(alreadyRelative,
CachingPath.getPathWithoutSchemeAndAuthority(alreadyRelative));
+ }
+}
diff --git
a/hudi-hadoop-common/src/test/java/org/apache/hudi/hadoop/fs/TestHadoopFSUtils.java
b/hudi-hadoop-common/src/test/java/org/apache/hudi/hadoop/fs/TestHadoopFSUtils.java
index da4e9a7500f4..92e7234eab7d 100644
---
a/hudi-hadoop-common/src/test/java/org/apache/hudi/hadoop/fs/TestHadoopFSUtils.java
+++
b/hudi-hadoop-common/src/test/java/org/apache/hudi/hadoop/fs/TestHadoopFSUtils.java
@@ -19,7 +19,12 @@
package org.apache.hudi.hadoop.fs;
+import org.apache.hudi.avro.model.HoodieFSPermission;
+import org.apache.hudi.avro.model.HoodieFileStatus;
+import org.apache.hudi.avro.model.HoodiePath;
+import org.apache.hudi.common.engine.HoodieLocalEngineContext;
import org.apache.hudi.exception.HoodieException;
+import org.apache.hudi.storage.StorageConfiguration;
import org.apache.hudi.storage.StoragePath;
import org.apache.hudi.storage.StoragePathInfo;
import org.apache.hudi.storage.hadoop.HoodieHadoopStorage;
@@ -31,6 +36,9 @@ import org.apache.hadoop.fs.FileSystem;
import org.apache.hadoop.fs.FilterFileSystem;
import org.apache.hadoop.fs.LocalFileSystem;
import org.apache.hadoop.fs.Path;
+import org.apache.hadoop.fs.permission.FsAction;
+import org.apache.hadoop.fs.permission.FsPermission;
+import org.apache.hadoop.hdfs.DistributedFileSystem;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
import org.junit.jupiter.params.ParameterizedTest;
@@ -40,6 +48,11 @@ import org.junit.jupiter.params.provider.ValueSource;
import java.io.IOException;
import java.net.URI;
import java.nio.file.Files;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.atomic.AtomicInteger;
import static
org.apache.hudi.hadoop.fs.HadoopFSUtils.convertToHadoopFileStatus;
import static org.apache.hudi.hadoop.fs.HadoopFSUtils.convertToHadoopPath;
@@ -48,7 +61,9 @@ import static
org.apache.hudi.hadoop.fs.HadoopFSUtils.convertToStoragePathInfo;
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertInstanceOf;
+import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
@@ -255,6 +270,121 @@ public class TestHadoopFSUtils {
convertedPathInfo, path, length, isDirectory, blockReplication,
blockSize, modificationTime);
}
+ @Test
+ public void testStorageConfigurationCopyAndFileSystemOverloads(@TempDir
java.nio.file.Path tempDir) {
+ Configuration conf = new Configuration(false);
+ conf.set("test.key", "before");
+ StorageConfiguration<Configuration> shared =
HadoopFSUtils.getStorageConf(conf);
+ StorageConfiguration<Configuration> copied =
HadoopFSUtils.getStorageConfWithCopy(conf);
+ conf.set("test.key", "after");
+
+ assertEquals("after", shared.getString("test.key").get());
+ assertEquals("before", copied.getString("test.key").get());
+ assertEquals("file", HadoopFSUtils.getFs(tempDir.toUri().toString(),
shared).getScheme());
+ assertEquals("file", HadoopFSUtils.getFs(tempDir.toUri().toString(),
shared, true).getScheme());
+ assertEquals("file", HadoopFSUtils.getFs(new Path(tempDir.toUri()),
shared).getScheme());
+ assertEquals("file", HadoopFSUtils.getFs(new Path(tempDir.toUri()),
shared, true).getScheme());
+ assertEquals("file", HadoopFSUtils.getFs(new StoragePath(tempDir.toUri()),
conf).getScheme());
+ assertEquals("file", HadoopFSUtils.getFs(tempDir.toString(), conf,
true).getScheme());
+ assertInstanceOf(StorageConfiguration.class,
HadoopFSUtils.getStorageConf());
+ }
+
+ @Test
+ public void testAvroPathPermissionAndStatusConversions() {
+ Path path = new Path("s3://bucket/table/file.parquet");
+ HoodiePath hoodiePath = HadoopFSUtils.fromPath(path);
+ assertEquals(path, HadoopFSUtils.toPath(hoodiePath));
+ assertNull(HadoopFSUtils.toPath(null));
+ assertNull(HadoopFSUtils.fromPath(null));
+
+ FsPermission permission = new FsPermission(
+ FsAction.ALL, FsAction.READ_EXECUTE, FsAction.READ, true);
+ HoodieFSPermission hoodiePermission =
HadoopFSUtils.fromFSPermission(permission);
+ assertEquals(permission, HadoopFSUtils.toFSPermission(hoodiePermission));
+ assertNull(HadoopFSUtils.toFSPermission(null));
+ assertNull(HadoopFSUtils.fromFSPermission(null));
+
+ FileStatus status = new FileStatus(123, false, 2, 4096, 1000, 900,
+ permission, "owner", "group", path);
+ HoodieFileStatus converted = HadoopFSUtils.fromFileStatus(status);
+ assertEquals(path, HadoopFSUtils.toPath(converted.getPath()));
+ assertEquals(123, converted.getLength());
+ assertEquals("owner", converted.getOwner());
+ assertEquals("group", converted.getGroup());
+ assertEquals(permission,
HadoopFSUtils.toFSPermission(converted.getPermission()));
+ assertNull(HadoopFSUtils.fromFileStatus(null));
+ }
+
+ @Test
+ public void testStatusLocationsAndFileNameHelpers() {
+ FileStatus fileStatus = new FileStatus(10, false, 1, 128, 1000, new
Path("/table/file.parquet"));
+ StoragePathInfo pathInfo = HadoopFSUtils.convertToStoragePathInfo(
+ fileStatus, new String[] {"host1", "host2"});
+ assertArrayEquals(new String[] {"host1", "host2"},
pathInfo.getLocations());
+
+ assertTrue(HadoopFSUtils.isBaseFile(new Path("fileId_1-0-1_000.parquet")));
+ assertTrue(HadoopFSUtils.isLogFile(new Path(".file_100.log.1_1-0-1")));
+ assertTrue(HadoopFSUtils.isDataFile(new Path("fileId_1-0-1_000.orc")));
+ assertFalse(HadoopFSUtils.isDataFile(new Path("README.md")));
+ assertEquals(new Path("file:///table/partition"),
+ HadoopFSUtils.constructAbsolutePathInHadoopPath("file:///table",
"partition"));
+ }
+
+ @Test
+ public void testDfsFullPartitionPath() throws IOException {
+ try (FileSystem fs = FileSystem.newInstanceLocal(new Configuration())) {
+ assertEquals(fs.getUri() + "/tmp/table",
+ HadoopFSUtils.getDFSFullPartitionPath(fs, new Path("/tmp/table")));
+ }
+ }
+
+ @Test
+ public void testFileNameAndRelativePathDelegates() {
+ assertEquals("partition", HadoopFSUtils.getRelativePartitionPath(
+ new Path("/table"), new Path("/table/partition")));
+ Path logPath = new Path("/table/.file-id_001.log.1_1-0-1");
+ assertEquals("file-id", HadoopFSUtils.getFileIdFromLogPath(logPath));
+ assertEquals("001", HadoopFSUtils.getDeltaCommitTimeFromLogPath(logPath));
+ }
+
+ @Test
+ public void testRecoverLeaseStopsAfterSuccess() throws Exception {
+ AtomicInteger attempts = new AtomicInteger();
+ DistributedFileSystem fs = new DistributedFileSystem() {
+ @Override
+ public boolean recoverLease(Path path) {
+ attempts.incrementAndGet();
+ return true;
+ }
+ };
+
+ assertTrue(HadoopFSUtils.recoverDFSFileLease(fs, new Path("/table/file")));
+ assertEquals(1, attempts.get());
+ }
+
+ @Test
+ public void testParallelFileProcessingAndStatusAtLevel(@TempDir
java.nio.file.Path tempDir) throws Exception {
+ Configuration conf = new Configuration();
+ HoodieLocalEngineContext context = new
HoodieLocalEngineContext(HadoopFSUtils.getStorageConf(conf));
+ try (FileSystem fs = FileSystem.newInstanceLocal(conf)) {
+ Map<String, Integer> lengths = HadoopFSUtils.parallelizeFilesProcess(
+ context, fs, 2, pair -> pair.getKey().length(), Arrays.asList("a",
"longer"));
+ assertEquals(1, lengths.get("a"));
+ assertEquals(6, lengths.get("longer"));
+ assertTrue(HadoopFSUtils.parallelizeFilesProcess(
+ context, fs, 2, pair -> pair.getKey().length(),
Collections.<String>emptyList()).isEmpty());
+
+ Path root = new Path(tempDir.resolve("root").toUri());
+ Path secondLevel = new Path(new Path(root, "first"), "second");
+ assertTrue(fs.mkdirs(secondLevel));
+ Path leaf = new Path(secondLevel, "leaf.txt");
+ assertTrue(fs.createNewFile(leaf));
+ List<FileStatus> statuses = HadoopFSUtils.getFileStatusAtLevel(context,
fs, root, 2, 2);
+ assertEquals(Collections.singletonList(leaf),
+
statuses.stream().map(FileStatus::getPath).collect(java.util.stream.Collectors.toList()));
+ }
+ }
+
private void assertFileStatus(FileStatus fileStatus,
String path,
long length,
diff --git
a/hudi-hadoop-common/src/test/java/org/apache/hudi/hadoop/fs/TestHoodieRetryWrapperFileSystem.java
b/hudi-hadoop-common/src/test/java/org/apache/hudi/hadoop/fs/TestHoodieRetryWrapperFileSystem.java
new file mode 100644
index 000000000000..3a091f283a2d
--- /dev/null
+++
b/hudi-hadoop-common/src/test/java/org/apache/hudi/hadoop/fs/TestHoodieRetryWrapperFileSystem.java
@@ -0,0 +1,158 @@
+/*
+ * 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.hadoop.fs;
+
+import org.apache.hadoop.conf.Configuration;
+import org.apache.hadoop.fs.CreateFlag;
+import org.apache.hadoop.fs.FSDataOutputStream;
+import org.apache.hadoop.fs.FileStatus;
+import org.apache.hadoop.fs.FileSystem;
+import org.apache.hadoop.fs.FilterFileSystem;
+import org.apache.hadoop.fs.Options;
+import org.apache.hadoop.fs.Path;
+import org.apache.hadoop.fs.permission.FsPermission;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
+
+import java.io.IOException;
+import java.util.EnumSet;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+class TestHoodieRetryWrapperFileSystem {
+
+ @Test
+ void testFileOperationsDelegateToWrappedFileSystem(@TempDir
java.nio.file.Path tempDir) throws IOException {
+ try (FileSystem local = FileSystem.newInstanceLocal(new Configuration())) {
+ HoodieRetryWrapperFileSystem fs = new
HoodieRetryWrapperFileSystem(local, 0, 0, 0, "");
+ Path directory = new Path(tempDir.toUri());
+ Path file = new Path(directory, "data.bin");
+
+ assertEquals(local.getUri(), fs.getUri());
+ assertEquals(local.getConf(), fs.getConf());
+ assertEquals("file", fs.getScheme());
+ assertEquals(local.getDefaultReplication(), fs.getDefaultReplication());
+ assertEquals(local.getDefaultReplication(file),
fs.getDefaultReplication(file));
+ fs.setWorkingDirectory(directory);
+ assertEquals(directory, fs.getWorkingDirectory());
+ assertTrue(fs.mkdirs(directory, FsPermission.getDirDefault()));
+
+ try (FSDataOutputStream output = fs.create(file)) {
+ output.write(new byte[] {1, 2, 3});
+ }
+ assertTrue(fs.exists(file));
+ assertEquals(3, fs.getFileStatus(file).getLen());
+ fs.open(file).close();
+ fs.open(file, 128).close();
+
+ Path[] files = new Path[] {file};
+ assertEquals(1, fs.listStatus(directory).length);
+ assertEquals(1, fs.listStatus(directory, path -> true).length);
+ assertEquals(1, fs.listStatus(files).length);
+ assertEquals(1, fs.listStatus(files, path -> true).length);
+ assertEquals(1, fs.globStatus(new Path(directory, "*.bin")).length);
+ assertEquals(1, fs.globStatus(new Path(directory, "*.bin"), path ->
true).length);
+ assertTrue(fs.listLocatedStatus(directory).hasNext());
+ assertTrue(fs.listFiles(directory, true).hasNext());
+
+ Path renamed = new Path(directory, "renamed.bin");
+ assertTrue(fs.rename(file, renamed));
+ assertTrue(fs.delete(renamed, false));
+ assertFalse(fs.exists(renamed));
+ assertTrue(fs.createNewFile(new Path(directory, "empty")));
+ }
+ }
+
+ @Test
+ void testCreateOverloadsDelegate(@TempDir java.nio.file.Path tempDir) throws
IOException {
+ try (FileSystem local = FileSystem.newInstanceLocal(new Configuration())) {
+ HoodieRetryWrapperFileSystem fs = new
HoodieRetryWrapperFileSystem(local, 0, 0, 0, "");
+ Path directory = new Path(tempDir.toUri());
+ FsPermission permission = FsPermission.getFileDefault();
+ short replication = local.getDefaultReplication(directory);
+ long blockSize = local.getDefaultBlockSize(directory);
+ AtomicInteger suffix = new AtomicInteger();
+
+ close(fs.create(nextPath(directory, suffix), true));
+ close(fs.create(nextPath(directory, suffix), () -> { }));
+ close(fs.create(nextPath(directory, suffix), replication));
+ close(fs.create(nextPath(directory, suffix), replication, () -> { }));
+ close(fs.create(nextPath(directory, suffix), true, 4096));
+ close(fs.create(nextPath(directory, suffix), true, 4096, () -> { }));
+ close(fs.create(nextPath(directory, suffix), true, 4096, replication,
blockSize));
+ close(fs.create(nextPath(directory, suffix), true, 4096, replication,
blockSize, () -> { }));
+ close(fs.create(nextPath(directory, suffix), permission, true, 4096,
replication, blockSize, () -> { }));
+ close(fs.create(nextPath(directory, suffix), permission,
EnumSet.of(CreateFlag.CREATE),
+ 4096, replication, blockSize, () -> { }));
+ close(fs.create(nextPath(directory, suffix), permission,
EnumSet.of(CreateFlag.CREATE),
+ 4096, replication, blockSize, () -> { },
Options.ChecksumOpt.createDisabled()));
+ }
+ }
+
+ @Test
+ void testDeleteRetriesWhenDelegateReportsExistingFile() throws IOException {
+ FlakyDeleteFileSystem delegate = new
FlakyDeleteFileSystem(FileSystem.newInstanceLocal(new Configuration()));
+ try {
+ HoodieRetryWrapperFileSystem fs = new
HoodieRetryWrapperFileSystem(delegate, 0, 2, 0, "");
+
+ assertTrue(fs.delete(new Path("file:///eventually-deleted")));
+ assertEquals(3, delegate.deleteAttempts);
+ assertEquals(2, delegate.existsChecks);
+ } finally {
+ delegate.close();
+ }
+ }
+
+ private static Path nextPath(Path directory, AtomicInteger suffix) {
+ return new Path(directory, "create-" + suffix.incrementAndGet());
+ }
+
+ private static void close(FSDataOutputStream stream) throws IOException {
+ stream.close();
+ }
+
+ private static class FlakyDeleteFileSystem extends FilterFileSystem {
+ private int deleteAttempts;
+ private int existsChecks;
+
+ private FlakyDeleteFileSystem(FileSystem delegate) {
+ super(delegate);
+ }
+
+ @Override
+ public boolean delete(Path path, boolean recursive) {
+ return ++deleteAttempts >= 3;
+ }
+
+ @Override
+ public boolean exists(Path path) {
+ existsChecks++;
+ return true;
+ }
+
+ @Override
+ public FileStatus getFileStatus(Path path) throws IOException {
+ return fs.getFileStatus(path);
+ }
+ }
+}
diff --git
a/hudi-hadoop-common/src/test/java/org/apache/hudi/hadoop/fs/TestHoodieWrapperFileSystemOperations.java
b/hudi-hadoop-common/src/test/java/org/apache/hudi/hadoop/fs/TestHoodieWrapperFileSystemOperations.java
new file mode 100644
index 000000000000..b9f6864464ce
--- /dev/null
+++
b/hudi-hadoop-common/src/test/java/org/apache/hudi/hadoop/fs/TestHoodieWrapperFileSystemOperations.java
@@ -0,0 +1,476 @@
+/*
+ * 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.hadoop.fs;
+
+import org.apache.hudi.common.fs.ConsistencyGuard;
+import org.apache.hudi.common.fs.NoOpConsistencyGuard;
+import org.apache.hudi.common.metrics.Registry;
+import org.apache.hudi.exception.HoodieException;
+import org.apache.hudi.storage.StoragePath;
+
+import org.apache.hadoop.conf.Configuration;
+import org.apache.hadoop.fs.CreateFlag;
+import org.apache.hadoop.fs.FSDataInputStream;
+import org.apache.hadoop.fs.FSDataOutputStream;
+import org.apache.hadoop.fs.FileStatus;
+import org.apache.hadoop.fs.FileSystem;
+import org.apache.hadoop.fs.Options;
+import org.apache.hadoop.fs.Path;
+import org.apache.hadoop.fs.XAttrSetFlag;
+import org.apache.hadoop.fs.permission.AclEntry;
+import org.apache.hadoop.fs.permission.FsAction;
+import org.apache.hadoop.fs.permission.FsPermission;
+import org.apache.hadoop.security.Credentials;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
+
+import java.io.IOException;
+import java.net.URI;
+import java.nio.file.Files;
+import java.util.Collections;
+import java.util.EnumSet;
+import java.util.List;
+import java.util.concurrent.TimeoutException;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import static org.junit.jupiter.api.Assertions.assertArrayEquals;
+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.assertSame;
+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.verify;
+import static org.mockito.Mockito.when;
+
+class TestHoodieWrapperFileSystemOperations {
+
+ @Test
+ void testPathTranslationAndInitialization(@TempDir java.nio.file.Path
tempDir) throws Exception {
+ Configuration conf = new Configuration();
+ try (FileSystem local = FileSystem.newInstanceLocal(conf)) {
+ HoodieWrapperFileSystem fs = new HoodieWrapperFileSystem(local, new
NoOpConsistencyGuard());
+ Path path = new Path(tempDir.resolve("file.parquet").toUri());
+
+ assertEquals("hoodie-file",
HoodieWrapperFileSystem.getHoodieScheme("file"));
+ assertThrows(IllegalArgumentException.class, () ->
HoodieWrapperFileSystem.getHoodieScheme("unknown"));
+ assertEquals("hoodie-file", HoodieWrapperFileSystem.convertToHoodiePath(
+ new StoragePath(path.toUri()), conf).toUri().getScheme());
+
+ Path converted = HoodieWrapperFileSystem.convertPathWithScheme(
+ new Path(new URI("file", null, path.toUri().getPath(), "query=1",
"fragment")), "hoodie-file");
+ assertTrue(converted instanceof CachingPath);
+ assertEquals("hoodie-file", converted.toUri().getScheme());
+ assertEquals("query=1", converted.toUri().getQuery());
+ assertEquals("fragment", converted.toUri().getFragment());
+
+ assertEquals("file", fs.getScheme());
+ assertEquals("hoodie-file",
fs.convertToHoodiePath(path).toUri().getScheme());
+ assertSame(local, fs.getFileSystem());
+ assertEquals(local.getUri(), fs.getUri());
+ assertEquals(local.getConf(), fs.getConf());
+ assertEquals(local.hashCode(), fs.hashCode());
+ assertTrue(fs.equals(local));
+ assertEquals(local.toString(), fs.toString());
+ fs.setConf(new Configuration());
+ assertSame(local.getConf(), fs.getConf());
+ fs.close();
+
+ HoodieWrapperFileSystem initialized = new HoodieWrapperFileSystem();
+ initialized.initialize(URI.create("hoodie-file:" +
tempDir.toUri().getPath()), conf);
+ assertEquals("file", initialized.getScheme());
+ assertEquals("file", initialized.getFileSystem().getScheme());
+
+ HoodieWrapperFileSystem initializedWithoutPrefix = new
HoodieWrapperFileSystem();
+ initializedWithoutPrefix.initialize(tempDir.toUri(), conf);
+ assertEquals("file", initializedWithoutPrefix.getScheme());
+ }
+ }
+
+ @Test
+ void testLocalFileOperationsAndStreamAccounting(@TempDir java.nio.file.Path
tempDir) throws IOException {
+ try (FileSystem local = FileSystem.newInstanceLocal(new Configuration())) {
+ HoodieWrapperFileSystem fs = new HoodieWrapperFileSystem(local, new
NoOpConsistencyGuard());
+ Path directory = new Path(tempDir.resolve("table").toUri());
+ Path file = new Path(directory, "data.bin");
+
+ assertTrue(fs.mkdirs(directory));
+ assertTrue(fs.mkdirs(directory, FsPermission.getDirDefault()));
+ fs.setWorkingDirectory(directory);
+ assertEquals("hoodie-file",
fs.getWorkingDirectory().toUri().getScheme());
+ assertEquals("hoodie-file", fs.getHomeDirectory().toUri().getScheme());
+
+ try (FSDataOutputStream output = fs.create(file)) {
+ output.write(new byte[] {1, 2, 3, 4}, 0, 4);
+ assertEquals(4, fs.getBytesWritten(file));
+ }
+ assertThrows(IllegalArgumentException.class, () ->
fs.getBytesWritten(file));
+
+ try (FSDataInputStream input = fs.open(file)) {
+ assertEquals(1, input.read());
+ }
+ try (FSDataInputStream input = fs.open(file, 128)) {
+ assertEquals(1, input.read());
+ }
+
+ assertTrue(fs.exists(file));
+ assertTrue(fs.isFile(file));
+ assertFalse(fs.isDirectory(file));
+ assertEquals(4, fs.getLength(file));
+ assertEquals(4, fs.getFileStatus(file).getLen());
+ assertEquals(4, fs.getContentSummary(file).getLength());
+ assertEquals(1, fs.listStatus(directory).length);
+ assertEquals(1, fs.listStatus(directory, candidate -> true).length);
+ assertEquals(1, fs.listStatus(new Path[] {file}).length);
+ assertEquals(1, fs.listStatus(new Path[] {file}, candidate ->
true).length);
+ assertEquals(0, fs.listStatus(new Path[0]).length);
+ assertEquals(0, fs.listStatus(new Path[0], candidate -> true).length);
+ assertEquals(1, fs.globStatus(new Path(directory, "*.bin")).length);
+ assertEquals(1, fs.globStatus(new Path(directory, "*.bin"), candidate ->
true).length);
+ assertTrue(fs.listLocatedStatus(directory).hasNext());
+ assertTrue(fs.listFiles(directory, true).hasNext());
+
+ FileStatus status = fs.getFileStatus(file);
+ assertNotNull(fs.getFileBlockLocations(status, 0, status.getLen()));
+ assertNotNull(fs.getFileBlockLocations(file, 0, status.getLen()));
+ assertNotNull(fs.getServerDefaults());
+ assertNotNull(fs.getServerDefaults(file));
+ assertNotNull(fs.getStatus());
+ assertNotNull(fs.getStatus(file));
+ assertTrue(fs.getBlockSize(file) > 0);
+ assertTrue(fs.getDefaultBlockSize() > 0);
+ assertTrue(fs.getDefaultBlockSize(file) > 0);
+ assertTrue(fs.getDefaultReplication() > 0);
+ assertTrue(fs.getDefaultReplication(file) > 0);
+ assertTrue(fs.getReplication(file) > 0);
+ assertTrue(fs.setReplication(file, fs.getReplication(file)));
+ fs.access(file, FsAction.READ);
+ fs.setPermission(file, FsPermission.getFileDefault());
+ fs.setTimes(file, System.currentTimeMillis(), -1);
+ fs.setVerifyChecksum(true);
+ fs.setWriteChecksum(true);
+ assertEquals(local.getFileChecksum(file), fs.getFileChecksum(file));
+
+ assertEquals("hoodie-file", fs.makeQualified(file).toUri().getScheme());
+ assertEquals("hoodie-file", fs.resolvePath(file).toUri().getScheme());
+ assertEquals(local.getCanonicalServiceName(),
fs.getCanonicalServiceName());
+ assertEquals(local.getName(), fs.getName());
+ assertEquals(local.supportsSymlinks(), fs.supportsSymlinks());
+ assertArrayEquals(local.getChildFileSystems(), fs.getChildFileSystems());
+
+ Path renamed = new Path(directory, "renamed.bin");
+ assertTrue(fs.rename(file, renamed));
+ assertTrue(fs.delete(renamed));
+ assertFalse(fs.exists(renamed));
+ assertTrue(fs.createNewFile(new Path(directory, "empty")));
+ }
+ }
+
+ @Test
+ void testCreateOverloads(@TempDir java.nio.file.Path tempDir) throws
IOException {
+ try (FileSystem local = FileSystem.newInstanceLocal(new Configuration())) {
+ HoodieWrapperFileSystem fs = new HoodieWrapperFileSystem(local, new
NoOpConsistencyGuard());
+ Path directory = new Path(tempDir.toUri());
+ FsPermission permission = FsPermission.getFileDefault();
+ short replication = local.getDefaultReplication(directory);
+ long blockSize = local.getDefaultBlockSize(directory);
+ AtomicInteger suffix = new AtomicInteger();
+
+ close(fs.create(nextPath(directory, suffix), true));
+ close(fs.create(nextPath(directory, suffix), () -> { }));
+ close(fs.create(nextPath(directory, suffix), replication));
+ close(fs.create(nextPath(directory, suffix), replication, () -> { }));
+ close(fs.create(nextPath(directory, suffix), true, 4096));
+ close(fs.create(nextPath(directory, suffix), true, 4096, () -> { }));
+ close(fs.create(nextPath(directory, suffix), true, 4096, replication,
blockSize));
+ close(fs.create(nextPath(directory, suffix), true, 4096, replication,
blockSize, () -> { }));
+ close(fs.create(nextPath(directory, suffix), permission, true, 4096,
replication, blockSize, () -> { }));
+ close(fs.create(nextPath(directory, suffix), permission,
EnumSet.of(CreateFlag.CREATE),
+ 4096, replication, blockSize, () -> { }));
+ close(fs.create(nextPath(directory, suffix), permission,
EnumSet.of(CreateFlag.CREATE),
+ 4096, replication, blockSize, () -> { },
Options.ChecksumOpt.createDisabled()));
+
+ Path nonRecursive = nextPath(directory, suffix);
+ close(fs.createNonRecursive(nonRecursive, true, 4096, replication,
blockSize, () -> { }));
+ close(fs.createNonRecursive(nextPath(directory, suffix), permission,
true,
+ 4096, replication, blockSize, () -> { }));
+ close(fs.createNonRecursive(nextPath(directory, suffix), permission,
EnumSet.of(CreateFlag.CREATE),
+ 4096, replication, blockSize, () -> { }));
+ }
+ }
+
+ @Test
+ void testDataAndMetadataMetricsAreSeparated(@TempDir java.nio.file.Path
tempDir) throws IOException {
+ Registry dataRegistry = Registry.getRegistry("wrapper-coverage-data-" +
System.nanoTime());
+ Registry metaRegistry = Registry.getRegistry("wrapper-coverage-meta-" +
System.nanoTime());
+ HoodieWrapperFileSystem.setMetricsRegistry(dataRegistry, metaRegistry);
+ try (FileSystem local = FileSystem.newInstanceLocal(new Configuration())) {
+ HoodieWrapperFileSystem fs = new HoodieWrapperFileSystem(local, new
NoOpConsistencyGuard());
+ Path dataDirectory = new Path(tempDir.resolve("data").toUri());
+ Path metaDirectory = new Path(tempDir.resolve(".hoodie").toUri());
+
+ assertTrue(fs.mkdirs(dataDirectory));
+ assertTrue(fs.mkdirs(metaDirectory));
+ assertEquals(1L, dataRegistry.getAllCounts().get("mkdirs"));
+ assertEquals(1L, metaRegistry.getAllCounts().get("mkdirs"));
+
+ HoodieWrapperFileSystem.executeFuncWithTimeAndByteMetrics(
+ "write", dataDirectory, 17, () -> true);
+ assertEquals(17L, dataRegistry.getAllCounts().get("write.totalBytes"));
+ assertEquals(1L, dataRegistry.getAllCounts().get("write"));
+ } finally {
+ HoodieWrapperFileSystem.setMetricsRegistry(null, null);
+ }
+ }
+
+ @Test
+ void testOptionalDelegatedFileSystemApis() throws IOException {
+ FileSystem delegate = mock(FileSystem.class);
+ when(delegate.getUri()).thenReturn(URI.create("file:///"));
+ when(delegate.getScheme()).thenReturn("file");
+ when(delegate.getUsed()).thenReturn(42L);
+
+ HoodieWrapperFileSystem fs = wrapper(delegate, new NoOpConsistencyGuard());
+ Path directory = new Path("hoodie-file:///optional");
+ Path file = new Path(directory, "data.bin");
+ Path link = new Path(directory, "data.link");
+ Path deleteOnExit = new Path(directory, "delete-on-exit");
+ Path defaultDirectory = new Path("file:///optional");
+ Path defaultFile = new Path(defaultDirectory, "data.bin");
+ Path defaultLink = new Path(defaultDirectory, "data.link");
+ Path defaultDeleteOnExit = new Path(defaultDirectory, "delete-on-exit");
+ Credentials credentials = new Credentials();
+ List<AclEntry> aclEntries = Collections.emptyList();
+ byte[] attribute = new byte[] {1, 2};
+ EnumSet<XAttrSetFlag> xAttrFlags = EnumSet.of(XAttrSetFlag.REPLACE);
+ List<String> xAttrNames = Collections.singletonList("user.hudi");
+
+ when(delegate.getLinkTarget(defaultLink)).thenReturn(defaultFile);
+ when(delegate.createSnapshot(defaultDirectory,
"snapshot")).thenReturn(defaultDirectory);
+ when(delegate.deleteOnExit(defaultDeleteOnExit)).thenReturn(true);
+ when(delegate.cancelDeleteOnExit(defaultDeleteOnExit)).thenReturn(true);
+
+ fs.getDelegationToken("renewer");
+ fs.addDelegationTokens("renewer", credentials);
+ fs.listCorruptFileBlocks(directory);
+ assertEquals(42L, fs.getUsed());
+ fs.getFileChecksum(file, 1);
+ fs.setOwner(file, "owner", null);
+ fs.createSymlink(file, link, false);
+ fs.getFileLinkStatus(link);
+ assertEquals("hoodie-file", fs.getLinkTarget(link).toUri().getScheme());
+ assertEquals("hoodie-file", fs.createSnapshot(directory,
"snapshot").toUri().getScheme());
+ fs.renameSnapshot(directory, "snapshot", "renamed");
+ fs.deleteSnapshot(directory, "renamed");
+ fs.modifyAclEntries(file, aclEntries);
+ fs.removeAclEntries(file, aclEntries);
+ fs.removeDefaultAcl(file);
+ fs.removeAcl(file);
+ fs.setAcl(file, aclEntries);
+ fs.getAclStatus(file);
+ fs.setXAttr(file, "user.hudi", attribute);
+ fs.setXAttr(file, "user.hudi", attribute, xAttrFlags);
+ fs.getXAttr(file, "user.hudi");
+ fs.getXAttrs(file);
+ fs.getXAttrs(file, xAttrNames);
+ fs.listXAttrs(file);
+ fs.removeXAttr(file, "user.hudi");
+ assertTrue(fs.deleteOnExit(deleteOnExit));
+ assertTrue(fs.cancelDeleteOnExit(deleteOnExit));
+
+ verify(delegate).getDelegationToken("renewer");
+ verify(delegate).addDelegationTokens("renewer", credentials);
+ verify(delegate).listCorruptFileBlocks(defaultDirectory);
+ verify(delegate).getUsed();
+ verify(delegate).getFileChecksum(defaultFile, 1);
+ verify(delegate).setOwner(defaultFile, "owner", null);
+ verify(delegate).createSymlink(defaultFile, defaultLink, false);
+ verify(delegate).getFileLinkStatus(defaultLink);
+ verify(delegate).getLinkTarget(defaultLink);
+ verify(delegate).createSnapshot(defaultDirectory, "snapshot");
+ verify(delegate).renameSnapshot(defaultDirectory, "snapshot", "renamed");
+ verify(delegate).deleteSnapshot(defaultDirectory, "renamed");
+ verify(delegate).modifyAclEntries(defaultFile, aclEntries);
+ verify(delegate).removeAclEntries(defaultFile, aclEntries);
+ verify(delegate).removeDefaultAcl(defaultFile);
+ verify(delegate).removeAcl(defaultFile);
+ verify(delegate).setAcl(defaultFile, aclEntries);
+ verify(delegate).getAclStatus(defaultFile);
+ verify(delegate).setXAttr(defaultFile, "user.hudi", attribute);
+ verify(delegate).setXAttr(defaultFile, "user.hudi", attribute, xAttrFlags);
+ verify(delegate).getXAttr(defaultFile, "user.hudi");
+ verify(delegate).getXAttrs(defaultFile);
+ verify(delegate).getXAttrs(defaultFile, xAttrNames);
+ verify(delegate).listXAttrs(defaultFile);
+ verify(delegate).removeXAttr(defaultFile, "user.hudi");
+ verify(delegate).deleteOnExit(defaultDeleteOnExit);
+ verify(delegate).cancelDeleteOnExit(defaultDeleteOnExit);
+ }
+
+ @Test
+ void testLocalCopyAndMoveOverloads(@TempDir java.nio.file.Path tempDir)
throws Exception {
+ try (FileSystem local = FileSystem.newInstanceLocal(new Configuration())) {
+ HoodieWrapperFileSystem fs = new HoodieWrapperFileSystem(local, new
NoOpConsistencyGuard());
+ Path destinationDirectory = new
Path(tempDir.resolve("destinations").toUri());
+ assertTrue(fs.mkdirs(destinationDirectory));
+
+ Path copied = new Path(destinationDirectory, "copied");
+ fs.copyFromLocalFile(localFile(tempDir, "copy-source", 1), copied);
+ assertTrue(fs.exists(copied));
+
+ Path moved = new Path(destinationDirectory, "moved");
+ fs.moveFromLocalFile(localFile(tempDir, "move-source", 2), moved);
+ assertTrue(fs.exists(moved));
+
+ Path copiedWithFlag = new Path(destinationDirectory, "copied-with-flag");
+ fs.copyFromLocalFile(false, localFile(tempDir, "copy-flag-source", 3),
copiedWithFlag);
+ assertTrue(fs.exists(copiedWithFlag));
+
+ Path copiedWithFlags = new Path(destinationDirectory,
"copied-with-flags");
+ fs.copyFromLocalFile(false, true, localFile(tempDir,
"copy-flags-source", 4), copiedWithFlags);
+ assertTrue(fs.exists(copiedWithFlags));
+
+ Path arrayDestination = new Path(destinationDirectory, "array-copy");
+ assertTrue(fs.mkdirs(arrayDestination));
+ fs.copyFromLocalFile(false, true,
+ new Path[] {localFile(tempDir, "array-source-1", 5),
localFile(tempDir, "array-source-2", 6)},
+ arrayDestination);
+ assertEquals(2, fs.listStatus(arrayDestination).length);
+
+ Path moveArrayDestination = new Path(destinationDirectory, "array-move");
+ assertTrue(fs.mkdirs(moveArrayDestination));
+ fs.moveFromLocalFile(
+ new Path[] {localFile(tempDir, "move-array-source-1", 7),
localFile(tempDir, "move-array-source-2", 8)},
+ moveArrayDestination);
+ assertEquals(2, fs.listStatus(moveArrayDestination).length);
+
+ Path localCopy = new Path(tempDir.resolve("local-copy").toUri());
+ fs.copyToLocalFile(copied, localCopy);
+ assertTrue(local.exists(localCopy));
+ Path localCopyWithFlag = new
Path(tempDir.resolve("local-copy-flag").toUri());
+ fs.copyToLocalFile(false, copied, localCopyWithFlag);
+ assertTrue(local.exists(localCopyWithFlag));
+ Path localRawCopy = new Path(tempDir.resolve("local-raw-copy").toUri());
+ fs.copyToLocalFile(false, copied, localRawCopy, true);
+ assertTrue(local.exists(localRawCopy));
+
+ Path localMove = new Path(tempDir.resolve("local-move").toUri());
+ fs.moveToLocalFile(copiedWithFlag, localMove);
+ assertTrue(local.exists(localMove));
+ }
+ }
+
+ @Test
+ void testConsistencyGuardTimeoutBranches(@TempDir java.nio.file.Path
tempDir) throws Exception {
+ try (FileSystem local = FileSystem.newInstanceLocal(new Configuration())) {
+ Path root = new Path(tempDir.toUri());
+
+ HoodieWrapperFileSystem mkdirFs = wrapper(local, new
FailingConsistencyGuard(1, 0));
+ assertThrows(HoodieException.class, () -> mkdirFs.mkdirs(new Path(root,
"mkdir-timeout")));
+ HoodieWrapperFileSystem permissionMkdirFs = wrapper(local, new
FailingConsistencyGuard(1, 0));
+ assertThrows(HoodieException.class, () -> permissionMkdirFs.mkdirs(
+ new Path(root, "permission-mkdir-timeout"),
FsPermission.getDirDefault()));
+
+ HoodieWrapperFileSystem createFs = wrapper(local, new
FailingConsistencyGuard(1, 0));
+ assertThrows(HoodieException.class, () -> createFs.createNewFile(new
Path(root, "create-timeout")));
+
+ Path statusFile = new Path(root, "status-timeout");
+ assertTrue(local.createNewFile(statusFile));
+ HoodieWrapperFileSystem statusFs = wrapper(local, new
FailingConsistencyGuard(1, 0));
+ assertEquals(0, statusFs.getFileStatus(statusFile).getLen());
+
+ Path deleteFile = new Path(root, "delete-timeout");
+ assertTrue(local.createNewFile(deleteFile));
+ HoodieWrapperFileSystem deleteFs = wrapper(local, new
FailingConsistencyGuard(0, 1));
+ assertThrows(HoodieException.class, () -> deleteFs.delete(deleteFile,
false));
+
+ Path renameBefore = new Path(root, "rename-before");
+ assertTrue(local.createNewFile(renameBefore));
+ HoodieWrapperFileSystem renameBeforeFs = wrapper(local, new
FailingConsistencyGuard(1, 0));
+ assertThrows(HoodieException.class,
+ () -> renameBeforeFs.rename(renameBefore, new Path(root,
"rename-before-destination")));
+
+ Path renameDestination = new Path(root, "rename-destination");
+ assertTrue(local.createNewFile(renameDestination));
+ HoodieWrapperFileSystem renameDestinationFs = wrapper(local, new
FailingConsistencyGuard(2, 0));
+ assertThrows(HoodieException.class,
+ () -> renameDestinationFs.rename(renameDestination, new Path(root,
"rename-destination-result")));
+
+ Path renameDisappear = new Path(root, "rename-disappear");
+ assertTrue(local.createNewFile(renameDisappear));
+ HoodieWrapperFileSystem renameDisappearFs = wrapper(local, new
FailingConsistencyGuard(0, 1));
+ assertThrows(HoodieException.class,
+ () -> renameDisappearFs.rename(renameDisappear, new Path(root,
"rename-disappear-result")));
+ }
+ }
+
+ private static Path nextPath(Path directory, AtomicInteger suffix) {
+ return new Path(directory, "create-" + suffix.incrementAndGet());
+ }
+
+ private static void close(FSDataOutputStream stream) throws IOException {
+ stream.close();
+ }
+
+ private static HoodieWrapperFileSystem wrapper(FileSystem fileSystem,
ConsistencyGuard consistencyGuard) {
+ return new HoodieWrapperFileSystem(fileSystem, consistencyGuard);
+ }
+
+ private static Path localFile(java.nio.file.Path tempDir, String name, int
value) throws IOException {
+ java.nio.file.Path file = tempDir.resolve(name);
+ Files.write(file, new byte[] {(byte) value});
+ return new Path(file.toUri());
+ }
+
+ private static final class FailingConsistencyGuard implements
ConsistencyGuard {
+ private final int failAppearCall;
+ private final int failDisappearCall;
+ private int appearCalls;
+ private int disappearCalls;
+
+ private FailingConsistencyGuard(int failAppearCall, int failDisappearCall)
{
+ this.failAppearCall = failAppearCall;
+ this.failDisappearCall = failDisappearCall;
+ }
+
+ @Override
+ public void waitTillFileAppears(StoragePath filePath) throws
TimeoutException {
+ if (++appearCalls == failAppearCall) {
+ throw new TimeoutException(filePath.toString());
+ }
+ }
+
+ @Override
+ public void waitTillFileDisappears(StoragePath filePath) throws
TimeoutException {
+ if (++disappearCalls == failDisappearCall) {
+ throw new TimeoutException(filePath.toString());
+ }
+ }
+
+ @Override
+ public void waitTillAllFilesAppear(String dirPath, List<String> files) {
+ }
+
+ @Override
+ public void waitTillAllFilesDisappear(String dirPath, List<String> files) {
+ }
+ }
+}
diff --git
a/hudi-hadoop-common/src/test/java/org/apache/hudi/io/storage/hadoop/TestHoodieVariantReconstruction.java
b/hudi-hadoop-common/src/test/java/org/apache/hudi/io/storage/hadoop/TestHoodieVariantReconstruction.java
index 872a98a16460..f522c21d1caf 100644
---
a/hudi-hadoop-common/src/test/java/org/apache/hudi/io/storage/hadoop/TestHoodieVariantReconstruction.java
+++
b/hudi-hadoop-common/src/test/java/org/apache/hudi/io/storage/hadoop/TestHoodieVariantReconstruction.java
@@ -19,6 +19,7 @@
package org.apache.hudi.io.storage.hadoop;
+import org.apache.hudi.common.avro.VariantShreddingProvider;
import org.apache.hudi.common.config.HoodieStorageConfig;
import org.apache.hudi.common.schema.HoodieSchema;
import org.apache.hudi.common.schema.HoodieSchemaField;
@@ -27,12 +28,20 @@ import org.apache.hudi.common.testutils.HoodieTestUtils;
import org.apache.hudi.exception.HoodieException;
import org.apache.hudi.storage.HoodieStorage;
+import org.apache.avro.Schema;
+import org.apache.avro.generic.GenericData;
+import org.apache.avro.generic.GenericRecord;
+import org.apache.avro.generic.IndexedRecord;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
+import java.nio.ByteBuffer;
import java.nio.file.Path;
+import java.util.Arrays;
import java.util.Collections;
+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;
@@ -72,4 +81,85 @@ class TestHoodieVariantReconstruction {
assertNull(HoodieVariantReconstruction.create(schema, schema,
storageWithReadingShredded(tmp, false)));
}
+
+ @Test
+ void returnsNullForNonRecordSchemas(@TempDir Path tmp) {
+ HoodieStorage storage = storageWithReadingShredded(tmp, true);
+ assertNull(HoodieVariantReconstruction.create(
+ HoodieSchema.create(HoodieSchemaType.STRING),
recordWithVariant(HoodieSchema.createVariant()), storage));
+ assertNull(HoodieVariantReconstruction.create(
+ recordWithVariant(HoodieSchema.createVariant()),
HoodieSchema.create(HoodieSchemaType.STRING), storage));
+ }
+
+ @Test
+ void failsFastWhenProviderIsUnavailable(@TempDir Path tmp) {
+ HoodieSchema fileSchema = recordWithVariant(
+
HoodieSchema.createVariantShredded(HoodieSchema.create(HoodieSchemaType.INT)));
+ HoodieSchema requestedSchema =
recordWithVariant(HoodieSchema.createVariant());
+
+ HoodieException exception = assertThrows(HoodieException.class, () ->
+ HoodieVariantReconstruction.create(fileSchema, requestedSchema,
+ storageWithReadingShredded(tmp, true)));
+ assertTrue(exception.getMessage().contains("no VariantShreddingProvider is
available"));
+ }
+
+ @Test
+ void reconstructsTargetFieldsAndPassesThroughOtherValues(@TempDir Path tmp) {
+ HoodieSchema fileSchema = recordWithIdAndVariant(
+
HoodieSchema.createVariantShredded(HoodieSchema.create(HoodieSchemaType.INT)));
+ HoodieSchema requestedSchema =
recordWithIdAndVariant(HoodieSchema.createVariant());
+ HoodieStorage storage = storageWithReadingShredded(tmp, true);
+
storage.getConf().set(HoodieStorageConfig.PARQUET_VARIANT_SHREDDING_PROVIDER_CLASS.key(),
+ TestVariantShreddingProvider.class.getName());
+
+ HoodieVariantReconstruction reconstruction =
HoodieVariantReconstruction.create(
+ fileSchema, requestedSchema, storage);
+ assertNotNull(reconstruction);
+
assertTrue(reconstruction.intermediateSchema().getField("v").get().schema().getNonNullType()
+ instanceof HoodieSchema.Variant);
+ assertTrue(((HoodieSchema.Variant)
reconstruction.intermediateSchema().getField("v").get()
+ .schema().getNonNullType()).isShredded());
+
+ GenericRecord shredded = new GenericData.Record(
+
reconstruction.intermediateSchema().getField("v").get().schema().getNonNullType().toAvroSchema());
+ shredded.put("metadata", ByteBuffer.wrap(new byte[] {1}));
+ shredded.put("value", null);
+ shredded.put("typed_value", 42);
+ GenericRecord input = new
GenericData.Record(reconstruction.intermediateSchema().toAvroSchema());
+ input.put("id", "record-1");
+ input.put("v", shredded);
+
+ IndexedRecord output = reconstruction.reconstruct(input);
+ assertEquals("record-1", output.get(0).toString());
+ GenericRecord variant = (GenericRecord) output.get(1);
+ assertEquals(ByteBuffer.wrap(new byte[] {1}), variant.get("metadata"));
+ assertEquals(ByteBuffer.wrap(new byte[] {42}), variant.get("value"));
+
+ input.put("v", null);
+ assertNull(reconstruction.reconstruct(input).get(1));
+ }
+
+ private static HoodieSchema recordWithIdAndVariant(HoodieSchema
variantSchema) {
+ return HoodieSchema.createRecord("test_record", "org.apache.hudi.test",
null, Arrays.asList(
+ HoodieSchemaField.of("id",
HoodieSchema.create(HoodieSchemaType.STRING)),
+ HoodieSchemaField.of("v", variantSchema)));
+ }
+
+ public static class TestVariantShreddingProvider implements
VariantShreddingProvider {
+ @Override
+ public GenericRecord shredVariantRecord(
+ GenericRecord unshreddedVariant, Schema shreddedSchema,
HoodieSchema.Variant variantSchema) {
+ throw new UnsupportedOperationException("Not used by the reconstruction
test");
+ }
+
+ @Override
+ public GenericRecord rebuildVariantRecord(
+ GenericRecord shreddedVariant, Schema shreddedSchema, Schema
unshreddedSchema) {
+ GenericRecord rebuilt = new GenericData.Record(unshreddedSchema);
+ rebuilt.put("metadata", shreddedVariant.get("metadata"));
+ rebuilt.put("value", ByteBuffer.wrap(new byte[] {
+ ((Number) shreddedVariant.get("typed_value")).byteValue()}));
+ return rebuilt;
+ }
+ }
}
diff --git
a/hudi-hadoop-common/src/test/java/org/apache/hudi/parquet/io/TestHoodieParquetBinaryCopyBasePaths.java
b/hudi-hadoop-common/src/test/java/org/apache/hudi/parquet/io/TestHoodieParquetBinaryCopyBasePaths.java
new file mode 100644
index 000000000000..8c65ec5dbb39
--- /dev/null
+++
b/hudi-hadoop-common/src/test/java/org/apache/hudi/parquet/io/TestHoodieParquetBinaryCopyBasePaths.java
@@ -0,0 +1,76 @@
+/*
+ * 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.parquet.io;
+
+import org.apache.hadoop.conf.Configuration;
+import org.apache.parquet.schema.MessageTypeParser;
+import org.junit.jupiter.api.Test;
+
+import java.util.Collections;
+import java.util.Map;
+
+import static org.junit.jupiter.api.Assertions.assertArrayEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+class TestHoodieParquetBinaryCopyBasePaths {
+
+ @Test
+ void testLegacyArrayPathsAreConverted() {
+ TestableBinaryCopy copy = new TestableBinaryCopy();
+ String[] genericLegacyPath = {"unknown", "bag", "array_element"};
+ assertTrue(copy.convertLegacy3LevelArray(genericLegacyPath));
+ assertArrayEquals(new String[] {"unknown", "list", "element"},
genericLegacyPath);
+
+ copy.requiredSchema = MessageTypeParser.parseMessageType(
+ "message record { optional group values (LIST) { repeated group list {
optional binary element; } } }");
+ String[] avroLegacyPath = {"values", "bag", "array"};
+ assertTrue(copy.convertLegacy3LevelArray(avroLegacyPath));
+ assertArrayEquals(new String[] {"values", "list", "element"},
avroLegacyPath);
+
+ String[] unchanged = {"values", "list", "element"};
+ assertFalse(copy.convertLegacy3LevelArray(unchanged));
+ }
+
+ @Test
+ void testLegacyMapPathsAreConvertedAndUnknownPathsAreIgnored() {
+ TestableBinaryCopy copy = new TestableBinaryCopy();
+ copy.requiredSchema = MessageTypeParser.parseMessageType(
+ "message record { optional group properties (MAP) { repeated group
key_value { "
+ + "required binary key; optional binary value; } } }");
+
+ String[] path = {"properties", "map", "value"};
+ assertTrue(copy.convertLegacyMap(path));
+ assertArrayEquals(new String[] {"properties", "key_value", "value"}, path);
+
+ assertFalse(copy.convertLegacyMap(new String[] {"missing", "map",
"value"}));
+ }
+
+ private static class TestableBinaryCopy extends HoodieParquetBinaryCopyBase {
+ private TestableBinaryCopy() {
+ super(new Configuration());
+ }
+
+ @Override
+ protected Map<String, String> finalizeMetadata() {
+ return Collections.emptyMap();
+ }
+ }
+}