This is an automated email from the ASF dual-hosted git repository.
rui-mo pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/gluten.git
The following commit(s) were added to refs/heads/main by this push:
new d52144e159 [GLUTEN-6887][VL] Pass Velox column mapping mode through
scan splits (#12884)
d52144e159 is described below
commit d52144e159fec87391f42c2581335d2e56c41c2e
Author: Rui Mo <[email protected]>
AuthorDate: Tue Sep 1 10:09:24 2026 +0100
[GLUTEN-6887][VL] Pass Velox column mapping mode through scan splits
(#12884)
---
.../delta/DeltaDeletionVectorScanInfoSuite.scala | 62 ++++++++++++-
.../delta/DeltaDeletionVectorScanInfoSuite.scala | 62 ++++++++++++-
.../gluten/execution/VeloxIcebergSuite.scala | 28 +++++-
.../backendsapi/velox/VeloxIteratorApi.scala | 24 +++++
.../org/apache/gluten/config/VeloxConfig.scala | 2 +-
cpp/velox/compute/VeloxPlanConverter.cc | 26 +++++-
cpp/velox/compute/WholeStageResultIterator.cc | 13 ++-
cpp/velox/compute/delta/DeltaSplit.cpp | 6 +-
cpp/velox/compute/delta/DeltaSplit.h | 3 +-
cpp/velox/compute/delta/tests/DeltaSplitTest.cpp | 5 +-
cpp/velox/config/VeloxConfig.h | 2 -
cpp/velox/substrait/SubstraitToVeloxPlan.h | 6 ++
.../tests/Substrait2VeloxPlanConversionTest.cc | 11 ++-
cpp/velox/tests/data/q6_first_stage_split.json | 11 ++-
cpp/velox/utils/ConfigExtractor.cc | 10 --
ep/build-velox/src/get-velox.sh | 4 +-
.../gluten/delta/DeltaDeletionVectorScanInfo.scala | 18 ++++
.../gluten/delta/DeltaDeletionVectorScanInfo.scala | 18 ++++
.../gluten/delta/DeltaDeletionVectorScanInfo.scala | 87 ++++++++++++++++-
.../gluten/delta/DeltaDeletionVectorScanInfo.scala | 87 ++++++++++++++++-
.../apache/gluten/delta/DeltaAddFileLookup.scala | 103 +++++++++++++++++++++
.../gluten/execution/DeltaScanTransformer.scala | 74 ++++++++++++++-
.../gluten/execution/IcebergScanTransformer.scala | 20 +++-
.../gluten/execution/PaimonScanTransformer.scala | 23 ++++-
.../gluten/substrait/rel/LocalFilesNode.java | 38 ++++++++
.../org/apache/gluten/config/GlutenConfig.scala | 30 ++----
.../hive/execution/GlutenHiveSQLQuerySuite.scala | 42 +++++++++
27 files changed, 751 insertions(+), 64 deletions(-)
diff --git
a/backends-velox/src-delta33/test/scala/org/apache/gluten/delta/DeltaDeletionVectorScanInfoSuite.scala
b/backends-velox/src-delta33/test/scala/org/apache/gluten/delta/DeltaDeletionVectorScanInfoSuite.scala
index 448bde4f84..633b19e32c 100644
---
a/backends-velox/src-delta33/test/scala/org/apache/gluten/delta/DeltaDeletionVectorScanInfoSuite.scala
+++
b/backends-velox/src-delta33/test/scala/org/apache/gluten/delta/DeltaDeletionVectorScanInfoSuite.scala
@@ -27,6 +27,7 @@ import org.apache.spark.sql.delta.{DeltaLog,
GlutenDeltaParquetFileFormat}
import org.apache.spark.sql.delta.actions.DeletionVectorDescriptor
import org.apache.spark.sql.delta.catalog.DeltaCatalog
import org.apache.spark.sql.delta.test.DeltaSQLTestUtils
+import org.apache.spark.sql.delta.util.DeltaFileOperations
import org.apache.spark.sql.execution.datasources.PartitionedFile
import org.apache.spark.sql.execution.metric.SQLMetric
import org.apache.spark.sql.internal.{SQLConf, StaticSQLConf}
@@ -239,6 +240,64 @@ class DeltaDeletionVectorScanInfoSuite
DeltaDeletionVectorScanInfo.normalize(Seq(partitionedFile),
tablePath).get._2.head
}
+ test("normalize uses current AddFile when split DV metadata is stale") {
+ withTempDir {
+ tempDir =>
+ val tablePath = new Path(tempDir.getCanonicalPath, "spark%dir%prefix")
+ Seq((1, "a"), (2, "b"), (3, "c"), (4, "d"))
+ .toDF("id", "value")
+ .coalesce(1)
+ .write
+ .format("delta")
+ .save(tablePath.toString)
+
+ spark.sql(
+ s"ALTER TABLE delta.`$tablePath` SET TBLPROPERTIES
('delta.enableDeletionVectors' = true)")
+ spark.sql(s"DELETE FROM delta.`$tablePath` WHERE id = 4")
+ val staleFile = DeltaLog
+ .forTable(spark, tablePath)
+ .update()
+ .allFiles
+ .collect()
+ .find(_.deletionVector != null)
+ .get
+ val partitionedFile = partitionedFileWithMetadata(
+ tablePath.toString,
+ staleFile.path,
+ staleFile.size,
+ Map(
+ GlutenDeltaParquetFileFormat.FILE_ROW_INDEX_FILTER_ID_ENCODED ->
+ staleFile.deletionVector.serializeToBase64(),
+ GlutenDeltaParquetFileFormat.FILE_ROW_INDEX_FILTER_TYPE ->
"IF_CONTAINED",
+ "kept_key" -> "kept_value"
+ )
+ )
+
+ spark.sql(s"DELETE FROM delta.`$tablePath` WHERE id = 3")
+ val currentFile = DeltaLog
+ .forTable(spark, tablePath)
+ .update()
+ .allFiles
+ .collect()
+ .find(_.path == staleFile.path)
+ .get
+ assert(currentFile.deletionVector.cardinality >
staleFile.deletionVector.cardinality)
+
+ val addFileLookup = DeltaDeletionVectorScanInfo
+ .buildAddFileLookup(tablePath, Seq(currentFile))
+ val result = DeltaDeletionVectorScanInfo.normalizeFromAddFiles(
+ Seq(partitionedFile),
+ tablePath,
+ addFileLookup)
+ assert(result.isDefined)
+ val (metadata, options) = result.get
+ assert(metadata.head.size() == 1)
+ assert(metadata.head.get("kept_key") == "kept_value")
+ assert(
+ options.head.deletionVectorCardinality ==
currentFile.deletionVector.cardinality)
+ }
+ }
+
override protected def partitionedFileWithMetadata(
tablePath: String,
relativeFilePath: String,
@@ -246,7 +305,8 @@ class DeltaDeletionVectorScanInfoSuite
metadata: Map[String, Object]): PartitionedFile = {
PartitionedFile(
partitionValues = InternalRow.empty,
- filePath = SparkPath.fromPath(new Path(tablePath, relativeFilePath)),
+ filePath = SparkPath.fromPath(
+ DeltaFileOperations.absolutePath(tablePath, relativeFilePath)),
start = 0L,
length = fileSize,
fileSize = fileSize,
diff --git
a/backends-velox/src-delta40/test/scala/org/apache/gluten/delta/DeltaDeletionVectorScanInfoSuite.scala
b/backends-velox/src-delta40/test/scala/org/apache/gluten/delta/DeltaDeletionVectorScanInfoSuite.scala
index 448bde4f84..633b19e32c 100644
---
a/backends-velox/src-delta40/test/scala/org/apache/gluten/delta/DeltaDeletionVectorScanInfoSuite.scala
+++
b/backends-velox/src-delta40/test/scala/org/apache/gluten/delta/DeltaDeletionVectorScanInfoSuite.scala
@@ -27,6 +27,7 @@ import org.apache.spark.sql.delta.{DeltaLog,
GlutenDeltaParquetFileFormat}
import org.apache.spark.sql.delta.actions.DeletionVectorDescriptor
import org.apache.spark.sql.delta.catalog.DeltaCatalog
import org.apache.spark.sql.delta.test.DeltaSQLTestUtils
+import org.apache.spark.sql.delta.util.DeltaFileOperations
import org.apache.spark.sql.execution.datasources.PartitionedFile
import org.apache.spark.sql.execution.metric.SQLMetric
import org.apache.spark.sql.internal.{SQLConf, StaticSQLConf}
@@ -239,6 +240,64 @@ class DeltaDeletionVectorScanInfoSuite
DeltaDeletionVectorScanInfo.normalize(Seq(partitionedFile),
tablePath).get._2.head
}
+ test("normalize uses current AddFile when split DV metadata is stale") {
+ withTempDir {
+ tempDir =>
+ val tablePath = new Path(tempDir.getCanonicalPath, "spark%dir%prefix")
+ Seq((1, "a"), (2, "b"), (3, "c"), (4, "d"))
+ .toDF("id", "value")
+ .coalesce(1)
+ .write
+ .format("delta")
+ .save(tablePath.toString)
+
+ spark.sql(
+ s"ALTER TABLE delta.`$tablePath` SET TBLPROPERTIES
('delta.enableDeletionVectors' = true)")
+ spark.sql(s"DELETE FROM delta.`$tablePath` WHERE id = 4")
+ val staleFile = DeltaLog
+ .forTable(spark, tablePath)
+ .update()
+ .allFiles
+ .collect()
+ .find(_.deletionVector != null)
+ .get
+ val partitionedFile = partitionedFileWithMetadata(
+ tablePath.toString,
+ staleFile.path,
+ staleFile.size,
+ Map(
+ GlutenDeltaParquetFileFormat.FILE_ROW_INDEX_FILTER_ID_ENCODED ->
+ staleFile.deletionVector.serializeToBase64(),
+ GlutenDeltaParquetFileFormat.FILE_ROW_INDEX_FILTER_TYPE ->
"IF_CONTAINED",
+ "kept_key" -> "kept_value"
+ )
+ )
+
+ spark.sql(s"DELETE FROM delta.`$tablePath` WHERE id = 3")
+ val currentFile = DeltaLog
+ .forTable(spark, tablePath)
+ .update()
+ .allFiles
+ .collect()
+ .find(_.path == staleFile.path)
+ .get
+ assert(currentFile.deletionVector.cardinality >
staleFile.deletionVector.cardinality)
+
+ val addFileLookup = DeltaDeletionVectorScanInfo
+ .buildAddFileLookup(tablePath, Seq(currentFile))
+ val result = DeltaDeletionVectorScanInfo.normalizeFromAddFiles(
+ Seq(partitionedFile),
+ tablePath,
+ addFileLookup)
+ assert(result.isDefined)
+ val (metadata, options) = result.get
+ assert(metadata.head.size() == 1)
+ assert(metadata.head.get("kept_key") == "kept_value")
+ assert(
+ options.head.deletionVectorCardinality ==
currentFile.deletionVector.cardinality)
+ }
+ }
+
override protected def partitionedFileWithMetadata(
tablePath: String,
relativeFilePath: String,
@@ -246,7 +305,8 @@ class DeltaDeletionVectorScanInfoSuite
metadata: Map[String, Object]): PartitionedFile = {
PartitionedFile(
partitionValues = InternalRow.empty,
- filePath = SparkPath.fromPath(new Path(tablePath, relativeFilePath)),
+ filePath = SparkPath.fromPath(
+ DeltaFileOperations.absolutePath(tablePath, relativeFilePath)),
start = 0L,
length = fileSize,
fileSize = fileSize,
diff --git
a/backends-velox/src-iceberg/test/scala/org/apache/gluten/execution/VeloxIcebergSuite.scala
b/backends-velox/src-iceberg/test/scala/org/apache/gluten/execution/VeloxIcebergSuite.scala
index 4e540c15af..c956029392 100644
---
a/backends-velox/src-iceberg/test/scala/org/apache/gluten/execution/VeloxIcebergSuite.scala
+++
b/backends-velox/src-iceberg/test/scala/org/apache/gluten/execution/VeloxIcebergSuite.scala
@@ -16,7 +16,7 @@
*/
package org.apache.gluten.execution
-import org.apache.gluten.config.GlutenConfig
+import org.apache.gluten.config.{GlutenConfig, VeloxConfig}
import org.apache.spark.sql.Row
import org.apache.spark.sql.connector.catalog.{Identifier, TableCatalog}
@@ -27,6 +27,32 @@ import org.apache.iceberg.spark.source.SparkTable
import org.apache.iceberg.types.{Type, Types}
class VeloxIcebergSuite extends IcebergSuite {
+ test("iceberg parquet split uses name mapping for projected columns") {
+ withTable("iceberg_parquet_name_mapping") {
+ withSQLConf(VeloxConfig.PARQUET_USE_COLUMN_NAMES.key -> "false") {
+ spark.sql("""
+ |CREATE TABLE iceberg_parquet_name_mapping (
+ | id BIGINT,
+ | amount DECIMAL(12, 2),
+ | note STRING
+ |)
+ |USING iceberg
+ |TBLPROPERTIES ('write.format.default' = 'parquet')
+ |""".stripMargin)
+ spark.sql("""
+ |INSERT INTO iceberg_parquet_name_mapping
+ |VALUES (CAST(1 AS BIGINT), CAST(10.50 AS DECIMAL(12, 2)),
'a')
+ |""".stripMargin)
+
+ runQueryAndCompare("SELECT amount FROM iceberg_parquet_name_mapping") {
+ df =>
+ checkAnswer(df, Seq(Row(BigDecimal("10.50"))))
+ checkGlutenPlan[IcebergScanTransformer](df)
+ }
+ }
+ }
+ }
+
testWithMinSparkVersion("iceberg v3 initial default for an added column",
"3.4") {
withTable("iceberg_v3_initial_default") {
withSQLConf(GlutenConfig.GLUTEN_ENABLED.key -> "false") {
diff --git
a/backends-velox/src/main/scala/org/apache/gluten/backendsapi/velox/VeloxIteratorApi.scala
b/backends-velox/src/main/scala/org/apache/gluten/backendsapi/velox/VeloxIteratorApi.scala
index 5f623abffc..24ae3aa7ae 100644
---
a/backends-velox/src/main/scala/org/apache/gluten/backendsapi/velox/VeloxIteratorApi.scala
+++
b/backends-velox/src/main/scala/org/apache/gluten/backendsapi/velox/VeloxIteratorApi.scala
@@ -25,6 +25,7 @@ import org.apache.gluten.metrics.{IMetrics,
IteratorMetricsJniWrapper}
import org.apache.gluten.sql.shims.SparkShimLoader
import org.apache.gluten.substrait.plan.PlanNode
import org.apache.gluten.substrait.rel.{LocalFilesBuilder, LocalFilesNode,
SplitInfo}
+import org.apache.gluten.substrait.rel.LocalFilesNode.ColumnMappingMode
import org.apache.gluten.substrait.rel.LocalFilesNode.ReadFileFormat
import org.apache.gluten.vectorized._
@@ -35,6 +36,7 @@ import
org.apache.spark.sql.catalyst.catalog.ExternalCatalogUtils
import org.apache.spark.sql.catalyst.util.{DateFormatter, TimestampFormatter}
import org.apache.spark.sql.execution.datasources.{FilePartition,
PartitionedFile}
import org.apache.spark.sql.execution.metric.SQLMetric
+import org.apache.spark.sql.internal.SQLConf
import org.apache.spark.sql.types._
import org.apache.spark.sql.utils.SparkInputMetricsUtil.InputMetricsWrapper
import org.apache.spark.sql.vectorized.ColumnarBatch
@@ -66,10 +68,32 @@ class VeloxIteratorApi extends IteratorApi with Logging {
) {
localFilesNode.setFileSchema(fileSchema)
}
+ columnMappingMode(fileFormat).foreach(localFilesNode.setColumnMappingMode)
localFilesNode
}
+ private def columnMappingMode(fileFormat: ReadFileFormat):
Option[ColumnMappingMode] = {
+ fileFormat match {
+ case ReadFileFormat.OrcReadFormat | ReadFileFormat.DwrfReadFormat =>
+ val defaultForcePosition =
+ org.apache.spark.SparkEnv.get.conf.get(
+ GlutenConfig.SPARK_ORC_FORCE_POSITIONAL_EVOLUTION,
+ "false")
+ val forcePosition =
+ SQLConf.get
+ .getConfString(GlutenConfig.SPARK_ORC_FORCE_POSITIONAL_EVOLUTION,
defaultForcePosition)
+ .toBoolean
+ Some(if (forcePosition) ColumnMappingMode.POSITION else
ColumnMappingMode.NAME)
+ case ReadFileFormat.ParquetReadFormat =>
+ Some(
+ if (VeloxConfig.get.parquetUseColumnNames) ColumnMappingMode.NAME
+ else ColumnMappingMode.POSITION)
+ case _ =>
+ None
+ }
+ }
+
override def genSplitInfo(
partitionIndex: Int,
partitions: Seq[Partition],
diff --git
a/backends-velox/src/main/scala/org/apache/gluten/config/VeloxConfig.scala
b/backends-velox/src/main/scala/org/apache/gluten/config/VeloxConfig.scala
index 67c7192d95..d1d9e365f4 100644
--- a/backends-velox/src/main/scala/org/apache/gluten/config/VeloxConfig.scala
+++ b/backends-velox/src/main/scala/org/apache/gluten/config/VeloxConfig.scala
@@ -939,7 +939,7 @@ object VeloxConfig extends ConfigRegistry {
.createWithDefault(100)
val PARQUET_USE_COLUMN_NAMES =
- buildConf("spark.gluten.sql.columnar.backend.velox.parquetUseColumnNames")
+ buildConf(GlutenConfig.VELOX_PARQUET_USE_COLUMN_NAMES)
.doc("Maps table field names to file field names using names, not
indices for Parquet files.")
.booleanConf
.createWithDefault(true)
diff --git a/cpp/velox/compute/VeloxPlanConverter.cc
b/cpp/velox/compute/VeloxPlanConverter.cc
index 82c1f29036..2b5fb8bd9a 100644
--- a/cpp/velox/compute/VeloxPlanConverter.cc
+++ b/cpp/velox/compute/VeloxPlanConverter.cc
@@ -53,6 +53,10 @@ VeloxPlanConverter::VeloxPlanConverter(
}
namespace {
+// Keep this key in sync with the JVM-side constant
+// LocalFilesNode.COLUMN_MAPPING_MODE_METADATA_KEY.
+constexpr std::string_view kColumnMappingModeMetadataKey =
"__gluten.column_mapping_mode";
+
std::optional<std::string> unpackMetadataValue(const google::protobuf::Any&
value) {
google::protobuf::BytesValue bytesValue;
if (value.UnpackTo(&bytesValue)) {
@@ -79,8 +83,9 @@ std::optional<std::string> unpackMetadataValue(const
google::protobuf::Any& valu
return std::to_string(doubleValue.value());
}
- // Matches the string encoding the JVM side uses for booleans, which are
packed through
- // SubstraitUtil.convertJavaObjectToAny's toString fallback rather than as
BoolValue.
+ // Matches the string encoding the JVM side uses for booleans, which are
+ // packed through SubstraitUtil.convertJavaObjectToAny's toString fallback
+ // rather than as BoolValue.
google::protobuf::BoolValue boolValue;
if (value.UnpackTo(&boolValue)) {
return boolValue.value() ? "true" : "false";
@@ -89,6 +94,14 @@ std::optional<std::string> unpackMetadataValue(const
google::protobuf::Any& valu
return std::nullopt;
}
+std::optional<velox::dwio::common::ColumnMappingMode>
parseColumnMappingMode(const google::protobuf::Any& value) {
+ auto unpacked = unpackMetadataValue(value);
+ if (!unpacked.has_value()) {
+ return std::nullopt;
+ }
+ return
velox::dwio::common::ColumnMappingModeName::tryToColumnMappingMode(*unpacked);
+}
+
delta::DeltaRowIndexFilterType parseDeltaRowIndexFilterType(int filterType) {
switch (filterType) {
case 1:
@@ -164,6 +177,15 @@ std::shared_ptr<SplitInfo> parseScanSplitInfo(
metadataColumnMap[metadataColumn.key()] = metadataColumn.value();
}
for (const auto& otherMetadataColumn :
file.other_const_metadata_columns()) {
+ if (otherMetadataColumn.key() == kColumnMappingModeMetadataKey) {
+ auto mode = parseColumnMappingMode(otherMetadataColumn.value());
+ VELOX_CHECK(mode.has_value(), "Invalid column mapping mode metadata
for key {}", kColumnMappingModeMetadataKey);
+ if (splitInfo->columnMappingMode.has_value()) {
+ VELOX_CHECK_EQ(*splitInfo->columnMappingMode, *mode, "A single
SplitInfo cannot mix column mapping modes");
+ }
+ splitInfo->columnMappingMode = *mode;
+ continue;
+ }
if (auto unpackedValue =
unpackMetadataValue(otherMetadataColumn.value())) {
metadataColumnMap[otherMetadataColumn.key()] =
std::move(*unpackedValue);
}
diff --git a/cpp/velox/compute/WholeStageResultIterator.cc
b/cpp/velox/compute/WholeStageResultIterator.cc
index 409f288c17..fcfb55bfc1 100644
--- a/cpp/velox/compute/WholeStageResultIterator.cc
+++ b/cpp/velox/compute/WholeStageResultIterator.cc
@@ -188,7 +188,10 @@ WholeStageResultIterator::WholeStageResultIterator(
true,
deleteFiles,
metadataColumn,
- properties[idx]);
+ properties[idx],
+ /*dataSequenceNumber=*/0,
+ /*identityPartitionKeys=*/std::unordered_map<int32_t,
std::optional<std::string>>{},
+ scanInfo->columnMappingMode);
} else if (isDeltaScan) {
std::unordered_map<std::string, std::string>
customSplitInfo{{"table_format", kDeltaTableFormat}};
std::optional<gluten::delta::DeltaDeletionVectorDescriptor>
deletionVector = std::nullopt;
@@ -215,7 +218,8 @@ WholeStageResultIterator::WholeStageResultIterator(
std::nullopt,
rowIndexFilterType,
metadataColumn,
- properties[idx]);
+ properties[idx],
+ scanInfo->columnMappingMode);
} else {
auto connectorId = connectorIds_.hive;
#ifdef GLUTEN_ENABLE_GPU
@@ -238,7 +242,10 @@ WholeStageResultIterator::WholeStageResultIterator(
0,
true,
metadataColumn,
- properties[idx]);
+ properties[idx],
+ std::nullopt,
+ std::nullopt,
+ scanInfo->columnMappingMode);
}
connectorSplits.emplace_back(split);
}
diff --git a/cpp/velox/compute/delta/DeltaSplit.cpp
b/cpp/velox/compute/delta/DeltaSplit.cpp
index b5b22a3ab5..bb72a82f64 100644
--- a/cpp/velox/compute/delta/DeltaSplit.cpp
+++ b/cpp/velox/compute/delta/DeltaSplit.cpp
@@ -35,7 +35,8 @@ HiveDeltaSplit::HiveDeltaSplit(
std::optional<DeltaFileStatistics> statistics,
DeltaRowIndexFilterType filterType,
const std::unordered_map<std::string, std::string>& infoColumns,
- std::optional<FileProperties> fileProperties)
+ std::optional<FileProperties> fileProperties,
+ std::optional<dwio::common::ColumnMappingMode> columnMappingMode)
: HiveConnectorSplit(
connectorId,
filePath,
@@ -52,7 +53,8 @@ HiveDeltaSplit::HiveDeltaSplit(
infoColumns,
fileProperties,
std::nullopt,
- std::nullopt),
+ std::nullopt,
+ columnMappingMode),
deletionVector(std::move(deletionVector)),
statistics(std::move(statistics)),
filterType(filterType) {}
diff --git a/cpp/velox/compute/delta/DeltaSplit.h
b/cpp/velox/compute/delta/DeltaSplit.h
index 3f9d9dd347..a99349ee47 100644
--- a/cpp/velox/compute/delta/DeltaSplit.h
+++ b/cpp/velox/compute/delta/DeltaSplit.h
@@ -101,7 +101,8 @@ struct HiveDeltaSplit : public
connector::hive::HiveConnectorSplit {
std::optional<DeltaFileStatistics> statistics = std::nullopt,
DeltaRowIndexFilterType filterType = DeltaRowIndexFilterType::kKeepAll,
const std::unordered_map<std::string, std::string>& infoColumns = {},
- std::optional<FileProperties> fileProperties = std::nullopt);
+ std::optional<FileProperties> fileProperties = std::nullopt,
+ std::optional<dwio::common::ColumnMappingMode> columnMappingMode =
std::nullopt);
};
} // namespace gluten::delta
diff --git a/cpp/velox/compute/delta/tests/DeltaSplitTest.cpp
b/cpp/velox/compute/delta/tests/DeltaSplitTest.cpp
index 13482ccec3..4687a5771e 100644
--- a/cpp/velox/compute/delta/tests/DeltaSplitTest.cpp
+++ b/cpp/velox/compute/delta/tests/DeltaSplitTest.cpp
@@ -56,13 +56,16 @@ TEST(DeltaSplitTest, SplitCarriesDeletionVectorDescriptor) {
std::nullopt,
DeltaRowIndexFilterType::kIfContained,
std::unordered_map<std::string, std::string>{},
- std::nullopt);
+ std::nullopt,
+ facebook::velox::dwio::common::ColumnMappingMode::kName);
ASSERT_TRUE(split->deletionVector.has_value());
EXPECT_EQ(split->deletionVector->cardinality, 2);
ASSERT_TRUE(split->deletionVector->serializedPayloadView.has_value());
EXPECT_EQ(split->deletionVector->serializedPayloadView->size,
payload.size());
EXPECT_EQ(split->filterType, DeltaRowIndexFilterType::kIfContained);
+ ASSERT_TRUE(split->columnMappingMode.has_value());
+ EXPECT_EQ(split->columnMappingMode.value(),
facebook::velox::dwio::common::ColumnMappingMode::kName);
}
TEST(DeltaSplitTest, LogicalRowCountSubtractsDeletionVectorCardinality) {
diff --git a/cpp/velox/config/VeloxConfig.h b/cpp/velox/config/VeloxConfig.h
index f9351560cf..8a37e36dcb 100644
--- a/cpp/velox/config/VeloxConfig.h
+++ b/cpp/velox/config/VeloxConfig.h
@@ -196,8 +196,6 @@ const std::string kMaxCoalescedBytes =
"spark.gluten.sql.columnar.backend.velox.
const std::string kCachePrefetchMinPct =
"spark.gluten.sql.columnar.backend.velox.cachePrefetchMinPct";
const std::string kMemoryPoolCapacityTransferAcrossTasks =
"spark.gluten.sql.columnar.backend.velox.memoryPoolCapacityTransferAcrossTasks";
-const std::string kOrcForcePositionalEvolution =
"spark.gluten.sql.columnar.backend.velox.orcForcePositionalEvolution";
-const std::string kParquetUseColumnNames =
"spark.gluten.sql.columnar.backend.velox.parquetUseColumnNames";
const std::string kAllowInt32Narrowing =
"spark.gluten.sql.columnar.backend.velox.allowInt32Narrowing";
// write files
diff --git a/cpp/velox/substrait/SubstraitToVeloxPlan.h
b/cpp/velox/substrait/SubstraitToVeloxPlan.h
index b0cf76fff3..2464881052 100644
--- a/cpp/velox/substrait/SubstraitToVeloxPlan.h
+++ b/cpp/velox/substrait/SubstraitToVeloxPlan.h
@@ -17,6 +17,8 @@
#pragma once
+#include <optional>
+
#include "SubstraitToVeloxExpr.h"
#include "TypeUtils.h"
#include "compute/VeloxConnectorIds.h"
@@ -68,6 +70,10 @@ struct SplitInfo {
/// The schema of the table being scanned.
RowTypePtr tableSchema;
+ /// Optional scan-level column mapping mode. Gluten may set this differently
+ /// for different scans or partitions in the same Velox QueryCtx.
+ std::optional<dwio::common::ColumnMappingMode> columnMappingMode;
+
/// Make SplitInfo polymorphic
virtual ~SplitInfo() = default;
diff --git a/cpp/velox/tests/Substrait2VeloxPlanConversionTest.cc
b/cpp/velox/tests/Substrait2VeloxPlanConversionTest.cc
index d002f34a14..aa02a3f4e8 100644
--- a/cpp/velox/tests/Substrait2VeloxPlanConversionTest.cc
+++ b/cpp/velox/tests/Substrait2VeloxPlanConversionTest.cc
@@ -59,11 +59,12 @@ class Substrait2VeloxPlanConversionTest : public
exec::test::HiveConnectorTestBa
auto path = fmt::format("{}{}", tmpDir_->getPath(), paths[i]);
auto start = starts[i];
auto length = lengths[i];
- auto split = facebook::velox::exec::test::HiveConnectorSplitBuilder(path)
- .fileFormat(fileFormat)
- .start(start)
- .length(length)
- .build();
+ facebook::velox::exec::test::HiveConnectorSplitBuilder
splitBuilder(path);
+ splitBuilder.fileFormat(fileFormat).start(start).length(length);
+ if (splitInfo->columnMappingMode.has_value()) {
+ splitBuilder.columnMappingMode(*splitInfo->columnMappingMode);
+ }
+ auto split = splitBuilder.build();
splits.emplace_back(split);
}
return splits;
diff --git a/cpp/velox/tests/data/q6_first_stage_split.json
b/cpp/velox/tests/data/q6_first_stage_split.json
index 7b02e4dc01..2ebcd63e93 100644
--- a/cpp/velox/tests/data/q6_first_stage_split.json
+++ b/cpp/velox/tests/data/q6_first_stage_split.json
@@ -5,7 +5,16 @@
"start": "0",
"length": "3719",
"uri_file": "/mock_lineitem.dwrf",
+ "other_const_metadata_columns": [
+ {
+ "key": "__gluten.column_mapping_mode",
+ "value": {
+ "@type":
"type.googleapis.com/google.protobuf.StringValue",
+ "value": "NAME"
+ }
+ }
+ ],
"dwrf": {}
}
]
-}
\ No newline at end of file
+}
diff --git a/cpp/velox/utils/ConfigExtractor.cc
b/cpp/velox/utils/ConfigExtractor.cc
index d38745416b..ed2850a811 100644
--- a/cpp/velox/utils/ConfigExtractor.cc
+++ b/cpp/velox/utils/ConfigExtractor.cc
@@ -269,18 +269,8 @@ std::shared_ptr<facebook::velox::config::ConfigBase>
createHiveConnectorSessionC
conf->get<std::string>(kParquetMaxTargetFileSize, "0B"); // 0 means no
limit on target file size
configs[facebook::velox::connector::hive::HiveConfig::kIgnoreMissingFilesSession]
=
conf->get<bool>(kIgnoreMissingFiles, false) ? "true" : "false";
-
configs[parquetSessionProperty(facebook::velox::parquet::ParquetConfig::kUseColumnNamesSession)]
=
- conf->get<bool>(kParquetUseColumnNames, true) ? "true" : "false";
configs[facebook::velox::connector::hive::HiveConfig::kAllowInt32NarrowingSession]
=
conf->get<bool>(kAllowInt32Narrowing, true) ? "true" : "false";
- // ORC/DWRF files are mapped to the requested schema by name by default,
- // matching vanilla Spark. Individual files whose physical schema is all Hive
- // placeholder names (_col0, ...) are still mapped by position per-file by
the
- // native reader. When Spark's orc.force.positional.evolution is set, force
- // position-based mapping for the whole scan by disabling name-based mapping
- // (ColumnMappingMode::kPosition), matching OrcUtils.requestedColumnIds.
-
configs[orcSessionProperty(facebook::velox::dwrf::Config::kOrcUseColumnNamesSession)]
=
- conf->get<bool>(kOrcForcePositionalEvolution, false) ? "false" : "true";
configs[parquetSessionProperty(facebook::velox::parquet::ParquetConfig::kWriterPageSizeSession)]
=
conf->get<std::string>(kWriteParquetPageSizeBytes, "1MB");
configs[parquetSessionProperty(facebook::velox::parquet::ParquetConfig::kWriterDictionaryPageSizeLimitSession)]
=
diff --git a/ep/build-velox/src/get-velox.sh b/ep/build-velox/src/get-velox.sh
index 7c8ec8cba3..b7f9f0558c 100755
--- a/ep/build-velox/src/get-velox.sh
+++ b/ep/build-velox/src/get-velox.sh
@@ -18,8 +18,8 @@ set -exu
CURRENT_DIR=$(cd "$(dirname "$BASH_SOURCE")"; pwd)
VELOX_REPO=https://github.com/IBM/velox.git
-VELOX_BRANCH=dft-2026_08_26
-VELOX_ENHANCED_BRANCH=ibm-2026_08_26
+VELOX_BRANCH=dft-2026_08_26_fix
+VELOX_ENHANCED_BRANCH=dft-2026_08_26_fix
VELOX_HOME=""
RUN_SETUP_SCRIPT=ON
ENABLE_ENHANCED_FEATURES=OFF
diff --git
a/gluten-delta/src-delta23/main/scala/org/apache/gluten/delta/DeltaDeletionVectorScanInfo.scala
b/gluten-delta/src-delta23/main/scala/org/apache/gluten/delta/DeltaDeletionVectorScanInfo.scala
index 084f3d3319..1fc0f2c025 100644
---
a/gluten-delta/src-delta23/main/scala/org/apache/gluten/delta/DeltaDeletionVectorScanInfo.scala
+++
b/gluten-delta/src-delta23/main/scala/org/apache/gluten/delta/DeltaDeletionVectorScanInfo.scala
@@ -18,6 +18,7 @@ package org.apache.gluten.delta
import org.apache.gluten.substrait.rel.DeltaLocalFilesNode.DeltaFileReadOptions
+import org.apache.spark.sql.delta.actions.AddFile
import org.apache.spark.sql.execution.datasources.PartitionedFile
import org.apache.hadoop.fs.Path
@@ -38,4 +39,21 @@ object DeltaDeletionVectorScanInfo {
tablePath: Path,
readMetrics: Option[DeletionVectorReadMetrics])
: Option[(Seq[JMap[String, Object]], Seq[DeltaFileReadOptions])] = None
+
+ private[gluten] def buildAddFileLookup(
+ tablePath: Path,
+ addFiles: Seq[AddFile]): DeltaAddFileLookup = DeltaAddFileLookup.empty
+
+ private[gluten] def normalizeFromAddFiles(
+ partitionFiles: Seq[PartitionedFile],
+ tablePath: Path,
+ addFileLookup: DeltaAddFileLookup)
+ : Option[(Seq[JMap[String, Object]], Seq[DeltaFileReadOptions])] = None
+
+ private[gluten] def normalizeFromAddFiles(
+ partitionFiles: Seq[PartitionedFile],
+ tablePath: Path,
+ addFileLookup: DeltaAddFileLookup,
+ readMetrics: Option[DeletionVectorReadMetrics])
+ : Option[(Seq[JMap[String, Object]], Seq[DeltaFileReadOptions])] = None
}
diff --git
a/gluten-delta/src-delta24/main/scala/org/apache/gluten/delta/DeltaDeletionVectorScanInfo.scala
b/gluten-delta/src-delta24/main/scala/org/apache/gluten/delta/DeltaDeletionVectorScanInfo.scala
index 084f3d3319..1fc0f2c025 100644
---
a/gluten-delta/src-delta24/main/scala/org/apache/gluten/delta/DeltaDeletionVectorScanInfo.scala
+++
b/gluten-delta/src-delta24/main/scala/org/apache/gluten/delta/DeltaDeletionVectorScanInfo.scala
@@ -18,6 +18,7 @@ package org.apache.gluten.delta
import org.apache.gluten.substrait.rel.DeltaLocalFilesNode.DeltaFileReadOptions
+import org.apache.spark.sql.delta.actions.AddFile
import org.apache.spark.sql.execution.datasources.PartitionedFile
import org.apache.hadoop.fs.Path
@@ -38,4 +39,21 @@ object DeltaDeletionVectorScanInfo {
tablePath: Path,
readMetrics: Option[DeletionVectorReadMetrics])
: Option[(Seq[JMap[String, Object]], Seq[DeltaFileReadOptions])] = None
+
+ private[gluten] def buildAddFileLookup(
+ tablePath: Path,
+ addFiles: Seq[AddFile]): DeltaAddFileLookup = DeltaAddFileLookup.empty
+
+ private[gluten] def normalizeFromAddFiles(
+ partitionFiles: Seq[PartitionedFile],
+ tablePath: Path,
+ addFileLookup: DeltaAddFileLookup)
+ : Option[(Seq[JMap[String, Object]], Seq[DeltaFileReadOptions])] = None
+
+ private[gluten] def normalizeFromAddFiles(
+ partitionFiles: Seq[PartitionedFile],
+ tablePath: Path,
+ addFileLookup: DeltaAddFileLookup,
+ readMetrics: Option[DeletionVectorReadMetrics])
+ : Option[(Seq[JMap[String, Object]], Seq[DeltaFileReadOptions])] = None
}
diff --git
a/gluten-delta/src-delta33/main/scala/org/apache/gluten/delta/DeltaDeletionVectorScanInfo.scala
b/gluten-delta/src-delta33/main/scala/org/apache/gluten/delta/DeltaDeletionVectorScanInfo.scala
index 71a2cd265a..9f717b25e8 100644
---
a/gluten-delta/src-delta33/main/scala/org/apache/gluten/delta/DeltaDeletionVectorScanInfo.scala
+++
b/gluten-delta/src-delta33/main/scala/org/apache/gluten/delta/DeltaDeletionVectorScanInfo.scala
@@ -22,7 +22,7 @@ import
org.apache.gluten.substrait.rel.DeltaLocalFilesNode.{DeletionVectorPayloa
import org.apache.spark.sql.SparkSession
import org.apache.spark.sql.delta.DeltaParquetFileFormat
-import org.apache.spark.sql.delta.actions.DeletionVectorDescriptor
+import org.apache.spark.sql.delta.actions.{AddFile, DeletionVectorDescriptor}
import org.apache.spark.sql.delta.deletionvectors.{RoaringBitmapArrayFormat,
StoredBitmap}
import org.apache.spark.sql.delta.storage.dv.{DeletionVectorStore,
HadoopFileSystemDVStore}
import org.apache.spark.sql.execution.datasources.PartitionedFile
@@ -104,6 +104,60 @@ object DeltaDeletionVectorScanInfo {
}
}
+ /**
+ * Materializes normal-table DV options from the AddFiles selected by
PreparedDeltaFileIndex.
+ * These are authoritative when a data file's DV is replaced by repeated
DML; the corresponding
+ * PartitionedFile may still contain the previous descriptor in its constant
metadata.
+ */
+ private[gluten] def buildAddFileLookup(
+ tablePath: Path,
+ addFiles: Seq[AddFile]): DeltaAddFileLookup = {
+ DeltaAddFileLookup(tablePath, addFiles, addFiles.exists(_.deletionVector
!= null))
+ }
+
+ private[gluten] def normalizeFromAddFiles(
+ partitionFiles: Seq[PartitionedFile],
+ tablePath: Path,
+ addFileLookup: DeltaAddFileLookup)
+ : Option[(Seq[JMap[String, Object]], Seq[DeltaFileReadOptions])] = {
+ normalizeFromAddFiles(partitionFiles, tablePath, addFileLookup, None)
+ }
+
+ private[gluten] def normalizeFromAddFiles(
+ partitionFiles: Seq[PartitionedFile],
+ tablePath: Path,
+ addFileLookup: DeltaAddFileLookup,
+ readMetrics: Option[DeletionVectorReadMetrics])
+ : Option[(Seq[JMap[String, Object]], Seq[DeltaFileReadOptions])] = {
+ if (partitionFiles.isEmpty) {
+ return None
+ }
+ val spark = activeSparkSession
+ val hadoopConf = spark.sessionState.newHadoopConf()
+ val serializableHadoopConf = new SerializableConfiguration(hadoopConf)
+ val hadDvMetadata = partitionFiles.exists {
+ file =>
+ val metadata = otherMetadataColumns(file)
+ metadata.contains(RowIndexFilterIdEncoded) ||
metadata.contains(RowIndexFilterTypeKey)
+ }
+ if (!hadDvMetadata && !addFileLookup.hasDeletionVector) {
+ return None
+ }
+ val scanInfos = partitionFiles.map {
+ file =>
+ val addFile = addFileLookup.find(file)
+ extract(file, hadoopConf, serializableHadoopConf, tablePath, addFile,
readMetrics)
+ }
+ if (hadDvMetadata ||
scanInfos.exists(_.deletionVectorInfo.hasDeletionVector)) {
+ Some(
+ (
+ scanInfos.map(_.normalizedOtherMetadataColumns.asJava),
+ scanInfos.map(info =>
toDeltaFileReadOptions(info.deletionVectorInfo))))
+ } else {
+ None
+ }
+ }
+
/** Public entry point for extracting DV info from a single file (used by
tests). */
def extract(
spark: SparkSession,
@@ -131,6 +185,37 @@ object DeltaDeletionVectorScanInfo {
PartitionFileScanInfo(normalizedMetadata, dvInfo)
}
+ private def extract(
+ file: PartitionedFile,
+ hadoopConf: Configuration,
+ serializableHadoopConf: SerializableConfiguration,
+ tablePath: Path,
+ addFile: AddFile,
+ readMetrics: Option[DeletionVectorReadMetrics]): PartitionFileScanInfo =
{
+ val metadata = otherMetadataColumns(file)
+ val normalizedMetadata = metadata -- Seq(RowIndexFilterIdEncoded,
RowIndexFilterTypeKey)
+ val dvInfo = Option(addFile.deletionVector) match {
+ case Some(descriptor) =>
+ DeletionVectorInfo(
+ true,
+ IF_CONTAINED,
+ descriptor.cardinality,
+ deletionVectorPayload(
+ hadoopConf,
+ serializableHadoopConf,
+ tablePath,
+ descriptor,
+ readMetrics))
+ case None =>
+ DeletionVectorInfo(
+ false,
+ KEEP_ALL,
+ 0L,
+ new InMemoryDeletionVectorPayload(Array.emptyByteArray))
+ }
+ PartitionFileScanInfo(normalizedMetadata, dvInfo)
+ }
+
private def toDeltaFileReadOptions(dvInfo: DeletionVectorInfo):
DeltaFileReadOptions = {
new DeltaFileReadOptions(
toSubstraitRowIndexFilterType(dvInfo.rowIndexFilterType),
diff --git
a/gluten-delta/src-delta40/main/scala/org/apache/gluten/delta/DeltaDeletionVectorScanInfo.scala
b/gluten-delta/src-delta40/main/scala/org/apache/gluten/delta/DeltaDeletionVectorScanInfo.scala
index e27d10774f..cf5dd1a3ac 100644
---
a/gluten-delta/src-delta40/main/scala/org/apache/gluten/delta/DeltaDeletionVectorScanInfo.scala
+++
b/gluten-delta/src-delta40/main/scala/org/apache/gluten/delta/DeltaDeletionVectorScanInfo.scala
@@ -22,7 +22,7 @@ import
org.apache.gluten.substrait.rel.DeltaLocalFilesNode.{DeletionVectorPayloa
import org.apache.spark.sql.SparkSession
import org.apache.spark.sql.delta.DeltaParquetFileFormat
-import org.apache.spark.sql.delta.actions.DeletionVectorDescriptor
+import org.apache.spark.sql.delta.actions.{AddFile, DeletionVectorDescriptor}
import org.apache.spark.sql.delta.deletionvectors.{RoaringBitmapArrayFormat,
StoredBitmap}
import org.apache.spark.sql.delta.storage.dv.{DeletionVectorStore,
HadoopFileSystemDVStore}
import org.apache.spark.sql.execution.datasources.PartitionedFile
@@ -105,6 +105,60 @@ object DeltaDeletionVectorScanInfo {
}
}
+ /**
+ * Materializes normal-table DV options from the AddFiles selected by
PreparedDeltaFileIndex.
+ * These are authoritative when a data file's DV is replaced by repeated
DML; the corresponding
+ * PartitionedFile may still contain the previous descriptor in its constant
metadata.
+ */
+ private[gluten] def buildAddFileLookup(
+ tablePath: Path,
+ addFiles: Seq[AddFile]): DeltaAddFileLookup = {
+ DeltaAddFileLookup(tablePath, addFiles, addFiles.exists(_.deletionVector
!= null))
+ }
+
+ private[gluten] def normalizeFromAddFiles(
+ partitionFiles: Seq[PartitionedFile],
+ tablePath: Path,
+ addFileLookup: DeltaAddFileLookup)
+ : Option[(Seq[JMap[String, Object]], Seq[DeltaFileReadOptions])] = {
+ normalizeFromAddFiles(partitionFiles, tablePath, addFileLookup, None)
+ }
+
+ private[gluten] def normalizeFromAddFiles(
+ partitionFiles: Seq[PartitionedFile],
+ tablePath: Path,
+ addFileLookup: DeltaAddFileLookup,
+ readMetrics: Option[DeletionVectorReadMetrics])
+ : Option[(Seq[JMap[String, Object]], Seq[DeltaFileReadOptions])] = {
+ if (partitionFiles.isEmpty) {
+ return None
+ }
+ val spark = activeSparkSession
+ val hadoopConf = spark.sessionState.newHadoopConf()
+ val serializableHadoopConf = new SerializableConfiguration(hadoopConf)
+ val hadDvMetadata = partitionFiles.exists {
+ file =>
+ val metadata = otherMetadataColumns(file)
+ metadata.contains(RowIndexFilterIdEncoded) ||
metadata.contains(RowIndexFilterTypeKey)
+ }
+ if (!hadDvMetadata && !addFileLookup.hasDeletionVector) {
+ return None
+ }
+ val scanInfos = partitionFiles.map {
+ file =>
+ val addFile = addFileLookup.find(file)
+ extract(file, hadoopConf, serializableHadoopConf, tablePath, addFile,
readMetrics)
+ }
+ if (hadDvMetadata ||
scanInfos.exists(_.deletionVectorInfo.hasDeletionVector)) {
+ Some(
+ (
+ scanInfos.map(_.normalizedOtherMetadataColumns.asJava),
+ scanInfos.map(info =>
toDeltaFileReadOptions(info.deletionVectorInfo))))
+ } else {
+ None
+ }
+ }
+
/** Public entry point for extracting DV info from a single file (used by
tests). */
def extract(
spark: SparkSession,
@@ -132,6 +186,37 @@ object DeltaDeletionVectorScanInfo {
PartitionFileScanInfo(normalizedMetadata, dvInfo)
}
+ private def extract(
+ file: PartitionedFile,
+ hadoopConf: Configuration,
+ serializableHadoopConf: SerializableConfiguration,
+ tablePath: Path,
+ addFile: AddFile,
+ readMetrics: Option[DeletionVectorReadMetrics]): PartitionFileScanInfo =
{
+ val metadata = otherMetadataColumns(file)
+ val normalizedMetadata = metadata -- Seq(RowIndexFilterIdEncoded,
RowIndexFilterTypeKey)
+ val dvInfo = Option(addFile.deletionVector) match {
+ case Some(descriptor) =>
+ DeletionVectorInfo(
+ true,
+ IF_CONTAINED,
+ descriptor.cardinality,
+ deletionVectorPayload(
+ hadoopConf,
+ serializableHadoopConf,
+ tablePath,
+ descriptor,
+ readMetrics))
+ case None =>
+ DeletionVectorInfo(
+ false,
+ KEEP_ALL,
+ 0L,
+ new InMemoryDeletionVectorPayload(Array.emptyByteArray))
+ }
+ PartitionFileScanInfo(normalizedMetadata, dvInfo)
+ }
+
private def toDeltaFileReadOptions(dvInfo: DeletionVectorInfo):
DeltaFileReadOptions = {
new DeltaFileReadOptions(
toSubstraitRowIndexFilterType(dvInfo.rowIndexFilterType),
diff --git
a/gluten-delta/src/main/scala/org/apache/gluten/delta/DeltaAddFileLookup.scala
b/gluten-delta/src/main/scala/org/apache/gluten/delta/DeltaAddFileLookup.scala
new file mode 100644
index 0000000000..8b2ff853f3
--- /dev/null
+++
b/gluten-delta/src/main/scala/org/apache/gluten/delta/DeltaAddFileLookup.scala
@@ -0,0 +1,103 @@
+/*
+ * 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.gluten.delta
+
+import org.apache.spark.sql.delta.actions.AddFile
+import org.apache.spark.sql.delta.util.DeltaFileOperations
+import org.apache.spark.sql.execution.datasources.PartitionedFile
+
+import org.apache.hadoop.fs.Path
+
+import scala.collection.mutable
+
+/** Driver-side AddFile index shared by all FilePartitions in one prepared
Delta scan. */
+final private[gluten] class DeltaAddFileLookup private (
+ addFiles: IndexedSeq[AddFile],
+ absolutePaths: IndexedSeq[Path],
+ candidateIndexes: Map[(Option[String], String), Vector[Int]],
+ val hasDeletionVector: Boolean) {
+
+ def find(file: PartitionedFile): AddFile = {
+ val partitionedFilePath = new Path(file.filePath.toString)
+ val candidates = mutable.BitSet.empty
+ DeltaAddFileLookup.pathVariants(partitionedFilePath).foreach {
+ key => candidateIndexes.get(key).foreach(indexes => candidates ++=
indexes)
+ }
+
+ // BitSet iteration preserves the original AddFile order used by the
previous Seq.find lookup.
+ candidates.iterator
+ .find(index => DeltaAddFileLookup.samePath(partitionedFilePath,
absolutePaths(index)))
+ .map(addFiles.apply)
+ .getOrElse {
+ throw new IllegalStateException(
+ s"Unable to find Delta AddFile metadata for split ${file.filePath}")
+ }
+ }
+}
+
+private[gluten] object DeltaAddFileLookup {
+ val empty: DeltaAddFileLookup =
+ new DeltaAddFileLookup(Vector.empty, Vector.empty, Map.empty,
hasDeletionVector = false)
+
+ def apply(
+ tablePath: Path,
+ addFiles: Seq[AddFile],
+ hasDeletionVector: Boolean): DeltaAddFileLookup = {
+ val indexedAddFiles = addFiles.toIndexedSeq
+ val absolutePaths = indexedAddFiles.map {
+ addFile => DeltaFileOperations.absolutePath(tablePath.toString,
addFile.path)
+ }
+ val candidateIndexes = mutable.HashMap.empty[
+ (Option[String], String),
+ mutable.ArrayBuffer[Int]]
+
+ absolutePaths.zipWithIndex.foreach {
+ case (absolutePath, index) =>
+ pathVariants(absolutePath).foreach {
+ key => candidateIndexes.getOrElseUpdate(key,
mutable.ArrayBuffer.empty) += index
+ }
+ }
+
+ new DeltaAddFileLookup(
+ indexedAddFiles,
+ absolutePaths,
+ candidateIndexes.iterator.map { case (key, indexes) => key ->
indexes.toVector }.toMap,
+ hasDeletionVector)
+ }
+
+ private def samePath(left: Path, right: Path): Boolean = {
+ pathVariants(left).intersect(pathVariants(right)).nonEmpty
+ }
+
+ private def pathVariants(path: Path): Set[(Option[String], String)] = {
+ val uri = path.toUri.normalize()
+ val authority = Option(uri.getAuthority)
+ Seq(uri.getRawPath, uri.getPath)
+ .filter(_ != null)
+ .flatMap(percentVariants)
+ .map(pathValue => authority -> pathValue)
+ .toSet
+ }
+
+ // SparkPath and DeltaFileOperations can expose literal '%' characters at
different URI escaping
+ // levels. Compare a bounded set of full-path variants while retaining the
URI authority.
+ private def percentVariants(path: String): Set[String] = {
+ (0 until 4).foldLeft(Set(path)) {
+ (variants, _) => variants ++ variants.map(_.replace("%25", "%"))
+ }
+ }
+}
diff --git
a/gluten-delta/src/main/scala/org/apache/gluten/execution/DeltaScanTransformer.scala
b/gluten-delta/src/main/scala/org/apache/gluten/execution/DeltaScanTransformer.scala
index 3d80c582d7..fe26accb71 100644
---
a/gluten-delta/src/main/scala/org/apache/gluten/execution/DeltaScanTransformer.scala
+++
b/gluten-delta/src/main/scala/org/apache/gluten/execution/DeltaScanTransformer.scala
@@ -19,6 +19,7 @@ package org.apache.gluten.execution
import org.apache.gluten.delta.{DeletionVectorReadMetrics,
DeltaDeletionVectorScanInfo}
import org.apache.gluten.sql.shims.SparkShimLoader
import org.apache.gluten.substrait.rel.{DeltaLocalFilesBuilder,
LocalFilesNode, SplitInfo}
+import org.apache.gluten.substrait.rel.LocalFilesNode.ColumnMappingMode
import org.apache.gluten.substrait.rel.LocalFilesNode.ReadFileFormat
import org.apache.spark.Partition
@@ -26,8 +27,9 @@ import org.apache.spark.sql.catalyst.TableIdentifier
import org.apache.spark.sql.catalyst.expressions.{Attribute,
AttributeReference, Expression}
import org.apache.spark.sql.catalyst.plans.QueryPlan
import org.apache.spark.sql.connector.read.streaming.SparkDataStream
-import org.apache.spark.sql.delta.{DeltaParquetFileFormat, NoMapping}
+import org.apache.spark.sql.delta.{DeltaParquetFileFormat, NameMapping,
NoMapping}
import org.apache.spark.sql.delta.files.{CdcAddFileIndex, TahoeFileIndex,
TahoeRemoveFileIndex}
+import org.apache.spark.sql.delta.stats.PreparedDeltaFileIndex
import org.apache.spark.sql.execution.FileSourceScanExec
import org.apache.spark.sql.execution.datasources.{FilePartition,
HadoopFsRelation}
import org.apache.spark.sql.execution.metric.{SQLMetric, SQLMetrics}
@@ -142,11 +144,63 @@ case class DeltaScanTransformer(
override def getSplitInfosFromPartitions(
partitions: Seq[(Partition, ReadFileFormat)]): Seq[SplitInfo] = {
val splitInfos = super.getSplitInfosFromPartitions(partitions)
- // Deletion vectors only exist on Delta tables read through a
TahoeFileIndex (which also covers
- // PreparedDeltaFileIndex). Its `path` is the authoritative table root and
is used to resolve
- // per-file DV locations. Any other location cannot carry Delta DV
metadata, so the generic
- // split representation is returned unchanged.
+ // Keep Delta's split decoration narrow. The generic Parquet path has
already attached the
+ // session-derived split mapping mode and only attaches file schema when
position mapping
+ // needs it. Delta name column mapping is the one case that must force
name mapping regardless
+ // of the generic Parquet setting because Gluten rewrites the scan schema
to physical names.
+ splitInfos.foreach {
+ case localFiles: LocalFilesNode =>
+ deltaColumnMappingMode.foreach {
+ mode =>
+ localFiles.clearFileSchema()
+ localFiles.setColumnMappingMode(mode)
+ }
+ case _ =>
+ }
+ // PreparedDeltaFileIndex contains the exact AddFiles selected for this
scan. Use these as the
+ // source of truth because PartitionedFile metadata can retain an older DV
descriptor after
+ // repeated DML updates the same data file.
relation.location match {
+ case prepared: PreparedDeltaFileIndex =>
+ val tableRootPath = prepared.path
+ val lookupStartedAt = System.nanoTime()
+ val addFileLookup =
+ try {
+ DeltaDeletionVectorScanInfo
+ .buildAddFileLookup(tableRootPath, prepared.preparedScan.files)
+ } finally {
+ metrics("dvDescriptorPreparationTime").add(System.nanoTime() -
lookupStartedAt)
+ }
+ splitInfos.zip(partitions).map {
+ case (localFiles: LocalFilesNode, (filePartition: FilePartition, _))
=>
+ val startedAt = System.nanoTime()
+ val normalized =
+ try {
+ DeltaDeletionVectorScanInfo
+ .normalizeFromAddFiles(
+ filePartition.files.toSeq,
+ tableRootPath,
+ addFileLookup,
+ Some(deletionVectorReadMetrics))
+ } finally {
+ metrics("dvDescriptorPreparationTime").add(System.nanoTime() -
startedAt)
+ }
+ normalized
+ .map {
+ case (otherMetadataColumns, deltaReadOptions) =>
+ metrics("dvDescriptorCount")
+ .add(deltaReadOptions.count(_.hasDeletionVector()).toLong)
+ DeltaLocalFilesBuilder.makeDeltaLocalFiles(
+ localFiles,
+ otherMetadataColumns.asJava,
+ deltaReadOptions.asJava): SplitInfo
+ }
+ .getOrElse(localFiles)
+ case (splitInfo, _) => splitInfo
+ }
+ // Other Tahoe indexes, such as CDF indexes, encode the row-index filter
type and DV
+ // descriptor in PartitionedFile metadata. Keep using that metadata for
these specialized
+ // scans because their semantics are not necessarily IF_CONTAINED.
case tahoe: TahoeFileIndex =>
val tableRootPath = tahoe.path
splitInfos.zip(partitions).map {
@@ -179,6 +233,16 @@ case class DeltaScanTransformer(
}
}
+ private def deltaColumnMappingMode: Option[ColumnMappingMode] =
relation.fileFormat match {
+ case d: DeltaParquetFileFormat =>
+ d.columnMappingMode match {
+ case NameMapping => Some(ColumnMappingMode.NAME)
+ // Preserves the previous Spark fallback behavior for IdMapping.
+ case _ => None
+ }
+ case _ => None
+ }
+
override def doCanonicalize(): DeltaScanTransformer = {
DeltaScanTransformer(
relation,
diff --git
a/gluten-iceberg/src/main/scala/org/apache/gluten/execution/IcebergScanTransformer.scala
b/gluten-iceberg/src/main/scala/org/apache/gluten/execution/IcebergScanTransformer.scala
index 16018e086d..911a98680b 100644
---
a/gluten-iceberg/src/main/scala/org/apache/gluten/execution/IcebergScanTransformer.scala
+++
b/gluten-iceberg/src/main/scala/org/apache/gluten/execution/IcebergScanTransformer.scala
@@ -21,6 +21,7 @@ import org.apache.gluten.exception.GlutenNotSupportException
import
org.apache.gluten.execution.IcebergScanTransformer.{containsMetadataColumn,
containsUuidOrFixedType}
import org.apache.gluten.sql.shims.SparkShimLoader
import org.apache.gluten.substrait.rel.{LocalFilesNode, SplitInfo}
+import org.apache.gluten.substrait.rel.LocalFilesNode.ColumnMappingMode
import org.apache.gluten.substrait.rel.LocalFilesNode.ReadFileFormat
import org.apache.spark.Partition
@@ -217,11 +218,15 @@ case class IcebergScanTransformer(
override def getSplitInfosFromPartitions(
partitions: Seq[(Partition, ReadFileFormat)]): Seq[SplitInfo] = {
val metadataColumnNames = getMetadataColumns().map(_.name)
- partitions.map { case (partition, _) => partitionToSplitInfo(partition,
metadataColumnNames) }
+ partitions.map {
+ case (partition, readFileFormat) =>
+ partitionToSplitInfo(partition, readFileFormat, metadataColumnNames)
+ }
}
private def partitionToSplitInfo(
partition: Partition,
+ readFileFormat: ReadFileFormat,
metadataColumnNames: Seq[String]): SplitInfo = {
val splitInfo = partition match {
case p: SparkDataSourceRDDPartition =>
@@ -233,10 +238,21 @@ case class IcebergScanTransformer(
icebergInitialDefaults)
case _ => throw new GlutenNotSupportException()
}
- numSplits.add(splitInfo.asInstanceOf[LocalFilesNode].getPaths.size())
+ val localFiles = splitInfo.asInstanceOf[LocalFilesNode]
+
icebergColumnMappingMode(readFileFormat).foreach(localFiles.setColumnMappingMode)
+ numSplits.add(localFiles.getPaths.size())
splitInfo
}
+ private def icebergColumnMappingMode(fileFormat: ReadFileFormat):
Option[ColumnMappingMode] = {
+ fileFormat match {
+ case ReadFileFormat.ParquetReadFormat | ReadFileFormat.OrcReadFormat =>
+ Some(ColumnMappingMode.NAME)
+ case _ =>
+ None
+ }
+ }
+
override def doCanonicalize(): IcebergScanTransformer = {
this.copy(
output = output.map(QueryPlan.normalizeExpressions(_, output)),
diff --git
a/gluten-paimon/src-paimon/main/scala/org/apache/gluten/execution/PaimonScanTransformer.scala
b/gluten-paimon/src-paimon/main/scala/org/apache/gluten/execution/PaimonScanTransformer.scala
index 12bccb1568..998ec6e084 100644
---
a/gluten-paimon/src-paimon/main/scala/org/apache/gluten/execution/PaimonScanTransformer.scala
+++
b/gluten-paimon/src-paimon/main/scala/org/apache/gluten/execution/PaimonScanTransformer.scala
@@ -19,6 +19,7 @@ package org.apache.gluten.execution
import org.apache.gluten.exception.GlutenNotSupportException
import org.apache.gluten.sql.shims.SparkShimLoader
import org.apache.gluten.substrait.rel.{PaimonLocalFilesBuilder, SplitInfo}
+import org.apache.gluten.substrait.rel.LocalFilesNode.ColumnMappingMode
import org.apache.gluten.substrait.rel.LocalFilesNode.ReadFileFormat
import org.apache.spark.Partition
@@ -83,7 +84,13 @@ case class PaimonScanTransformer(
throw new GlutenNotSupportException("Only support PaimonScan.")
}
- override def getDataSchema: StructType = new StructType()
+ override def getDataSchema: StructType = scan match {
+ case paimonScan: PaimonScan =>
+ val partitionKeys = paimonScan.table.partitionKeys()
+ StructType(scan.readSchema().filterNot(field =>
partitionKeys.contains(field.name)))
+ case _ =>
+ throw new GlutenNotSupportException("Only support PaimonScan.")
+ }
override def withNewPushdownFilters(filters: Seq[Expression]):
PaimonScanTransformer = {
this.copy(pushDownFilters = Some(filters))
@@ -165,7 +172,7 @@ case class PaimonScanTransformer(
throw new GlutenNotSupportException(s"Unsupported input partition
type: $o")
}
- PaimonLocalFilesBuilder.makePaimonLocalFiles(
+ val localFiles = PaimonLocalFilesBuilder.makePaimonLocalFiles(
p.index,
paths.asJava,
starts.asJava,
@@ -178,10 +185,22 @@ case class PaimonScanTransformer(
.asJava,
new JHashMap[String, String]()
)
+ localFiles.setFileSchema(getDataSchema)
+
paimonColumnMappingMode(fileFormat).foreach(localFiles.setColumnMappingMode)
+ localFiles
case _ => throw new GlutenNotSupportException()
}
}
+ private def paimonColumnMappingMode(fileFormat: ReadFileFormat):
Option[ColumnMappingMode] = {
+ fileFormat match {
+ case ReadFileFormat.ParquetReadFormat | ReadFileFormat.OrcReadFormat =>
+ Some(ColumnMappingMode.NAME)
+ case _ =>
+ None
+ }
+ }
+
override def doCanonicalize(): PaimonScanTransformer = {
this.copy(
output = output.map(QueryPlan.normalizeExpressions(_, output)),
diff --git
a/gluten-substrait/src/main/java/org/apache/gluten/substrait/rel/LocalFilesNode.java
b/gluten-substrait/src/main/java/org/apache/gluten/substrait/rel/LocalFilesNode.java
index dfea5dd753..5070a88581 100644
---
a/gluten-substrait/src/main/java/org/apache/gluten/substrait/rel/LocalFilesNode.java
+++
b/gluten-substrait/src/main/java/org/apache/gluten/substrait/rel/LocalFilesNode.java
@@ -31,6 +31,8 @@ import java.util.List;
import java.util.Map;
public class LocalFilesNode implements SplitInfo {
+ public static final String COLUMN_MAPPING_MODE_METADATA_KEY =
"__gluten.column_mapping_mode";
+
private final Integer index;
private final List<String> paths = new ArrayList<>();
private final List<Long> starts = new ArrayList<>();
@@ -55,10 +57,29 @@ public class LocalFilesNode implements SplitInfo {
UnknownFormat()
}
+ // This is fully aligned with mapping modes defined in Velox:
velox/dwio/common/Options.cpp.
+ public enum ColumnMappingMode {
+ POSITION("POSITION"),
+ NAME("NAME"),
+ PARQUET_FIELD_ID("PARQUET_FIELD_ID"),
+ FIELD_ID("FIELD_ID");
+
+ private final String nativeName;
+
+ ColumnMappingMode(String nativeName) {
+ this.nativeName = nativeName;
+ }
+
+ public String nativeName() {
+ return nativeName;
+ }
+ }
+
protected ReadFileFormat fileFormat = ReadFileFormat.UnknownFormat;
private Boolean iterAsInput = false;
private StructType fileSchema;
private Map<String, String> fileReadProperties;
+ private ColumnMappingMode columnMappingMode;
LocalFilesNode(
Integer index,
@@ -114,6 +135,7 @@ public class LocalFilesNode implements SplitInfo {
this.fileReadProperties = other.fileReadProperties;
this.iterAsInput = other.iterAsInput;
this.fileSchema = other.fileSchema;
+ this.columnMappingMode = other.columnMappingMode;
this.otherMetadataColumns.addAll(otherMetadataColumns);
}
@@ -130,6 +152,14 @@ public class LocalFilesNode implements SplitInfo {
this.fileSchema = schema;
}
+ public void clearFileSchema() {
+ this.fileSchema = null;
+ }
+
+ public void setColumnMappingMode(ColumnMappingMode columnMappingMode) {
+ this.columnMappingMode = columnMappingMode;
+ }
+
private NamedStruct buildNamedStruct() {
NamedStruct.Builder namedStructBuilder = NamedStruct.newBuilder();
@@ -236,6 +266,14 @@ public class LocalFilesNode implements SplitInfo {
});
}
}
+ if (columnMappingMode != null) {
+
ReadRel.LocalFiles.FileOrFiles.otherConstantMetadataColumnValues.Builder
builder =
+
ReadRel.LocalFiles.FileOrFiles.otherConstantMetadataColumnValues.newBuilder();
+ builder
+ .setKey(COLUMN_MAPPING_MODE_METADATA_KEY)
+
.setValue(SubstraitUtil.convertJavaObjectToAny(columnMappingMode.nativeName()));
+ fileBuilder.addOtherConstMetadataColumns(builder.build());
+ }
switch (fileFormat) {
case ParquetReadFormat:
diff --git
a/gluten-substrait/src/main/scala/org/apache/gluten/config/GlutenConfig.scala
b/gluten-substrait/src/main/scala/org/apache/gluten/config/GlutenConfig.scala
index 21a60b57bf..39d5acfb33 100644
---
a/gluten-substrait/src/main/scala/org/apache/gluten/config/GlutenConfig.scala
+++
b/gluten-substrait/src/main/scala/org/apache/gluten/config/GlutenConfig.scala
@@ -451,6 +451,8 @@ object GlutenConfig extends ConfigRegistry {
val SPARK_S3_AWS_IMDS_ENABLED: String = HADOOP_PREFIX + S3_AWS_IMDS_ENABLED
val ORC_FORCE_POSITIONAL_EVOLUTION = "orc.force.positional.evolution"
val SPARK_ORC_FORCE_POSITIONAL_EVOLUTION = HADOOP_PREFIX +
ORC_FORCE_POSITIONAL_EVOLUTION
+ val VELOX_PARQUET_USE_COLUMN_NAMES =
+ "spark.gluten.sql.columnar.backend.velox.parquetUseColumnNames"
// ABFS config
val ABFS_PREFIX = "fs.azure."
@@ -593,32 +595,20 @@ object GlutenConfig extends ConfigRegistry {
val confPrefixSession = prefixSessionOf(backendName)
val confPrefix = prefixOf(backendName)
+ // Column mapping mode is passed to Velox through scan splits, not through
+ // native session configs.
+ val veloxSplitColumnMappingConfigs = Set(VELOX_PARQUET_USE_COLUMN_NAMES)
conf
.filter {
case (k, _) =>
- // Backend's dynamic session conf only.
- k.startsWith(confPrefix) && !SQLConf.isStaticConfigKey(k) ||
- // put in all gluten velox configs
- k.startsWith(confPrefixSession)
+ val isBackendDynamicConf = k.startsWith(confPrefix) &&
!SQLConf.isStaticConfigKey(k)
+ val isBackendSessionConf = k.startsWith(confPrefixSession)
+ val isVeloxSplitColumnMappingConf =
+ backendName == "velox" &&
veloxSplitColumnMappingConfigs.contains(k)
+ (isBackendDynamicConf || isBackendSessionConf) &&
!isVeloxSplitColumnMappingConf
}
.foreach { case (k, v) => nativeConfMap.put(k, v) }
- // When `orc.force.positional.evolution=true`, vanilla Spark maps ORC
columns by
- // position rather than by name (see OrcUtils.requestedColumnIds). Forward
the flag to
- // the native (Velox) reader so it maps ORC/DWRF files by position too,
otherwise
- // name-based matching against a mismatched file schema reads columns back
as null/empty.
- // The native reader still decides per file (files with all-`_col*`
physical names are
- // always mapped by position). Harmless for backends that ignore this key.
- // String literal is used because gluten-substrait cannot depend on
backends-velox.
- if (
- backendName == "velox" &&
- conf.getOrElse(SPARK_ORC_FORCE_POSITIONAL_EVOLUTION, "false").toBoolean
- ) {
- nativeConfMap.put(
- "spark.gluten.sql.columnar.backend.velox.orcForcePositionalEvolution",
- "true")
- }
-
// Pass the latest tokens to native
nativeConfMap.put(
ReservedKeys.GLUTEN_UGI_TOKENS,
diff --git
a/gluten-ut/spark35/src/test/scala/org/apache/spark/sql/hive/execution/GlutenHiveSQLQuerySuite.scala
b/gluten-ut/spark35/src/test/scala/org/apache/spark/sql/hive/execution/GlutenHiveSQLQuerySuite.scala
index ddd61613ee..e17fcc70e2 100644
---
a/gluten-ut/spark35/src/test/scala/org/apache/spark/sql/hive/execution/GlutenHiveSQLQuerySuite.scala
+++
b/gluten-ut/spark35/src/test/scala/org/apache/spark/sql/hive/execution/GlutenHiveSQLQuerySuite.scala
@@ -149,6 +149,48 @@ class GlutenHiveSQLQuerySuite extends
GlutenHiveSQLQuerySuiteBase {
}
}
+ testGluten("ORC positional and Parquet name mapping can coexist in one Velox
query") {
+ val hiveClient: HiveClient =
+
spark.sharedState.externalCatalog.unwrapped.asInstanceOf[HiveExternalCatalog].client
+
+ withSQLConf(
+ "spark.sql.hive.convertMetastoreOrc" -> "false",
+ "spark.sql.hive.convertMetastoreParquet" -> "false",
+ "spark.hadoop.orc.force.positional.evolution" -> "true"
+ ) {
+ withTempDir {
+ dir =>
+ val orcLoc = dir.toPath.resolve("test_orc_pos").toUri.toString
+ val parquetLoc =
dir.toPath.resolve("test_parquet_name").toUri.toString
+ withTable(
+ "test_orc_pos",
+ "test_orc_pos_renamed",
+ "test_parquet_name",
+ "test_parquet_name_reordered") {
+ hiveClient.runSqlHive(
+ s"create table test_orc_pos(c1 int, c2 int) stored as orc
location '$orcLoc'")
+ hiveClient.runSqlHive("insert into test_orc_pos select 1, 2")
+ hiveClient.runSqlHive(
+ s"create table test_orc_pos_renamed(x int, y int) stored as orc
location '$orcLoc'")
+
+ hiveClient.runSqlHive(
+ s"create table test_parquet_name(p int, q int) stored as parquet
" +
+ s"location '$parquetLoc'")
+ hiveClient.runSqlHive("insert into test_parquet_name select 3, 4")
+ hiveClient.runSqlHive(
+ s"create table test_parquet_name_reordered(q int, p int) stored
as parquet " +
+ s"location '$parquetLoc'")
+
+ val df = sql(
+ "select o.x, o.y, p.q, p.p from test_orc_pos_renamed o " +
+ "cross join test_parquet_name_reordered p")
+ checkAnswer(df, Seq(Row(1, 2, 4, 3)))
+ checkOperatorMatch[HiveTableScanExecTransformer](df)
+ }
+ }
+ }
+ }
+
testGluten(
"GLUTEN: Hive ORC files with _col* names read by position without
positional flag") {
// Regression for the case where two ORC tables must use OPPOSITE column
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]