This is an automated email from the ASF dual-hosted git repository.
voonhous 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 0acf20fed0cc refactor(spark): share the Spark 3.x legacy parquet file
format via a common base (#19341)
0acf20fed0cc is described below
commit 0acf20fed0cc376ed68680b67df71ddbb0db423b
Author: Y Ethan Guo <[email protected]>
AuthorDate: Wed Jul 22 02:10:30 2026 -0700
refactor(spark): share the Spark 3.x legacy parquet file format via a
common base (#19341)
---
.../Spark3LegacyHoodieParquetFileFormat.scala} | 96 +++--
.../Spark33LegacyHoodieParquetFileFormat.scala | 460 ++-------------------
.../Spark34LegacyHoodieParquetFileFormat.scala | 438 ++------------------
.../Spark35LegacyHoodieParquetFileFormat.scala | 439 ++------------------
4 files changed, 141 insertions(+), 1292 deletions(-)
diff --git
a/hudi-spark-datasource/hudi-spark3.5.x/src/main/scala/org/apache/spark/sql/execution/datasources/parquet/Spark35LegacyHoodieParquetFileFormat.scala
b/hudi-spark-datasource/hudi-spark3-common/src/main/scala/org/apache/spark/sql/execution/datasources/parquet/Spark3LegacyHoodieParquetFileFormat.scala
similarity index 88%
copy from
hudi-spark-datasource/hudi-spark3.5.x/src/main/scala/org/apache/spark/sql/execution/datasources/parquet/Spark35LegacyHoodieParquetFileFormat.scala
copy to
hudi-spark-datasource/hudi-spark3-common/src/main/scala/org/apache/spark/sql/execution/datasources/parquet/Spark3LegacyHoodieParquetFileFormat.scala
index 22c14a5fe561..9a93e0b04e0e 100644
---
a/hudi-spark-datasource/hudi-spark3.5.x/src/main/scala/org/apache/spark/sql/execution/datasources/parquet/Spark35LegacyHoodieParquetFileFormat.scala
+++
b/hudi-spark-datasource/hudi-spark3-common/src/main/scala/org/apache/spark/sql/execution/datasources/parquet/Spark3LegacyHoodieParquetFileFormat.scala
@@ -31,6 +31,7 @@ import org.apache.hudi.common.util.collection.Pair
import org.apache.hudi.hadoop.fs.HadoopFSUtils
import org.apache.hadoop.conf.Configuration
+import org.apache.hadoop.fs.Path
import org.apache.hadoop.mapred.FileSplit
import org.apache.hadoop.mapreduce.{JobID, TaskAttemptID, TaskID, TaskType}
import org.apache.hadoop.mapreduce.task.TaskAttemptContextImpl
@@ -41,13 +42,11 @@ import org.apache.parquet.hadoop.{ParquetInputFormat,
ParquetRecordReader}
import org.apache.spark.TaskContext
import org.apache.spark.sql.SparkSession
import org.apache.spark.sql.catalyst.InternalRow
-import org.apache.spark.sql.catalyst.expressions.{Cast, JoinedRow}
+import org.apache.spark.sql.catalyst.expressions.{Attribute, Cast, JoinedRow}
import
org.apache.spark.sql.catalyst.expressions.codegen.GenerateUnsafeProjection
-import org.apache.spark.sql.catalyst.types.DataTypeUtils
import org.apache.spark.sql.catalyst.util.DateTimeUtils
-import org.apache.spark.sql.execution.WholeStageCodegenExec
import org.apache.spark.sql.execution.datasources.{DataSourceUtils,
PartitionedFile, RecordReaderIterator}
-import
org.apache.spark.sql.execution.datasources.parquet.Spark35LegacyHoodieParquetFileFormat._
+import
org.apache.spark.sql.execution.datasources.parquet.Spark3LegacyHoodieParquetFileFormat._
import org.apache.spark.sql.internal.SQLConf
import org.apache.spark.sql.sources._
import org.apache.spark.sql.types.{AtomicType, DataType, StructField,
StructType}
@@ -56,25 +55,52 @@ import org.apache.spark.util.SerializableConfiguration
import scala.collection.convert.ImplicitConversions.`collection
AsScalaIterable`
/**
- * This class is an extension of [[ParquetFileFormat]] overriding
Spark-specific behavior
- * that's not possible to customize in any other way
+ * Base [[ParquetFileFormat]] shared by the Spark 3.3, 3.4 and 3.5 legacy
readers.
*
- * NOTE: This is a version of [[AvroDeserializer]] impl from Spark 3.2.1 w/ w/
the following changes applied to it:
+ * It holds the logic common to all three versions. Every expression that
relies on a
+ * version-specific Spark API is delegated to a protected hook that each
concrete subclass
+ * overrides, so this class compiles unchanged against each supported Spark
3.x version.
+ *
+ * This is an extension of [[ParquetFileFormat]] overriding Spark-specific
behavior
+ * that's not possible to customize in any other way, with the following
changes applied:
* <ol>
* <li>Avoiding appending partition values to the rows read from the data
file</li>
* <li>Schema on-read</li>
* </ol>
*/
-class Spark35LegacyHoodieParquetFileFormat(private val
shouldAppendPartitionValues: Boolean) extends ParquetFileFormat {
-
- def supportsColumnar(sparkSession: SparkSession, schema: StructType):
Boolean = {
- val conf = sparkSession.sessionState.conf
- // Only output columnar if there is WSCG to read it.
- val requiredWholeStageCodegenSettings =
- conf.wholeStageEnabled && !WholeStageCodegenExec.isTooManyFields(conf,
schema)
- requiredWholeStageCodegenSettings &&
- supportBatch(sparkSession, schema)
- }
+abstract class
Spark3LegacyHoodieParquetFileFormat(shouldAppendPartitionValues: Boolean)
extends ParquetFileFormat {
+
+ /**
+ * Converts a [[StructType]] to its attributes. Spark 3.3/3.4 expose
[[StructType.toAttributes]]
+ * while Spark 3.5 moved it to
[[org.apache.spark.sql.catalyst.types.DataTypeUtils]].
+ */
+ protected def toAttributes(structType: StructType): Seq[Attribute]
+
+ /**
+ * Extracts the [[Path]] of the file being read. Spark 3.3 keeps the path as
a string while
+ * Spark 3.4+ wraps it in a `SparkPath`.
+ */
+ protected def getFilePath(file: PartitionedFile): Path
+
+ /**
+ * Whether the vectorized reader is enabled for the given schema.
+ */
+ protected def isVectorizedReaderEnabled(sparkSession: SparkSession,
resultSchema: StructType): Boolean
+
+ /**
+ * Whether string-predicate push-down is enabled (renamed between Spark 3.3
and 3.4).
+ */
+ protected def getPushDownStringPredicate(sqlConf: SQLConf): Boolean
+
+ /**
+ * Whether the reader should return columnar batches.
+ */
+ protected def getReturningBatch(sparkSession: SparkSession, resultSchema:
StructType): Boolean
+
+ /**
+ * Sets the version-specific timestamp and nanos-as-long related flags on
the hadoop conf.
+ */
+ protected def setParquetTimeConfs(hadoopConf: Configuration, sparkSession:
SparkSession): Unit
override def buildReaderWithPartitionValues(sparkSession: SparkSession,
dataSchema: StructType,
@@ -109,15 +135,8 @@ class Spark35LegacyHoodieParquetFileFormat(private val
shouldAppendPartitionValu
hadoopConf.setBoolean(
SQLConf.PARQUET_INT96_AS_TIMESTAMP.key,
sparkSession.sessionState.conf.isParquetINT96AsTimestamp)
- // Using string value of this conf to preserve compatibility across spark
versions.
- hadoopConf.setBoolean(
- SQLConf.LEGACY_PARQUET_NANOS_AS_LONG.key,
- sparkSession.sessionState.conf.getConfString(
- SQLConf.LEGACY_PARQUET_NANOS_AS_LONG.key,
- SQLConf.LEGACY_PARQUET_NANOS_AS_LONG.defaultValueString).toBoolean
- )
- hadoopConf.setBoolean(SQLConf.PARQUET_INFER_TIMESTAMP_NTZ_ENABLED.key,
sparkSession.sessionState.conf.parquetInferTimestampNTZEnabled)
- hadoopConf.setBoolean(SQLConf.LEGACY_PARQUET_NANOS_AS_LONG.key,
sparkSession.sessionState.conf.legacyParquetNanosAsLong)
+ // Version-specific timestamp and nanos-as-long flags.
+ setParquetTimeConfs(hadoopConf, sparkSession)
val internalSchemaStr =
hadoopConf.get(SparkInternalSchemaConverter.HOODIE_QUERY_SCHEMA)
// For Spark DataSource v1, there's no Physical Plan projection/schema
pruning w/in Spark itself,
// therefore it's safe to do schema projection here
@@ -136,7 +155,7 @@ class Spark35LegacyHoodieParquetFileFormat(private val
shouldAppendPartitionValu
val resultSchema = StructType(partitionSchema.fields ++
requiredSchema.fields)
val sqlConf = sparkSession.sessionState.conf
val enableOffHeapColumnVector = sqlConf.offHeapColumnVectorEnabled
- val enableVectorizedReader: Boolean = supportBatch(sparkSession,
resultSchema)
+ val enableVectorizedReader: Boolean =
isVectorizedReaderEnabled(sparkSession, resultSchema)
val enableRecordFilter: Boolean = sqlConf.parquetRecordFilterEnabled
val timestampConversion: Boolean =
sqlConf.isParquetINT96TimestampConversion
val capacity = sqlConf.parquetVectorizedReaderBatchSize
@@ -144,23 +163,20 @@ class Spark35LegacyHoodieParquetFileFormat(private val
shouldAppendPartitionValu
val pushDownDate = sqlConf.parquetFilterPushDownDate
val pushDownTimestamp = sqlConf.parquetFilterPushDownTimestamp
val pushDownDecimal = sqlConf.parquetFilterPushDownDecimal
- val pushDownStringStartWith = sqlConf.parquetFilterPushDownStringPredicate
+ val pushDownStringStartWith = getPushDownStringPredicate(sqlConf)
val pushDownInFilterThreshold =
sqlConf.parquetFilterPushDownInFilterThreshold
val isCaseSensitive = sqlConf.caseSensitiveAnalysis
val parquetOptions = new ParquetOptions(options,
sparkSession.sessionState.conf)
val datetimeRebaseModeInRead = parquetOptions.datetimeRebaseModeInRead
val int96RebaseModeInRead = parquetOptions.int96RebaseModeInRead
val timeZoneId = Option(sqlConf.sessionLocalTimeZone)
- // Should always be set by FileSourceScanExec creating this.
- // Check conf before checking option, to allow working around an issue by
changing conf.
- val returningBatch =
sparkSession.sessionState.conf.parquetVectorizedReaderEnabled &&
- supportsColumnar(sparkSession, resultSchema).toString.equals("true")
-
+ // Whole stage codegen (PhysicalRDD) is able to deal with batches directly.
+ val returningBatch = getReturningBatch(sparkSession, resultSchema)
(file: PartitionedFile) => {
assert(!shouldAppendPartitionValues || file.partitionValues.numFields ==
partitionSchema.size)
- val filePath = file.filePath.toPath
+ val filePath = getFilePath(file)
val split = new FileSplit(filePath, file.start, file.length,
Array.empty[String])
val sharedConf = broadcastedHadoopConf.value.value
@@ -176,9 +192,9 @@ class Spark35LegacyHoodieParquetFileFormat(private val
shouldAppendPartitionValu
val fileSchema = if (shouldUseInternalSchema) {
val commitInstantTime = FSUtils.getCommitTime(filePath.getName).toLong;
val validCommits =
sharedConf.get(SparkInternalSchemaConverter.HOODIE_VALID_COMMITS_LIST)
- val storage = HoodieStorageUtils.getStorage(tablePath,
HadoopFSUtils.getStorageConf(sharedConf))
//TODO: HARDCODED TIMELINE OBJECT
val layout =
TimelineLayout.fromVersion(TimelineLayoutVersion.CURR_LAYOUT_VERSION)
+ val storage = HoodieStorageUtils.getStorage(tablePath,
HadoopFSUtils.getStorageConf(sharedConf))
InternalSchemaCache.getInternalSchemaByVersionId(
commitInstantTime, tablePath, storage, if (validCommits == null) ""
else validCommits,
layout)
@@ -192,7 +208,7 @@ class Spark35LegacyHoodieParquetFileFormat(private val
shouldAppendPartitionValu
val pushed = if (enableParquetFilterPushDown) {
val parquetSchema = footerFileMetaData.getSchema
val datetimeRebaseSpec =
-
DataSourceUtils.datetimeRebaseSpec(footerFileMetaData.getKeyValueMetaData.get,
datetimeRebaseModeInRead)
+
DataSourceUtils.datetimeRebaseSpec(footerFileMetaData.getKeyValueMetaData.get,
datetimeRebaseModeInRead)
val parquetFilters = new ParquetFilters(
parquetSchema,
pushDownDate,
@@ -330,7 +346,7 @@ class Spark35LegacyHoodieParquetFileFormat(private val
shouldAppendPartitionValu
} else {
logDebug(s"Falling back to parquet-mr")
val int96RebaseSpec =
-
DataSourceUtils.int96RebaseSpec(footerFileMetaData.getKeyValueMetaData.get,
int96RebaseModeInRead)
+
DataSourceUtils.int96RebaseSpec(footerFileMetaData.getKeyValueMetaData.get,
int96RebaseModeInRead)
val datetimeRebaseSpec =
DataSourceUtils.datetimeRebaseSpec(footerFileMetaData.getKeyValueMetaData.get,
datetimeRebaseModeInRead)
val readSupport = new HoodieParquetReadSupport(
@@ -350,7 +366,7 @@ class Spark35LegacyHoodieParquetFileFormat(private val
shouldAppendPartitionValu
try {
reader.initialize(split, hadoopAttemptContext)
- val fullSchema = DataTypeUtils.toAttributes(requiredSchema) ++
DataTypeUtils.toAttributes(partitionSchema)
+ val fullSchema = toAttributes(requiredSchema) ++
toAttributes(partitionSchema)
val unsafeProjection = if (typeChangeInfos.isEmpty) {
GenerateUnsafeProjection.generate(fullSchema, fullSchema)
} else {
@@ -360,7 +376,7 @@ class Spark35LegacyHoodieParquetFileFormat(private val
shouldAppendPartitionValu
StructField(f.name, typeChangeInfos.get(i).getRight,
f.nullable, f.metadata)
} else f
})
- val newFullSchema = DataTypeUtils.toAttributes(newSchema) ++
DataTypeUtils.toAttributes(partitionSchema)
+ val newFullSchema = toAttributes(newSchema) ++
toAttributes(partitionSchema)
val castSchema = newFullSchema.zipWithIndex.map { case (attr, i) =>
if (typeChangeInfos.containsKey(i)) {
val srcType = typeChangeInfos.get(i).getRight
@@ -393,7 +409,7 @@ class Spark35LegacyHoodieParquetFileFormat(private val
shouldAppendPartitionValu
}
}
-object Spark35LegacyHoodieParquetFileFormat {
+object Spark3LegacyHoodieParquetFileFormat {
def pruneInternalSchema(internalSchemaStr: String, requiredSchema:
StructType): String = {
val querySchemaOption = SerDeHelper.fromJson(internalSchemaStr)
diff --git
a/hudi-spark-datasource/hudi-spark3.3.x/src/main/scala/org/apache/spark/sql/execution/datasources/parquet/Spark33LegacyHoodieParquetFileFormat.scala
b/hudi-spark-datasource/hudi-spark3.3.x/src/main/scala/org/apache/spark/sql/execution/datasources/parquet/Spark33LegacyHoodieParquetFileFormat.scala
index e015c292d77c..fc1df0b16f8e 100644
---
a/hudi-spark-datasource/hudi-spark3.3.x/src/main/scala/org/apache/spark/sql/execution/datasources/parquet/Spark33LegacyHoodieParquetFileFormat.scala
+++
b/hudi-spark-datasource/hudi-spark3.3.x/src/main/scala/org/apache/spark/sql/execution/datasources/parquet/Spark33LegacyHoodieParquetFileFormat.scala
@@ -1,451 +1,61 @@
/*
- * 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
+ * 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
+ * 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.
+ * 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.spark.sql.execution.datasources.parquet
-import org.apache.hudi.client.utils.SparkInternalSchemaConverter
-import org.apache.hudi.common.fs.FSUtils
-import org.apache.hudi.common.schema.internal.InternalSchema
-import org.apache.hudi.common.schema.internal.action.InternalSchemaMerger
-import org.apache.hudi.common.schema.internal.utils.{InternalSchemaUtils,
SerDeHelper}
-import org.apache.hudi.common.table.timeline.TimelineLayout
-import org.apache.hudi.common.table.timeline.versioning.TimelineLayoutVersion
-import org.apache.hudi.common.util.HoodieStorageUtils
-import org.apache.hudi.common.util.InternalSchemaCache
-import org.apache.hudi.common.util.StringUtils.isNullOrEmpty
-import org.apache.hudi.common.util.collection.Pair
-import org.apache.hudi.hadoop.fs.HadoopFSUtils
-
import org.apache.hadoop.conf.Configuration
import org.apache.hadoop.fs.Path
-import org.apache.hadoop.mapred.FileSplit
-import org.apache.hadoop.mapreduce.{JobID, TaskAttemptID, TaskID, TaskType}
-import org.apache.hadoop.mapreduce.task.TaskAttemptContextImpl
-import org.apache.parquet.filter2.compat.FilterCompat
-import org.apache.parquet.filter2.predicate.FilterApi
-import
org.apache.parquet.format.converter.ParquetMetadataConverter.SKIP_ROW_GROUPS
-import org.apache.parquet.hadoop.{ParquetInputFormat, ParquetRecordReader}
-import org.apache.spark.TaskContext
import org.apache.spark.sql.SparkSession
-import org.apache.spark.sql.catalyst.InternalRow
-import org.apache.spark.sql.catalyst.expressions.{Cast, JoinedRow}
-import
org.apache.spark.sql.catalyst.expressions.codegen.GenerateUnsafeProjection
-import org.apache.spark.sql.catalyst.util.DateTimeUtils
-import org.apache.spark.sql.execution.datasources.{DataSourceUtils,
PartitionedFile, RecordReaderIterator}
-import
org.apache.spark.sql.execution.datasources.parquet.Spark33LegacyHoodieParquetFileFormat._
+import org.apache.spark.sql.catalyst.expressions.Attribute
+import org.apache.spark.sql.execution.datasources.PartitionedFile
import org.apache.spark.sql.internal.SQLConf
-import org.apache.spark.sql.sources._
-import org.apache.spark.sql.types.{AtomicType, DataType, StructField,
StructType}
-import org.apache.spark.util.SerializableConfiguration
+import org.apache.spark.sql.types.StructType
import java.net.URI
-import scala.collection.convert.ImplicitConversions.`collection
AsScalaIterable`
-
/**
- * This class is an extension of [[ParquetFileFormat]] overriding
Spark-specific behavior
- * that's not possible to customize in any other way
- *
- * NOTE: This is a version of [[AvroDeserializer]] impl from Spark 3.2.1 w/ w/
the following changes applied to it:
- * <ol>
- * <li>Avoiding appending partition values to the rows read from the data
file</li>
- * <li>Schema on-read</li>
- * </ol>
+ * Spark 3.3 concrete implementation of
[[Spark3LegacyHoodieParquetFileFormat]]. It only overrides
+ * the version-specific hooks; the shared reader logic lives in the base class.
*/
-class Spark33LegacyHoodieParquetFileFormat(private val
shouldAppendPartitionValues: Boolean) extends ParquetFileFormat {
+class Spark33LegacyHoodieParquetFileFormat(appendPartitionValues: Boolean)
+ extends Spark3LegacyHoodieParquetFileFormat(appendPartitionValues) {
- override def buildReaderWithPartitionValues(sparkSession: SparkSession,
- dataSchema: StructType,
- partitionSchema: StructType,
- requiredSchema: StructType,
- filters: Seq[Filter],
- options: Map[String, String],
- hadoopConf: Configuration):
PartitionedFile => Iterator[InternalRow] = {
- hadoopConf.set(ParquetInputFormat.READ_SUPPORT_CLASS,
classOf[ParquetReadSupport].getName)
- hadoopConf.set(
- ParquetReadSupport.SPARK_ROW_REQUESTED_SCHEMA,
- requiredSchema.json)
- hadoopConf.set(
- ParquetWriteSupport.SPARK_ROW_SCHEMA,
- requiredSchema.json)
- hadoopConf.set(
- SQLConf.SESSION_LOCAL_TIMEZONE.key,
- sparkSession.sessionState.conf.sessionLocalTimeZone)
- hadoopConf.setBoolean(
- SQLConf.NESTED_SCHEMA_PRUNING_ENABLED.key,
- sparkSession.sessionState.conf.nestedSchemaPruningEnabled)
- hadoopConf.setBoolean(
- SQLConf.CASE_SENSITIVE.key,
- sparkSession.sessionState.conf.caseSensitiveAnalysis)
+ override protected def toAttributes(structType: StructType): Seq[Attribute] =
+ structType.toAttributes
- ParquetWriteSupport.setSchema(requiredSchema, hadoopConf)
+ override protected def getFilePath(file: PartitionedFile): Path =
+ new Path(new URI(file.filePath))
- // Sets flags for `ParquetToSparkSchemaConverter`
- hadoopConf.setBoolean(
- SQLConf.PARQUET_BINARY_AS_STRING.key,
- sparkSession.sessionState.conf.isParquetBinaryAsString)
- hadoopConf.setBoolean(
- SQLConf.PARQUET_INT96_AS_TIMESTAMP.key,
- sparkSession.sessionState.conf.isParquetINT96AsTimestamp)
+ override protected def isVectorizedReaderEnabled(sparkSession: SparkSession,
+ resultSchema: StructType):
Boolean =
+ ParquetUtils.isBatchReadSupportedForSchema(sparkSession.sessionState.conf,
resultSchema)
+
+ override protected def getPushDownStringPredicate(sqlConf: SQLConf): Boolean
=
+ sqlConf.parquetFilterPushDownStringStartWith
+
+ override protected def getReturningBatch(sparkSession: SparkSession,
+ resultSchema: StructType): Boolean =
+ supportBatch(sparkSession, resultSchema)
+
+ override protected def setParquetTimeConfs(hadoopConf: Configuration,
sparkSession: SparkSession): Unit = {
// Using string value of this conf to preserve compatibility across spark
versions.
hadoopConf.setBoolean(
"spark.sql.legacy.parquet.nanosAsLong",
sparkSession.sessionState.conf.getConfString("spark.sql.legacy.parquet.nanosAsLong",
"false").toBoolean
)
- val internalSchemaStr =
hadoopConf.get(SparkInternalSchemaConverter.HOODIE_QUERY_SCHEMA)
- // For Spark DataSource v1, there's no Physical Plan projection/schema
pruning w/in Spark itself,
- // therefore it's safe to do schema projection here
- if (!isNullOrEmpty(internalSchemaStr)) {
- val prunedInternalSchemaStr =
- pruneInternalSchema(internalSchemaStr, requiredSchema)
- hadoopConf.set(SparkInternalSchemaConverter.HOODIE_QUERY_SCHEMA,
prunedInternalSchemaStr)
- }
-
- val broadcastedHadoopConf =
- sparkSession.sparkContext.broadcast(new
SerializableConfiguration(hadoopConf))
-
- // TODO: if you move this into the closure it reverts to the default
values.
- // If true, enable using the custom RecordReader for parquet. This only
works for
- // a subset of the types (no complex types).
- val resultSchema = StructType(partitionSchema.fields ++
requiredSchema.fields)
- val sqlConf = sparkSession.sessionState.conf
- val enableOffHeapColumnVector = sqlConf.offHeapColumnVectorEnabled
- val enableVectorizedReader: Boolean =
- ParquetUtils.isBatchReadSupportedForSchema(sqlConf, resultSchema)
- val enableRecordFilter: Boolean = sqlConf.parquetRecordFilterEnabled
- val timestampConversion: Boolean =
sqlConf.isParquetINT96TimestampConversion
- val capacity = sqlConf.parquetVectorizedReaderBatchSize
- val enableParquetFilterPushDown: Boolean = sqlConf.parquetFilterPushDown
- // Whole stage codegen (PhysicalRDD) is able to deal with batches directly
- val returningBatch = supportBatch(sparkSession, resultSchema)
- val pushDownDate = sqlConf.parquetFilterPushDownDate
- val pushDownTimestamp = sqlConf.parquetFilterPushDownTimestamp
- val pushDownDecimal = sqlConf.parquetFilterPushDownDecimal
- val pushDownStringStartWith = sqlConf.parquetFilterPushDownStringStartWith
- val pushDownInFilterThreshold =
sqlConf.parquetFilterPushDownInFilterThreshold
- val isCaseSensitive = sqlConf.caseSensitiveAnalysis
- val parquetOptions = new ParquetOptions(options,
sparkSession.sessionState.conf)
- val datetimeRebaseModeInRead = parquetOptions.datetimeRebaseModeInRead
- val int96RebaseModeInRead = parquetOptions.int96RebaseModeInRead
- val timeZoneId = Option(sqlConf.sessionLocalTimeZone)
-
- (file: PartitionedFile) => {
- assert(!shouldAppendPartitionValues || file.partitionValues.numFields ==
partitionSchema.size)
-
- val filePath = new Path(new URI(file.filePath))
- val split = new FileSplit(filePath, file.start, file.length,
Array.empty[String])
-
- val sharedConf = broadcastedHadoopConf.value.value
-
- // Fetch internal schema
- val internalSchemaStr =
sharedConf.get(SparkInternalSchemaConverter.HOODIE_QUERY_SCHEMA)
- // Internal schema has to be pruned at this point
- val querySchemaOption = SerDeHelper.fromJson(internalSchemaStr)
-
- var shouldUseInternalSchema = !isNullOrEmpty(internalSchemaStr) &&
querySchemaOption.isPresent
-
- val tablePath =
sharedConf.get(SparkInternalSchemaConverter.HOODIE_TABLE_PATH)
- val fileSchema = if (shouldUseInternalSchema) {
- val commitInstantTime = FSUtils.getCommitTime(filePath.getName).toLong;
- val validCommits =
sharedConf.get(SparkInternalSchemaConverter.HOODIE_VALID_COMMITS_LIST)
- //TODO: HARDCODED TIMELINE OBJECT
- val layout =
TimelineLayout.fromVersion(TimelineLayoutVersion.CURR_LAYOUT_VERSION)
- val storage = HoodieStorageUtils.getStorage(tablePath,
HadoopFSUtils.getStorageConf(sharedConf))
- InternalSchemaCache.getInternalSchemaByVersionId(
- commitInstantTime, tablePath, storage, if (validCommits == null) ""
else validCommits,
- layout)
- } else {
- null
- }
-
- lazy val footerFileMetaData =
- ParquetFooterReader.readFooter(sharedConf, filePath,
SKIP_ROW_GROUPS).getFileMetaData
- // Try to push down filters when filter push-down is enabled.
- val pushed = if (enableParquetFilterPushDown) {
- val parquetSchema = footerFileMetaData.getSchema
- val datetimeRebaseSpec =
-
DataSourceUtils.datetimeRebaseSpec(footerFileMetaData.getKeyValueMetaData.get,
datetimeRebaseModeInRead)
- val parquetFilters = new ParquetFilters(
- parquetSchema,
- pushDownDate,
- pushDownTimestamp,
- pushDownDecimal,
- pushDownStringStartWith,
- pushDownInFilterThreshold,
- isCaseSensitive,
- datetimeRebaseSpec)
- filters.map(rebuildFilterFromParquet(_, fileSchema,
querySchemaOption.orElse(null)))
- // Collects all converted Parquet filter predicates. Notice that not
all predicates can be
- // converted (`ParquetFilters.createFilter` returns an `Option`).
That's why a `flatMap`
- // is used here.
- .flatMap(parquetFilters.createFilter)
- .reduceOption(FilterApi.and)
- } else {
- None
- }
-
- // PARQUET_INT96_TIMESTAMP_CONVERSION says to apply timezone conversions
to int96 timestamps'
- // *only* if the file was created by something other than "parquet-mr",
so check the actual
- // writer here for this file. We have to do this per-file, as each file
in the table may
- // have different writers.
- // Define isCreatedByParquetMr as function to avoid unnecessary parquet
footer reads.
- def isCreatedByParquetMr: Boolean =
- footerFileMetaData.getCreatedBy().startsWith("parquet-mr")
-
- val convertTz =
- if (timestampConversion && !isCreatedByParquetMr) {
-
Some(DateTimeUtils.getZoneId(sharedConf.get(SQLConf.SESSION_LOCAL_TIMEZONE.key)))
- } else {
- None
- }
-
- val attemptId = new TaskAttemptID(new TaskID(new JobID(), TaskType.MAP,
0), 0)
-
- // Clone new conf
- val hadoopAttemptConf = new
Configuration(broadcastedHadoopConf.value.value)
- val typeChangeInfos: java.util.Map[Integer, Pair[DataType, DataType]] =
if (shouldUseInternalSchema) {
- val mergedInternalSchema = new InternalSchemaMerger(fileSchema,
querySchemaOption.get(), true, true).mergeSchema()
- val mergedSchema =
SparkInternalSchemaConverter.constructSparkSchemaFromInternalSchema(mergedInternalSchema)
-
- hadoopAttemptConf.set(ParquetReadSupport.SPARK_ROW_REQUESTED_SCHEMA,
mergedSchema.json)
-
-
SparkInternalSchemaConverter.collectTypeChangedCols(querySchemaOption.get(),
mergedInternalSchema)
- } else {
- val (implicitTypeChangeInfo, sparkRequestSchema) =
HoodieParquetFileFormatHelper.buildImplicitSchemaChangeInfo(hadoopAttemptConf,
footerFileMetaData, requiredSchema)
- if (!implicitTypeChangeInfo.isEmpty) {
- shouldUseInternalSchema = true
- hadoopAttemptConf.set(ParquetReadSupport.SPARK_ROW_REQUESTED_SCHEMA,
sparkRequestSchema.json)
- }
- implicitTypeChangeInfo
- }
-
- if (enableVectorizedReader && shouldUseInternalSchema &&
- !typeChangeInfos.values().forall(_.getLeft.isInstanceOf[AtomicType])) {
- throw new IllegalArgumentException(
- "Nested types with type changes(implicit or explicit) cannot be read
in vectorized mode. " +
- "To workaround this issue, set
spark.sql.parquet.enableVectorizedReader=false.")
- }
-
- val hadoopAttemptContext =
- new TaskAttemptContextImpl(hadoopAttemptConf, 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)
- }
- val taskContext = Option(TaskContext.get())
- if (enableVectorizedReader) {
- val vectorizedReader =
- if (shouldUseInternalSchema) {
- val int96RebaseSpec =
-
DataSourceUtils.int96RebaseSpec(footerFileMetaData.getKeyValueMetaData.get,
int96RebaseModeInRead)
- val datetimeRebaseSpec =
-
DataSourceUtils.datetimeRebaseSpec(footerFileMetaData.getKeyValueMetaData.get,
datetimeRebaseModeInRead)
- new HoodieVectorizedParquetRecordReader(
- convertTz.orNull,
- datetimeRebaseSpec.mode.toString,
- datetimeRebaseSpec.timeZone,
- int96RebaseSpec.mode.toString,
- int96RebaseSpec.timeZone,
- enableOffHeapColumnVector && taskContext.isDefined,
- capacity,
- typeChangeInfos)
- } else {
- val int96RebaseSpec =
-
DataSourceUtils.int96RebaseSpec(footerFileMetaData.getKeyValueMetaData.get,
int96RebaseModeInRead)
- val datetimeRebaseSpec =
-
DataSourceUtils.datetimeRebaseSpec(footerFileMetaData.getKeyValueMetaData.get,
datetimeRebaseModeInRead)
- new VectorizedParquetRecordReader(
- convertTz.orNull,
- datetimeRebaseSpec.mode.toString,
- datetimeRebaseSpec.timeZone,
- int96RebaseSpec.mode.toString,
- int96RebaseSpec.timeZone,
- enableOffHeapColumnVector && taskContext.isDefined,
- capacity)
- }
-
- // SPARK-37089: We cannot register a task completion listener to close
this iterator here
- // because downstream exec nodes have already registered their
listeners. Since listeners
- // are executed in reverse order of registration, a listener
registered here would close the
- // iterator while downstream exec nodes are still running. When
off-heap column vectors are
- // enabled, this can cause a use-after-free bug leading to a segfault.
- //
- // Instead, we use FileScanRDD's task completion listener to close
this iterator.
- val iter = new RecordReaderIterator(vectorizedReader)
- try {
- vectorizedReader.initialize(split, hadoopAttemptContext)
-
- // NOTE: We're making appending of the partitioned values to the
rows read from the
- // data file configurable
- if (shouldAppendPartitionValues) {
- logDebug(s"Appending $partitionSchema ${file.partitionValues}")
- vectorizedReader.initBatch(partitionSchema, file.partitionValues)
- } else {
- vectorizedReader.initBatch(StructType(Nil), InternalRow.empty)
- }
-
- if (returningBatch) {
- vectorizedReader.enableReturningBatches()
- }
-
- // UnsafeRowParquetRecordReader appends the columns internally to
avoid another copy.
- iter.asInstanceOf[Iterator[InternalRow]]
- } catch {
- case e: Throwable =>
- // SPARK-23457: In case there is an exception in initialization,
close the iterator to
- // avoid leaking resources.
- iter.close()
- throw e
- }
- } else {
- logDebug(s"Falling back to parquet-mr")
- val int96RebaseSpec =
-
DataSourceUtils.int96RebaseSpec(footerFileMetaData.getKeyValueMetaData.get,
int96RebaseModeInRead)
- val datetimeRebaseSpec =
-
DataSourceUtils.datetimeRebaseSpec(footerFileMetaData.getKeyValueMetaData.get,
datetimeRebaseModeInRead)
- val readSupport = new HoodieParquetReadSupport(
- convertTz,
- enableVectorizedReader = false,
- enableTimestampFieldRepair = true,
- datetimeRebaseSpec,
- int96RebaseSpec)
-
- val reader = if (pushed.isDefined && enableRecordFilter) {
- val parquetFilter = FilterCompat.get(pushed.get, null)
- new ParquetRecordReader[InternalRow](readSupport, parquetFilter)
- } else {
- new ParquetRecordReader[InternalRow](readSupport)
- }
- val iter = new RecordReaderIterator[InternalRow](reader)
- try {
- reader.initialize(split, hadoopAttemptContext)
-
- val fullSchema = requiredSchema.toAttributes ++
partitionSchema.toAttributes
- val unsafeProjection = if (typeChangeInfos.isEmpty) {
- GenerateUnsafeProjection.generate(fullSchema, fullSchema)
- } else {
- // find type changed.
- val newFullSchema = new
StructType(requiredSchema.fields.zipWithIndex.map { case (f, i) =>
- if (typeChangeInfos.containsKey(i)) {
- StructField(f.name, typeChangeInfos.get(i).getRight,
f.nullable, f.metadata)
- } else f
- }).toAttributes ++ partitionSchema.toAttributes
- val castSchema = newFullSchema.zipWithIndex.map { case (attr, i) =>
- if (typeChangeInfos.containsKey(i)) {
- val srcType = typeChangeInfos.get(i).getRight
- val dstType = typeChangeInfos.get(i).getLeft
- val needTimeZone = Cast.needsTimeZone(srcType, dstType)
- Cast(attr, dstType, if (needTimeZone) timeZoneId else None)
- } else attr
- }
- GenerateUnsafeProjection.generate(castSchema, newFullSchema)
- }
-
- // NOTE: We're making appending of the partitioned values to the
rows read from the
- // data file configurable
- if (!shouldAppendPartitionValues || partitionSchema.length == 0) {
- // There is no partition columns
- iter.map(unsafeProjection)
- } else {
- val joinedRow = new JoinedRow()
- iter.map(d => unsafeProjection(joinedRow(d, file.partitionValues)))
- }
- } catch {
- case e: Throwable =>
- // SPARK-23457: In case there is an exception in initialization,
close the iterator to
- // avoid leaking resources.
- iter.close()
- throw e
- }
- }
- }
- }
-}
-
-object Spark33LegacyHoodieParquetFileFormat {
-
- def pruneInternalSchema(internalSchemaStr: String, requiredSchema:
StructType): String = {
- val querySchemaOption = SerDeHelper.fromJson(internalSchemaStr)
- if (querySchemaOption.isPresent && requiredSchema.nonEmpty) {
- val prunedSchema =
SparkInternalSchemaConverter.convertAndPruneStructTypeToInternalSchema(requiredSchema,
querySchemaOption.get())
- SerDeHelper.toJson(prunedSchema)
- } else {
- internalSchemaStr
- }
- }
-
- private def rebuildFilterFromParquet(oldFilter: Filter, fileSchema:
InternalSchema, querySchema: InternalSchema): Filter = {
- if (fileSchema == null || querySchema == null) {
- oldFilter
- } else {
- oldFilter match {
- case eq: EqualTo =>
- val newAttribute =
InternalSchemaUtils.reBuildFilterName(eq.attribute, fileSchema, querySchema)
- if (newAttribute.isEmpty) AlwaysTrue else eq.copy(attribute =
newAttribute)
- case eqs: EqualNullSafe =>
- val newAttribute =
InternalSchemaUtils.reBuildFilterName(eqs.attribute, fileSchema, querySchema)
- if (newAttribute.isEmpty) AlwaysTrue else eqs.copy(attribute =
newAttribute)
- case gt: GreaterThan =>
- val newAttribute =
InternalSchemaUtils.reBuildFilterName(gt.attribute, fileSchema, querySchema)
- if (newAttribute.isEmpty) AlwaysTrue else gt.copy(attribute =
newAttribute)
- case gtr: GreaterThanOrEqual =>
- val newAttribute =
InternalSchemaUtils.reBuildFilterName(gtr.attribute, fileSchema, querySchema)
- if (newAttribute.isEmpty) AlwaysTrue else gtr.copy(attribute =
newAttribute)
- case lt: LessThan =>
- val newAttribute =
InternalSchemaUtils.reBuildFilterName(lt.attribute, fileSchema, querySchema)
- if (newAttribute.isEmpty) AlwaysTrue else lt.copy(attribute =
newAttribute)
- case lte: LessThanOrEqual =>
- val newAttribute =
InternalSchemaUtils.reBuildFilterName(lte.attribute, fileSchema, querySchema)
- if (newAttribute.isEmpty) AlwaysTrue else lte.copy(attribute =
newAttribute)
- case i: In =>
- val newAttribute =
InternalSchemaUtils.reBuildFilterName(i.attribute, fileSchema, querySchema)
- if (newAttribute.isEmpty) AlwaysTrue else i.copy(attribute =
newAttribute)
- case isn: IsNull =>
- val newAttribute =
InternalSchemaUtils.reBuildFilterName(isn.attribute, fileSchema, querySchema)
- if (newAttribute.isEmpty) AlwaysTrue else isn.copy(attribute =
newAttribute)
- case isnn: IsNotNull =>
- val newAttribute =
InternalSchemaUtils.reBuildFilterName(isnn.attribute, fileSchema, querySchema)
- if (newAttribute.isEmpty) AlwaysTrue else isnn.copy(attribute =
newAttribute)
- case And(left, right) =>
- And(rebuildFilterFromParquet(left, fileSchema, querySchema),
rebuildFilterFromParquet(right, fileSchema, querySchema))
- case Or(left, right) =>
- Or(rebuildFilterFromParquet(left, fileSchema, querySchema),
rebuildFilterFromParquet(right, fileSchema, querySchema))
- case Not(child) =>
- Not(rebuildFilterFromParquet(child, fileSchema, querySchema))
- case ssw: StringStartsWith =>
- val newAttribute =
InternalSchemaUtils.reBuildFilterName(ssw.attribute, fileSchema, querySchema)
- if (newAttribute.isEmpty) AlwaysTrue else ssw.copy(attribute =
newAttribute)
- case ses: StringEndsWith =>
- val newAttribute =
InternalSchemaUtils.reBuildFilterName(ses.attribute, fileSchema, querySchema)
- if (newAttribute.isEmpty) AlwaysTrue else ses.copy(attribute =
newAttribute)
- case sc: StringContains =>
- val newAttribute =
InternalSchemaUtils.reBuildFilterName(sc.attribute, fileSchema, querySchema)
- if (newAttribute.isEmpty) AlwaysTrue else sc.copy(attribute =
newAttribute)
- case AlwaysTrue =>
- AlwaysTrue
- case AlwaysFalse =>
- AlwaysFalse
- case _ =>
- AlwaysTrue
- }
- }
}
}
diff --git
a/hudi-spark-datasource/hudi-spark3.4.x/src/main/scala/org/apache/spark/sql/execution/datasources/parquet/Spark34LegacyHoodieParquetFileFormat.scala
b/hudi-spark-datasource/hudi-spark3.4.x/src/main/scala/org/apache/spark/sql/execution/datasources/parquet/Spark34LegacyHoodieParquetFileFormat.scala
index e8f2f61f7ace..abcb6d5c8b90 100644
---
a/hudi-spark-datasource/hudi-spark3.4.x/src/main/scala/org/apache/spark/sql/execution/datasources/parquet/Spark34LegacyHoodieParquetFileFormat.scala
+++
b/hudi-spark-datasource/hudi-spark3.4.x/src/main/scala/org/apache/spark/sql/execution/datasources/parquet/Spark34LegacyHoodieParquetFileFormat.scala
@@ -17,54 +17,21 @@
package org.apache.spark.sql.execution.datasources.parquet
-import org.apache.hudi.client.utils.SparkInternalSchemaConverter
-import org.apache.hudi.common.fs.FSUtils
-import org.apache.hudi.common.schema.internal.InternalSchema
-import org.apache.hudi.common.schema.internal.action.InternalSchemaMerger
-import org.apache.hudi.common.schema.internal.utils.{InternalSchemaUtils,
SerDeHelper}
-import org.apache.hudi.common.table.timeline.TimelineLayout
-import org.apache.hudi.common.table.timeline.versioning.TimelineLayoutVersion
-import org.apache.hudi.common.util.HoodieStorageUtils
-import org.apache.hudi.common.util.InternalSchemaCache
-import org.apache.hudi.common.util.StringUtils.isNullOrEmpty
-import org.apache.hudi.common.util.collection.Pair
-import org.apache.hudi.hadoop.fs.HadoopFSUtils
-
import org.apache.hadoop.conf.Configuration
-import org.apache.hadoop.mapred.FileSplit
-import org.apache.hadoop.mapreduce.{JobID, TaskAttemptID, TaskID, TaskType}
-import org.apache.hadoop.mapreduce.task.TaskAttemptContextImpl
-import org.apache.parquet.filter2.compat.FilterCompat
-import org.apache.parquet.filter2.predicate.FilterApi
-import
org.apache.parquet.format.converter.ParquetMetadataConverter.SKIP_ROW_GROUPS
-import org.apache.parquet.hadoop.{ParquetInputFormat, ParquetRecordReader}
-import org.apache.spark.TaskContext
+import org.apache.hadoop.fs.Path
import org.apache.spark.sql.SparkSession
-import org.apache.spark.sql.catalyst.InternalRow
-import org.apache.spark.sql.catalyst.expressions.{Cast, JoinedRow}
-import
org.apache.spark.sql.catalyst.expressions.codegen.GenerateUnsafeProjection
-import org.apache.spark.sql.catalyst.util.DateTimeUtils
+import org.apache.spark.sql.catalyst.expressions.Attribute
import org.apache.spark.sql.execution.WholeStageCodegenExec
-import org.apache.spark.sql.execution.datasources.{DataSourceUtils,
PartitionedFile, RecordReaderIterator}
-import
org.apache.spark.sql.execution.datasources.parquet.Spark34LegacyHoodieParquetFileFormat._
+import org.apache.spark.sql.execution.datasources.PartitionedFile
import org.apache.spark.sql.internal.SQLConf
-import org.apache.spark.sql.sources._
-import org.apache.spark.sql.types.{AtomicType, DataType, StructField,
StructType}
-import org.apache.spark.util.SerializableConfiguration
-
-import scala.collection.convert.ImplicitConversions.`collection
AsScalaIterable`
+import org.apache.spark.sql.types.StructType
/**
- * This class is an extension of [[ParquetFileFormat]] overriding
Spark-specific behavior
- * that's not possible to customize in any other way
- *
- * NOTE: This is a version of [[AvroDeserializer]] impl from Spark 3.2.1 w/ w/
the following changes applied to it:
- * <ol>
- * <li>Avoiding appending partition values to the rows read from the data
file</li>
- * <li>Schema on-read</li>
- * </ol>
+ * Spark 3.4 concrete implementation of
[[Spark3LegacyHoodieParquetFileFormat]]. It only overrides
+ * the version-specific hooks; the shared reader logic lives in the base class.
*/
-class Spark34LegacyHoodieParquetFileFormat(private val
shouldAppendPartitionValues: Boolean) extends ParquetFileFormat {
+class Spark34LegacyHoodieParquetFileFormat(appendPartitionValues: Boolean)
+ extends Spark3LegacyHoodieParquetFileFormat(appendPartitionValues) {
def supportsColumnar(sparkSession: SparkSession, schema: StructType):
Boolean = {
val conf = sparkSession.sessionState.conf
@@ -75,39 +42,25 @@ class Spark34LegacyHoodieParquetFileFormat(private val
shouldAppendPartitionValu
supportBatch(sparkSession, schema)
}
- override def buildReaderWithPartitionValues(sparkSession: SparkSession,
- dataSchema: StructType,
- partitionSchema: StructType,
- requiredSchema: StructType,
- filters: Seq[Filter],
- options: Map[String, String],
- hadoopConf: Configuration):
PartitionedFile => Iterator[InternalRow] = {
- hadoopConf.set(ParquetInputFormat.READ_SUPPORT_CLASS,
classOf[ParquetReadSupport].getName)
- hadoopConf.set(
- ParquetReadSupport.SPARK_ROW_REQUESTED_SCHEMA,
- requiredSchema.json)
- hadoopConf.set(
- ParquetWriteSupport.SPARK_ROW_SCHEMA,
- requiredSchema.json)
- hadoopConf.set(
- SQLConf.SESSION_LOCAL_TIMEZONE.key,
- sparkSession.sessionState.conf.sessionLocalTimeZone)
- hadoopConf.setBoolean(
- SQLConf.NESTED_SCHEMA_PRUNING_ENABLED.key,
- sparkSession.sessionState.conf.nestedSchemaPruningEnabled)
- hadoopConf.setBoolean(
- SQLConf.CASE_SENSITIVE.key,
- sparkSession.sessionState.conf.caseSensitiveAnalysis)
+ override protected def toAttributes(structType: StructType): Seq[Attribute] =
+ structType.toAttributes
- ParquetWriteSupport.setSchema(requiredSchema, hadoopConf)
+ override protected def getFilePath(file: PartitionedFile): Path =
+ file.filePath.toPath
- // Sets flags for `ParquetToSparkSchemaConverter`
- hadoopConf.setBoolean(
- SQLConf.PARQUET_BINARY_AS_STRING.key,
- sparkSession.sessionState.conf.isParquetBinaryAsString)
- hadoopConf.setBoolean(
- SQLConf.PARQUET_INT96_AS_TIMESTAMP.key,
- sparkSession.sessionState.conf.isParquetINT96AsTimestamp)
+ override protected def isVectorizedReaderEnabled(sparkSession: SparkSession,
+ resultSchema: StructType):
Boolean =
+ supportBatch(sparkSession, resultSchema)
+
+ override protected def getPushDownStringPredicate(sqlConf: SQLConf): Boolean
=
+ sqlConf.parquetFilterPushDownStringPredicate
+
+ override protected def getReturningBatch(sparkSession: SparkSession,
+ resultSchema: StructType): Boolean =
+ sparkSession.sessionState.conf.parquetVectorizedReaderEnabled &&
+ supportsColumnar(sparkSession, resultSchema).toString.equals("true")
+
+ override protected def setParquetTimeConfs(hadoopConf: Configuration,
sparkSession: SparkSession): Unit = {
// Using string value of this conf to preserve compatibility across spark
versions.
hadoopConf.setBoolean(
SQLConf.LEGACY_PARQUET_NANOS_AS_LONG.key,
@@ -117,346 +70,5 @@ class Spark34LegacyHoodieParquetFileFormat(private val
shouldAppendPartitionValu
)
hadoopConf.setBoolean(SQLConf.PARQUET_INFER_TIMESTAMP_NTZ_ENABLED.key,
sparkSession.sessionState.conf.parquetInferTimestampNTZEnabled)
hadoopConf.setBoolean(SQLConf.LEGACY_PARQUET_NANOS_AS_LONG.key,
sparkSession.sessionState.conf.legacyParquetNanosAsLong)
- val internalSchemaStr =
hadoopConf.get(SparkInternalSchemaConverter.HOODIE_QUERY_SCHEMA)
- // For Spark DataSource v1, there's no Physical Plan projection/schema
pruning w/in Spark itself,
- // therefore it's safe to do schema projection here
- if (!isNullOrEmpty(internalSchemaStr)) {
- val prunedInternalSchemaStr =
- pruneInternalSchema(internalSchemaStr, requiredSchema)
- hadoopConf.set(SparkInternalSchemaConverter.HOODIE_QUERY_SCHEMA,
prunedInternalSchemaStr)
- }
-
- val broadcastedHadoopConf =
- sparkSession.sparkContext.broadcast(new
SerializableConfiguration(hadoopConf))
-
- // TODO: if you move this into the closure it reverts to the default
values.
- // If true, enable using the custom RecordReader for parquet. This only
works for
- // a subset of the types (no complex types).
- val resultSchema = StructType(partitionSchema.fields ++
requiredSchema.fields)
- val sqlConf = sparkSession.sessionState.conf
- val enableOffHeapColumnVector = sqlConf.offHeapColumnVectorEnabled
- val enableVectorizedReader: Boolean = supportBatch(sparkSession,
resultSchema)
- val enableRecordFilter: Boolean = sqlConf.parquetRecordFilterEnabled
- val timestampConversion: Boolean =
sqlConf.isParquetINT96TimestampConversion
- val capacity = sqlConf.parquetVectorizedReaderBatchSize
- val enableParquetFilterPushDown: Boolean = sqlConf.parquetFilterPushDown
- val pushDownDate = sqlConf.parquetFilterPushDownDate
- val pushDownTimestamp = sqlConf.parquetFilterPushDownTimestamp
- val pushDownDecimal = sqlConf.parquetFilterPushDownDecimal
- val pushDownStringStartWith = sqlConf.parquetFilterPushDownStringPredicate
- val pushDownInFilterThreshold =
sqlConf.parquetFilterPushDownInFilterThreshold
- val isCaseSensitive = sqlConf.caseSensitiveAnalysis
- val parquetOptions = new ParquetOptions(options,
sparkSession.sessionState.conf)
- val datetimeRebaseModeInRead = parquetOptions.datetimeRebaseModeInRead
- val int96RebaseModeInRead = parquetOptions.int96RebaseModeInRead
- val timeZoneId = Option(sqlConf.sessionLocalTimeZone)
- // Should always be set by FileSourceScanExec creating this.
- // Check conf before checking option, to allow working around an issue by
changing conf.
- val returningBatch =
sparkSession.sessionState.conf.parquetVectorizedReaderEnabled &&
- supportsColumnar(sparkSession, resultSchema).toString.equals("true")
-
-
- (file: PartitionedFile) => {
- assert(!shouldAppendPartitionValues || file.partitionValues.numFields ==
partitionSchema.size)
-
- val filePath = file.filePath.toPath
- val split = new FileSplit(filePath, file.start, file.length,
Array.empty[String])
-
- val sharedConf = broadcastedHadoopConf.value.value
-
- // Fetch internal schema
- val internalSchemaStr =
sharedConf.get(SparkInternalSchemaConverter.HOODIE_QUERY_SCHEMA)
- // Internal schema has to be pruned at this point
- val querySchemaOption = SerDeHelper.fromJson(internalSchemaStr)
-
- var shouldUseInternalSchema = !isNullOrEmpty(internalSchemaStr) &&
querySchemaOption.isPresent
-
- val tablePath =
sharedConf.get(SparkInternalSchemaConverter.HOODIE_TABLE_PATH)
- val fileSchema = if (shouldUseInternalSchema) {
- val commitInstantTime = FSUtils.getCommitTime(filePath.getName).toLong;
- val validCommits =
sharedConf.get(SparkInternalSchemaConverter.HOODIE_VALID_COMMITS_LIST)
- //TODO: HARDCODED TIMELINE OBJECT
- val layout =
TimelineLayout.fromVersion(TimelineLayoutVersion.CURR_LAYOUT_VERSION)
- val storage = HoodieStorageUtils.getStorage(tablePath,
HadoopFSUtils.getStorageConf(sharedConf))
- InternalSchemaCache.getInternalSchemaByVersionId(commitInstantTime,
tablePath, storage,
- if (validCommits == null) "" else validCommits,
- layout)
- } else {
- null
- }
-
- lazy val footerFileMetaData =
- ParquetFooterReader.readFooter(sharedConf, filePath,
SKIP_ROW_GROUPS).getFileMetaData
- // Try to push down filters when filter push-down is enabled.
- val pushed = if (enableParquetFilterPushDown) {
- val parquetSchema = footerFileMetaData.getSchema
- val datetimeRebaseSpec =
-
DataSourceUtils.datetimeRebaseSpec(footerFileMetaData.getKeyValueMetaData.get,
datetimeRebaseModeInRead)
- val parquetFilters = new ParquetFilters(
- parquetSchema,
- pushDownDate,
- pushDownTimestamp,
- pushDownDecimal,
- pushDownStringStartWith,
- pushDownInFilterThreshold,
- isCaseSensitive,
- datetimeRebaseSpec)
- filters.map(rebuildFilterFromParquet(_, fileSchema,
querySchemaOption.orElse(null)))
- // Collects all converted Parquet filter predicates. Notice that not
all predicates can be
- // converted (`ParquetFilters.createFilter` returns an `Option`).
That's why a `flatMap`
- // is used here.
- .flatMap(parquetFilters.createFilter)
- .reduceOption(FilterApi.and)
- } else {
- None
- }
-
- // PARQUET_INT96_TIMESTAMP_CONVERSION says to apply timezone conversions
to int96 timestamps'
- // *only* if the file was created by something other than "parquet-mr",
so check the actual
- // writer here for this file. We have to do this per-file, as each file
in the table may
- // have different writers.
- // Define isCreatedByParquetMr as function to avoid unnecessary parquet
footer reads.
- def isCreatedByParquetMr: Boolean =
- footerFileMetaData.getCreatedBy().startsWith("parquet-mr")
-
- val convertTz =
- if (timestampConversion && !isCreatedByParquetMr) {
-
Some(DateTimeUtils.getZoneId(sharedConf.get(SQLConf.SESSION_LOCAL_TIMEZONE.key)))
- } else {
- None
- }
-
- val attemptId = new TaskAttemptID(new TaskID(new JobID(), TaskType.MAP,
0), 0)
-
- // Clone new conf
- val hadoopAttemptConf = new
Configuration(broadcastedHadoopConf.value.value)
- val typeChangeInfos: java.util.Map[Integer, Pair[DataType, DataType]] =
if (shouldUseInternalSchema) {
- val mergedInternalSchema = new InternalSchemaMerger(fileSchema,
querySchemaOption.get(), true, true).mergeSchema()
- val mergedSchema =
SparkInternalSchemaConverter.constructSparkSchemaFromInternalSchema(mergedInternalSchema)
-
- hadoopAttemptConf.set(ParquetReadSupport.SPARK_ROW_REQUESTED_SCHEMA,
mergedSchema.json)
-
-
SparkInternalSchemaConverter.collectTypeChangedCols(querySchemaOption.get(),
mergedInternalSchema)
- } else {
- val (implicitTypeChangeInfo, sparkRequestSchema) =
HoodieParquetFileFormatHelper.buildImplicitSchemaChangeInfo(hadoopAttemptConf,
footerFileMetaData, requiredSchema)
- if (!implicitTypeChangeInfo.isEmpty) {
- shouldUseInternalSchema = true
- hadoopAttemptConf.set(ParquetReadSupport.SPARK_ROW_REQUESTED_SCHEMA,
sparkRequestSchema.json)
- }
- implicitTypeChangeInfo
- }
-
- if (enableVectorizedReader && shouldUseInternalSchema &&
- !typeChangeInfos.values().forall(_.getLeft.isInstanceOf[AtomicType])) {
- throw new IllegalArgumentException(
- "Nested types with type changes(implicit or explicit) cannot be read
in vectorized mode. " +
- "To workaround this issue, set
spark.sql.parquet.enableVectorizedReader=false.")
- }
-
- val hadoopAttemptContext =
- new TaskAttemptContextImpl(hadoopAttemptConf, 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)
- }
- val taskContext = Option(TaskContext.get())
- if (enableVectorizedReader) {
- val vectorizedReader =
- if (shouldUseInternalSchema) {
- val int96RebaseSpec =
-
DataSourceUtils.int96RebaseSpec(footerFileMetaData.getKeyValueMetaData.get,
int96RebaseModeInRead)
- val datetimeRebaseSpec =
-
DataSourceUtils.datetimeRebaseSpec(footerFileMetaData.getKeyValueMetaData.get,
datetimeRebaseModeInRead)
- new HoodieVectorizedParquetRecordReader(
- convertTz.orNull,
- datetimeRebaseSpec.mode.toString,
- datetimeRebaseSpec.timeZone,
- int96RebaseSpec.mode.toString,
- int96RebaseSpec.timeZone,
- enableOffHeapColumnVector && taskContext.isDefined,
- capacity,
- typeChangeInfos)
- } else {
- val int96RebaseSpec =
-
DataSourceUtils.int96RebaseSpec(footerFileMetaData.getKeyValueMetaData.get,
int96RebaseModeInRead)
- val datetimeRebaseSpec =
-
DataSourceUtils.datetimeRebaseSpec(footerFileMetaData.getKeyValueMetaData.get,
datetimeRebaseModeInRead)
- new VectorizedParquetRecordReader(
- convertTz.orNull,
- datetimeRebaseSpec.mode.toString,
- datetimeRebaseSpec.timeZone,
- int96RebaseSpec.mode.toString,
- int96RebaseSpec.timeZone,
- enableOffHeapColumnVector && taskContext.isDefined,
- capacity)
- }
-
- // SPARK-37089: We cannot register a task completion listener to close
this iterator here
- // because downstream exec nodes have already registered their
listeners. Since listeners
- // are executed in reverse order of registration, a listener
registered here would close the
- // iterator while downstream exec nodes are still running. When
off-heap column vectors are
- // enabled, this can cause a use-after-free bug leading to a segfault.
- //
- // Instead, we use FileScanRDD's task completion listener to close
this iterator.
- val iter = new RecordReaderIterator(vectorizedReader)
- try {
- vectorizedReader.initialize(split, hadoopAttemptContext)
-
- // NOTE: We're making appending of the partitioned values to the
rows read from the
- // data file configurable
- if (shouldAppendPartitionValues) {
- logDebug(s"Appending $partitionSchema ${file.partitionValues}")
- vectorizedReader.initBatch(partitionSchema, file.partitionValues)
- } else {
- vectorizedReader.initBatch(StructType(Nil), InternalRow.empty)
- }
-
- if (returningBatch) {
- vectorizedReader.enableReturningBatches()
- }
-
- // UnsafeRowParquetRecordReader appends the columns internally to
avoid another copy.
- iter.asInstanceOf[Iterator[InternalRow]]
- } catch {
- case e: Throwable =>
- // SPARK-23457: In case there is an exception in initialization,
close the iterator to
- // avoid leaking resources.
- iter.close()
- throw e
- }
- } else {
- logDebug(s"Falling back to parquet-mr")
- val int96RebaseSpec =
-
DataSourceUtils.int96RebaseSpec(footerFileMetaData.getKeyValueMetaData.get,
int96RebaseModeInRead)
- val datetimeRebaseSpec =
-
DataSourceUtils.datetimeRebaseSpec(footerFileMetaData.getKeyValueMetaData.get,
datetimeRebaseModeInRead)
- val readSupport = new HoodieParquetReadSupport(
- convertTz,
- enableVectorizedReader = false,
- enableTimestampFieldRepair = true,
- datetimeRebaseSpec,
- int96RebaseSpec)
-
- val reader = if (pushed.isDefined && enableRecordFilter) {
- val parquetFilter = FilterCompat.get(pushed.get, null)
- new ParquetRecordReader[InternalRow](readSupport, parquetFilter)
- } else {
- new ParquetRecordReader[InternalRow](readSupport)
- }
- val iter = new RecordReaderIterator[InternalRow](reader)
- try {
- reader.initialize(split, hadoopAttemptContext)
-
- val fullSchema = requiredSchema.toAttributes ++
partitionSchema.toAttributes
- val unsafeProjection = if (typeChangeInfos.isEmpty) {
- GenerateUnsafeProjection.generate(fullSchema, fullSchema)
- } else {
- // find type changed.
- val newFullSchema = new
StructType(requiredSchema.fields.zipWithIndex.map { case (f, i) =>
- if (typeChangeInfos.containsKey(i)) {
- StructField(f.name, typeChangeInfos.get(i).getRight,
f.nullable, f.metadata)
- } else f
- }).toAttributes ++ partitionSchema.toAttributes
- val castSchema = newFullSchema.zipWithIndex.map { case (attr, i) =>
- if (typeChangeInfos.containsKey(i)) {
- val srcType = typeChangeInfos.get(i).getRight
- val dstType = typeChangeInfos.get(i).getLeft
- val needTimeZone = Cast.needsTimeZone(srcType, dstType)
- Cast(attr, dstType, if (needTimeZone) timeZoneId else None)
- } else attr
- }
- GenerateUnsafeProjection.generate(castSchema, newFullSchema)
- }
-
- // NOTE: We're making appending of the partitioned values to the
rows read from the
- // data file configurable
- if (!shouldAppendPartitionValues || partitionSchema.length == 0) {
- // There is no partition columns
- iter.map(unsafeProjection)
- } else {
- val joinedRow = new JoinedRow()
- iter.map(d => unsafeProjection(joinedRow(d, file.partitionValues)))
- }
- } catch {
- case e: Throwable =>
- // SPARK-23457: In case there is an exception in initialization,
close the iterator to
- // avoid leaking resources.
- iter.close()
- throw e
- }
- }
- }
- }
-}
-
-object Spark34LegacyHoodieParquetFileFormat {
-
- def pruneInternalSchema(internalSchemaStr: String, requiredSchema:
StructType): String = {
- val querySchemaOption = SerDeHelper.fromJson(internalSchemaStr)
- if (querySchemaOption.isPresent && requiredSchema.nonEmpty) {
- val prunedSchema =
SparkInternalSchemaConverter.convertAndPruneStructTypeToInternalSchema(requiredSchema,
querySchemaOption.get())
- SerDeHelper.toJson(prunedSchema)
- } else {
- internalSchemaStr
- }
- }
-
- private def rebuildFilterFromParquet(oldFilter: Filter, fileSchema:
InternalSchema, querySchema: InternalSchema): Filter = {
- if (fileSchema == null || querySchema == null) {
- oldFilter
- } else {
- oldFilter match {
- case eq: EqualTo =>
- val newAttribute =
InternalSchemaUtils.reBuildFilterName(eq.attribute, fileSchema, querySchema)
- if (newAttribute.isEmpty) AlwaysTrue else eq.copy(attribute =
newAttribute)
- case eqs: EqualNullSafe =>
- val newAttribute =
InternalSchemaUtils.reBuildFilterName(eqs.attribute, fileSchema, querySchema)
- if (newAttribute.isEmpty) AlwaysTrue else eqs.copy(attribute =
newAttribute)
- case gt: GreaterThan =>
- val newAttribute =
InternalSchemaUtils.reBuildFilterName(gt.attribute, fileSchema, querySchema)
- if (newAttribute.isEmpty) AlwaysTrue else gt.copy(attribute =
newAttribute)
- case gtr: GreaterThanOrEqual =>
- val newAttribute =
InternalSchemaUtils.reBuildFilterName(gtr.attribute, fileSchema, querySchema)
- if (newAttribute.isEmpty) AlwaysTrue else gtr.copy(attribute =
newAttribute)
- case lt: LessThan =>
- val newAttribute =
InternalSchemaUtils.reBuildFilterName(lt.attribute, fileSchema, querySchema)
- if (newAttribute.isEmpty) AlwaysTrue else lt.copy(attribute =
newAttribute)
- case lte: LessThanOrEqual =>
- val newAttribute =
InternalSchemaUtils.reBuildFilterName(lte.attribute, fileSchema, querySchema)
- if (newAttribute.isEmpty) AlwaysTrue else lte.copy(attribute =
newAttribute)
- case i: In =>
- val newAttribute =
InternalSchemaUtils.reBuildFilterName(i.attribute, fileSchema, querySchema)
- if (newAttribute.isEmpty) AlwaysTrue else i.copy(attribute =
newAttribute)
- case isn: IsNull =>
- val newAttribute =
InternalSchemaUtils.reBuildFilterName(isn.attribute, fileSchema, querySchema)
- if (newAttribute.isEmpty) AlwaysTrue else isn.copy(attribute =
newAttribute)
- case isnn: IsNotNull =>
- val newAttribute =
InternalSchemaUtils.reBuildFilterName(isnn.attribute, fileSchema, querySchema)
- if (newAttribute.isEmpty) AlwaysTrue else isnn.copy(attribute =
newAttribute)
- case And(left, right) =>
- And(rebuildFilterFromParquet(left, fileSchema, querySchema),
rebuildFilterFromParquet(right, fileSchema, querySchema))
- case Or(left, right) =>
- Or(rebuildFilterFromParquet(left, fileSchema, querySchema),
rebuildFilterFromParquet(right, fileSchema, querySchema))
- case Not(child) =>
- Not(rebuildFilterFromParquet(child, fileSchema, querySchema))
- case ssw: StringStartsWith =>
- val newAttribute =
InternalSchemaUtils.reBuildFilterName(ssw.attribute, fileSchema, querySchema)
- if (newAttribute.isEmpty) AlwaysTrue else ssw.copy(attribute =
newAttribute)
- case ses: StringEndsWith =>
- val newAttribute =
InternalSchemaUtils.reBuildFilterName(ses.attribute, fileSchema, querySchema)
- if (newAttribute.isEmpty) AlwaysTrue else ses.copy(attribute =
newAttribute)
- case sc: StringContains =>
- val newAttribute =
InternalSchemaUtils.reBuildFilterName(sc.attribute, fileSchema, querySchema)
- if (newAttribute.isEmpty) AlwaysTrue else sc.copy(attribute =
newAttribute)
- case AlwaysTrue =>
- AlwaysTrue
- case AlwaysFalse =>
- AlwaysFalse
- case _ =>
- AlwaysTrue
- }
- }
}
}
diff --git
a/hudi-spark-datasource/hudi-spark3.5.x/src/main/scala/org/apache/spark/sql/execution/datasources/parquet/Spark35LegacyHoodieParquetFileFormat.scala
b/hudi-spark-datasource/hudi-spark3.5.x/src/main/scala/org/apache/spark/sql/execution/datasources/parquet/Spark35LegacyHoodieParquetFileFormat.scala
index 22c14a5fe561..b6887c881496 100644
---
a/hudi-spark-datasource/hudi-spark3.5.x/src/main/scala/org/apache/spark/sql/execution/datasources/parquet/Spark35LegacyHoodieParquetFileFormat.scala
+++
b/hudi-spark-datasource/hudi-spark3.5.x/src/main/scala/org/apache/spark/sql/execution/datasources/parquet/Spark35LegacyHoodieParquetFileFormat.scala
@@ -17,55 +17,22 @@
package org.apache.spark.sql.execution.datasources.parquet
-import org.apache.hudi.client.utils.SparkInternalSchemaConverter
-import org.apache.hudi.common.fs.FSUtils
-import org.apache.hudi.common.schema.internal.InternalSchema
-import org.apache.hudi.common.schema.internal.action.InternalSchemaMerger
-import org.apache.hudi.common.schema.internal.utils.{InternalSchemaUtils,
SerDeHelper}
-import org.apache.hudi.common.table.timeline.TimelineLayout
-import org.apache.hudi.common.table.timeline.versioning.TimelineLayoutVersion
-import org.apache.hudi.common.util.HoodieStorageUtils
-import org.apache.hudi.common.util.InternalSchemaCache
-import org.apache.hudi.common.util.StringUtils.isNullOrEmpty
-import org.apache.hudi.common.util.collection.Pair
-import org.apache.hudi.hadoop.fs.HadoopFSUtils
-
import org.apache.hadoop.conf.Configuration
-import org.apache.hadoop.mapred.FileSplit
-import org.apache.hadoop.mapreduce.{JobID, TaskAttemptID, TaskID, TaskType}
-import org.apache.hadoop.mapreduce.task.TaskAttemptContextImpl
-import org.apache.parquet.filter2.compat.FilterCompat
-import org.apache.parquet.filter2.predicate.FilterApi
-import
org.apache.parquet.format.converter.ParquetMetadataConverter.SKIP_ROW_GROUPS
-import org.apache.parquet.hadoop.{ParquetInputFormat, ParquetRecordReader}
-import org.apache.spark.TaskContext
+import org.apache.hadoop.fs.Path
import org.apache.spark.sql.SparkSession
-import org.apache.spark.sql.catalyst.InternalRow
-import org.apache.spark.sql.catalyst.expressions.{Cast, JoinedRow}
-import
org.apache.spark.sql.catalyst.expressions.codegen.GenerateUnsafeProjection
+import org.apache.spark.sql.catalyst.expressions.Attribute
import org.apache.spark.sql.catalyst.types.DataTypeUtils
-import org.apache.spark.sql.catalyst.util.DateTimeUtils
import org.apache.spark.sql.execution.WholeStageCodegenExec
-import org.apache.spark.sql.execution.datasources.{DataSourceUtils,
PartitionedFile, RecordReaderIterator}
-import
org.apache.spark.sql.execution.datasources.parquet.Spark35LegacyHoodieParquetFileFormat._
+import org.apache.spark.sql.execution.datasources.PartitionedFile
import org.apache.spark.sql.internal.SQLConf
-import org.apache.spark.sql.sources._
-import org.apache.spark.sql.types.{AtomicType, DataType, StructField,
StructType}
-import org.apache.spark.util.SerializableConfiguration
-
-import scala.collection.convert.ImplicitConversions.`collection
AsScalaIterable`
+import org.apache.spark.sql.types.StructType
/**
- * This class is an extension of [[ParquetFileFormat]] overriding
Spark-specific behavior
- * that's not possible to customize in any other way
- *
- * NOTE: This is a version of [[AvroDeserializer]] impl from Spark 3.2.1 w/ w/
the following changes applied to it:
- * <ol>
- * <li>Avoiding appending partition values to the rows read from the data
file</li>
- * <li>Schema on-read</li>
- * </ol>
+ * Spark 3.5 concrete implementation of
[[Spark3LegacyHoodieParquetFileFormat]]. It only overrides
+ * the version-specific hooks; the shared reader logic lives in the base class.
*/
-class Spark35LegacyHoodieParquetFileFormat(private val
shouldAppendPartitionValues: Boolean) extends ParquetFileFormat {
+class Spark35LegacyHoodieParquetFileFormat(appendPartitionValues: Boolean)
+ extends Spark3LegacyHoodieParquetFileFormat(appendPartitionValues) {
def supportsColumnar(sparkSession: SparkSession, schema: StructType):
Boolean = {
val conf = sparkSession.sessionState.conf
@@ -76,39 +43,25 @@ class Spark35LegacyHoodieParquetFileFormat(private val
shouldAppendPartitionValu
supportBatch(sparkSession, schema)
}
- override def buildReaderWithPartitionValues(sparkSession: SparkSession,
- dataSchema: StructType,
- partitionSchema: StructType,
- requiredSchema: StructType,
- filters: Seq[Filter],
- options: Map[String, String],
- hadoopConf: Configuration):
PartitionedFile => Iterator[InternalRow] = {
- hadoopConf.set(ParquetInputFormat.READ_SUPPORT_CLASS,
classOf[ParquetReadSupport].getName)
- hadoopConf.set(
- ParquetReadSupport.SPARK_ROW_REQUESTED_SCHEMA,
- requiredSchema.json)
- hadoopConf.set(
- ParquetWriteSupport.SPARK_ROW_SCHEMA,
- requiredSchema.json)
- hadoopConf.set(
- SQLConf.SESSION_LOCAL_TIMEZONE.key,
- sparkSession.sessionState.conf.sessionLocalTimeZone)
- hadoopConf.setBoolean(
- SQLConf.NESTED_SCHEMA_PRUNING_ENABLED.key,
- sparkSession.sessionState.conf.nestedSchemaPruningEnabled)
- hadoopConf.setBoolean(
- SQLConf.CASE_SENSITIVE.key,
- sparkSession.sessionState.conf.caseSensitiveAnalysis)
+ override protected def toAttributes(structType: StructType): Seq[Attribute] =
+ DataTypeUtils.toAttributes(structType)
- ParquetWriteSupport.setSchema(requiredSchema, hadoopConf)
+ override protected def getFilePath(file: PartitionedFile): Path =
+ file.filePath.toPath
- // Sets flags for `ParquetToSparkSchemaConverter`
- hadoopConf.setBoolean(
- SQLConf.PARQUET_BINARY_AS_STRING.key,
- sparkSession.sessionState.conf.isParquetBinaryAsString)
- hadoopConf.setBoolean(
- SQLConf.PARQUET_INT96_AS_TIMESTAMP.key,
- sparkSession.sessionState.conf.isParquetINT96AsTimestamp)
+ override protected def isVectorizedReaderEnabled(sparkSession: SparkSession,
+ resultSchema: StructType):
Boolean =
+ supportBatch(sparkSession, resultSchema)
+
+ override protected def getPushDownStringPredicate(sqlConf: SQLConf): Boolean
=
+ sqlConf.parquetFilterPushDownStringPredicate
+
+ override protected def getReturningBatch(sparkSession: SparkSession,
+ resultSchema: StructType): Boolean =
+ sparkSession.sessionState.conf.parquetVectorizedReaderEnabled &&
+ supportsColumnar(sparkSession, resultSchema).toString.equals("true")
+
+ override protected def setParquetTimeConfs(hadoopConf: Configuration,
sparkSession: SparkSession): Unit = {
// Using string value of this conf to preserve compatibility across spark
versions.
hadoopConf.setBoolean(
SQLConf.LEGACY_PARQUET_NANOS_AS_LONG.key,
@@ -118,347 +71,5 @@ class Spark35LegacyHoodieParquetFileFormat(private val
shouldAppendPartitionValu
)
hadoopConf.setBoolean(SQLConf.PARQUET_INFER_TIMESTAMP_NTZ_ENABLED.key,
sparkSession.sessionState.conf.parquetInferTimestampNTZEnabled)
hadoopConf.setBoolean(SQLConf.LEGACY_PARQUET_NANOS_AS_LONG.key,
sparkSession.sessionState.conf.legacyParquetNanosAsLong)
- val internalSchemaStr =
hadoopConf.get(SparkInternalSchemaConverter.HOODIE_QUERY_SCHEMA)
- // For Spark DataSource v1, there's no Physical Plan projection/schema
pruning w/in Spark itself,
- // therefore it's safe to do schema projection here
- if (!isNullOrEmpty(internalSchemaStr)) {
- val prunedInternalSchemaStr =
- pruneInternalSchema(internalSchemaStr, requiredSchema)
- hadoopConf.set(SparkInternalSchemaConverter.HOODIE_QUERY_SCHEMA,
prunedInternalSchemaStr)
- }
-
- val broadcastedHadoopConf =
- sparkSession.sparkContext.broadcast(new
SerializableConfiguration(hadoopConf))
-
- // TODO: if you move this into the closure it reverts to the default
values.
- // If true, enable using the custom RecordReader for parquet. This only
works for
- // a subset of the types (no complex types).
- val resultSchema = StructType(partitionSchema.fields ++
requiredSchema.fields)
- val sqlConf = sparkSession.sessionState.conf
- val enableOffHeapColumnVector = sqlConf.offHeapColumnVectorEnabled
- val enableVectorizedReader: Boolean = supportBatch(sparkSession,
resultSchema)
- val enableRecordFilter: Boolean = sqlConf.parquetRecordFilterEnabled
- val timestampConversion: Boolean =
sqlConf.isParquetINT96TimestampConversion
- val capacity = sqlConf.parquetVectorizedReaderBatchSize
- val enableParquetFilterPushDown: Boolean = sqlConf.parquetFilterPushDown
- val pushDownDate = sqlConf.parquetFilterPushDownDate
- val pushDownTimestamp = sqlConf.parquetFilterPushDownTimestamp
- val pushDownDecimal = sqlConf.parquetFilterPushDownDecimal
- val pushDownStringStartWith = sqlConf.parquetFilterPushDownStringPredicate
- val pushDownInFilterThreshold =
sqlConf.parquetFilterPushDownInFilterThreshold
- val isCaseSensitive = sqlConf.caseSensitiveAnalysis
- val parquetOptions = new ParquetOptions(options,
sparkSession.sessionState.conf)
- val datetimeRebaseModeInRead = parquetOptions.datetimeRebaseModeInRead
- val int96RebaseModeInRead = parquetOptions.int96RebaseModeInRead
- val timeZoneId = Option(sqlConf.sessionLocalTimeZone)
- // Should always be set by FileSourceScanExec creating this.
- // Check conf before checking option, to allow working around an issue by
changing conf.
- val returningBatch =
sparkSession.sessionState.conf.parquetVectorizedReaderEnabled &&
- supportsColumnar(sparkSession, resultSchema).toString.equals("true")
-
-
- (file: PartitionedFile) => {
- assert(!shouldAppendPartitionValues || file.partitionValues.numFields ==
partitionSchema.size)
-
- val filePath = file.filePath.toPath
- val split = new FileSplit(filePath, file.start, file.length,
Array.empty[String])
-
- val sharedConf = broadcastedHadoopConf.value.value
-
- // Fetch internal schema
- val internalSchemaStr =
sharedConf.get(SparkInternalSchemaConverter.HOODIE_QUERY_SCHEMA)
- // Internal schema has to be pruned at this point
- val querySchemaOption = SerDeHelper.fromJson(internalSchemaStr)
-
- var shouldUseInternalSchema = !isNullOrEmpty(internalSchemaStr) &&
querySchemaOption.isPresent
-
- val tablePath =
sharedConf.get(SparkInternalSchemaConverter.HOODIE_TABLE_PATH)
- val fileSchema = if (shouldUseInternalSchema) {
- val commitInstantTime = FSUtils.getCommitTime(filePath.getName).toLong;
- val validCommits =
sharedConf.get(SparkInternalSchemaConverter.HOODIE_VALID_COMMITS_LIST)
- val storage = HoodieStorageUtils.getStorage(tablePath,
HadoopFSUtils.getStorageConf(sharedConf))
- //TODO: HARDCODED TIMELINE OBJECT
- val layout =
TimelineLayout.fromVersion(TimelineLayoutVersion.CURR_LAYOUT_VERSION)
- InternalSchemaCache.getInternalSchemaByVersionId(
- commitInstantTime, tablePath, storage, if (validCommits == null) ""
else validCommits,
- layout)
- } else {
- null
- }
-
- lazy val footerFileMetaData =
- ParquetFooterReader.readFooter(sharedConf, filePath,
SKIP_ROW_GROUPS).getFileMetaData
- // Try to push down filters when filter push-down is enabled.
- val pushed = if (enableParquetFilterPushDown) {
- val parquetSchema = footerFileMetaData.getSchema
- val datetimeRebaseSpec =
-
DataSourceUtils.datetimeRebaseSpec(footerFileMetaData.getKeyValueMetaData.get,
datetimeRebaseModeInRead)
- val parquetFilters = new ParquetFilters(
- parquetSchema,
- pushDownDate,
- pushDownTimestamp,
- pushDownDecimal,
- pushDownStringStartWith,
- pushDownInFilterThreshold,
- isCaseSensitive,
- datetimeRebaseSpec)
- filters.map(rebuildFilterFromParquet(_, fileSchema,
querySchemaOption.orElse(null)))
- // Collects all converted Parquet filter predicates. Notice that not
all predicates can be
- // converted (`ParquetFilters.createFilter` returns an `Option`).
That's why a `flatMap`
- // is used here.
- .flatMap(parquetFilters.createFilter)
- .reduceOption(FilterApi.and)
- } else {
- None
- }
-
- // PARQUET_INT96_TIMESTAMP_CONVERSION says to apply timezone conversions
to int96 timestamps'
- // *only* if the file was created by something other than "parquet-mr",
so check the actual
- // writer here for this file. We have to do this per-file, as each file
in the table may
- // have different writers.
- // Define isCreatedByParquetMr as function to avoid unnecessary parquet
footer reads.
- def isCreatedByParquetMr: Boolean =
- footerFileMetaData.getCreatedBy().startsWith("parquet-mr")
-
- val convertTz =
- if (timestampConversion && !isCreatedByParquetMr) {
-
Some(DateTimeUtils.getZoneId(sharedConf.get(SQLConf.SESSION_LOCAL_TIMEZONE.key)))
- } else {
- None
- }
-
- val attemptId = new TaskAttemptID(new TaskID(new JobID(), TaskType.MAP,
0), 0)
-
- // Clone new conf
- val hadoopAttemptConf = new
Configuration(broadcastedHadoopConf.value.value)
- val typeChangeInfos: java.util.Map[Integer, Pair[DataType, DataType]] =
if (shouldUseInternalSchema) {
- val mergedInternalSchema = new InternalSchemaMerger(fileSchema,
querySchemaOption.get(), true, true).mergeSchema()
- val mergedSchema =
SparkInternalSchemaConverter.constructSparkSchemaFromInternalSchema(mergedInternalSchema)
-
- hadoopAttemptConf.set(ParquetReadSupport.SPARK_ROW_REQUESTED_SCHEMA,
mergedSchema.json)
-
-
SparkInternalSchemaConverter.collectTypeChangedCols(querySchemaOption.get(),
mergedInternalSchema)
- } else {
- val (implicitTypeChangeInfo, sparkRequestSchema) =
HoodieParquetFileFormatHelper.buildImplicitSchemaChangeInfo(hadoopAttemptConf,
footerFileMetaData, requiredSchema)
- if (!implicitTypeChangeInfo.isEmpty) {
- shouldUseInternalSchema = true
- hadoopAttemptConf.set(ParquetReadSupport.SPARK_ROW_REQUESTED_SCHEMA,
sparkRequestSchema.json)
- }
- implicitTypeChangeInfo
- }
-
- if (enableVectorizedReader && shouldUseInternalSchema &&
- !typeChangeInfos.values().forall(_.getLeft.isInstanceOf[AtomicType])) {
- throw new IllegalArgumentException(
- "Nested types with type changes(implicit or explicit) cannot be read
in vectorized mode. " +
- "To workaround this issue, set
spark.sql.parquet.enableVectorizedReader=false.")
- }
-
- val hadoopAttemptContext =
- new TaskAttemptContextImpl(hadoopAttemptConf, 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)
- }
- val taskContext = Option(TaskContext.get())
- if (enableVectorizedReader) {
- val vectorizedReader =
- if (shouldUseInternalSchema) {
- val int96RebaseSpec =
-
DataSourceUtils.int96RebaseSpec(footerFileMetaData.getKeyValueMetaData.get,
int96RebaseModeInRead)
- val datetimeRebaseSpec =
-
DataSourceUtils.datetimeRebaseSpec(footerFileMetaData.getKeyValueMetaData.get,
datetimeRebaseModeInRead)
- new HoodieVectorizedParquetRecordReader(
- convertTz.orNull,
- datetimeRebaseSpec.mode.toString,
- datetimeRebaseSpec.timeZone,
- int96RebaseSpec.mode.toString,
- int96RebaseSpec.timeZone,
- enableOffHeapColumnVector && taskContext.isDefined,
- capacity,
- typeChangeInfos)
- } else {
- val int96RebaseSpec =
-
DataSourceUtils.int96RebaseSpec(footerFileMetaData.getKeyValueMetaData.get,
int96RebaseModeInRead)
- val datetimeRebaseSpec =
-
DataSourceUtils.datetimeRebaseSpec(footerFileMetaData.getKeyValueMetaData.get,
datetimeRebaseModeInRead)
- new VectorizedParquetRecordReader(
- convertTz.orNull,
- datetimeRebaseSpec.mode.toString,
- datetimeRebaseSpec.timeZone,
- int96RebaseSpec.mode.toString,
- int96RebaseSpec.timeZone,
- enableOffHeapColumnVector && taskContext.isDefined,
- capacity)
- }
-
- // SPARK-37089: We cannot register a task completion listener to close
this iterator here
- // because downstream exec nodes have already registered their
listeners. Since listeners
- // are executed in reverse order of registration, a listener
registered here would close the
- // iterator while downstream exec nodes are still running. When
off-heap column vectors are
- // enabled, this can cause a use-after-free bug leading to a segfault.
- //
- // Instead, we use FileScanRDD's task completion listener to close
this iterator.
- val iter = new RecordReaderIterator(vectorizedReader)
- try {
- vectorizedReader.initialize(split, hadoopAttemptContext)
-
- // NOTE: We're making appending of the partitioned values to the
rows read from the
- // data file configurable
- if (shouldAppendPartitionValues) {
- logDebug(s"Appending $partitionSchema ${file.partitionValues}")
- vectorizedReader.initBatch(partitionSchema, file.partitionValues)
- } else {
- vectorizedReader.initBatch(StructType(Nil), InternalRow.empty)
- }
-
- if (returningBatch) {
- vectorizedReader.enableReturningBatches()
- }
-
- // UnsafeRowParquetRecordReader appends the columns internally to
avoid another copy.
- iter.asInstanceOf[Iterator[InternalRow]]
- } catch {
- case e: Throwable =>
- // SPARK-23457: In case there is an exception in initialization,
close the iterator to
- // avoid leaking resources.
- iter.close()
- throw e
- }
- } else {
- logDebug(s"Falling back to parquet-mr")
- val int96RebaseSpec =
-
DataSourceUtils.int96RebaseSpec(footerFileMetaData.getKeyValueMetaData.get,
int96RebaseModeInRead)
- val datetimeRebaseSpec =
-
DataSourceUtils.datetimeRebaseSpec(footerFileMetaData.getKeyValueMetaData.get,
datetimeRebaseModeInRead)
- val readSupport = new HoodieParquetReadSupport(
- convertTz,
- enableVectorizedReader = false,
- enableTimestampFieldRepair = true,
- datetimeRebaseSpec,
- int96RebaseSpec)
-
- val reader = if (pushed.isDefined && enableRecordFilter) {
- val parquetFilter = FilterCompat.get(pushed.get, null)
- new ParquetRecordReader[InternalRow](readSupport, parquetFilter)
- } else {
- new ParquetRecordReader[InternalRow](readSupport)
- }
- val iter = new RecordReaderIterator[InternalRow](reader)
- try {
- reader.initialize(split, hadoopAttemptContext)
-
- val fullSchema = DataTypeUtils.toAttributes(requiredSchema) ++
DataTypeUtils.toAttributes(partitionSchema)
- val unsafeProjection = if (typeChangeInfos.isEmpty) {
- GenerateUnsafeProjection.generate(fullSchema, fullSchema)
- } else {
- // find type changed.
- val newSchema = new
StructType(requiredSchema.fields.zipWithIndex.map { case (f, i) =>
- if (typeChangeInfos.containsKey(i)) {
- StructField(f.name, typeChangeInfos.get(i).getRight,
f.nullable, f.metadata)
- } else f
- })
- val newFullSchema = DataTypeUtils.toAttributes(newSchema) ++
DataTypeUtils.toAttributes(partitionSchema)
- val castSchema = newFullSchema.zipWithIndex.map { case (attr, i) =>
- if (typeChangeInfos.containsKey(i)) {
- val srcType = typeChangeInfos.get(i).getRight
- val dstType = typeChangeInfos.get(i).getLeft
- val needTimeZone = Cast.needsTimeZone(srcType, dstType)
- Cast(attr, dstType, if (needTimeZone) timeZoneId else None)
- } else attr
- }
- GenerateUnsafeProjection.generate(castSchema, newFullSchema)
- }
-
- // NOTE: We're making appending of the partitioned values to the
rows read from the
- // data file configurable
- if (!shouldAppendPartitionValues || partitionSchema.length == 0) {
- // There is no partition columns
- iter.map(unsafeProjection)
- } else {
- val joinedRow = new JoinedRow()
- iter.map(d => unsafeProjection(joinedRow(d, file.partitionValues)))
- }
- } catch {
- case e: Throwable =>
- // SPARK-23457: In case there is an exception in initialization,
close the iterator to
- // avoid leaking resources.
- iter.close()
- throw e
- }
- }
- }
- }
-}
-
-object Spark35LegacyHoodieParquetFileFormat {
-
- def pruneInternalSchema(internalSchemaStr: String, requiredSchema:
StructType): String = {
- val querySchemaOption = SerDeHelper.fromJson(internalSchemaStr)
- if (querySchemaOption.isPresent && requiredSchema.nonEmpty) {
- val prunedSchema =
SparkInternalSchemaConverter.convertAndPruneStructTypeToInternalSchema(requiredSchema,
querySchemaOption.get())
- SerDeHelper.toJson(prunedSchema)
- } else {
- internalSchemaStr
- }
- }
-
- private def rebuildFilterFromParquet(oldFilter: Filter, fileSchema:
InternalSchema, querySchema: InternalSchema): Filter = {
- if (fileSchema == null || querySchema == null) {
- oldFilter
- } else {
- oldFilter match {
- case eq: EqualTo =>
- val newAttribute =
InternalSchemaUtils.reBuildFilterName(eq.attribute, fileSchema, querySchema)
- if (newAttribute.isEmpty) AlwaysTrue else eq.copy(attribute =
newAttribute)
- case eqs: EqualNullSafe =>
- val newAttribute =
InternalSchemaUtils.reBuildFilterName(eqs.attribute, fileSchema, querySchema)
- if (newAttribute.isEmpty) AlwaysTrue else eqs.copy(attribute =
newAttribute)
- case gt: GreaterThan =>
- val newAttribute =
InternalSchemaUtils.reBuildFilterName(gt.attribute, fileSchema, querySchema)
- if (newAttribute.isEmpty) AlwaysTrue else gt.copy(attribute =
newAttribute)
- case gtr: GreaterThanOrEqual =>
- val newAttribute =
InternalSchemaUtils.reBuildFilterName(gtr.attribute, fileSchema, querySchema)
- if (newAttribute.isEmpty) AlwaysTrue else gtr.copy(attribute =
newAttribute)
- case lt: LessThan =>
- val newAttribute =
InternalSchemaUtils.reBuildFilterName(lt.attribute, fileSchema, querySchema)
- if (newAttribute.isEmpty) AlwaysTrue else lt.copy(attribute =
newAttribute)
- case lte: LessThanOrEqual =>
- val newAttribute =
InternalSchemaUtils.reBuildFilterName(lte.attribute, fileSchema, querySchema)
- if (newAttribute.isEmpty) AlwaysTrue else lte.copy(attribute =
newAttribute)
- case i: In =>
- val newAttribute =
InternalSchemaUtils.reBuildFilterName(i.attribute, fileSchema, querySchema)
- if (newAttribute.isEmpty) AlwaysTrue else i.copy(attribute =
newAttribute)
- case isn: IsNull =>
- val newAttribute =
InternalSchemaUtils.reBuildFilterName(isn.attribute, fileSchema, querySchema)
- if (newAttribute.isEmpty) AlwaysTrue else isn.copy(attribute =
newAttribute)
- case isnn: IsNotNull =>
- val newAttribute =
InternalSchemaUtils.reBuildFilterName(isnn.attribute, fileSchema, querySchema)
- if (newAttribute.isEmpty) AlwaysTrue else isnn.copy(attribute =
newAttribute)
- case And(left, right) =>
- And(rebuildFilterFromParquet(left, fileSchema, querySchema),
rebuildFilterFromParquet(right, fileSchema, querySchema))
- case Or(left, right) =>
- Or(rebuildFilterFromParquet(left, fileSchema, querySchema),
rebuildFilterFromParquet(right, fileSchema, querySchema))
- case Not(child) =>
- Not(rebuildFilterFromParquet(child, fileSchema, querySchema))
- case ssw: StringStartsWith =>
- val newAttribute =
InternalSchemaUtils.reBuildFilterName(ssw.attribute, fileSchema, querySchema)
- if (newAttribute.isEmpty) AlwaysTrue else ssw.copy(attribute =
newAttribute)
- case ses: StringEndsWith =>
- val newAttribute =
InternalSchemaUtils.reBuildFilterName(ses.attribute, fileSchema, querySchema)
- if (newAttribute.isEmpty) AlwaysTrue else ses.copy(attribute =
newAttribute)
- case sc: StringContains =>
- val newAttribute =
InternalSchemaUtils.reBuildFilterName(sc.attribute, fileSchema, querySchema)
- if (newAttribute.isEmpty) AlwaysTrue else sc.copy(attribute =
newAttribute)
- case AlwaysTrue =>
- AlwaysTrue
- case AlwaysFalse =>
- AlwaysFalse
- case _ =>
- AlwaysTrue
- }
- }
}
}