This is an automated email from the ASF dual-hosted git repository.
yihua pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/hudi.git
The following commit(s) were added to refs/heads/master by this push:
new 308f78e71dea fix(spark): skip the catalog lookup in HoodieFileIndex
unless the read declares a bucket index (#20240)
308f78e71dea is described below
commit 308f78e71deabf7a9eec4b0a2ea26b1360658030
Author: Y Ethan Guo <[email protected]>
AuthorDate: Thu Oct 8 21:52:11 2026 -0700
fix(spark): skip the catalog lookup in HoodieFileIndex unless the read
declares a bucket index (#20240)
---
.../scala/org/apache/hudi/HoodieFileIndex.scala | 34 ++++++---
.../sql/hudi/common/TestHoodieFileIndex.scala | 85 +++++++++++++++++++++-
2 files changed, 109 insertions(+), 10 deletions(-)
diff --git
a/hudi-spark-datasource/hudi-spark-common/src/main/scala/org/apache/hudi/HoodieFileIndex.scala
b/hudi-spark-datasource/hudi-spark-common/src/main/scala/org/apache/hudi/HoodieFileIndex.scala
index 003c571acf16..eafff4e8ddb8 100644
---
a/hudi-spark-datasource/hudi-spark-common/src/main/scala/org/apache/hudi/HoodieFileIndex.scala
+++
b/hudi-spark-datasource/hudi-spark-common/src/main/scala/org/apache/hudi/HoodieFileIndex.scala
@@ -24,10 +24,12 @@ import org.apache.hudi.common.config.{HoodieConfig,
HoodieMetadataConfig, TypedP
import
org.apache.hudi.common.config.TimestampKeyGeneratorConfig.{TIMESTAMP_INPUT_DATE_FORMAT,
TIMESTAMP_OUTPUT_DATE_FORMAT}
import org.apache.hudi.common.model.{FileSlice, HoodieBaseFile, HoodieLogFile}
import org.apache.hudi.common.table.{HoodieTableConfig, HoodieTableMetaClient}
-import org.apache.hudi.common.util.StringUtils
+import org.apache.hudi.common.util.{ConfigUtils, StringUtils}
+import org.apache.hudi.config.HoodieIndexConfig
import org.apache.hudi.core.read.BaseHoodieTableFileIndex
import org.apache.hudi.core.read.BaseHoodieTableFileIndex.PartitionPath
import org.apache.hudi.exception.HoodieException
+import org.apache.hudi.index.HoodieIndex.IndexType
import org.apache.hudi.index.bucket.partition.PartitionBucketIndexUtils
import org.apache.hudi.keygen.{TimestampBasedAvroKeyGenerator,
TimestampBasedKeyGenerator}
import org.apache.hudi.storage.{StoragePath, StoragePathInfo}
@@ -655,14 +657,21 @@ object HoodieFileIndex extends Logging {
properties.setProperty(PARTITIONPATH_FIELD.key,
HoodieTableConfig.getPartitionFieldPropForKeyGenerator(tableConfig).orElse(""))
properties.setProperty(HoodieTableConfig.PARTITION_EXTRACTOR_CLASS.key(),
tableConfig.getPartitionExtractorClass.orElse(""))
- // for simple bucket index, we need to set the INDEX_TYPE,
BUCKET_INDEX_HASH_FIELD, BUCKET_INDEX_NUM_BUCKETS
- val database = getDatabaseName(tableConfig,
spark.catalog.currentDatabase)
- val tableName = tableConfig.getTableName
-
- if (spark.catalog.tableExists(database, tableName)) {
- val tableIdentifier = TableIdentifier(tableName, Some(database))
- val table = HoodieCatalogTable(spark, tableIdentifier)
- table.catalogProperties.foreach(kv => properties.setProperty(kv._1,
kv._2))
+ // The catalog entry only completes the simple bucket index options,
which hoodie.properties does not record,
+ // so it is consulted only when the read declares a bucket index and
only those options are copied.
+ if (isBucketIndexDeclared(properties)) {
+ val database = getDatabaseName(tableConfig,
spark.catalog.currentDatabase)
+ val tableName = tableConfig.getTableName
+ try {
+ if (spark.catalog.tableExists(database, tableName)) {
+ val table = HoodieCatalogTable(spark, TableIdentifier(tableName,
Some(database)))
+ table.catalogProperties.filter { case (key, _) =>
bucketIndexCatalogKeys.contains(key) }
+ .foreach(kv => properties.setProperty(kv._1, kv._2))
+ }
+ } catch {
+ case NonFatal(e) =>
+ logWarning(s"Skipping catalog properties for table $tableName:
lookup of $database.$tableName failed", e)
+ }
}
}
@@ -721,6 +730,13 @@ object HoodieFileIndex extends Logging {
Seq(new StoragePath(path))
}
+ private val bucketIndexCatalogKeys: Set[String] = Set(
+ HoodieIndexConfig.INDEX_TYPE, HoodieIndexConfig.BUCKET_INDEX_ENGINE_TYPE,
HoodieIndexConfig.BUCKET_INDEX_HASH_FIELD,
+ HoodieIndexConfig.BUCKET_INDEX_NUM_BUCKETS,
HoodieIndexConfig.BUCKET_QUERY_INDEX).map(_.key)
+
+ private def isBucketIndexDeclared(properties: TypedProperties): Boolean =
+
IndexType.BUCKET.name.equalsIgnoreCase(ConfigUtils.getStringWithAltKeys(properties,
HoodieIndexConfig.INDEX_TYPE, ""))
+
// if database name is not set, fall back to use 'default' instead of failing
def getDatabaseName(tableConfig: HoodieTableConfig, defaultDatabase: String)
= {
if (StringUtils.isNullOrEmpty(tableConfig.getDatabaseName)) {
diff --git
a/hudi-spark-datasource/hudi-spark-common/src/test/scala/org/apache/spark/sql/hudi/common/TestHoodieFileIndex.scala
b/hudi-spark-datasource/hudi-spark-common/src/test/scala/org/apache/spark/sql/hudi/common/TestHoodieFileIndex.scala
index 37e23508da79..697b73b287dc 100644
---
a/hudi-spark-datasource/hudi-spark-common/src/test/scala/org/apache/spark/sql/hudi/common/TestHoodieFileIndex.scala
+++
b/hudi-spark-datasource/hudi-spark-common/src/test/scala/org/apache/spark/sql/hudi/common/TestHoodieFileIndex.scala
@@ -21,9 +21,24 @@ package org.apache.spark.sql.hudi.common
import org.apache.hudi.HoodieFileIndex
import org.apache.hudi.common.table.HoodieTableConfig
+import org.apache.hudi.config.HoodieIndexConfig.{BUCKET_INDEX_NUM_BUCKETS,
INDEX_TYPE}
+import org.apache.hudi.index.HoodieIndex.IndexType
-import org.junit.jupiter.api.Assertions.assertEquals
+import org.apache.hadoop.conf.Configuration
+import org.apache.spark.sql.SparkSession
+import org.apache.spark.sql.catalog.Catalog
+import org.apache.spark.sql.catalyst.TableIdentifier
+import org.apache.spark.sql.catalyst.catalog.{CatalogStorageFormat,
CatalogTable, CatalogTableType, SessionCatalog}
+import org.apache.spark.sql.internal.{SessionState, SQLConf}
+import org.apache.spark.sql.types.StructType
+import org.junit.jupiter.api.Assertions.{assertEquals, assertFalse}
import org.junit.jupiter.api.Test
+import org.junit.jupiter.api.io.TempDir
+import org.mockito.ArgumentMatchers.anyString
+import org.mockito.Mockito.{mock, never, verify, when}
+
+import java.net.URI
+import java.nio.file.Path
class TestHoodieFileIndex {
@Test
@@ -31,4 +46,72 @@ class TestHoodieFileIndex {
assertEquals("default", HoodieFileIndex.getDatabaseName(new
HoodieTableConfig(), null))
assertEquals("default_db", HoodieFileIndex.getDatabaseName(new
HoodieTableConfig(), "default_db"))
}
+
+ @Test
+ def testCatalogIsNotConsultedWithoutBucketIndex(): Unit = {
+ val (spark, catalog, _) = mockSpark()
+ when(catalog.tableExists(anyString(), anyString()))
+ .thenThrow(new IllegalStateException("database default is not allowed"))
+
+ val props = HoodieFileIndex.getConfigProperties(spark, Map.empty,
tableConfig("tbl"))
+
+ verify(catalog, never()).tableExists(anyString(), anyString())
+ assertFalse(props.containsKey(BUCKET_INDEX_NUM_BUCKETS.key))
+ }
+
+ @Test
+ def testCatalogPropertiesMergedWhenBucketIndexDeclared(@TempDir dir: Path):
Unit = {
+ val (spark, catalog, sessionCatalog) = mockSpark()
+ val id = TableIdentifier("tbl", Some("default"))
+ when(catalog.tableExists("default", "tbl")).thenReturn(true)
+
when(sessionCatalog.getTableMetadata(id)).thenReturn(bucketCatalogTable(id,
dir))
+ val options = Map(INDEX_TYPE.key -> IndexType.BUCKET.name)
+
+ val props = HoodieFileIndex.getConfigProperties(spark, options,
tableConfig("tbl"))
+
+ assertEquals("3", props.getProperty(BUCKET_INDEX_NUM_BUCKETS.key))
+ assertFalse(props.containsKey("preCombineField"))
+ verify(catalog).tableExists("default", "tbl")
+ }
+
+ @Test
+ def testCatalogFailureWithBucketIndexDeclaredDoesNotFailRead(): Unit = {
+ val (spark, catalog, _) = mockSpark()
+ when(catalog.tableExists(anyString(), anyString()))
+ .thenThrow(new IllegalStateException("database default is not allowed"))
+ val options = Map(INDEX_TYPE.key -> IndexType.BUCKET.name)
+
+ val props = HoodieFileIndex.getConfigProperties(spark, options,
tableConfig("tbl"))
+
+ verify(catalog).tableExists("default", "tbl")
+ assertFalse(props.containsKey(BUCKET_INDEX_NUM_BUCKETS.key))
+ }
+
+ private def tableConfig(name: String): HoodieTableConfig = {
+ val config = new HoodieTableConfig()
+ config.setValue(HoodieTableConfig.NAME, name)
+ config
+ }
+
+ private def bucketCatalogTable(id: TableIdentifier, location: Path):
CatalogTable = CatalogTable(
+ identifier = id,
+ tableType = CatalogTableType.EXTERNAL,
+ storage = CatalogStorageFormat.empty.copy(locationUri = Some(new
URI("file:" + location.toAbsolutePath))),
+ schema = new StructType(),
+ provider = Some("hudi"),
+ properties = Map(BUCKET_INDEX_NUM_BUCKETS.key -> "3", "preCombineField" ->
"ts"))
+
+ private def mockSpark(): (SparkSession, Catalog, SessionCatalog) = {
+ val spark = mock(classOf[SparkSession])
+ val sessionState = mock(classOf[SessionState])
+ val catalog = mock(classOf[Catalog])
+ val sessionCatalog = mock(classOf[SessionCatalog])
+ when(spark.sessionState).thenReturn(sessionState)
+ when(sessionState.conf).thenReturn(new SQLConf)
+ when(sessionState.newHadoopConf()).thenReturn(new Configuration())
+ when(sessionState.catalog).thenReturn(sessionCatalog)
+ when(spark.catalog).thenReturn(catalog)
+ when(catalog.currentDatabase).thenReturn("default")
+ (spark, catalog, sessionCatalog)
+ }
}