This is an automated email from the ASF dual-hosted git repository.
yihua 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 1c30d058c579 perf(spark): stop per-file Hadoop configuration parsing
and copies on the parquet read path (#20079)
1c30d058c579 is described below
commit 1c30d058c57978dd77abf423c6209579797d7c66
Author: Y Ethan Guo <[email protected]>
AuthorDate: Mon Oct 5 20:54:21 2026 -0700
perf(spark): stop per-file Hadoop configuration parsing and copies on the
parquet read path (#20079)
---
.../hudi/io/storage/HoodieSparkParquetReader.java | 26 ++-
.../org/apache/hudi/common/util/ParquetUtils.java | 22 +++
.../io/storage/hadoop/HoodieAvroParquetReader.java | 9 +-
.../apache/parquet/avro/HoodieAvroReadSupport.java | 2 +-
.../testutils/FooterKeyDecryptionFactory.java | 96 +++++++++++
.../HadoopDefaultResourceLoadCounter.java | 68 ++++++++
.../hadoop/TestHoodieAvroParquetReader.java | 100 ++++++++++++
.../parquet/ParquetSchemaEvolutionUtils.scala | 46 ++++--
.../parquet/SparkParquetReaderBase.scala | 5 +-
.../parquet/TestParquetSchemaEvolutionUtils.scala | 51 +++++-
.../io/storage/TestHoodieSparkParquetReader.java | 133 +++++++++++++++
.../io/storage/TestParquetReadOptionsParity.java | 148 +++++++++++++++++
.../hudi/functional/TestBasicSchemaEvolution.scala | 2 +-
.../io/storage/ParquetPerFileReadBenchmark.scala | 179 +++++++++++++++++++++
.../Spark3LegacyHoodieParquetFileFormat.scala | 2 +-
.../datasources/parquet/Spark33ParquetReader.scala | 8 +-
.../datasources/parquet/Spark34ParquetReader.scala | 8 +-
.../datasources/parquet/Spark35ParquetReader.scala | 8 +-
.../Spark40LegacyHoodieParquetFileFormat.scala | 2 +-
.../datasources/parquet/Spark40ParquetReader.scala | 8 +-
.../Spark41LegacyHoodieParquetFileFormat.scala | 2 +-
.../datasources/parquet/Spark41ParquetReader.scala | 8 +-
.../Spark42LegacyHoodieParquetFileFormat.scala | 2 +-
.../datasources/parquet/Spark42ParquetReader.scala | 8 +-
24 files changed, 876 insertions(+), 67 deletions(-)
diff --git
a/hudi-client/hudi-spark-client/src/main/java/org/apache/hudi/io/storage/HoodieSparkParquetReader.java
b/hudi-client/hudi-spark-client/src/main/java/org/apache/hudi/io/storage/HoodieSparkParquetReader.java
index 40497ea01fa8..d84c62f168b6 100644
---
a/hudi-client/hudi-spark-client/src/main/java/org/apache/hudi/io/storage/HoodieSparkParquetReader.java
+++
b/hudi-client/hudi-spark-client/src/main/java/org/apache/hudi/io/storage/HoodieSparkParquetReader.java
@@ -44,6 +44,8 @@ import org.apache.parquet.filter2.predicate.FilterApi;
import org.apache.parquet.filter2.predicate.FilterPredicate;
import org.apache.parquet.hadoop.ParquetInputFormat;
import org.apache.parquet.hadoop.ParquetReader;
+import org.apache.parquet.hadoop.api.ReadSupport;
+import org.apache.parquet.hadoop.util.HadoopInputFile;
import org.apache.parquet.schema.MessageType;
import org.apache.parquet.schema.SchemaRepair;
import org.apache.spark.sql.HoodieInternalRowUtils;
@@ -209,8 +211,8 @@ public class HoodieSparkParquetReader implements
HoodieSparkFileReader {
Option$.MODULE$.empty(), true, true,
rebaseDateSpec,
messageSchema);
- ParquetReader<InternalRow> reader = ParquetReader.builder(readSupport, new
Path(path.toUri()))
- .withConf(storage.getConf().unwrapAs(Configuration.class))
+ HadoopInputFile inputFile = HadoopInputFile.fromPath(new
Path(path.toUri()), storage.getConf().unwrapAs(Configuration.class));
+ ParquetReader<InternalRow> reader = ParquetUtils.withHadoopReadOptions(new
InputFileReaderBuilder(readSupport, inputFile), inputFile)
.build();
UnsafeProjection projection = evolution.generateUnsafeProjection();
ParquetReaderIterator<InternalRow> parquetReaderIterator = new
ParquetReaderIterator<>(reader);
@@ -275,4 +277,24 @@ public class HoodieSparkParquetReader implements
HoodieSparkFileReader {
public long getTotalRecords() {
return parquetUtils.getRowCount(storage, path);
}
+
+ /**
+ * Builds the reader from a {@link HadoopInputFile}, whose {@link
Configuration} becomes the reader's
+ * configuration (see {@link ParquetUtils#withHadoopReadOptions}). {@code
ParquetReader.builder(ReadSupport, Path)}
+ * creates a new {@link Configuration} and loads the Hadoop default
resources for every file, even when
+ * {@code withConf} replaces it.
+ */
+ private static class InputFileReaderBuilder extends
ParquetReader.Builder<InternalRow> {
+ private final ReadSupport<InternalRow> readSupport;
+
+ InputFileReaderBuilder(ReadSupport<InternalRow> readSupport,
HadoopInputFile file) {
+ super(file);
+ this.readSupport = readSupport;
+ }
+
+ @Override
+ protected ReadSupport<InternalRow> getReadSupport() {
+ return readSupport;
+ }
+ }
}
diff --git
a/hudi-hadoop-common/src/main/java/org/apache/hudi/common/util/ParquetUtils.java
b/hudi-hadoop-common/src/main/java/org/apache/hudi/common/util/ParquetUtils.java
index 87961f8223cf..90f0dc64952f 100644
---
a/hudi-hadoop-common/src/main/java/org/apache/hudi/common/util/ParquetUtils.java
+++
b/hudi-hadoop-common/src/main/java/org/apache/hudi/common/util/ParquetUtils.java
@@ -54,6 +54,7 @@ import org.apache.hadoop.fs.Path;
import org.apache.parquet.avro.AvroParquetReader;
import org.apache.parquet.avro.AvroReadSupport;
import org.apache.parquet.column.statistics.Statistics;
+import org.apache.parquet.crypto.DecryptionPropertiesFactory;
import org.apache.parquet.format.converter.ParquetMetadataConverter;
import org.apache.parquet.hadoop.ParquetFileReader;
import org.apache.parquet.hadoop.ParquetReader;
@@ -61,6 +62,7 @@ import org.apache.parquet.hadoop.ParquetWriter;
import org.apache.parquet.hadoop.metadata.BlockMetaData;
import org.apache.parquet.hadoop.metadata.CompressionCodecName;
import org.apache.parquet.hadoop.metadata.ParquetMetadata;
+import org.apache.parquet.hadoop.util.HadoopInputFile;
import org.apache.parquet.io.api.Binary;
import org.apache.parquet.schema.DecimalMetadata;
import org.apache.parquet.schema.MessageType;
@@ -197,6 +199,26 @@ public class ParquetUtils extends FileFormatUtils {
}
}
+ /**
+ * Sets the Hadoop read options of a reader built with {@code
ParquetReader.Builder(InputFile)} from the
+ * file's {@link Configuration}, as {@code ParquetReader.Builder(Path)}
followed by {@code withConf} does,
+ * without creating a new {@link Configuration}. {@code withConf} rebuilds
the read options from the
+ * configuration and the builder's {@code path} field, which the {@code
InputFile} constructor leaves null.
+ * The decryption properties are the only option that takes the file path,
so parquet would hand the
+ * decryption factory a null path; they are resolved here with the file's
path instead. On parquet 1.14+
+ * the {@code InputFile} constructor alone builds plain {@code
ParquetReadOptions}, which never consult the
+ * {@code parquet.crypto.factory.class} decryption factory.
+ */
+ public static <T> ParquetReader.Builder<T>
withHadoopReadOptions(ParquetReader.Builder<T> builder, HadoopInputFile file) {
+ Configuration conf = file.getConfiguration();
+ builder.withConf(conf);
+ DecryptionPropertiesFactory decryptionFactory =
DecryptionPropertiesFactory.loadFactory(conf);
+ if (decryptionFactory != null) {
+
builder.withDecryption(decryptionFactory.getFileDecryptionProperties(conf,
file.getPath()));
+ }
+ return builder;
+ }
+
public static ParquetMetadata readMetadata(HoodieStorage storage,
StoragePath parquetFilePath) {
return readMetadata(storage, parquetFilePath, NO_FILTER);
}
diff --git
a/hudi-hadoop-common/src/main/java/org/apache/hudi/io/storage/hadoop/HoodieAvroParquetReader.java
b/hudi-hadoop-common/src/main/java/org/apache/hudi/io/storage/hadoop/HoodieAvroParquetReader.java
index 06df845ddfe4..118e7c63856b 100644
---
a/hudi-hadoop-common/src/main/java/org/apache/hudi/io/storage/hadoop/HoodieAvroParquetReader.java
+++
b/hudi-hadoop-common/src/main/java/org/apache/hudi/io/storage/hadoop/HoodieAvroParquetReader.java
@@ -31,6 +31,7 @@ import org.apache.hudi.common.util.FileFormatUtils;
import org.apache.hudi.common.util.HoodieAvroParquetReaderIterator;
import org.apache.hudi.common.util.Option;
import org.apache.hudi.common.util.ParquetReaderIterator;
+import org.apache.hudi.common.util.ParquetUtils;
import org.apache.hudi.common.util.collection.ClosableIterator;
import org.apache.hudi.common.util.collection.CloseableMappingIterator;
import org.apache.hudi.common.util.collection.Pair;
@@ -44,11 +45,13 @@ import org.apache.hudi.storage.StoragePath;
import org.apache.avro.generic.GenericRecord;
import org.apache.avro.generic.IndexedRecord;
import org.apache.hadoop.conf.Configuration;
+import org.apache.hadoop.fs.Path;
import org.apache.parquet.avro.AvroReadSupport;
import org.apache.parquet.avro.AvroSchemaConverter;
import org.apache.parquet.avro.HoodieAvroParquetReaderBuilder;
import org.apache.parquet.hadoop.ParquetInputFormat;
import org.apache.parquet.hadoop.ParquetReader;
+import org.apache.parquet.hadoop.util.HadoopInputFile;
import java.io.IOException;
import java.util.ArrayList;
@@ -199,10 +202,10 @@ public class HoodieAvroParquetReader extends
HoodieAvroFileReader {
AvroReadSupport.setAvroReadSchema(hadoopConf, readSchema.toAvroSchema());
AvroReadSupport.setRequestedProjection(hadoopConf,
readSchema.toAvroSchema());
}
+ HadoopInputFile inputFile = HadoopInputFile.fromPath(new
Path(path.toUri()), hadoopConf);
ParquetReader<IndexedRecord> reader =
- new HoodieAvroParquetReaderBuilder<IndexedRecord>(path)
-
.withTableSchema(getAvroSchemaConverter(hadoopConf).convert(readSchema))
- .withConf(hadoopConf)
+ ParquetUtils.withHadoopReadOptions(new
HoodieAvroParquetReaderBuilder<IndexedRecord>(inputFile)
+
.withTableSchema(getAvroSchemaConverter(hadoopConf).convert(readSchema)),
inputFile)
.set(AvroSchemaConverter.ADD_LIST_ELEMENT_RECORDS,
hadoopConf.get(AvroSchemaConverter.ADD_LIST_ELEMENT_RECORDS))
.set(ParquetInputFormat.STRICT_TYPE_CHECKING,
hadoopConf.get(ParquetInputFormat.STRICT_TYPE_CHECKING))
.build();
diff --git
a/hudi-hadoop-common/src/main/java/org/apache/parquet/avro/HoodieAvroReadSupport.java
b/hudi-hadoop-common/src/main/java/org/apache/parquet/avro/HoodieAvroReadSupport.java
index a895db255639..fe7532ff780a 100644
---
a/hudi-hadoop-common/src/main/java/org/apache/parquet/avro/HoodieAvroReadSupport.java
+++
b/hudi-hadoop-common/src/main/java/org/apache/parquet/avro/HoodieAvroReadSupport.java
@@ -88,7 +88,7 @@ public class HoodieAvroReadSupport<T> extends
AvroReadSupport<T> {
String requestedProjectionString =
configuration.get(AVRO_REQUESTED_PROJECTION);
if (requestedProjectionString != null) {
HoodieSchema avroRequestedProjection =
HoodieSchema.parse(requestedProjectionString);
- Configuration conf = new Configuration();
+ Configuration conf = new Configuration(false);
configuration.forEach(entry -> conf.set(entry.getKey(),
entry.getValue()));
projection = new
AvroSchemaConverter(conf).convert(avroRequestedProjection.toAvroSchema());
}
diff --git
a/hudi-hadoop-common/src/test/java/org/apache/hudi/common/testutils/FooterKeyDecryptionFactory.java
b/hudi-hadoop-common/src/test/java/org/apache/hudi/common/testutils/FooterKeyDecryptionFactory.java
new file mode 100644
index 000000000000..6863dff14936
--- /dev/null
+++
b/hudi-hadoop-common/src/test/java/org/apache/hudi/common/testutils/FooterKeyDecryptionFactory.java
@@ -0,0 +1,96 @@
+/*
+ * 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.testutils;
+
+import org.apache.avro.Schema;
+import org.apache.avro.SchemaBuilder;
+import org.apache.avro.generic.GenericData;
+import org.apache.avro.generic.GenericRecord;
+import org.apache.hadoop.conf.Configuration;
+import org.apache.hadoop.fs.Path;
+import org.apache.parquet.avro.AvroParquetWriter;
+import org.apache.parquet.crypto.DecryptionPropertiesFactory;
+import org.apache.parquet.crypto.FileDecryptionProperties;
+import org.apache.parquet.crypto.FileEncryptionProperties;
+import org.apache.parquet.hadoop.ParquetWriter;
+import org.apache.parquet.hadoop.util.HadoopOutputFile;
+
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * A parquet decryption factory ({@code parquet.crypto.factory.class}) that
decrypts files written by
+ * {@link #writeEncryptedFile} with one fixed footer key, and records the file
path of every request.
+ */
+public class FooterKeyDecryptionFactory implements DecryptionPropertiesFactory
{
+
+ public static final String CRYPTO_FACTORY_CLASS =
"parquet.crypto.factory.class";
+ public static final Schema SCHEMA = SchemaBuilder.record("record").fields()
+ .requiredString("_row_key")
+ .requiredLong("ts")
+ .endRecord();
+
+ private static final byte[] FOOTER_KEY =
"0123456789012345".getBytes(StandardCharsets.UTF_8);
+ private static final List<Path> REQUESTED_PATHS =
Collections.synchronizedList(new ArrayList<>());
+
+ @Override
+ public FileDecryptionProperties getFileDecryptionProperties(Configuration
hadoopConfig, Path filePath) {
+ REQUESTED_PATHS.add(filePath);
+ return
FileDecryptionProperties.builder().withFooterKey(FOOTER_KEY).build();
+ }
+
+ /**
+ * Returns and clears the file paths requested so far.
+ */
+ public static List<Path> drainRequestedPaths() {
+ synchronized (REQUESTED_PATHS) {
+ List<Path> paths = new ArrayList<>(REQUESTED_PATHS);
+ REQUESTED_PATHS.clear();
+ return paths;
+ }
+ }
+
+ /**
+ * Writes {@code numRecords} records of {@link #SCHEMA} with an encrypted
footer and encrypted columns.
+ */
+ public static void writeEncryptedFile(Configuration conf, Path path, int
numRecords) throws IOException {
+ writeFile(conf, path, numRecords, true);
+ }
+
+ /**
+ * Writes {@code numRecords} records of {@link #SCHEMA}, encrypted as by
{@link #writeEncryptedFile} or in plaintext.
+ */
+ public static void writeFile(Configuration conf, Path path, int numRecords,
boolean encrypted) throws IOException {
+ try (ParquetWriter<GenericRecord> writer =
AvroParquetWriter.<GenericRecord>builder(HadoopOutputFile.fromPath(path, conf))
+ .withSchema(SCHEMA)
+ .withConf(conf)
+ .withEncryption(encrypted ?
FileEncryptionProperties.builder(FOOTER_KEY).build() : null)
+ .build()) {
+ for (int i = 0; i < numRecords; i++) {
+ GenericRecord record = new GenericData.Record(SCHEMA);
+ record.put("_row_key", "key" + i);
+ record.put("ts", (long) i);
+ writer.write(record);
+ }
+ }
+ }
+}
diff --git
a/hudi-hadoop-common/src/test/java/org/apache/hudi/common/testutils/HadoopDefaultResourceLoadCounter.java
b/hudi-hadoop-common/src/test/java/org/apache/hudi/common/testutils/HadoopDefaultResourceLoadCounter.java
new file mode 100644
index 000000000000..693f4ad6c30b
--- /dev/null
+++
b/hudi-hadoop-common/src/test/java/org/apache/hudi/common/testutils/HadoopDefaultResourceLoadCounter.java
@@ -0,0 +1,68 @@
+/*
+ * 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.testutils;
+
+import org.junit.jupiter.api.function.Executable;
+
+import java.net.URL;
+import java.util.concurrent.atomic.AtomicInteger;
+
+/**
+ * Counts the Hadoop default resources ({@code core-default.xml}, {@code
core-site.xml}) loaded
+ * while an action runs on the current thread.
+ *
+ * <p>A Hadoop {@code Configuration} captures the thread context class loader
when it is created
+ * and loads the default resources through it on first access, while a copy of
an existing
+ * configuration keeps the loader of its source. Running the action under a
counting context
+ * class loader therefore counts only the configurations the action creates
from scratch.
+ */
+public final class HadoopDefaultResourceLoadCounter {
+
+ private HadoopDefaultResourceLoadCounter() {
+ }
+
+ public static int countDefaultResourceLoads(Executable action) throws
Throwable {
+ Thread thread = Thread.currentThread();
+ ClassLoader original = thread.getContextClassLoader();
+ CountingClassLoader counting = new CountingClassLoader(original);
+ thread.setContextClassLoader(counting);
+ try {
+ action.execute();
+ } finally {
+ thread.setContextClassLoader(original);
+ }
+ return counting.loads.get();
+ }
+
+ private static class CountingClassLoader extends ClassLoader {
+ private final AtomicInteger loads = new AtomicInteger();
+
+ CountingClassLoader(ClassLoader parent) {
+ super(parent);
+ }
+
+ @Override
+ public URL getResource(String name) {
+ if ("core-default.xml".equals(name) || "core-site.xml".equals(name)) {
+ loads.incrementAndGet();
+ }
+ return super.getResource(name);
+ }
+ }
+}
diff --git
a/hudi-hadoop-common/src/test/java/org/apache/hudi/io/storage/hadoop/TestHoodieAvroParquetReader.java
b/hudi-hadoop-common/src/test/java/org/apache/hudi/io/storage/hadoop/TestHoodieAvroParquetReader.java
new file mode 100644
index 000000000000..6d3b2c26d5a2
--- /dev/null
+++
b/hudi-hadoop-common/src/test/java/org/apache/hudi/io/storage/hadoop/TestHoodieAvroParquetReader.java
@@ -0,0 +1,100 @@
+/*
+ * 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.io.storage.hadoop;
+
+import org.apache.hudi.common.config.HoodieConfig;
+import org.apache.hudi.common.config.HoodieStorageConfig;
+import org.apache.hudi.common.engine.LocalTaskContextSupplier;
+import org.apache.hudi.common.schema.HoodieSchema;
+import org.apache.hudi.common.testutils.FooterKeyDecryptionFactory;
+import org.apache.hudi.common.testutils.HoodieTestDataGenerator;
+import org.apache.hudi.common.testutils.HoodieTestUtils;
+import org.apache.hudi.common.util.collection.ClosableIterator;
+import org.apache.hudi.storage.HoodieStorage;
+import org.apache.hudi.storage.StoragePath;
+
+import org.apache.avro.generic.GenericRecord;
+import org.apache.avro.generic.IndexedRecord;
+import org.apache.hadoop.conf.Configuration;
+import org.apache.hadoop.fs.Path;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.stream.Collectors;
+import java.util.stream.IntStream;
+
+import static
org.apache.hudi.common.testutils.HadoopDefaultResourceLoadCounter.countDefaultResourceLoads;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+
+/**
+ * Tests for {@link HoodieAvroParquetReader}.
+ */
+class TestHoodieAvroParquetReader {
+
+ @TempDir
+ java.nio.file.Path tmpDir;
+
+ @Test
+ void testReadDoesNotLoadHadoopDefaultResources() throws Throwable {
+ HoodieStorage storage = HoodieTestUtils.getStorage(tmpDir.toString());
+ StoragePath parquetPath = new
StoragePath(tmpDir.resolve("file.parquet").toAbsolutePath().toString());
+ List<GenericRecord> records = new
HoodieTestDataGenerator(0xDEED).generateGenericRecords(20);
+ HoodieSchema schema =
HoodieSchema.fromAvroSchema(records.get(0).getSchema());
+ HoodieConfig config = new HoodieConfig();
+ config.setValue(HoodieStorageConfig.PARQUET_COMPRESSION_CODEC_NAME,
"uncompressed");
+ HoodieAvroParquetWriter writer = (HoodieAvroParquetWriter) new
HoodieAvroFileWriterFactory(storage)
+ .newParquetFileWriter("001", parquetPath, config, schema, new
LocalTaskContextSupplier());
+ for (GenericRecord record : records) {
+ writer.writeAvro((String) record.get("_row_key"), record);
+ }
+ writer.close();
+
+ List<String> readKeys = new ArrayList<>();
+ int loads = countDefaultResourceLoads(() -> {
+ try (HoodieAvroParquetReader reader = new
HoodieAvroParquetReader(storage, parquetPath);
+ ClosableIterator<IndexedRecord> iterator =
reader.getIndexedRecordIterator(schema)) {
+ iterator.forEachRemaining(record -> readKeys.add(((GenericRecord)
record).get("_row_key").toString()));
+ }
+ });
+ assertEquals(records.stream().map(record ->
record.get("_row_key").toString()).collect(Collectors.toList()), readKeys);
+ assertEquals(0, loads, "Reading a parquet file must reuse the storage
configuration");
+ }
+
+ @Test
+ void testReadEncryptedFileResolvesDecryptionWithFilePath() throws Exception {
+ HoodieStorage storage = HoodieTestUtils.getStorage(tmpDir.toString());
+ storage.getConf().set(FooterKeyDecryptionFactory.CRYPTO_FACTORY_CLASS,
FooterKeyDecryptionFactory.class.getName());
+ StoragePath parquetPath = new
StoragePath(tmpDir.resolve("encrypted.parquet").toAbsolutePath().toString());
+
FooterKeyDecryptionFactory.writeEncryptedFile(storage.getConf().unwrapAs(Configuration.class),
new Path(parquetPath.toUri()), 10);
+ FooterKeyDecryptionFactory.drainRequestedPaths();
+
+ List<String> readKeys = new ArrayList<>();
+ try (HoodieAvroParquetReader reader = new HoodieAvroParquetReader(storage,
parquetPath);
+ ClosableIterator<IndexedRecord> iterator =
reader.getIndexedRecordIterator(HoodieSchema.fromAvroSchema(FooterKeyDecryptionFactory.SCHEMA)))
{
+ iterator.forEachRemaining(record -> readKeys.add(((GenericRecord)
record).get("_row_key").toString()));
+ }
+ assertEquals(IntStream.range(0, 10).mapToObj(i -> "key" +
i).collect(Collectors.toList()), readKeys);
+ List<Path> requestedPaths =
FooterKeyDecryptionFactory.drainRequestedPaths();
+ assertFalse(requestedPaths.isEmpty());
+ requestedPaths.forEach(requested -> assertEquals("encrypted.parquet",
requested == null ? null : requested.getName()));
+ }
+}
diff --git
a/hudi-spark-datasource/hudi-spark-common/src/main/scala/org/apache/spark/sql/execution/datasources/parquet/ParquetSchemaEvolutionUtils.scala
b/hudi-spark-datasource/hudi-spark-common/src/main/scala/org/apache/spark/sql/execution/datasources/parquet/ParquetSchemaEvolutionUtils.scala
index 9df771b115e6..b9470141aac1 100644
---
a/hudi-spark-datasource/hudi-spark-common/src/main/scala/org/apache/spark/sql/execution/datasources/parquet/ParquetSchemaEvolutionUtils.scala
+++
b/hudi-spark-datasource/hudi-spark-common/src/main/scala/org/apache/spark/sql/execution/datasources/parquet/ParquetSchemaEvolutionUtils.scala
@@ -36,6 +36,9 @@ import org.apache.hudi.hadoop.fs.HadoopFSUtils
import org.apache.hadoop.conf.Configuration
import org.apache.hadoop.fs.Path
+import org.apache.hadoop.mapred.JobConf
+import org.apache.parquet.filter2.predicate.FilterPredicate
+import org.apache.parquet.hadoop.ParquetInputFormat
import org.apache.parquet.hadoop.metadata.FileMetaData
import org.apache.parquet.schema.{GroupType, MessageType, Type => ParquetType}
import org.apache.parquet.schema.PrimitiveType.PrimitiveTypeName
@@ -50,7 +53,7 @@ import java.time.ZoneId
import scala.collection.convert.ImplicitConversions.`collection
AsScalaIterable`
-class ParquetSchemaEvolutionUtils(sharedConf: Configuration,
+class ParquetSchemaEvolutionUtils(readConf: Configuration,
filePath: Path,
requiredSchema: StructType,
partitionSchema: StructType,
@@ -62,14 +65,14 @@ class ParquetSchemaEvolutionUtils(sharedConf: Configuration,
private lazy val schemaUtils: HoodieSchemaUtils = sparkAdapter.getSchemaUtils
- private lazy val tablePath: String =
sharedConf.get(SparkInternalSchemaConverter.HOODIE_TABLE_PATH)
+ private lazy val tablePath: String =
readConf.get(SparkInternalSchemaConverter.HOODIE_TABLE_PATH)
private lazy val fileSchema: InternalSchema = if (shouldUseInternalSchema) {
val commitInstantTime = FSUtils.getCommitTime(filePath.getName).toLong
//TODO: HARDCODED TIMELINE OBJECT
- val validCommits =
sharedConf.get(SparkInternalSchemaConverter.HOODIE_VALID_COMMITS_LIST)
+ val validCommits =
readConf.get(SparkInternalSchemaConverter.HOODIE_VALID_COMMITS_LIST)
val layout =
TimelineLayout.fromVersion(TimelineLayoutVersion.CURR_LAYOUT_VERSION)
InternalSchemaCache.getInternalSchemaByVersionId(commitInstantTime,
tablePath,
- HoodieStorageUtils.getStorage(tablePath,
HadoopFSUtils.getStorageConf(sharedConf)), if (validCommits == null) "" else
validCommits, layout)
+ HoodieStorageUtils.getStorage(tablePath,
HadoopFSUtils.getStorageConf(readConf)), if (validCommits == null) "" else
validCommits, layout)
} else {
null
}
@@ -80,9 +83,26 @@ class ParquetSchemaEvolutionUtils(sharedConf: Configuration,
protected var typeChangeInfos: java.util.Map[Integer, Pair[DataType,
DataType]] = null
- def getHadoopConfClone(footerFileMetaData: FileMetaData,
enableVectorizedReader: Boolean): Configuration = {
- // Clone new conf
- val hadoopAttemptConf = new Configuration(sharedConf)
+ /**
+ * Returns the configuration to read the file with: the read configuration
itself when the file needs no
+ * keys of its own, otherwise a copy with the file's requested schema or the
pushed row-group filter. The
+ * read configuration is never modified, since other readers may share it,
so callers must not modify the
+ * returned configuration either.
+ */
+ def getFileReadConf(footerFileMetaData: FileMetaData,
+ enableVectorizedReader: Boolean,
+ pushedFilter: Option[FilterPredicate]): Configuration = {
+ var fileReadConf: Configuration = readConf
+ def fileOwnConf(): Configuration = {
+ if (fileReadConf eq readConf) {
+ // A JobConf, so the task attempt context built on it does not copy it
again: JobContextImpl reuses a
+ // JobConf and copies any other Configuration
+ fileReadConf = new JobConf(readConf)
+ }
+ fileReadConf
+ }
+ def setRequestedSchema(schema: StructType): Unit =
+ fileOwnConf().set(ParquetReadSupport.SPARK_ROW_REQUESTED_SCHEMA,
schema.json)
typeChangeInfos = if (shouldUseInternalSchema) {
// Empty projections (count(*), select 1) read no column data, so there
is nothing to
// reconstruct - and querySchemaOption is the UNPRUNED table schema in
that case (see
@@ -93,14 +113,14 @@ class ParquetSchemaEvolutionUtils(sharedConf:
Configuration,
val mergedInternalSchema = new InternalSchemaMerger(fileSchema,
querySchemaOption.get(), true, true).mergeSchema()
val mergedSchema =
SparkInternalSchemaConverter.constructSparkSchemaFromInternalSchema(mergedInternalSchema)
- hadoopAttemptConf.set(ParquetReadSupport.SPARK_ROW_REQUESTED_SCHEMA,
mergedSchema.json)
+ setRequestedSchema(mergedSchema)
SparkInternalSchemaConverter.collectTypeChangedCols(querySchemaOption.get(),
mergedInternalSchema)
} else {
- val (implicitTypeChangeInfo, sparkRequestSchema) =
HoodieParquetFileFormatHelper.buildImplicitSchemaChangeInfo(hadoopAttemptConf,
footerFileMetaData, requiredSchema)
+ val (implicitTypeChangeInfo, sparkRequestSchema) =
HoodieParquetFileFormatHelper.buildImplicitSchemaChangeInfo(readConf,
footerFileMetaData, requiredSchema)
if (!implicitTypeChangeInfo.isEmpty) {
shouldUseInternalSchema = true
- hadoopAttemptConf.set(ParquetReadSupport.SPARK_ROW_REQUESTED_SCHEMA,
sparkRequestSchema.json)
+ setRequestedSchema(sparkRequestSchema)
}
implicitTypeChangeInfo
}
@@ -112,7 +132,9 @@ class ParquetSchemaEvolutionUtils(sharedConf: Configuration,
"To workaround this issue, set
spark.sql.parquet.enableVectorizedReader=false.")
}
- hadoopAttemptConf
+ // Row-group level push-down, not individual records
+ pushedFilter.foreach(ParquetInputFormat.setFilterPredicate(fileOwnConf(),
_))
+ fileReadConf
}
def generateUnsafeProjection(fullSchema: Seq[AttributeReference],
timeZoneId: Option[String]): UnsafeProjection = {
@@ -255,7 +277,7 @@ object ParquetSchemaEvolutionUtils {
* names both routes rather than blaming pushVariantIntoScan on a read that
never set it.
* Real support is #18285.
*
- * Shared by [[ParquetSchemaEvolutionUtils.getHadoopConfClone]] and the
per-version legacy
+ * Shared by [[ParquetSchemaEvolutionUtils.getFileReadConf]] and the
per-version legacy
* file formats, which carry a copy of the same schema-merge block. Callers
gate on a
* non-empty projection: empty-projection queries (count(*), select 1) read
no column data
* and must keep working, and the query schema is unpruned in that case.
diff --git
a/hudi-spark-datasource/hudi-spark-common/src/main/scala/org/apache/spark/sql/execution/datasources/parquet/SparkParquetReaderBase.scala
b/hudi-spark-datasource/hudi-spark-common/src/main/scala/org/apache/spark/sql/execution/datasources/parquet/SparkParquetReaderBase.scala
index d901c42dae6e..bdc4ed9700d6 100644
---
a/hudi-spark-datasource/hudi-spark-common/src/main/scala/org/apache/spark/sql/execution/datasources/parquet/SparkParquetReaderBase.scala
+++
b/hudi-spark-datasource/hudi-spark-common/src/main/scala/org/apache/spark/sql/execution/datasources/parquet/SparkParquetReaderBase.scala
@@ -25,6 +25,7 @@ import org.apache.hudi.common.util
import org.apache.hudi.storage.StorageConfiguration
import org.apache.hadoop.conf.Configuration
+import org.apache.hadoop.mapred.JobConf
import org.apache.spark.sql.catalyst.InternalRow
import org.apache.spark.sql.execution.datasources.{PartitionedFile,
SparkColumnarFileReader}
import org.apache.spark.sql.internal.SQLConf
@@ -64,7 +65,9 @@ abstract class SparkParquetReaderBase(enableVectorizedReader:
Boolean,
filters: Seq[Filter],
storageConf: StorageConfiguration[Configuration],
tableSchemaOpt:
util.Option[org.apache.parquet.schema.MessageType] = util.Option.empty()):
Iterator[InternalRow] = {
- val conf = storageConf.unwrapCopy()
+ // A JobConf, so the task attempt context doRead builds on it reuses it
instead of copying it again:
+ // JobContextImpl reuses a JobConf and copies any other Configuration
+ val conf = new JobConf(storageConf.unwrap())
conf.set(ParquetReadSupport.SPARK_ROW_REQUESTED_SCHEMA,
requiredSchema.json)
conf.set(ParquetWriteSupport.SPARK_ROW_SCHEMA, requiredSchema.json)
diff --git
a/hudi-spark-datasource/hudi-spark-common/src/test/scala/org/apache/spark/sql/execution/datasources/parquet/TestParquetSchemaEvolutionUtils.scala
b/hudi-spark-datasource/hudi-spark-common/src/test/scala/org/apache/spark/sql/execution/datasources/parquet/TestParquetSchemaEvolutionUtils.scala
index 0df5f28af986..463c1993b888 100644
---
a/hudi-spark-datasource/hudi-spark-common/src/test/scala/org/apache/spark/sql/execution/datasources/parquet/TestParquetSchemaEvolutionUtils.scala
+++
b/hudi-spark-datasource/hudi-spark-common/src/test/scala/org/apache/spark/sql/execution/datasources/parquet/TestParquetSchemaEvolutionUtils.scala
@@ -22,14 +22,21 @@ package org.apache.spark.sql.execution.datasources.parquet
import org.apache.hudi.common.schema.{HoodieSchema, HoodieSchemaField,
HoodieSchemaType}
import org.apache.hudi.common.schema.internal.InternalSchema
import org.apache.hudi.common.schema.internal.convert.InternalSchemaConverter
+import org.apache.hudi.common.util
import org.apache.hudi.exception.HoodieException
+import org.apache.hadoop.conf.Configuration
+import org.apache.hadoop.fs.Path
+import org.apache.parquet.filter2.compat.FilterCompat
+import org.apache.parquet.filter2.predicate.{FilterApi, FilterPredicate}
+import org.apache.parquet.hadoop.ParquetInputFormat
import org.apache.parquet.hadoop.metadata.FileMetaData
import org.apache.parquet.schema.{MessageType, Type, Types}
import org.apache.parquet.schema.PrimitiveType.PrimitiveTypeName
import
org.apache.spark.sql.execution.datasources.parquet.VariantParquetTestFixtures.{shreddedVariant,
stringKeyMap, threeLevelList, twoLevelList, unshreddedVariant}
+import org.apache.spark.sql.internal.SQLConf
import org.apache.spark.sql.sources.{AlwaysFalse, AlwaysTrue, And,
EqualNullSafe, EqualTo, Filter, GreaterThan, GreaterThanOrEqual, In, IsNotNull,
IsNull, LessThan, LessThanOrEqual, Not, Or, StringContains, StringEndsWith,
StringStartsWith}
-import org.apache.spark.sql.types.{ArrayType, BinaryType, IntegerType,
MapType, MetadataBuilder, StringType, StructField, StructType}
+import org.apache.spark.sql.types.{ArrayType, BinaryType, IntegerType,
LongType, MapType, MetadataBuilder, StringType, StructField, StructType}
import org.junit.jupiter.api.{Assertions, Test}
import java.util.{Arrays, Collections, HashMap}
@@ -387,6 +394,48 @@ class TestParquetSchemaEvolutionUtils {
ParquetSchemaEvolutionUtils.rebuildFilterFromParquet(untouched, null,
null))
}
+ /**
+ * The read configuration may be shared by other readers: a file that needs
its own requested schema or a
+ * pushed filter gets a copy, and any other file reads with the read
configuration itself.
+ */
+ @Test
+ def testGetFileReadConfNeverModifiesTheReadConf(): Unit = {
+ val readConf = new Configuration(false)
+ // The flags ParquetToSparkSchemaConverter needs, as
SparkParquetReaderBase.read sets them
+ readConf.setBoolean(SQLConf.CASE_SENSITIVE.key, false)
+ readConf.setBoolean(SQLConf.PARQUET_BINARY_AS_STRING.key, false)
+ readConf.setBoolean(SQLConf.PARQUET_INT96_AS_TIMESTAMP.key, true)
+ readConf.setBoolean(SQLConf.LEGACY_PARQUET_NANOS_AS_LONG.key, false)
+ readConf.setBoolean("spark.sql.parquet.inferTimestampNTZ.enabled", true)
+ val requiredSchema = new StructType().add("id", LongType)
+ readConf.set(ParquetReadSupport.SPARK_ROW_REQUESTED_SCHEMA,
requiredSchema.json)
+
+ def fileReadConf(fileType: PrimitiveTypeName, pushedFilter:
Option[FilterPredicate]): Configuration =
+ new ParquetSchemaEvolutionUtils(readConf, new Path("/tmp/file.parquet"),
requiredSchema, new StructType(), util.Option.empty())
+ .getFileReadConf(footerOf(Types.optional(fileType).named("id")),
enableVectorizedReader = false, pushedFilter)
+
+ def pushedFilterOf(conf: Configuration): FilterPredicate =
ParquetInputFormat.getFilter(conf) match {
+ case predicate: FilterCompat.FilterPredicateCompat =>
predicate.getFilterPredicate
+ case _ => null
+ }
+
+ val filter = FilterApi.eq(FilterApi.longColumn("id"),
java.lang.Long.valueOf(1L))
+ Assertions.assertSame(readConf, fileReadConf(PrimitiveTypeName.INT64,
None))
+ val filtered = fileReadConf(PrimitiveTypeName.INT64, Some(filter))
+ Assertions.assertNotSame(readConf, filtered)
+ Assertions.assertEquals(requiredSchema.json,
filtered.get(ParquetReadSupport.SPARK_ROW_REQUESTED_SCHEMA))
+ Assertions.assertEquals(filter, pushedFilterOf(filtered))
+ // An int file read as long gets a copy that requests the int column
+ for (pushedFilter <- Seq(None, Some(filter))) {
+ val promoted = fileReadConf(PrimitiveTypeName.INT32, pushedFilter)
+ Assertions.assertNotSame(readConf, promoted)
+ Assertions.assertEquals(new StructType().add("id", IntegerType).json,
promoted.get(ParquetReadSupport.SPARK_ROW_REQUESTED_SCHEMA))
+ Assertions.assertEquals(pushedFilter.orNull, pushedFilterOf(promoted))
+ }
+ Assertions.assertEquals(requiredSchema.json,
readConf.get(ParquetReadSupport.SPARK_ROW_REQUESTED_SCHEMA))
+ Assertions.assertNull(pushedFilterOf(readConf))
+ }
+
/** An internal schema over the given top-level columns, in order; field ids
are positional. */
private def internalSchemaOf(fields: (String, HoodieSchema)*):
InternalSchema =
InternalSchemaConverter.convert(HoodieSchema.createRecord("query",
"org.apache.hudi.test", null,
diff --git
a/hudi-spark-datasource/hudi-spark/src/test/java/org/apache/hudi/io/storage/TestHoodieSparkParquetReader.java
b/hudi-spark-datasource/hudi-spark/src/test/java/org/apache/hudi/io/storage/TestHoodieSparkParquetReader.java
new file mode 100644
index 000000000000..37f12b0e91a5
--- /dev/null
+++
b/hudi-spark-datasource/hudi-spark/src/test/java/org/apache/hudi/io/storage/TestHoodieSparkParquetReader.java
@@ -0,0 +1,133 @@
+/*
+ * 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.io.storage;
+
+import org.apache.hudi.common.config.HoodieConfig;
+import org.apache.hudi.common.config.HoodieStorageConfig;
+import org.apache.hudi.common.engine.LocalTaskContextSupplier;
+import org.apache.hudi.common.model.HoodieRecord;
+import org.apache.hudi.common.schema.HoodieSchema;
+import org.apache.hudi.common.testutils.FooterKeyDecryptionFactory;
+import org.apache.hudi.common.testutils.HoodieTestDataGenerator;
+import org.apache.hudi.common.testutils.HoodieTestUtils;
+import org.apache.hudi.common.util.collection.ClosableIterator;
+import org.apache.hudi.core.io.storage.HoodieFileWriterFactory;
+import org.apache.hudi.io.storage.hadoop.HoodieAvroParquetWriter;
+import org.apache.hudi.storage.HoodieStorage;
+import org.apache.hudi.storage.StoragePath;
+
+import org.apache.avro.generic.GenericRecord;
+import org.apache.hadoop.conf.Configuration;
+import org.apache.hadoop.fs.Path;
+import org.apache.spark.sql.catalyst.expressions.UnsafeRow;
+import org.apache.spark.sql.sources.EqualTo;
+import org.apache.spark.sql.sources.Filter;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.stream.Collectors;
+import java.util.stream.LongStream;
+
+import static
org.apache.hudi.common.testutils.HadoopDefaultResourceLoadCounter.countDefaultResourceLoads;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+
+/**
+ * Tests for {@link HoodieSparkParquetReader}.
+ */
+class TestHoodieSparkParquetReader {
+
+ private static final int NUM_RECORDS = 20;
+
+ @TempDir
+ java.nio.file.Path tmpDir;
+
+ private HoodieStorage storage;
+ private StoragePath parquetPath;
+ private HoodieSchema schema;
+ private List<GenericRecord> records;
+
+ @BeforeEach
+ void setUp() throws Exception {
+ storage = HoodieTestUtils.getStorage(tmpDir.toString());
+ parquetPath = new
StoragePath(tmpDir.resolve("file.parquet").toAbsolutePath().toString());
+ records = new
HoodieTestDataGenerator(0xDEED).generateGenericRecords(NUM_RECORDS);
+ schema = HoodieSchema.fromAvroSchema(records.get(0).getSchema());
+ HoodieConfig config = new HoodieConfig();
+ config.setValue(HoodieStorageConfig.PARQUET_COMPRESSION_CODEC_NAME,
"uncompressed");
+ HoodieAvroParquetWriter writer = (HoodieAvroParquetWriter)
HoodieFileWriterFactory.getFileWriter(
+ "001", parquetPath, storage, config, schema, new
LocalTaskContextSupplier(), HoodieRecord.HoodieRecordType.AVRO);
+ for (GenericRecord record : records) {
+ writer.writeAvro((String) record.get("_row_key"), record);
+ }
+ writer.close();
+ }
+
+ @Test
+ void testReadDoesNotLoadHadoopDefaultResources() throws Throwable {
+ int[] rowCount = new int[1];
+ int loads = countDefaultResourceLoads(() -> rowCount[0] =
readRows(Collections.emptyList()));
+ assertEquals(NUM_RECORDS, rowCount[0]);
+ assertEquals(0, loads, "Reading a parquet file must reuse the storage
configuration");
+ }
+
+ @Test
+ void testReadAppliesPushedDownFilters() throws Exception {
+ String firstKey = (String) records.get(0).get("_row_key");
+ // Row group statistics keep the matching row group, and drop it for a key
above every UUID key.
+ assertEquals(NUM_RECORDS, readRows(Collections.singletonList(new
EqualTo("_row_key", firstKey))));
+ assertEquals(0, readRows(Collections.singletonList(new EqualTo("_row_key",
"~"))));
+ }
+
+ @Test
+ void testReadEncryptedFileResolvesDecryptionWithFilePath() throws Exception {
+ storage.getConf().set(FooterKeyDecryptionFactory.CRYPTO_FACTORY_CLASS,
FooterKeyDecryptionFactory.class.getName());
+ StoragePath encryptedPath = new
StoragePath(tmpDir.resolve("encrypted.parquet").toAbsolutePath().toString());
+
FooterKeyDecryptionFactory.writeEncryptedFile(storage.getConf().unwrapAs(Configuration.class),
new Path(encryptedPath.toUri()), 10);
+ FooterKeyDecryptionFactory.drainRequestedPaths();
+
+ List<Long> readTs = new ArrayList<>();
+ try (HoodieSparkParquetReader reader = (HoodieSparkParquetReader) new
HoodieSparkFileReaderFactory(storage)
+ .newParquetFileReader(encryptedPath);
+ ClosableIterator<UnsafeRow> iterator =
reader.getUnsafeRowIterator(HoodieSchema.fromAvroSchema(FooterKeyDecryptionFactory.SCHEMA)))
{
+ iterator.forEachRemaining(row -> readTs.add(row.getLong(1)));
+ }
+ assertEquals(LongStream.range(0, 10).boxed().collect(Collectors.toList()),
readTs);
+ List<Path> requestedPaths =
FooterKeyDecryptionFactory.drainRequestedPaths();
+ assertFalse(requestedPaths.isEmpty());
+ requestedPaths.forEach(requested -> assertEquals("encrypted.parquet",
requested == null ? null : requested.getName()));
+ }
+
+ private int readRows(List<Filter> filters) throws Exception {
+ int count = 0;
+ try (HoodieSparkParquetReader reader = (HoodieSparkParquetReader) new
HoodieSparkFileReaderFactory(storage)
+ .newParquetFileReader(parquetPath);
+ ClosableIterator<UnsafeRow> iterator =
reader.getUnsafeRowIterator(schema, filters)) {
+ while (iterator.hasNext()) {
+ iterator.next();
+ count++;
+ }
+ }
+ return count;
+ }
+}
diff --git
a/hudi-spark-datasource/hudi-spark/src/test/java/org/apache/hudi/io/storage/TestParquetReadOptionsParity.java
b/hudi-spark-datasource/hudi-spark/src/test/java/org/apache/hudi/io/storage/TestParquetReadOptionsParity.java
new file mode 100644
index 000000000000..84076f7dfbe9
--- /dev/null
+++
b/hudi-spark-datasource/hudi-spark/src/test/java/org/apache/hudi/io/storage/TestParquetReadOptionsParity.java
@@ -0,0 +1,148 @@
+/*
+ * 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.io.storage;
+
+import org.apache.hudi.common.testutils.FooterKeyDecryptionFactory;
+import org.apache.hudi.common.util.ParquetUtils;
+import org.apache.hudi.storage.StoragePath;
+
+import org.apache.avro.generic.GenericRecord;
+import org.apache.avro.generic.IndexedRecord;
+import org.apache.hadoop.conf.Configuration;
+import org.apache.hadoop.fs.Path;
+import org.apache.parquet.HadoopReadOptions;
+import org.apache.parquet.ParquetReadOptions;
+import org.apache.parquet.avro.HoodieAvroParquetReaderBuilder;
+import org.apache.parquet.crypto.FileDecryptionProperties;
+import org.apache.parquet.filter2.compat.FilterCompat;
+import org.apache.parquet.filter2.predicate.FilterApi;
+import org.apache.parquet.hadoop.ParquetInputFormat;
+import org.apache.parquet.hadoop.ParquetReader;
+import org.apache.parquet.hadoop.util.HadoopInputFile;
+import org.junit.jupiter.api.io.TempDir;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.CsvSource;
+
+import java.io.IOException;
+import java.lang.reflect.Field;
+import java.lang.reflect.Modifier;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.stream.Collectors;
+import java.util.stream.IntStream;
+
+import static org.junit.jupiter.api.Assertions.assertArrayEquals;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertSame;
+
+/**
+ * Tests {@link ParquetUtils#withHadoopReadOptions} against the parquet
version on the classpath. It lives in
+ * this module, rather than next to {@link ParquetUtils}, because the parquet
version differs per Spark
+ * profile and CI runs this module's tests under each one.
+ */
+class TestParquetReadOptionsParity {
+
+ @TempDir
+ java.nio.file.Path tempDir;
+
+ /**
+ * A reader built with {@link ParquetUtils#withHadoopReadOptions} must get
the read options parquet builds
+ * from the configuration and the file path, field by field, and read the
same rows as a reader built from
+ * the path with {@code withConf}, both with the read options left at their
defaults and with them set.
+ */
+ @ParameterizedTest
+ @CsvSource({"false,false", "false,true", "true,false", "true,true"})
+ void testWithHadoopReadOptionsMatchesPathBasedReader(boolean encrypted,
boolean setReadOptions) throws Exception {
+ Configuration conf = new Configuration();
+ Path path = new Path(tempDir.resolve("options.parquet").toUri());
+ FooterKeyDecryptionFactory.writeFile(conf, path, 10, encrypted);
+ if (encrypted) {
+ conf.set(FooterKeyDecryptionFactory.CRYPTO_FACTORY_CLASS,
FooterKeyDecryptionFactory.class.getName());
+ }
+ if (setReadOptions) {
+ // Non-default values, so options that ignore the configuration differ
+ conf.setBoolean(ParquetInputFormat.STATS_FILTERING_ENABLED, false);
+ conf.setBoolean(ParquetInputFormat.DICTIONARY_FILTERING_ENABLED, false);
+ conf.setBoolean(ParquetInputFormat.COLUMN_INDEX_FILTERING_ENABLED,
false);
+ conf.setBoolean(ParquetInputFormat.BLOOM_FILTERING_ENABLED, false);
+ conf.setBoolean(ParquetInputFormat.PAGE_VERIFY_CHECKSUM_ENABLED, true);
+ conf.setInt("parquet.read.allocation.size", 4 * 1024 * 1024);
+ ParquetInputFormat.setFilterPredicate(conf,
FilterApi.eq(FilterApi.longColumn("ts"), 5L));
+ }
+
+ HadoopInputFile inputFile = HadoopInputFile.fromPath(path, conf);
+ ParquetReadOptions expectedOptions = HadoopReadOptions.builder(conf,
inputFile.getPath()).build();
+ FooterKeyDecryptionFactory.drainRequestedPaths();
+ try (ParquetReader<IndexedRecord> reader =
ParquetUtils.withHadoopReadOptions(
+ new HoodieAvroParquetReaderBuilder<IndexedRecord>(inputFile),
inputFile).build()) {
+ assertEquals(encrypted ? Collections.singletonList(inputFile.getPath())
: Collections.emptyList(),
+ FooterKeyDecryptionFactory.drainRequestedPaths());
+ Field options = ParquetReader.class.getDeclaredField("options");
+ options.setAccessible(true);
+ assertSameFields(expectedOptions, options.get(reader), "options");
+
+ List<String> keys = readKeys(reader);
+ assertEquals(setReadOptions ? Collections.singletonList("key5")
+ : IntStream.range(0, 10).mapToObj(i -> "key" +
i).collect(Collectors.toList()), keys);
+ try (ParquetReader<IndexedRecord> pathReader = new
HoodieAvroParquetReaderBuilder<IndexedRecord>(
+ new StoragePath(path.toUri())).withConf(conf).build()) {
+ assertEquals(readKeys(pathReader), keys);
+ }
+ }
+ }
+
+ private static void assertSameFields(Object expected, Object actual, String
name) throws ReflectiveOperationException {
+ assertEquals(expected.getClass(), actual.getClass(), name);
+ for (Class<?> clazz = expected.getClass(); clazz != Object.class; clazz =
clazz.getSuperclass()) {
+ for (Field field : clazz.getDeclaredFields()) {
+ if (Modifier.isStatic(field.getModifiers())) {
+ continue;
+ }
+ field.setAccessible(true);
+ Object expectedValue = field.get(expected);
+ Object actualValue = field.get(actual);
+ String fieldName = name + "." + field.getName();
+ if (expectedValue == null || actualValue == null || expectedValue
instanceof Configuration) {
+ assertSame(expectedValue, actualValue, fieldName);
+ } else if (expectedValue instanceof byte[]) {
+ assertArrayEquals((byte[]) expectedValue, (byte[]) actualValue,
fieldName);
+ } else if (expectedValue instanceof FileDecryptionProperties) {
+ assertSameFields(expectedValue, actualValue, fieldName);
+ } else if (expectedValue instanceof
FilterCompat.FilterPredicateCompat) {
+ assertEquals(((FilterCompat.FilterPredicateCompat)
expectedValue).getFilterPredicate(),
+ ((FilterCompat.FilterPredicateCompat)
actualValue).getFilterPredicate(), fieldName);
+ } else if (expectedValue.getClass().getMethod("equals",
Object.class).getDeclaringClass() != Object.class) {
+ assertEquals(expectedValue, actualValue, fieldName);
+ } else {
+ // Created per build without value equality (codec factory,
allocator, configuration wrapper)
+ assertEquals(expectedValue.getClass(), actualValue.getClass(),
fieldName);
+ }
+ }
+ }
+ }
+
+ private static List<String> readKeys(ParquetReader<IndexedRecord> reader)
throws IOException {
+ List<String> keys = new ArrayList<>();
+ for (IndexedRecord record = reader.read(); record != null; record =
reader.read()) {
+ keys.add(((GenericRecord) record).get("_row_key").toString());
+ }
+ return keys;
+ }
+}
diff --git
a/hudi-spark-datasource/hudi-spark/src/test/scala/org/apache/hudi/functional/TestBasicSchemaEvolution.scala
b/hudi-spark-datasource/hudi-spark/src/test/scala/org/apache/hudi/functional/TestBasicSchemaEvolution.scala
index 07829da4e0e9..709c2727dd16 100644
---
a/hudi-spark-datasource/hudi-spark/src/test/scala/org/apache/hudi/functional/TestBasicSchemaEvolution.scala
+++
b/hudi-spark-datasource/hudi-spark/src/test/scala/org/apache/hudi/functional/TestBasicSchemaEvolution.scala
@@ -468,7 +468,7 @@ class TestBasicSchemaEvolution extends
HoodieSparkClientTestBase with ScalaAsser
* spans a base-only int file (p2) and an int base file merged with a long
log file (p1). The
* top-level `age` promotion is atomic and is read with the vectorized
parquet reader on. When the
* same promotion is applied inside the `nested` struct the changed
top-level column is no longer
- * atomic: ParquetSchemaEvolutionUtils.getHadoopConfClone must reject it
fast on the base slice
+ * atomic: ParquetSchemaEvolutionUtils.getFileReadConf must reject it fast
on the base slice
* instead of returning corrupt columns, and the workaround it advertises
(disabling the vectorized
* reader) must actually widen `nested.a` across both shapes. COW is covered
by
* TestLegacyParquetReadPath#testCowSnapshotReadWithNestedTypeChange.
diff --git
a/hudi-spark-datasource/hudi-spark/src/test/scala/org/apache/hudi/io/storage/ParquetPerFileReadBenchmark.scala
b/hudi-spark-datasource/hudi-spark/src/test/scala/org/apache/hudi/io/storage/ParquetPerFileReadBenchmark.scala
new file mode 100644
index 000000000000..504ef0e7c4b0
--- /dev/null
+++
b/hudi-spark-datasource/hudi-spark/src/test/scala/org/apache/hudi/io/storage/ParquetPerFileReadBenchmark.scala
@@ -0,0 +1,179 @@
+/*
+ * 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.io.storage
+
+import org.apache.hudi.SparkAdapterSupport.sparkAdapter
+import org.apache.hudi.common.util.{HoodieStorageUtils, Option => HOption}
+import org.apache.hudi.hadoop.fs.HadoopFSUtils
+import org.apache.hudi.storage.StoragePath
+import org.apache.hudi.util.CloseableInternalRowIterator
+
+import org.apache.hadoop.conf.Configuration
+import
org.apache.parquet.avro.HoodieAvroParquetSchemaConverter.getAvroSchemaConverter
+import org.apache.spark.sql.SparkSession
+import org.apache.spark.sql.avro.HoodieSparkSchemaConverters
+import org.apache.spark.sql.catalyst.InternalRow
+import org.apache.spark.sql.execution.datasources.FileFormat
+import org.apache.spark.sql.types.StructType
+import org.junit.jupiter.api.Assertions.assertEquals
+import org.junit.jupiter.api.Test
+import org.junit.jupiter.api.condition.EnabledIfSystemProperty
+import org.junit.jupiter.api.io.TempDir
+
+import java.lang.management.ManagementFactory
+import java.nio.file.{Files, Path}
+
+import scala.collection.JavaConverters._
+
+/**
+ * Manual benchmark of the per-file CPU cost of the Spark parquet read paths:
the log-file reader
+ * ([[HoodieSparkParquetReader]]) and the base-file reader built by the Spark
adapter. Runs only when
+ * `benchmark.files` is set, so it stays out of CI. Every round reads the same
small files and checks
+ * the row count and a checksum, so a faster but wrong reader fails the run.
Run it with surefire
+ * directly, since `mvn test` in this module also runs every scalatest suite:
+ *
+ * {{{
+ * mvn test-compile -pl hudi-spark-datasource/hudi-spark
+ * mvn surefire:test -pl hudi-spark-datasource/hudi-spark
-Dtest=ParquetPerFileReadBenchmark \
+ * -Dbenchmark.files=400 -Dbenchmark.rounds=10 -Dbenchmark.warmupRounds=3
+ * }}}
+ */
+@EnabledIfSystemProperty(named = "benchmark.files", matches = "\\d+")
+class ParquetPerFileReadBenchmark {
+
+ @TempDir
+ var tmpDir: Path = _
+
+ @Test
+ def benchmark(): Unit = {
+ val numFiles = Integer.getInteger("benchmark.files").intValue()
+ val rounds = Integer.getInteger("benchmark.rounds", 10).intValue()
+ val warmupRounds = Integer.getInteger("benchmark.warmupRounds",
3).intValue()
+ val rowsPerFile = 20
+ val spark = SparkSession.builder()
+ .master("local[1]")
+ .appName("ParquetPerFileReadBenchmark")
+ .config("spark.ui.enabled", "false")
+ .getOrCreate()
+ try {
+ val dir = tmpDir.resolve("data").toString
+ spark.range(0, numFiles.toLong * rowsPerFile, 1, numFiles)
+ .selectExpr("concat('key', cast(id as string)) as key", "id as ts",
"concat('value', cast(id as string)) as value")
+ .write.parquet(dir)
+ val files = Files.list(tmpDir.resolve("data")).iterator().asScala
+
.filter(_.getFileName.toString.endsWith(".parquet")).map(_.toString).toSeq.sorted
+ assertEquals(numFiles, files.size)
+
+ val hadoopConf = spark.sessionState.newHadoopConf()
+ val storageConf = HadoopFSUtils.getStorageConf(hadoopConf)
+ val storage = HoodieStorageUtils.getStorage(new StoragePath(files.head),
storageConf)
+ val structType = spark.read.parquet(files.head).schema
+ val schema = HoodieSparkSchemaConverters.toHoodieType(structType,
nullable = false, "record", "")
+ val baseFileReader = sparkAdapter.createParquetFileReader(vectorized =
true, spark.sessionState.conf,
+ Map(FileFormat.OPTION_RETURNING_BATCH -> "false"), hadoopConf)
+ val expectedChecksum = (0L until numFiles.toLong * rowsPerFile).sum
+
+ def readLogPath(file: String): (Long, Long) = {
+ val reader = new
HoodieSparkFileReaderFactory(storage).newParquetFileReader(new
StoragePath(file))
+ .asInstanceOf[HoodieSparkParquetReader]
+ try {
+ val iterator = reader.getUnsafeRowIterator(schema)
+ var rows = 0L
+ var checksum = 0L
+ while (iterator.hasNext) {
+ checksum += iterator.next().getLong(1)
+ rows += 1
+ }
+ (rows, checksum)
+ } finally {
+ reader.close()
+ }
+ }
+
+ def readBasePath(file: String): (Long, Long) = {
+ val partitionedFile = sparkAdapter.getSparkPartitionedFileUtils
+ .createPartitionedFile(InternalRow.empty, new StoragePath(file), 0,
Files.size(java.nio.file.Paths.get(file)))
+ // Mirrors SparkFileFormatInternalRowReaderContext, which converts the
table schema for every base file.
+ val tableSchema =
getAvroSchemaConverter(storageConf.unwrap()).convert(schema)
+ val iterator = new
CloseableInternalRowIterator(baseFileReader.read(partitionedFile, structType,
+ new StructType(), HOption.empty(), Seq.empty, storageConf,
HOption.of(tableSchema)))
+ try {
+ var rows = 0L
+ var checksum = 0L
+ while (iterator.hasNext) {
+ checksum += iterator.next().getLong(1)
+ rows += 1
+ }
+ (rows, checksum)
+ } finally {
+ iterator.close()
+ }
+ }
+
+ // Reference costs of single per-file steps, measured the same way; they
read no rows.
+ def copyConf(file: String): (Long, Long) = {
+ new Configuration(hadoopConf)
+ (-1L, -1L)
+ }
+
+ def newConfiguration(file: String): (Long, Long) = {
+ new Configuration().size()
+ (-1L, -1L)
+ }
+
+ def convertTableSchema(file: String): (Long, Long) = {
+ getAvroSchemaConverter(storageConf.unwrap()).convert(schema)
+ (-1L, -1L)
+ }
+
+ val threadMx = ManagementFactory.getThreadMXBean
+ val paths = Seq("log" -> (readLogPath _), "base" -> (readBasePath _),
"confCopy" -> (copyConf _),
+ "newConfiguration" -> (newConfiguration _), "tableSchemaConversion" ->
(convertTableSchema _))
+ val results = paths.map { case (name, read) =>
+ val perFileCpuMicros = (0 until warmupRounds + rounds).map { _ =>
+ val start = threadMx.getCurrentThreadCpuTime
+ var rows = 0L
+ var checksum = 0L
+ files.foreach { file =>
+ val (r, c) = read(file)
+ rows += r
+ checksum += c
+ }
+ val elapsed = threadMx.getCurrentThreadCpuTime - start
+ if (rows >= 0) {
+ assertEquals(numFiles.toLong * rowsPerFile, rows)
+ assertEquals(expectedChecksum, checksum)
+ }
+ elapsed / 1000.0 / numFiles
+ }.drop(warmupRounds).sorted
+ name -> perFileCpuMicros
+ }
+ // scalastyle:off println
+ println(s"ParquetPerFileReadBenchmark: files=$numFiles
rowsPerFile=$rowsPerFile rounds=$rounds " +
+ s"warmupRounds=$warmupRounds hadoopConfEntries=${hadoopConf.size()}")
+ results.foreach { case (name, micros) =>
+ println(f"ParquetPerFileReadBenchmark: path=$name cpuMicrosPerFile
median=${micros(micros.size / 2)}%.1f " +
+ f"min=${micros.head}%.1f max=${micros.last}%.1f")
+ }
+ // scalastyle:on println
+ } finally {
+ spark.stop()
+ }
+ }
+}
diff --git
a/hudi-spark-datasource/hudi-spark3-common/src/main/scala/org/apache/spark/sql/execution/datasources/parquet/Spark3LegacyHoodieParquetFileFormat.scala
b/hudi-spark-datasource/hudi-spark3-common/src/main/scala/org/apache/spark/sql/execution/datasources/parquet/Spark3LegacyHoodieParquetFileFormat.scala
index a7135b0352b7..620541a1deb6 100644
---
a/hudi-spark-datasource/hudi-spark3-common/src/main/scala/org/apache/spark/sql/execution/datasources/parquet/Spark3LegacyHoodieParquetFileFormat.scala
+++
b/hudi-spark-datasource/hudi-spark3-common/src/main/scala/org/apache/spark/sql/execution/datasources/parquet/Spark3LegacyHoodieParquetFileFormat.scala
@@ -259,7 +259,7 @@ abstract class
Spark3LegacyHoodieParquetFileFormat(shouldAppendPartitionValues:
ParquetSchemaEvolutionUtils.validateNoShreddedVariantStructs(requiredSchema,
footerFileMetaData.getSchema)
}
val typeChangeInfos: java.util.Map[Integer, Pair[DataType, DataType]] =
if (shouldUseInternalSchema) {
- // Same guard as ParquetSchemaEvolutionUtils.getHadoopConfClone:
schema-on-read cannot
+ // Same guard as ParquetSchemaEvolutionUtils.getFileReadConf:
schema-on-read cannot
// reconstruct shredded variants, so fail loudly instead of silently
dropping typed_value.
// Empty projections (count(*)) read no column data and keep working.
if (requiredSchema.nonEmpty) {
diff --git
a/hudi-spark-datasource/hudi-spark3.3.x/src/main/scala/org/apache/spark/sql/execution/datasources/parquet/Spark33ParquetReader.scala
b/hudi-spark-datasource/hudi-spark3.3.x/src/main/scala/org/apache/spark/sql/execution/datasources/parquet/Spark33ParquetReader.scala
index e5f94df47866..ad6f79bb74e1 100644
---
a/hudi-spark-datasource/hudi-spark3.3.x/src/main/scala/org/apache/spark/sql/execution/datasources/parquet/Spark33ParquetReader.scala
+++
b/hudi-spark-datasource/hudi-spark3.3.x/src/main/scala/org/apache/spark/sql/execution/datasources/parquet/Spark33ParquetReader.scala
@@ -164,13 +164,7 @@ class Spark33ParquetReader(enableVectorizedReader: Boolean,
val attemptId = new TaskAttemptID(new TaskID(new JobID(), TaskType.MAP,
0), 0)
val hadoopAttemptContext =
- new
TaskAttemptContextImpl(schemaEvolutionUtils.getHadoopConfClone(footerFileMetaData,
enableVectorizedReader), attemptId)
-
- // Try to push down filters when filter push-down is enabled.
- // Notice: This push-down is RowGroups level, not individual records.
- if (pushed.isDefined) {
-
ParquetInputFormat.setFilterPredicate(hadoopAttemptContext.getConfiguration,
pushed.get)
- }
+ new
TaskAttemptContextImpl(schemaEvolutionUtils.getFileReadConf(footerFileMetaData,
enableVectorizedReader, pushed), attemptId)
val taskContext = Option(TaskContext.get())
if (enableVectorizedReader) {
val vectorizedReader = schemaEvolutionUtils.buildVectorizedReader(
diff --git
a/hudi-spark-datasource/hudi-spark3.4.x/src/main/scala/org/apache/spark/sql/execution/datasources/parquet/Spark34ParquetReader.scala
b/hudi-spark-datasource/hudi-spark3.4.x/src/main/scala/org/apache/spark/sql/execution/datasources/parquet/Spark34ParquetReader.scala
index f350afacfecf..a14956b40417 100644
---
a/hudi-spark-datasource/hudi-spark3.4.x/src/main/scala/org/apache/spark/sql/execution/datasources/parquet/Spark34ParquetReader.scala
+++
b/hudi-spark-datasource/hudi-spark3.4.x/src/main/scala/org/apache/spark/sql/execution/datasources/parquet/Spark34ParquetReader.scala
@@ -161,13 +161,7 @@ class Spark34ParquetReader(enableVectorizedReader: Boolean,
val attemptId = new TaskAttemptID(new TaskID(new JobID(), TaskType.MAP,
0), 0)
val hadoopAttemptContext =
- new
TaskAttemptContextImpl(schemaEvolutionUtils.getHadoopConfClone(footerFileMetaData,
enableVectorizedReader), attemptId)
-
- // Try to push down filters when filter push-down is enabled.
- // Notice: This push-down is RowGroups level, not individual records.
- if (pushed.isDefined) {
-
ParquetInputFormat.setFilterPredicate(hadoopAttemptContext.getConfiguration,
pushed.get)
- }
+ new
TaskAttemptContextImpl(schemaEvolutionUtils.getFileReadConf(footerFileMetaData,
enableVectorizedReader, pushed), attemptId)
val taskContext = Option(TaskContext.get())
if (enableVectorizedReader) {
val vectorizedReader = schemaEvolutionUtils.buildVectorizedReader(
diff --git
a/hudi-spark-datasource/hudi-spark3.5.x/src/main/scala/org/apache/spark/sql/execution/datasources/parquet/Spark35ParquetReader.scala
b/hudi-spark-datasource/hudi-spark3.5.x/src/main/scala/org/apache/spark/sql/execution/datasources/parquet/Spark35ParquetReader.scala
index 31955d13dbdf..eacf3be55b7a 100644
---
a/hudi-spark-datasource/hudi-spark3.5.x/src/main/scala/org/apache/spark/sql/execution/datasources/parquet/Spark35ParquetReader.scala
+++
b/hudi-spark-datasource/hudi-spark3.5.x/src/main/scala/org/apache/spark/sql/execution/datasources/parquet/Spark35ParquetReader.scala
@@ -168,13 +168,7 @@ class Spark35ParquetReader(enableVectorizedReader: Boolean,
val attemptId = new TaskAttemptID(new TaskID(new JobID(), TaskType.MAP,
0), 0)
val hadoopAttemptContext =
- new
TaskAttemptContextImpl(schemaEvolutionUtils.getHadoopConfClone(footerFileMetaData,
enableVectorizedReader), attemptId)
-
- // Try to push down filters when filter push-down is enabled.
- // Notice: This push-down is RowGroups level, not individual records.
- if (pushed.isDefined) {
-
ParquetInputFormat.setFilterPredicate(hadoopAttemptContext.getConfiguration,
pushed.get)
- }
+ new
TaskAttemptContextImpl(schemaEvolutionUtils.getFileReadConf(footerFileMetaData,
enableVectorizedReader, pushed), attemptId)
val taskContext = Option(TaskContext.get())
if (enableVectorizedReader) {
val vectorizedReader = schemaEvolutionUtils.buildVectorizedReader(
diff --git
a/hudi-spark-datasource/hudi-spark4.0.x/src/main/scala/org/apache/spark/sql/execution/datasources/parquet/Spark40LegacyHoodieParquetFileFormat.scala
b/hudi-spark-datasource/hudi-spark4.0.x/src/main/scala/org/apache/spark/sql/execution/datasources/parquet/Spark40LegacyHoodieParquetFileFormat.scala
index 37980b6dfa41..36fd0b7b614f 100644
---
a/hudi-spark-datasource/hudi-spark4.0.x/src/main/scala/org/apache/spark/sql/execution/datasources/parquet/Spark40LegacyHoodieParquetFileFormat.scala
+++
b/hudi-spark-datasource/hudi-spark4.0.x/src/main/scala/org/apache/spark/sql/execution/datasources/parquet/Spark40LegacyHoodieParquetFileFormat.scala
@@ -242,7 +242,7 @@ class Spark40LegacyHoodieParquetFileFormat(private val
shouldAppendPartitionValu
// Clone new conf
val hadoopAttemptConf = new
Configuration(broadcastedHadoopConf.value.value)
val typeChangeInfos: java.util.Map[Integer, Pair[DataType, DataType]] =
if (shouldUseInternalSchema) {
- // Same guard as ParquetSchemaEvolutionUtils.getHadoopConfClone:
schema-on-read cannot
+ // Same guard as ParquetSchemaEvolutionUtils.getFileReadConf:
schema-on-read cannot
// reconstruct shredded variants, so fail loudly instead of silently
dropping typed_value.
// Empty projections (count(*)) read no column data and keep working.
if (requiredSchema.nonEmpty) {
diff --git
a/hudi-spark-datasource/hudi-spark4.0.x/src/main/scala/org/apache/spark/sql/execution/datasources/parquet/Spark40ParquetReader.scala
b/hudi-spark-datasource/hudi-spark4.0.x/src/main/scala/org/apache/spark/sql/execution/datasources/parquet/Spark40ParquetReader.scala
index c98567a70936..d1ccc8139fb3 100644
---
a/hudi-spark-datasource/hudi-spark4.0.x/src/main/scala/org/apache/spark/sql/execution/datasources/parquet/Spark40ParquetReader.scala
+++
b/hudi-spark-datasource/hudi-spark4.0.x/src/main/scala/org/apache/spark/sql/execution/datasources/parquet/Spark40ParquetReader.scala
@@ -171,13 +171,7 @@ class Spark40ParquetReader(enableVectorizedReader: Boolean,
val attemptId = new TaskAttemptID(new TaskID(new JobID(), TaskType.MAP,
0), 0)
val hadoopAttemptContext =
- new
TaskAttemptContextImpl(schemaEvolutionUtils.getHadoopConfClone(footerFileMetaData,
enableVectorizedReader), attemptId)
-
- // Try to push down filters when filter push-down is enabled.
- // Notice: This push-down is RowGroups level, not individual records.
- if (pushed.isDefined) {
-
ParquetInputFormat.setFilterPredicate(hadoopAttemptContext.getConfiguration,
pushed.get)
- }
+ new
TaskAttemptContextImpl(schemaEvolutionUtils.getFileReadConf(footerFileMetaData,
enableVectorizedReader, pushed), attemptId)
val taskContext = Option(TaskContext.get())
if (enableVectorizedReader) {
val vectorizedReader = schemaEvolutionUtils.buildVectorizedReader(
diff --git
a/hudi-spark-datasource/hudi-spark4.1.x/src/main/scala/org/apache/spark/sql/execution/datasources/parquet/Spark41LegacyHoodieParquetFileFormat.scala
b/hudi-spark-datasource/hudi-spark4.1.x/src/main/scala/org/apache/spark/sql/execution/datasources/parquet/Spark41LegacyHoodieParquetFileFormat.scala
index 08a7e7816702..186d9bec76c3 100644
---
a/hudi-spark-datasource/hudi-spark4.1.x/src/main/scala/org/apache/spark/sql/execution/datasources/parquet/Spark41LegacyHoodieParquetFileFormat.scala
+++
b/hudi-spark-datasource/hudi-spark4.1.x/src/main/scala/org/apache/spark/sql/execution/datasources/parquet/Spark41LegacyHoodieParquetFileFormat.scala
@@ -245,7 +245,7 @@ class Spark41LegacyHoodieParquetFileFormat(private val
shouldAppendPartitionValu
// Clone new conf
val hadoopAttemptConf = new
Configuration(broadcastedHadoopConf.value.value)
val typeChangeInfos: java.util.Map[Integer, Pair[DataType, DataType]]
= if (shouldUseInternalSchema) {
- // Same guard as ParquetSchemaEvolutionUtils.getHadoopConfClone:
schema-on-read cannot
+ // Same guard as ParquetSchemaEvolutionUtils.getFileReadConf:
schema-on-read cannot
// reconstruct shredded variants, so fail loudly instead of silently
dropping typed_value.
// Empty projections (count(*)) read no column data and keep working.
if (requiredSchema.nonEmpty) {
diff --git
a/hudi-spark-datasource/hudi-spark4.1.x/src/main/scala/org/apache/spark/sql/execution/datasources/parquet/Spark41ParquetReader.scala
b/hudi-spark-datasource/hudi-spark4.1.x/src/main/scala/org/apache/spark/sql/execution/datasources/parquet/Spark41ParquetReader.scala
index 940fbb281e8f..f5b04301adfe 100644
---
a/hudi-spark-datasource/hudi-spark4.1.x/src/main/scala/org/apache/spark/sql/execution/datasources/parquet/Spark41ParquetReader.scala
+++
b/hudi-spark-datasource/hudi-spark4.1.x/src/main/scala/org/apache/spark/sql/execution/datasources/parquet/Spark41ParquetReader.scala
@@ -171,13 +171,7 @@ class Spark41ParquetReader(enableVectorizedReader: Boolean,
val attemptId = new TaskAttemptID(new TaskID(new JobID(), TaskType.MAP,
0), 0)
val hadoopAttemptContext =
- new
TaskAttemptContextImpl(schemaEvolutionUtils.getHadoopConfClone(footerFileMetaData,
enableVectorizedReader), attemptId)
-
- // Try to push down filters when filter push-down is enabled.
- // Notice: This push-down is RowGroups level, not individual records.
- pushed.foreach {
-
ParquetInputFormat.setFilterPredicate(hadoopAttemptContext.getConfiguration, _)
- }
+ new
TaskAttemptContextImpl(schemaEvolutionUtils.getFileReadConf(footerFileMetaData,
enableVectorizedReader, pushed), attemptId)
if (enableVectorizedReader) {
buildVectorizedIterator(
hadoopAttemptContext, split, file.partitionValues, partitionSchema,
convertTz,
diff --git
a/hudi-spark-datasource/hudi-spark4.2.x/src/main/scala/org/apache/spark/sql/execution/datasources/parquet/Spark42LegacyHoodieParquetFileFormat.scala
b/hudi-spark-datasource/hudi-spark4.2.x/src/main/scala/org/apache/spark/sql/execution/datasources/parquet/Spark42LegacyHoodieParquetFileFormat.scala
index 27c8b1f7b731..fe462522f6ac 100644
---
a/hudi-spark-datasource/hudi-spark4.2.x/src/main/scala/org/apache/spark/sql/execution/datasources/parquet/Spark42LegacyHoodieParquetFileFormat.scala
+++
b/hudi-spark-datasource/hudi-spark4.2.x/src/main/scala/org/apache/spark/sql/execution/datasources/parquet/Spark42LegacyHoodieParquetFileFormat.scala
@@ -245,7 +245,7 @@ class Spark42LegacyHoodieParquetFileFormat(private val
shouldAppendPartitionValu
// Clone new conf
val hadoopAttemptConf = new
Configuration(broadcastedHadoopConf.value.value)
val typeChangeInfos: java.util.Map[Integer, Pair[DataType, DataType]]
= if (shouldUseInternalSchema) {
- // Same guard as ParquetSchemaEvolutionUtils.getHadoopConfClone:
schema-on-read cannot
+ // Same guard as ParquetSchemaEvolutionUtils.getFileReadConf:
schema-on-read cannot
// reconstruct shredded variants, so fail loudly instead of silently
dropping typed_value.
// Empty projections (count(*)) read no column data and keep working.
if (requiredSchema.nonEmpty) {
diff --git
a/hudi-spark-datasource/hudi-spark4.2.x/src/main/scala/org/apache/spark/sql/execution/datasources/parquet/Spark42ParquetReader.scala
b/hudi-spark-datasource/hudi-spark4.2.x/src/main/scala/org/apache/spark/sql/execution/datasources/parquet/Spark42ParquetReader.scala
index 9b265af6b98f..a3519a7a0337 100644
---
a/hudi-spark-datasource/hudi-spark4.2.x/src/main/scala/org/apache/spark/sql/execution/datasources/parquet/Spark42ParquetReader.scala
+++
b/hudi-spark-datasource/hudi-spark4.2.x/src/main/scala/org/apache/spark/sql/execution/datasources/parquet/Spark42ParquetReader.scala
@@ -171,13 +171,7 @@ class Spark42ParquetReader(enableVectorizedReader: Boolean,
val attemptId = new TaskAttemptID(new TaskID(new JobID(), TaskType.MAP,
0), 0)
val hadoopAttemptContext =
- new
TaskAttemptContextImpl(schemaEvolutionUtils.getHadoopConfClone(footerFileMetaData,
enableVectorizedReader), attemptId)
-
- // Try to push down filters when filter push-down is enabled.
- // Notice: This push-down is RowGroups level, not individual records.
- pushed.foreach {
-
ParquetInputFormat.setFilterPredicate(hadoopAttemptContext.getConfiguration, _)
- }
+ new
TaskAttemptContextImpl(schemaEvolutionUtils.getFileReadConf(footerFileMetaData,
enableVectorizedReader, pushed), attemptId)
if (enableVectorizedReader) {
buildVectorizedIterator(
hadoopAttemptContext, split, file.partitionValues, partitionSchema,
convertTz,