This is an automated email from the ASF dual-hosted git repository.

cloud-fan pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/spark.git


The following commit(s) were added to refs/heads/master by this push:
     new 7e87aa545a43 [SPARK-58849][SQL] Make DESCRIBE TABLE resilient to 
corrupt partition metadata
7e87aa545a43 is described below

commit 7e87aa545a431efd4290c7cdb8b9592ddfd82954
Author: Szehon Ho <[email protected]>
AuthorDate: Fri Aug 21 12:52:26 2026 +0800

    [SPARK-58849][SQL] Make DESCRIBE TABLE resilient to corrupt partition 
metadata
    
    ### What changes were proposed in this pull request?
    
    Make `DESCRIBE TABLE` tolerate inconsistent partition metadata. When a 
table's declared partition
    columns do not match the last fields of the schema being described, the 
text output replaces the
    usual `# Partition Information` section with an `# Invalid Partition 
Information` section that
    lists both the declared partition columns and the last columns of the table 
schema, so neither set
    is presented as the valid one.
    
    The condition is checked explicitly instead of catching the 
`AssertionError` raised by
    `CatalogTable.partitionSchema`. The check runs against the schema that is 
actually described (for a
    legacy table with an empty stored schema, that is the inferred one) and 
compares names with
    `conf.resolver` rather than exact string equality.
    
    `DESCRIBE TABLE ... AS JSON` is deliberately left unchanged. Its 
`partition_columns` field comes
    from `CatalogTable.toJsonLinkedHashMap`, which reads `partitionColumnNames` 
directly and never
    evaluates the assertion, so that path already worked for corrupt metadata 
and keeps reporting the
    declared partition columns.
    
    The following paths still use the strict `partitionSchema` assertion and 
are intentionally out of
    scope:
    
    - `DESC ... PARTITION`, `SHOW TABLE EXTENDED ... PARTITION`, `SHOW 
PARTITIONS ... PARTITION`, and
      `SHOW CREATE TABLE ... AS SERDE`. Unfiltered `SHOW PARTITIONS` and plain 
`SHOW CREATE TABLE` do
      not call `partitionSchema` for this mismatch.
    - `DataSource.resolveRelation`, which calls `partitionSchema` while 
inferring the schema of a
      legacy table whose stored schema is empty. `DESCRIBE TABLE` on such a 
partitioned table therefore
      still fails, inside schema inference rather than in the partition section.
    
    Replacing that assertion with a structured Spark error can be handled 
separately.
    
    ### Why are the changes needed?
    
    `DescribeTableCommand.describePartitionInfo` called 
`CatalogTable.partitionSchema`, which asserts
    that the schema's last fields match `partitionColumnNames`. Corrupt catalog 
metadata therefore made
    `DESCRIBE TABLE` itself fail with an `AssertionError`, preventing users 
from inspecting the
    remaining metadata that would help them diagnose or repair the table.
    
    ### Does this PR introduce _any_ user-facing change?
    
    Yes. Previously `DESCRIBE TABLE` failed with an `AssertionError` when the 
declared partition columns
    did not match the last columns of the schema. It now returns the rest of 
the table information and
    reports both column lists under an `# Invalid Partition Information` 
section. `DESCRIBE TABLE ... AS
    JSON` output is unchanged.
    
    ### How was this patch tested?
    
    Added `DESCRIBE TABLE is resilient to corrupt partition metadata` to
    `org.apache.spark.sql.execution.command.v1.DescribeTableSuite`, covering 
plain `DESCRIBE TABLE`,
    `DESCRIBE TABLE EXTENDED`, and asserting that `DESCRIBE TABLE EXTENDED ... 
AS JSON` still reports
    `partition_columns` unchanged.
    
    Run locally, with no failures:
    
    - `build/sbt 'sql/testOnly *DescribeTableSuite'` -- 106 tests, covering the 
V1 and V2 suites with
      each test run for both the V1 and V2 command versions.
    - `build/sbt 'hive/testOnly *command.DescribeTableSuite'` -- 68 tests.
    
    The fixture stays in the V1 in-memory suite rather than moving up to 
`DescribeTableSuiteBase`,
    which the Hive suite also extends. Under the Hive catalog the deliberately 
malformed `CatalogTable`
    cannot be read back at all: `HiveExternalCatalog.restoreHiveSerdeTable` 
calls `reorderSchema`, which
    resolves each declared partition column by name and throws 
