This is an automated email from the ASF dual-hosted git repository.
diqiu50 pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/gravitino.git
The following commit(s) were added to refs/heads/main by this push:
new 7ff63ddbfd [#13614] improvement(spark-connector): Upgrade Kyuubi Spark
Hive connector to 1.12.0 to support dynamic partition pruning (#13615)
7ff63ddbfd is described below
commit 7ff63ddbfda3bfb0f35bd71568f0fdf1ecee73be
Author: Yuhui <[email protected]>
AuthorDate: Fri Oct 9 17:44:45 2026 +0800
[#13614] improvement(spark-connector): Upgrade Kyuubi Spark Hive connector
to 1.12.0 to support dynamic partition pruning (#13615)
### What changes were proposed in this pull request?
Upgrade Kyuubi Spark Hive connector from 1.11.0 to 1.12.0, add an
integration test for dynamic partition pruning, and document the
supported table formats.
### Why are the changes needed?
Kyuubi 1.12.0 supports DPP in HiveScan, so partitioned Hive tables can
be pruned at runtime.
Fix: #13614
### Does this PR introduce _any_ user-facing change?
DPP applies to Hive tables read by the Hive SerDe reader. Parquet and
ORC tables need
`spark.sql.kyuubi.hive.connector.read.convertMetastoreParquet/Orc=false`
to use DPP.
### How was this patch tested?
- New IT `SparkHiveCatalogIT#testDynamicPartitionPruning` verifies only
the matching partition is scanned, on Spark 3.5 and 4.0.
- Existing Spark 3.5/4.0 Hive ITs and unit tests pass.
---
docs/spark-connector/spark-catalog-hive.md | 18 ++++
gradle/libs.versions.toml | 2 +-
.../integration/test/hive/SparkHiveCatalogIT.java | 98 ++++++++++++++++++++++
3 files changed, 117 insertions(+), 1 deletion(-)
diff --git a/docs/spark-connector/spark-catalog-hive.md
b/docs/spark-connector/spark-catalog-hive.md
index 3a43421df6..59d7d0d21a 100644
--- a/docs/spark-connector/spark-catalog-hive.md
+++ b/docs/spark-connector/spark-catalog-hive.md
@@ -71,6 +71,24 @@ The current implementation materializes all view results on
the Spark driver usi
SELECT * FROM employee_view;
```
+## Dynamic Partition Pruning
+
+Dynamic partition pruning (DPP) is supported for partitioned Hive tables that
are read through the Hive SerDe reader.
+
+| Table storage format | Supports DPP
|
+|-------------------------------------------|--------------------------------------------------------------------------------------------|
+| Text, Avro, CSV, JSON, SequenceFile, etc. | Yes
|
+| Parquet | Only when
`spark.sql.kyuubi.hive.connector.read.convertMetastoreParquet` is set to
`false` |
+| ORC | Only when
`spark.sql.kyuubi.hive.connector.read.convertMetastoreOrc` is set to `false`
|
+
+Both `convertMetastore*` options default to `true`, which reads Parquet and
ORC tables with Spark's native readers. The native readers are faster but don't
support DPP. Setting an option to `false` enables DPP for that format, but
reads the data with the Hive SerDe reader instead.
+
+```sql
+SET spark.sql.kyuubi.hive.connector.read.convertMetastoreParquet=false;
+```
+
+DPP only prunes partitions, and the query must also meet Spark's own
conditions for dynamic partition pruning, see
`spark.sql.optimizer.dynamicPartitionPruning.enabled` in [Spark
configuration](https://spark.apache.org/docs/latest/configuration.html#runtime-sql-configuration).
+
## Catalog Properties
Gravitino spark connector will transform below property names which are
defined in catalog properties to Spark Hive connector configuration.
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index 179e9a3953..c491cc5134 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -79,7 +79,7 @@ spark33 = "3.3.4"
spark34 = "3.4.3"
spark35 = "3.5.3"
spark40 = "4.0.3"
-kyuubi4spark = "1.11.0"
+kyuubi4spark = "1.12.0"
kyuubi4authz = "1.10.2"
trino = '435'
# Trino version the iceberg-rest-trino-it module's in-process query runner is
tested against.
diff --git
a/spark-connector/spark-common/src/test/java/org/apache/gravitino/spark/connector/integration/test/hive/SparkHiveCatalogIT.java
b/spark-connector/spark-common/src/test/java/org/apache/gravitino/spark/connector/integration/test/hive/SparkHiveCatalogIT.java
index 1d70bdd708..bca934c64e 100644
---
a/spark-connector/spark-common/src/test/java/org/apache/gravitino/spark/connector/integration/test/hive/SparkHiveCatalogIT.java
+++
b/spark-connector/spark-common/src/test/java/org/apache/gravitino/spark/connector/integration/test/hive/SparkHiveCatalogIT.java
@@ -19,11 +19,14 @@
package org.apache.gravitino.spark.connector.integration.test.hive;
import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Maps;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
+import java.util.HashSet;
import java.util.List;
+import java.util.Locale;
import java.util.Map;
import java.util.Set;
import org.apache.gravitino.NameIdentifier;
@@ -39,14 +42,24 @@ import
org.apache.gravitino.spark.connector.integration.test.util.SparkTableInfo
import
org.apache.gravitino.spark.connector.integration.test.util.SparkTableInfoChecker;
import org.apache.hadoop.fs.Path;
import org.apache.spark.sql.AnalysisException;
+import org.apache.spark.sql.Dataset;
+import org.apache.spark.sql.Row;
import org.apache.spark.sql.catalyst.analysis.PartitionsAlreadyExistException;
import org.apache.spark.sql.connector.catalog.TableCatalog;
+import org.apache.spark.sql.connector.read.InputPartition;
+import org.apache.spark.sql.execution.SparkPlan;
+import org.apache.spark.sql.execution.adaptive.AdaptiveSparkPlanExec;
+import org.apache.spark.sql.execution.adaptive.QueryStageExec;
+import org.apache.spark.sql.execution.datasources.FilePartition;
+import org.apache.spark.sql.execution.datasources.PartitionedFile;
+import org.apache.spark.sql.execution.datasources.v2.BatchScanExec;
import org.apache.spark.sql.types.DataTypes;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Tag;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;
+import scala.collection.JavaConverters;
@Tag("gravitino-docker-test")
public abstract class SparkHiveCatalogIT extends SparkCommonIT {
@@ -693,4 +706,89 @@ public abstract class SparkHiveCatalogIT extends
SparkCommonIT {
dropTableIfExists(tableName);
}
}
+
+ @ParameterizedTest
+ @ValueSource(strings = {"TEXTFILE", "PARQUET"})
+ void testDynamicPartitionPruning(String format) {
+ String factTable = "dpp_fact_" + format.toLowerCase(Locale.ROOT);
+ String dimTable = "dpp_dim_" + format.toLowerCase(Locale.ROOT);
+ // Parquet tables are read by Spark's native ParquetScan unless the
conversion is disabled,
+ // and only the Kyuubi HiveScan supports runtime filtering.
+ String convertParquetConf =
"spark.sql.kyuubi.hive.connector.read.convertMetastoreParquet";
+ getSparkSession().conf().set(convertParquetConf, "false");
+
+ dropTableIfExists(factTable);
+ dropTableIfExists(dimTable);
+ try {
+ sql(
+ String.format(
+ "CREATE TABLE %s (id INT, amount INT) PARTITIONED BY (dt STRING)
STORED AS %s",
+ factTable, format));
+ sql(String.format("CREATE TABLE %s (dt STRING, flag STRING) STORED AS
%s", dimTable, format));
+ sql(
+ String.format(
+ "INSERT INTO %s VALUES (1, 10, '2026-01-01'), (2, 20,
'2026-01-02'), "
+ + "(3, 30, '2026-01-03')",
+ factTable));
+ sql(
+ String.format(
+ "INSERT INTO %s VALUES ('2026-01-01', 'n'), ('2026-01-02', 'y'),
"
+ + "('2026-01-03', 'n')",
+ dimTable));
+
+ Dataset<Row> df =
+ getSparkSession()
+ .sql(
+ String.format(
+ "SELECT f.id, f.amount FROM %s f JOIN %s d ON f.dt =
d.dt WHERE d.flag = 'y'",
+ factTable, dimTable));
+ List<Row> rows = df.collectAsList();
+ Assertions.assertEquals(1, rows.size());
+ Assertions.assertEquals(2, rows.get(0).getInt(0));
+ Assertions.assertEquals(20, rows.get(0).getInt(1));
+
+ String plan = df.queryExecution().executedPlan().toString();
+ Assertions.assertTrue(
+ plan.toLowerCase(Locale.ROOT).contains("dynamicpruningexpression"),
+ "Expected dynamic partition pruning in plan:\n" + plan);
+
+ // Spark pushes the runtime filters into the scan before planning input
partitions, so
+ // re-planning the executed scan returns only the files of the pruned
partitions.
+ BatchScanExec factScan =
findBatchScan(df.queryExecution().executedPlan(), factTable);
+ Assertions.assertNotNull(factScan, "No BatchScanExec for " + factTable +
" in:\n" + plan);
+ Set<String> scannedPartitions = new HashSet<>();
+ for (InputPartition partition :
factScan.scan().toBatch().planInputPartitions()) {
+ for (PartitionedFile file : ((FilePartition) partition).files()) {
+
scannedPartitions.add(file.partitionValues().getUTF8String(0).toString());
+ }
+ }
+ Assertions.assertEquals(ImmutableSet.of("2026-01-02"),
scannedPartitions);
+ } finally {
+ getSparkSession().conf().unset(convertParquetConf);
+ dropTableIfExists(factTable);
+ dropTableIfExists(dimTable);
+ }
+ }
+
+ // Walks through adaptive wrappers and query stages, which are leaf nodes in
the final plan.
+ @SuppressWarnings("deprecation")
+ private BatchScanExec findBatchScan(SparkPlan plan, String tableName) {
+ if (plan instanceof AdaptiveSparkPlanExec) {
+ return findBatchScan(((AdaptiveSparkPlanExec) plan).executedPlan(),
tableName);
+ }
+ if (plan instanceof QueryStageExec) {
+ return findBatchScan(((QueryStageExec) plan).plan(), tableName);
+ }
+ if (plan instanceof BatchScanExec
+ && ((BatchScanExec) plan).table().name().contains(tableName)) {
+ return (BatchScanExec) plan;
+ }
+ for (SparkPlan child : JavaConverters.seqAsJavaList(plan.children())) {
+ BatchScanExec scan = findBatchScan(child, tableName);
+ if (scan != null) {
+ return scan;
+ }
+ }
+ return null;
+ }
}