Skip to content

Commit 3a685b1

Browse files
committed
[SPARK-56678][SQL] Use structured Catalog/Namespace/Table rows in DESCRIBE TABLE EXTENDED for v2 tables and views
### What changes were proposed in this pull request? Standardize the `# Detailed Table Information` / `# Detailed View Information` block in `DESCRIBE TABLE EXTENDED` output for v2 tables and views to emit structured rows derived from the resolved identifier: - For tables (`DescribeTableExec`): the single `Name` row that came from `Table.name()` is replaced by `Catalog`, `Namespace`, `Database`, and `Table`. - For views (`DescribeV2ViewExec`): the `Catalog` + `Identifier` pair (where `Identifier` was a single string concatenating namespace and name with `.`) is replaced by `Catalog`, `Namespace`, `Database`, and `View`. The catalog name and resolved `Identifier` are threaded from `ResolvedTable` / `ResolvedPersistentView` through the v2 execs. `DescribeTablePartitionExec` is updated to pass the catalog name to the inner `DescribeTableExec` it constructs for the schema/partition header. The `Namespace` row uses `Identifier.namespace().quoted` — dot-separated, with back-tick quoting only on segments that need it — matching the existing Spark convention for multi-segment namespaces. This keeps the row round-trip-safe for namespaces with dots in segments while staying readable for the common single-level case. #### `Database` row for v1 compatibility v1 `DescribeTableCommand` (via `CatalogTable.toJsonLinkedHashMap`) emits `Catalog` / `Database` / `Table` rows, where `Database` is the single-string `database` field of `TableIdentifier`. To keep DESCRIBE consumers that read the `Database` row working uniformly across v1 (HMS) and v2, this PR also emits a `Database` row in the v2 output. The row is **always present**: - For a single-segment namespace, `Database` is that single segment (matches v1 exactly). - For a multi-segment namespace, `Database` is the trailing segment — multi-segment namespaces still surface their leaf segment under the v1-compat row, while consumers that need the full namespace read `Namespace`. - For a root-level entity (empty namespace), `Database` is the empty string. The row is still emitted so the layout is uniform across all v2 namespaces. `Database` alone is not round-trip-safe for multi-segment cases; `Namespace` is the canonical v2 representation. ### Why are the changes needed? In a multi-catalog deployment, the catalog name is a first-class part of a v2 table or view identifier. The previous output buried it inside connector-controlled strings: - `Table.name()` for tables is connector-defined; some connectors return `catalog.namespace.name`, others just `namespace.name`, others use a custom format. The result is that `DESCRIBE TABLE` output looks different across catalogs even for the same logical table shape. - `Identifier` for v2 views collapsed namespace and name into a single dotted string, so consumers had to parse the dot back out and could not unambiguously round-trip multi-level namespaces with dots in segments. Splitting the components into `Catalog`, `Namespace`, `Database`, and `Table` / `View` rows: - gives `DESCRIBE TABLE EXTENDED` a uniform shape across v2 connectors, - makes the catalog name explicit and surfaceable when multiple v2 catalogs are configured, - handles multi-level namespaces naturally via `Identifier.namespace().quoted`, - aligns the table and view sections so consumers can read the same rows from either, switching only on the section header (`# Detailed Table Information` vs `# Detailed View Information`), - with the always-emitted `Database` compatibility row, lets consumers built for v1 (HMS) keep working without changes, - is parseable programmatically without splitting strings. ### Does this PR introduce any user-facing change? Yes, slight output change in `DESCRIBE TABLE EXTENDED` for v2 tables and v2 views. For v2 tables, single-segment namespace (most common): - Before: `Name | testcat.ns.t | ` - After: `Catalog | testcat | `, `Namespace | ns | `, `Database | ns | `, `Table | t | `. For v2 tables, multi-segment namespace: - Before: `Name | testcat.ns1.ns2.t | ` - After: `Catalog | testcat | `, `Namespace | ns1.ns2 | `, `Database | ns2 | `, `Table | t | `. For v2 views, single-segment namespace: - Before: `Catalog | testcat | `, `Identifier | ns.v | ` - After: `Catalog | testcat | `, `Namespace | ns | `, `Database | ns | `, `View | v | `. For v2 views, multi-segment namespace: - Before: `Catalog | testcat | `, `Identifier | ns1.ns2.v | ` - After: `Catalog | testcat | `, `Namespace | ns1.ns2 | `, `Database | ns2 | `, `View | v | `. v1 paths (session-catalog tables and views via HMS) are unchanged. Tools that read DESCRIBE output should switch from concatenating `Name` / `Identifier` to reading the structured rows. ### How was this patch tested? - Updated the affected golden assertion in `DescribeTableSuite` (`DESCRIBE TABLE EXTENDED of a partitioned table`) to match the new row layout including the `Database` compatibility row. - Added focused tests in v2 `DescribeTableSuite` pinning the structured rows on a freshly created v2 table for both single-segment (`ns`) and multi-segment (`ns1.ns2`) namespaces — the multi-segment test pins that `Database` carries the trailing segment while `Namespace` carries the full dot-joined form. - Added parallel tests in v2 `DescribeViewSuite` pinning the same layout for v2 views (single-segment and multi-segment). - Removed the now-redundant `DESCRIBE TABLE EXTENDED on a non-view MetadataTable shows the real identifier` test in `DataSourceV2MetadataTableSuite` (the structured-row layout is what's pinned by the new tests in `v2.DescribeTableSuite`; the identifier-passthrough behavior is no longer tied to `MetadataTable.name()`). Ran: build/sbt 'sql/testOnly \ org.apache.spark.sql.execution.command.v2.DescribeTableSuite \ org.apache.spark.sql.execution.command.v2.DescribeViewSuite \ org.apache.spark.sql.connector.DataSourceV2MetadataTableSuite \ org.apache.spark.sql.connector.DataSourceV2MetadataViewSuite' ### Was this patch authored or co-authored using generative AI tooling? Generated-by: Claude Opus 4.7 Closes #55625 from cloud-fan/describe-table-view-structured-rows. Authored-by: Wenchen Fan <wenchen@databricks.com> Signed-off-by: Wenchen Fan <wenchen@databricks.com>
1 parent 520835a commit 3a685b1