`AnalysisException`
    `_LEGACY_ERROR_TEMP_3088` ("The metadata is corrupted. Unable to find the 
partition column names
    from the schema") when one is absent from the schema. The table therefore 
fails before
    `DESCRIBE TABLE` can inspect it.
    
    ### Was this patch authored or co-authored using generative AI tooling?
    
    Generated-by: OpenAI Codex (GPT-5).
    
    Closes #58092 from szehon-ho/codex/es-1878719-hardening.
    
    Authored-by: Szehon Ho <[email protected]>
    Signed-off-by: Wenchen Fan <[email protected]>
---
 docs/sql-ref-syntax-aux-describe-table.md          | 10 ++++++
 .../spark/sql/execution/command/tables.scala       | 36 +++++++++++++++-----
 .../execution/command/v1/DescribeTableSuite.scala  | 38 +++++++++++++++++++++-
 3 files changed, 75 insertions(+), 9 deletions(-)

diff --git a/docs/sql-ref-syntax-aux-describe-table.md 
b/docs/sql-ref-syntax-aux-describe-table.md
index 5c417689dde2..e2db0ab3d88c 100644
--- a/docs/sql-ref-syntax-aux-describe-table.md
+++ b/docs/sql-ref-syntax-aux-describe-table.md
@@ -164,6 +164,16 @@ to return the metadata pertaining to a partition or column 
respectively.
 | MapType               | `{ "name" : "map", "key_type": <type_json>, 
"value_type": <type_json>, "value_nullable": <boolean> }`                       
                                     |
 | StructType            | `{ "name" : "struct", "fields": [ {"name" : 
"field1", "type" : <type_json>, “nullable”: <boolean>, "comment": “<comment>”, 
"default": “<default_val>”}, ... ] }` |
 
+**Note**
+- If a V1 (session catalog) table's declared partition columns do not match 
the last columns
+  of its schema, the catalog metadata is considered inconsistent. `DESCRIBE 
TABLE` still
+  returns the rest of the metadata and reports both column lists under an
+  `# Invalid Partition Information` section in place of `# Partition 
Information`. Repair
+  the table metadata so the declared partition columns match the last columns 
of the
+  schema. Until then, commands that inspect a specific partition may still 
fail:
+  `DESCRIBE TABLE ... PARTITION`, `SHOW TABLE EXTENDED ... PARTITION`, and
+  `SHOW PARTITIONS ... PARTITION`.
+
 ### Examples
 
 ```sql
diff --git 
a/sql/core/src/main/scala/org/apache/spark/sql/execution/command/tables.scala 
b/sql/core/src/main/scala/org/apache/spark/sql/execution/command/tables.scala
index df050742ec66..4d513b4d3b06 100644
--- 
a/sql/core/src/main/scala/org/apache/spark/sql/execution/command/tables.scala
+++ 
b/sql/core/src/main/scala/org/apache/spark/sql/execution/command/tables.scala
@@ -654,15 +654,16 @@ case class DescribeTableCommand(
       }
       describeSchema(metadata.schema, result, header = false)
     } else {
-      if (metadata.schema.isEmpty) {
+      val schema = if (metadata.schema.isEmpty) {
         // In older version(prior to 2.1) of Spark, the table schema can be 
empty and should be
         // inferred at runtime. We should still support it.
-        describeSchema(sparkSession.table(metadata.identifier).schema, result, 
header = false)
+        sparkSession.table(metadata.identifier).schema
       } else {
-        describeSchema(metadata.schema, result, header = false)
+        metadata.schema
       }
+      describeSchema(schema, result, header = false)
 
-      describePartitionInfo(metadata, result)
+      describePartitionInfo(metadata, schema, result)
       describeClusteringInfo(metadata, result)
 
       if (partitionSpec.nonEmpty) {
@@ -682,10 +683,29 @@ case class DescribeTableCommand(
     result.toSeq
   }
 
-  private def describePartitionInfo(table: CatalogTable, buffer: 
ArrayBuffer[Row]): Unit = {
-    if (table.partitionColumnNames.nonEmpty) {
-      append(buffer, "# Partition Information", "", "")
-      describeSchema(table.partitionSchema, buffer, header = true)
+  private def describePartitionInfo(
+      table: CatalogTable,
+      schema: StructType,
+      buffer: ArrayBuffer[Row]): Unit = {
+    val partitionColumnNames = table.partitionColumnNames
+    if (partitionColumnNames.nonEmpty) {
+      // Same positional convention as `CatalogTable.partitionSchema`, but 
reported instead of
+      // asserted so that a table with inconsistent metadata stays describable.
+      val partitionFields = schema.takeRight(partitionColumnNames.length)
+      val consistent = partitionFields.length == partitionColumnNames.length &&
+        partitionFields.map(_.name).zip(partitionColumnNames).forall {
+          case (schemaColumn, partitionColumn) => conf.resolver(schemaColumn, 
partitionColumn)
+        }
+      if (consistent) {
+        append(buffer, "# Partition Information", "", "")
+        describeSchema(StructType(partitionFields), buffer, header = true)
+      } else {
+        append(buffer, "# Invalid Partition Information", "", "")
+        append(buffer, "Declared Partition Columns",
+          partitionColumnNames.mkString("[", ", ", "]"), "")
+        append(buffer, "Last Columns in Table Schema",
+          partitionFields.map(_.name).mkString("[", ", ", "]"), "")
+      }
     }
   }
 
diff --git 
a/sql/core/src/test/scala/org/apache/spark/sql/execution/command/v1/DescribeTableSuite.scala
 
b/sql/core/src/test/scala/org/apache/spark/sql/execution/command/v1/DescribeTableSuite.scala
index 97fa08fef3a6..4e2c4a4c6975 100644
--- 
a/sql/core/src/test/scala/org/apache/spark/sql/execution/command/v1/DescribeTableSuite.scala
+++ 
b/sql/core/src/test/scala/org/apache/spark/sql/execution/command/v1/DescribeTableSuite.scala
@@ -24,11 +24,13 @@ import org.json4s.jackson.JsonMethods.parse
 
 import org.apache.spark.SPARK_VERSION
 import org.apache.spark.sql.{AnalysisException, QueryTest, Row}
+import org.apache.spark.sql.catalyst.TableIdentifier
+import org.apache.spark.sql.catalyst.catalog.{CatalogStorageFormat, 
CatalogTable, CatalogTableType}
 import 
org.apache.spark.sql.connector.catalog.CatalogManager.SESSION_CATALOG_NAME
 import org.apache.spark.sql.execution.command
 import org.apache.spark.sql.execution.command.{DescribeTableJson, Field, 
SqlPathEntry, TableColumn, Type}
 import org.apache.spark.sql.internal.SQLConf
-import org.apache.spark.sql.types.StringType
+import org.apache.spark.sql.types.{IntegerType, StringType, StructType}
 
 /**
  * This base suite contains unified tests for the `DESCRIBE TABLE` command 
that checks V1
@@ -874,6 +876,40 @@ class DescribeTableSuite extends DescribeTableSuiteBase 
with CommandSuiteBase {
     }
   }
 
+  test("DESCRIBE TABLE is resilient to corrupt partition metadata") {
+    withNamespaceAndTable("ns", "table") { tbl =>
+      // Simulate corrupt metadata where the declared partition column does 
not match the
+      // last field in the table schema.
+      val table = CatalogTable(
+        identifier = TableIdentifier("table", Some("ns")),
+        tableType = CatalogTableType.MANAGED,
+        storage = CatalogStorageFormat.empty,
+        schema = new StructType()
+          .add("id", IntegerType)
+          .add("actual_part", StringType),
+        provider = Some(getProvider()),
+        partitionColumnNames = Seq("declared_part"))
+      spark.sessionState.catalog.createTable(table, ignoreIfExists = false)
+
+      val expectedInvalidInfo = Seq(
+        Row("# Invalid Partition Information", "", ""),
+        Row("Declared Partition Columns", "[declared_part]", ""),
+        Row("Last Columns in Table Schema", "[actual_part]", ""))
+
+      Seq("DESCRIBE TABLE", "DESCRIBE TABLE EXTENDED").foreach { command =>
+        val description = spark.sql(s"$command $tbl").collect().toSeq
+        assert(description.contains(Row("id", "int", null)))
+        assert(description.contains(Row("actual_part", "string", null)))
+        assert(description.containsSlice(expectedInvalidInfo))
+        assert(!description.exists(_.getString(0) == "# Partition 
Information"))
+      }
+
+      val jsonValue = spark.sql(s"DESCRIBE TABLE EXTENDED $tbl AS 
JSON").head().getString(0)
+      val parsedOutput = parse(jsonValue).extract[DescribeTableJson]
+      assert(parsedOutput.partition_columns === Some(List("declared_part")))
+    }
+  }
+
   test("DESCRIBE TABLE EXTENDED of a table with a default column value") {
     withTable("t") {
       spark.sql(s"CREATE TABLE t (id bigint default 42) $defaultUsing")


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to