9 files changed

Lines changed: 239 additions & 119 deletions

File tree

sql/catalyst/src/main/java/org/apache/spark/sql/connector/catalog/MetadataTable.java

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -50,9 +50,12 @@ public class MetadataTable implements Table {
5050

5151
/**
5252
* @param info metadata for the table or view. Pass a {@link ViewInfo} for a view.
53-
* @param name human-readable name for this table, used by places that read {@link #name()}
54-
* (e.g. the {@code Name} row of {@code DESCRIBE TABLE EXTENDED}). Catalogs
55-
* returning a {@code MetadataTable} from {@link TableCatalog#loadTable} or
53+
* @param name human-readable name for this table, returned by {@link #name()} and surfaced
54+
* in places that read it (e.g. {@code BatchScan} plan-tree labels and
55+
* partition-management error messages). {@code DESCRIBE TABLE EXTENDED} does
56+
* not read this field; it emits the resolved identifier as structured
57+
* {@code Catalog} / {@code Namespace} / {@code Table} rows. Catalogs returning
58+
* a {@code MetadataTable} from {@link TableCatalog#loadTable} or
5659
* {@link TableViewCatalog#loadTableOrView} should typically pass
5760
* {@code ident.toString()}, matching the quoted multi-part form used elsewhere
5861
* for v2 identifiers.

sql/core/src/main/scala/org/apache/spark/sql/execution/datasources/v2/DataSourceV2Strategy.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -547,7 +547,7 @@ class DataSourceV2Strategy(session: SparkSession) extends Strategy with Predicat
547547
DescribeNamespaceExec(output, catalog.asNamespaceCatalog, ns, extended) :: Nil
548548

549549
case DescribeRelation(r: ResolvedTable, isExtended, output) =>
550-
DescribeTableExec(output, r.table, isExtended) :: Nil
550+
DescribeTableExec(output, r.catalog.name(), r.identifier, r.table, isExtended) :: Nil
551551

552552
case DescribeTablePartition(r: ResolvedTable, part, isExtended, output) =>
553553
DescribeTablePartitionExec(output, r.table.asPartitionable, r.identifier,

sql/core/src/main/scala/org/apache/spark/sql/execution/datasources/v2/DescribeTableExec.scala

Lines changed: 139 additions & 82 deletions
Original file line numberDiff line numberDiff line change
@@ -24,17 +24,153 @@ import org.apache.spark.sql.catalyst.InternalRow
2424
import org.apache.spark.sql.catalyst.catalog.{CatalogTableType, ClusterBySpec}
2525
import org.apache.spark.sql.catalyst.expressions.Attribute
2626
import org.apache.spark.sql.catalyst.util.{quoteIfNeeded, ResolveDefaultColumns}
27-
import org.apache.spark.sql.connector.catalog.{CatalogV2Util, SupportsMetadataColumns, SupportsRead, Table, TableCatalog}
27+
import org.apache.spark.sql.connector.catalog.{CatalogV2Util, Identifier, SupportsMetadataColumns, SupportsRead, Table, TableCatalog}
28+
import org.apache.spark.sql.connector.catalog.CatalogV2Implicits.NamespaceHelper
2829
import org.apache.spark.sql.connector.expressions.{ClusterByTransform, IdentityTransform}
2930
import org.apache.spark.sql.connector.read.SupportsReportStatistics
3031
import org.apache.spark.sql.errors.QueryExecutionErrors
3132
import org.apache.spark.sql.util.CaseInsensitiveStringMap
3233
import org.apache.spark.util.ArrayImplicits._
3334

35+
/**
36+
* Catalog / Namespace / Database / <entity> row formatting shared by
37+
* `DescribeTableExec.addTableDetails` and `DescribeV2ViewExec.run`. Hosting it in one place
38+
* keeps the row layout (including the v1-compat `Database` row) as a single source of truth
39+
* so the table and view paths can't drift.
40+
*/
41+
private[v2] trait DescribeIdentifierRows extends LeafV2CommandExec {
42+
/**
43+
* Append the structured identifier rows (`Catalog`, `Namespace`, `Database`,
44+
* `<entityLabel>`) to `rows`. `entityLabel` is `"Table"` for a v2 table and `"View"` for a
45+
* v2 view -- the only divergence between the two paths.
46+
*
47+
* Row shapes:
48+
* - `Catalog` carries the catalog plugin name (always present for v2).
49+
* - `Namespace` is the canonical multi-segment representation, joined with `.` and with
50+
* `quoteIfNeeded` applied per segment (so segments containing dots round-trip). Always
51+
* emitted; for an empty namespace (root-level entity) the value is the empty string,
52+
* so the row's presence stays uniform across v2 outputs.
53+
* - `Database` is always emitted for v1 compatibility. Its value is the trailing
54+
* namespace segment (so multi-segment namespaces still surface their leaf segment),
55+
* or the empty string when the namespace is the catalog root. Consumers that need
56+
* the full namespace should read `Namespace`; `Database` alone is not round-trip-safe
57+
* for multi-segment cases.
58+
* - `<entityLabel>` is the unqualified entity name from `Identifier.name()`.
59+
*/
60+
protected def addIdentifierRows(
61+
rows: ArrayBuffer[InternalRow],
62+
catalogName: String,
63+
identifier: Identifier,
64+
entityLabel: String): Unit = {
65+
rows += toCatalystRow("Catalog", catalogName, "")
66+
rows += toCatalystRow("Namespace", identifier.namespace().quoted, "")
67+
rows += toCatalystRow("Database", identifier.namespace().lastOption.getOrElse(""), "")
68+
rows += toCatalystRow(entityLabel, identifier.name(), "")
69+
}
70+
}
71+
72+
/**
73+
* Schema + partitioning + clustering row formatting shared by `DescribeTableExec.run()` (which
74+
* uses it for the schema-row prefix) and `DescribeTablePartitionExec.run()` (which uses it as
75+
* the entire pre-partition section). Mixing the helpers into a trait lets each exec invoke
76+
* them directly off `this`, so the partition exec doesn't need to thread the table-only
77+
* `catalogName` / `identifier` arguments that `DescribeTableExec` consumes for the EXTENDED
78+
* `# Detailed Table Information` block.
79+
*
80+
* Kept orthogonal to [[DescribeIdentifierRows]] so `DescribeTablePartitionExec` (which only
81+
* needs the schema/partitioning rows) doesn't inherit identifier-row helpers it never calls.
82+
* `DescribeTableExec` mixes both traits in.
83+
*/
84+
private[v2] trait DescribeTableBaseRows extends LeafV2CommandExec {
85+
def table: Table
86+
87+
/** A blank `("", "", "")` row used as a section separator in DESCRIBE output. */
88+
protected def emptyRow(): InternalRow = toCatalystRow("", "", "")
89+
90+
/** Schema + partitioning + clustering rows, shared with DescribeTablePartitionExec. */
91+
protected def addBaseDescription(rows: ArrayBuffer[InternalRow]): Unit = {
92+
addSchema(rows)
93+
addPartitioning(rows)
94+
addClustering(rows)
95+
}
96+
97+
private def addSchema(rows: ArrayBuffer[InternalRow]): Unit = {
98+
rows ++= table.columns().map{ column =>
99+
toCatalystRow(
100+
column.name, column.dataType.simpleString, column.comment)
101+
}
102+
}
103+
104+
private def addClusteringToRows(
105+
clusterBySpec: ClusterBySpec,
106+
rows: ArrayBuffer[InternalRow]): Unit = {
107+
rows += toCatalystRow("# Clustering Information", "", "")
108+
rows += toCatalystRow(s"# ${output.head.name}", output(1).name, output(2).name)
109+
rows ++= clusterBySpec.columnNames.map { fieldNames =>
110+
val schema = CatalogV2Util.v2ColumnsToStructType(table.columns())
111+
val nestedField = schema.findNestedField(fieldNames.fieldNames.toIndexedSeq)
112+
assert(nestedField.isDefined,
113+
"The clustering column " +
114+
s"${fieldNames.fieldNames.map(quoteIfNeeded).mkString(".")} " +
115+
s"was not found in the table schema ${schema.catalogString}.")
116+
nestedField.get
117+
}.map { case (path, field) =>
118+
toCatalystRow(
119+
(path :+ field.name).map(quoteIfNeeded).mkString("."),
120+
field.dataType.simpleString,
121+
field.getComment().orNull)
122+
}
123+
}
124+
125+
private def addClustering(rows: ArrayBuffer[InternalRow]): Unit = {
126+
ClusterBySpec.extractClusterBySpec(table.partitioning.toIndexedSeq).foreach { clusterBySpec =>
127+
addClusteringToRows(clusterBySpec, rows)
128+
}
129+
}
130+
131+
private def addPartitioning(rows: ArrayBuffer[InternalRow]): Unit = {
132+
// Clustering columns are handled in addClustering().
133+
val partitioning = table.partitioning
134+
.filter(t => !t.isInstanceOf[ClusterByTransform])
135+
if (partitioning.nonEmpty) {
136+
val partitionColumnsOnly = table.partitioning.forall(t => t.isInstanceOf[IdentityTransform])
137+
if (partitionColumnsOnly) {
138+
rows += toCatalystRow("# Partition Information", "", "")
139+
rows += toCatalystRow(s"# ${output(0).name}", output(1).name, output(2).name)
140+
val schema = CatalogV2Util.v2ColumnsToStructType(table.columns())
141+
rows ++= table.partitioning
142+
.map(_.asInstanceOf[IdentityTransform].ref.fieldNames())
143+
.map { fieldNames =>
144+
val nestedField = schema.findNestedField(fieldNames.toImmutableArraySeq)
145+
if (nestedField.isEmpty) {
146+
throw QueryExecutionErrors.partitionColumnNotFoundInTheTableSchemaError(
147+
fieldNames.toSeq,
148+
schema)
149+
}
150+
nestedField.get
151+
}.map { case (path, field) =>
152+
toCatalystRow(
153+
(path :+ field.name).map(quoteIfNeeded(_)).mkString("."),
154+
field.dataType.simpleString,
155+
field.getComment().orNull)
156+
}
157+
} else {
158+
rows += emptyRow()
159+
rows += toCatalystRow("# Partitioning", "", "")
160+
rows ++= table.partitioning.zipWithIndex.map {
161+
case (transform, index) => toCatalystRow(s"Part $index", transform.describe(), "")
162+
}
163+
}
164+
}
165+
}
166+
}
167+
34168
case class DescribeTableExec(
35169
output: Seq[Attribute],
170+
catalogName: String,
171+
identifier: Identifier,
36172
table: Table,
37-
isExtended: Boolean) extends LeafV2CommandExec {
173+
isExtended: Boolean) extends DescribeTableBaseRows with DescribeIdentifierRows {
38174
override protected def run(): Seq[InternalRow] = {
39175
val rows = new ArrayBuffer[InternalRow]()
40176
addBaseDescription(rows)
@@ -48,17 +184,10 @@ case class DescribeTableExec(
48184
rows.toSeq
49185
}
50186

51-
/** Schema + partitioning + clustering rows, shared with DescribeTablePartitionExec. */
52-
private[v2] def addBaseDescription(rows: ArrayBuffer[InternalRow]): Unit = {
53-
addSchema(rows)
54-
addPartitioning(rows)
55-
addClustering(rows)
56-
}
57-
58187
private def addTableDetails(rows: ArrayBuffer[InternalRow]): Unit = {
59188
rows += emptyRow()
60189
rows += toCatalystRow("# Detailed Table Information", "", "")
61-
rows += toCatalystRow("Name", table.name(), "")
190+
addIdentifierRows(rows, catalogName, identifier, entityLabel = "Table")
62191

63192
val tableType = if (table.properties().containsKey(TableCatalog.PROP_EXTERNAL)) {
64193
CatalogTableType.EXTERNAL.name
@@ -87,13 +216,6 @@ case class DescribeTableExec(
87216
}
88217
}
89218

90-
private def addSchema(rows: ArrayBuffer[InternalRow]): Unit = {
91-
rows ++= table.columns().map{ column =>
92-
toCatalystRow(
93-
column.name, column.dataType.simpleString, column.comment)
94-
}
95-
}
96-
97219
private def addTableConstraints(rows: ArrayBuffer[InternalRow]): Unit = {
98220
if (table.constraints.nonEmpty) {
99221
rows += emptyRow()
@@ -117,33 +239,6 @@ case class DescribeTableExec(
117239
case _ =>
118240
}
119241

120-
private def addClusteringToRows(
121-
clusterBySpec: ClusterBySpec,
122-
rows: ArrayBuffer[InternalRow]): Unit = {
123-
rows += toCatalystRow("# Clustering Information", "", "")
124-
rows += toCatalystRow(s"# ${output.head.name}", output(1).name, output(2).name)
125-
rows ++= clusterBySpec.columnNames.map { fieldNames =>
126-
val schema = CatalogV2Util.v2ColumnsToStructType(table.columns())
127-
val nestedField = schema.findNestedField(fieldNames.fieldNames.toIndexedSeq)
128-
assert(nestedField.isDefined,
129-
"The clustering column " +
130-
s"${fieldNames.fieldNames.map(quoteIfNeeded).mkString(".")} " +
131-
s"was not found in the table schema ${schema.catalogString}.")
132-
nestedField.get
133-
}.map { case (path, field) =>
134-
toCatalystRow(
135-
(path :+ field.name).map(quoteIfNeeded).mkString("."),
136-
field.dataType.simpleString,
137-
field.getComment().orNull)
138-
}
139-
}
140-
141-
private def addClustering(rows: ArrayBuffer[InternalRow]): Unit = {
142-
ClusterBySpec.extractClusterBySpec(table.partitioning.toIndexedSeq).foreach { clusterBySpec =>
143-
addClusteringToRows(clusterBySpec, rows)
144-
}
145-
}
146-
147242
private def addTableStats(rows: ArrayBuffer[InternalRow]): Unit = table match {
148243
case read: SupportsRead =>
149244
read.newScanBuilder(CaseInsensitiveStringMap.empty()).build() match {
@@ -160,42 +255,4 @@ case class DescribeTableExec(
160255
}
161256
case _ =>
162257
}
163-
164-
private def addPartitioning(rows: ArrayBuffer[InternalRow]): Unit = {
165-
// Clustering columns are handled in addClustering().
166-
val partitioning = table.partitioning
167-
.filter(t => !t.isInstanceOf[ClusterByTransform])
168-
if (partitioning.nonEmpty) {
169-
val partitionColumnsOnly = table.partitioning.forall(t => t.isInstanceOf[IdentityTransform])
170-
if (partitionColumnsOnly) {
171-
rows += toCatalystRow("# Partition Information", "", "")
172-
rows += toCatalystRow(s"# ${output(0).name}", output(1).name, output(2).name)
173-
val schema = CatalogV2Util.v2ColumnsToStructType(table.columns())
174-
rows ++= table.partitioning
175-
.map(_.asInstanceOf[IdentityTransform].ref.fieldNames())
176-
.map { fieldNames =>
177-
val nestedField = schema.findNestedField(fieldNames.toImmutableArraySeq)
178-
if (nestedField.isEmpty) {
179-
throw QueryExecutionErrors.partitionColumnNotFoundInTheTableSchemaError(
180-
fieldNames.toSeq,
181-
schema)
182-
}
183-
nestedField.get
184-
}.map { case (path, field) =>
185-
toCatalystRow(
186-
(path :+ field.name).map(quoteIfNeeded(_)).mkString("."),
187-
field.dataType.simpleString,
188-
field.getComment().orNull)
189-
}
190-
} else {
191-
rows += emptyRow()
192-
rows += toCatalystRow("# Partitioning", "", "")
193-
rows ++= table.partitioning.zipWithIndex.map {
194-
case (transform, index) => toCatalystRow(s"Part $index", transform.describe(), "")
195-
}
196-
}
197-
}
198-
}
199-
200-
private def emptyRow(): InternalRow = toCatalystRow("", "", "")
201258
}

sql/core/src/main/scala/org/apache/spark/sql/execution/datasources/v2/DescribeTablePartitionExec.scala

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -32,14 +32,16 @@ case class DescribeTablePartitionExec(
3232
table: SupportsPartitionManagement,
3333
tableIdent: Identifier,
3434
partSpec: ResolvedPartitionSpec,
35-
isExtended: Boolean) extends LeafV2CommandExec {
35+
isExtended: Boolean) extends DescribeTableBaseRows {
3636

3737
override protected def run(): Seq[InternalRow] = {
3838
val partitionRow = validateAndGetPartition()
3939

40-
// Delegate schema + partitioning + clustering to DescribeTableExec.
40+
// Schema + partitioning + clustering rows come from the shared `DescribeTableBaseRows`
41+
// trait, which is mixed in by both this exec and `DescribeTableExec` so each can call
42+
// the helper directly off `this`.
4143
val rows = new ArrayBuffer[InternalRow]()
42-
DescribeTableExec(output, table, isExtended = false).addBaseDescription(rows)
44+
addBaseDescription(rows)
4345

4446
if (isExtended) {
4547
addPartitionDetails(rows, partitionRow)

sql/core/src/main/scala/org/apache/spark/sql/execution/datasources/v2/V2ViewInspectionExecs.scala

Lines changed: 8 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -146,17 +146,19 @@ case class ShowV2ViewColumnsExec(
146146

147147
/**
148148
* Physical plan node for DESCRIBE TABLE on a v2 view. Schema rows first; when EXTENDED is
149-
* specified, an additional `# Detailed View Information` block emits the v2-native fields
150-
* (catalog, identifier, view text, captured creation context, schema-binding mode, query
151-
* column names, user TBLPROPERTIES). v2 views are unpartitioned by definition, so the
152-
* partition-spec branch from v1 `DescribeTableCommand` is unreachable here.
149+
* specified, an additional `# Detailed View Information` block emits the v2-native fields:
150+
* the resolved-identifier components via [[DescribeIdentifierRows#addIdentifierRows]] (which
151+
* also surfaces a v1-compat `Database` row for single-segment namespaces), followed by view
152+
* text, captured creation context, schema-binding mode, query column names, and user
153+
* TBLPROPERTIES. v2 views are unpartitioned by definition, so the partition-spec branch from
154+
* v1 `DescribeTableCommand` is unreachable here.
153155
*/
154156
case class DescribeV2ViewExec(
155157
output: Seq[Attribute],
156158
catalogName: String,
157159
identifier: Identifier,
158160
viewInfo: ViewInfo,
159-
isExtended: Boolean) extends LeafV2CommandExec with SQLConfHelper {
161+
isExtended: Boolean) extends DescribeIdentifierRows with SQLConfHelper {
160162

161163
override protected def run(): Seq[InternalRow] = {
162164
val result = new ArrayBuffer[InternalRow]
@@ -166,10 +168,7 @@ case class DescribeV2ViewExec(
166168
if (isExtended) {
167169
result += toCatalystRow("", "", "")
168170
result += toCatalystRow("# Detailed View Information", "", "")
169-
result += toCatalystRow("Catalog", catalogName, "")
170-
val qualified = (identifier.namespace() :+ identifier.name())
171-
.map(quoteIfNeeded).mkString(".")
172-
result += toCatalystRow("Identifier", qualified, "")
171+
addIdentifierRows(result, catalogName, identifier, entityLabel = "View")
173172
// Promote first-class reserved fields (Owner / Comment / Collation) to top-level rows
174173
// before the EXTENDED Properties block, mirroring v1 `CatalogTable.toJsonLinkedHashMap`
175174
// which renders these as their own rows rather than burying them in `Table Properties`.

sql/core/src/test/scala/org/apache/spark/sql/connector/DataSourceV2MetadataTableSuite.scala

Lines changed: 0 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -83,24 +83,6 @@ class DataSourceV2MetadataTableSuite extends QueryTest with SharedSparkSession {
8383
checkAnswer(spark.table(tableName), 0.until(10).map(i => Row(i, -i)))
8484
}
8585

86-
test("DESCRIBE TABLE EXTENDED on a non-view MetadataTable shows the real identifier") {
87-
// MetadataTable.name() is read by DescribeTableExec's "Name" row. Pin that it
88-
// reflects the catalog-supplied identifier (here TestingDataSourceTableCatalog passes
89-
// `ident.toString`) rather than a generic placeholder, so the DESCRIBE output is
90-
// meaningful for users.
91-
withTempPath { path =>
92-
val loc = path.getCanonicalPath
93-
val tableName = s"table_catalog.`$loc`.test_json"
94-
spark.range(1).select($"id".cast("string").as("col")).write.json(loc)
95-
val nameRow = sql(s"DESCRIBE TABLE EXTENDED $tableName")
96-
.collect()
97-
.find(_.getString(0) == "Name")
98-
.getOrElse(fail("DESCRIBE output missing the `Name` row"))
99-
val rendered = nameRow.getString(1)
100-
assert(rendered.contains("test_json"), s"expected the real identifier, got: $rendered")
101-
}
102-
}
103-
10486
test("fully-qualified column reference uses the real catalog name") {
10587
withTempPath { path =>
10688
val loc = path.getCanonicalPath

sql/core/src/test/scala/org/apache/spark/sql/connector/DataSourceV2SQLSuite.scala

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3912,8 +3912,8 @@ class DataSourceV2SQLSuiteV1Filter
39123912
QueryTest.checkAnswer(
39133913
descriptionDf.filter(
39143914
"!(col_name in ('Catalog', 'Created Time', 'Created By', 'Database', " +
3915-
"'index', 'Location', 'Name', 'Owner', 'Provider', 'Table', 'Table Properties', " +
3916-
"'Type', '_partition', ''))"),
3915+
"'index', 'Location', 'Name', 'Namespace', 'Owner', 'Provider', 'Table', " +
3916+
"'Table Properties', 'Type', '_partition', ''))"),
39173917
Seq(
39183918
Row("# Detailed Table Information", "", ""),
39193919
Row("# Column Default Values", "", ""),

0 commit comments

Comments
 (0)