dongjoon-hyun commented on code in PR #59036:
URL: https://github.com/apache/spark/pull/59036#discussion_r4140435786
##########
sql/core/src/test/scala/org/apache/spark/sql/execution/command/v2/DescribeTableSuite.scala:
##########
@@ -220,6 +222,38 @@ class DescribeTableSuite extends
command.DescribeTableSuiteBase
}
}
+ gridTest("display properties preserve table type and reserved
metadata")(Seq(false, true)) {
+ external =>
+ withNamespaceAndTable("ns", "table") { tbl =>
+ sql(s"CREATE TABLE $tbl (id bigint) $defaultUsing " +
+ "COMMENT 'table comment' LOCATION 'file:/tmp/display-properties' " +
+ "TBLPROPERTIES ('persisted' = 'stored')")
+ val table = loadTable(catalog, "ns", "table")
+ if (external) {
+ table.properties.put(TableCatalog.PROP_EXTERNAL, "true")
+ }
+ table.setDisplayProperties(Map(
Review Comment:
**Apart from `external`, every reserved key in this display map is also
stored on the table, so the test only checks which value wins.** The table is
created with `COMMENT`, `LOCATION`, `USING _` and the default owner, so the
display `comment`, `location`, `provider` and `owner` all collide with stored
values. A display-only `location`, `collation`, `table_type` or
`is_managed_location` is never exercised, which was the ask in
https://github.com/apache/spark/pull/59036#discussion_r4118738223.
For example, suppose the reserved-row loop (`DescribeTableExec` L198-204,
`ShowTablesExtendedExec` L93-99) were changed to read `displayProperties ++
properties` while `Type` stays on `properties()`. A table without a stored
location would then show the display `Location` (or a `Collation` or
`Table_type` row), but all the new tests would still pass because every table
they use has a stored location. The `col_name IN ('Type', 'Comment',
'Location', 'Provider', 'Owner', 'Table Properties')` filter at L246 would also
hide such a leaked row.
Could the display map be built from
`CatalogV2Util.TABLE_RESERVED_PROPERTIES`, as `CatalogV2UtilSuite` does, with
one table created without `LOCATION`, and could the filter cover all the
reserved rows? The same 15-line fixture is repeated in `ShowTablesSuite`
L77-91, so a shared helper next to `loadTable` in the v2 `CommandSuiteBase`
would keep the suites in sync.
##########
sql/catalyst/src/main/scala/org/apache/spark/sql/connector/catalog/CatalogV2Util.scala:
##########
@@ -677,6 +677,12 @@ private[sql] object CatalogV2Util {
builder
}
+ /** Returns non-reserved table properties with additional display-only
metadata. */
+ def tablePropertiesForDisplay(table: Table): Map[String, String] = {
+ (table.displayProperties().asScala.toMap ++ table.properties().asScala)
Review Comment:
**Display-only keys share the key namespace that `ALTER TABLE ... SET/UNSET
TBLPROPERTIES` manage, but those commands only change `properties()`.** With
the fixture in `ShowTblPropertiesSuite` (stored `persisted=stored`; display
`persisted=display-value` and `catalog-label=catalog-value`):
- `ALTER TABLE t UNSET TBLPROPERTIES ('persisted')` removes the stored key,
and then `SHOW TBLPROPERTIES t ('persisted')` returns `display-value` instead
of "Table ... does not have property: persisted". `getTableProperties` still
contains `persisted`, while `SHOW CREATE TABLE` no longer does.
- `ALTER TABLE t UNSET TBLPROPERTIES ('catalog-label')` succeeds, because V2
`UnsetTableProperties` only emits `TableChange.removeProperty` (a no-op for a
missing key, per `TableChange.java` L63), but the key is still listed
afterwards. No SQL can remove it, so a tool that syncs properties by diffing
`getTableProperties` against a desired set never converges.
- `SET TBLPROPERTIES ('catalog-label' = 'x')` followed by `UNSET` brings
`catalog-value` back.
Neither the Javadoc nor the tests cover this; `ShowTblPropertiesSuite` only
exercises `SET`. Could the `Table#displayProperties()` Javadoc state that
`ALTER TABLE ... SET/UNSET TBLPROPERTIES` only affect `properties()`, and that
a display entry shows through once a colliding stored key is unset? An `UNSET`
step in that test would pin it. Alternatively, rendering the display entries
separately (the other option in
https://github.com/apache/spark/pull/59036#discussion_r4118738209) would avoid
the shared namespace.
##########
sql/catalyst/src/main/java/org/apache/spark/sql/connector/catalog/TableInfo.java:
##########
@@ -59,19 +62,40 @@ public Map<String, String> properties() {
return properties;
}
+ /**
+ * Returns the additional display-only properties described by {@link
Table#displayProperties()}.
+ *
+ * @since 4.4.0
+ */
+ public Map<String, String> displayProperties() {
Review Comment:
**`TableInfo` is also the input of `TableCatalog.createTable(Identifier,
TableInfo)`, `createTableLike` and `StagingTableCatalog.stage*`, but this new
field has no documented meaning on that path.**
- Spark never sets it: `CreateTableExec`, `ReplaceTableExec`,
`WriteToDataSourceV2Exec` (CTAS/RTAS), `CreateTableLikeExec` and the pipelines
`DatasetManager` build `TableInfo` without it.
- The default `TableCatalog.createTable(ident, tableInfo)`
(`TableCatalog.java` L359-362) and the `StagingTableCatalog.stage*` defaults
flatten `TableInfo` to `(columns, partitions, properties)` and drop it.
`V2SessionCatalog` and `JDBCTableCatalog` use that default, and
`DelegatingCatalogExtension` doesn't override it, so it never reaches the
delegate.
- The test catalogs in this PR persist it at create time
(`BasicInMemoryTableCatalog.createTable` and
`InMemoryRowLevelOperationTableCatalog.createTable`), and
`InMemoryRelationCatalog` stores the whole `TableInfo`.
So a connector modeled on the test catalogs persists "display-only" entries
at create time. Code that copies a metadata-only table with
`catB.createTable(ident, delegatingTable.getTableInfo())` freezes the source
catalog's transient labels into `catB` on some catalogs and silently drops them
on others. Could the Javadoc here and on `withDisplayProperties` say that Spark
doesn't set it for create, replace, stage or `createTableLike`, and that
catalogs may ignore it there?
##########
sql/catalyst/src/main/java/org/apache/spark/sql/connector/catalog/DelegatingTable.java:
##########
@@ -74,6 +74,11 @@ public Map<String, String> properties() {
return Collections.unmodifiableMap(info.properties());
}
+ @Override
+ public Map<String, String> displayProperties() {
Review Comment:
**The metadata-only path this enables
(`TableInfo.Builder.withDisplayProperties` + `DelegatingTable`) is only covered
by unit tests.** `CatalogSuite` and `CatalogV2UtilSuite` call
`displayProperties()` and `tablePropertiesForDisplay` directly, and every
SQL-level test uses `InMemoryBaseTable.setDisplayProperties`. No test runs
`DESCRIBE TABLE EXTENDED`, `SHOW TBLPROPERTIES` or `SHOW TABLE EXTENDED`
against a catalog that returns a `DelegatingTable`.
That path is special-cased elsewhere: `RelationResolution` L464-468 treats
`DelegatingTable` as an "interpret via v1" sentinel for reads, and
`V1Table.toCatalogTable(catalog, ident, DelegatingTable)` reads only
`info.properties`. If the metadata commands were ever routed the same way,
display entries would disappear for metadata-only catalogs without any failing
test. Could we add one end-to-end test? `InMemoryRelationCatalog` is already
registered in v2 `ShowTablesSuite`, and a catalog that builds the `TableInfo`
in `loadTable`, like `TestingDataSourceTableCatalog` in
`DataSourceV2MetadataTableSuite`, would also work.
##########
sql/core/src/test/scala/org/apache/spark/sql/execution/command/v2/ShowTblPropertiesSuite.scala:
##########
@@ -17,10 +17,35 @@
package org.apache.spark.sql.execution.command.v2
+import scala.jdk.CollectionConverters._
+
+import org.apache.spark.sql.Row
+import org.apache.spark.sql.connector.catalog.TableCatalog
import org.apache.spark.sql.execution.command
/**
* The class contains tests for the `SHOW TBLPROPERTIES` command to check V2
table catalogs.
*/
class ShowTblPropertiesSuite extends command.ShowTblPropertiesSuiteBase with
CommandSuiteBase {
+ test("display properties supplement current properties and exclude reserved
keys") {
Review Comment:
**Display-only entries are a new input to three redacted outputs, but no
test checks that they are redacted.** Today `DescribeTableExec` L206,
`ShowTablePropertiesExec` L35 and `ShowTablesExtendedExec` L102 redact the
merged map, which is right. However, the new fixtures only use `catalog-label`,
`persisted` and reserved keys, none of which matches `spark.redaction.regex` or
`spark.sql.redaction.options.regex`. The existing redaction test (`SHOW
TBLPROPERTIES BASIC` in `ShowTblPropertiesSuiteBase`) only covers a stored
`password` property.
If a later change redacted `properties()` before merging, a connector's
display entry such as `password` or `jdbc_url` would be printed in clear by
`DESCRIBE TABLE EXTENDED`, `SHOW TBLPROPERTIES`, `SHOW TABLE EXTENDED` and
`getTableProperties`, and no test would fail. Could this test add a
display-only `password` entry and expect `*********(redacted)`?
##########
sql/core/src/test/scala/org/apache/spark/sql/internal/CatalogSuite.scala:
##########
@@ -1201,6 +1201,32 @@ class CatalogSuite extends SharedSparkSession with
AnalysisTest with BeforeAndAf
spark.catalog.dropTable(t)
}
+ test("catalog API: getTableProperties includes V2 display-only properties") {
+ val catalogName = "display_catalog"
Review Comment:
nit: the suite's `before` block already registers `testcat` as the same
`InMemoryCatalog` class for every test, and the other V2 tests in this suite
use it. Could this test use `testcat` and drop the `withSQLConf` wrapper?
##########
sql/api/src/main/scala/org/apache/spark/sql/catalog/Catalog.scala:
##########
@@ -846,6 +846,10 @@ abstract class Catalog {
/**
* Returns all table properties as a map (same as `SHOW TBLPROPERTIES`).
*
+ * For V2 tables, this also includes the table's display-only properties.
These entries may be
Review Comment:
**The Spark Connect Scala client doesn't get this note.**
`org.apache.spark.sql.connect.Catalog.getTableProperties`
(`sql/connect/common/src/main/scala/org/apache/spark/sql/connect/Catalog.scala`
L853-860) has its own Scaladoc, "Returns all table properties as a map (same as
`SHOW TBLPROPERTIES`).", which replaces this one. The classic `Catalog`
inherits this doc, and PySpark Connect reuses the classic docstring
(`python/pyspark/sql/connect/catalog.py` L381), so only the Connect Scala copy
is stale, although Connect returns the same display-only entries. Could we add
the same paragraph there, or drop that override's doc so it inherits this one?
##########
sql/catalyst/src/test/scala/org/apache/spark/sql/connector/catalog/InMemoryTableCatalog.scala:
##########
@@ -168,8 +168,11 @@ class BasicInMemoryTableCatalog extends TableCatalog {
}
override def createTable(ident: Identifier, tableInfo: TableInfo): Table = {
- createTable(ident, tableInfo.columns(), tableInfo.partitions(),
tableInfo.properties(),
+ val table = createTable(
+ ident, tableInfo.columns(), tableInfo.partitions(),
tableInfo.properties(),
Distributions.unspecified(), Array.empty, None, None,
tableInfo.constraints())
+
table.asInstanceOf[InMemoryBaseTable].setDisplayProperties(tableInfo.displayProperties())
Review Comment:
**Only two in-memory catalogs honor `TableInfo.displayProperties()` at
create time, and this one needs a cast.** This path and
`InMemoryRowLevelOperationTableCatalog.createTable` set it, but the other
`createTable(ident, TableInfo)` and `stage*` overrides ignore it. That includes
`InMemoryPartitionTableCatalog` (L69-71), which is the v2 command suites'
`test_catalog`, and
`StagingInMemoryTableCatalog.stageCreate/stageReplace/stageCreateOrReplace`,
which is the CTAS/RTAS path of `atomic_test_catalog`.
`NullTableIdInMemoryTableCatalog.createTable` (L39-47) calls
`super.createTable`, which now sets the map, and then replaces the table with
`new InMemoryTable(..., id = null)` without `copyTableStateFrom`, unlike its
`NullColumnId`, `NullTableIdAndNullColumnId` and `MixedColumnId` siblings.
So a `TableInfo` built with `withDisplayProperties(...)` keeps its entries
on `non_part_test_catalog` and `row_level_op_catalog`, but loses them on
`test_catalog`, on the `atomic_test_catalog` stage path and on
`NullTableIdInMemoryTableCatalog`. Since Spark never sets this field at create
time and only the catalyst `CatalogSuite` grid test uses this path, could that
test set the entries on the live table returned by `createTable`, as the
sql/core tests do? Then this change, the one in
`InMemoryRowLevelOperationTableCatalog.createTable` and the cast could go.
##########
sql/catalyst/src/main/java/org/apache/spark/sql/connector/catalog/Table.java:
##########
@@ -95,6 +95,24 @@ default Map<String, String> properties() {
return Collections.emptyMap();
}
+ /**
+ * Returns additional, display-only table properties.
+ *
+ * <p>Spark adds these entries to the non-reserved properties displayed by
V2 table metadata
Review Comment:
**For the session catalog, `SHOW TABLE EXTENDED` doesn't include these
entries even when the catalog returns a V2 table.** `ResolveSessionCatalog`
L397-406 rewrites every `ShowTablesExtended(ResolvedV1Database(db), ...)` to
the v1 `ShowTablesCommand`, while `DESCRIBE TABLE EXTENDED` and `SHOW
TBLPROPERTIES` fall back to v1 only for a `V1Table`. So for a
`CatalogExtension` that returns its own `Table` with `displayProperties()` =
`{label=x}`, `DESCRIBE TABLE EXTENDED`, `SHOW TBLPROPERTIES` and
`getTableProperties` show `label`, but `SHOW TABLE EXTENDED IN spark_catalog.db
LIKE 't'` doesn't.
The PR description explains this, but it isn't in the API docs that
connector authors read. Could we add something like "Commands that run through
V1 implementations, such as `SHOW TABLE EXTENDED` on the session catalog, do
not include these entries."?
##########
sql/catalyst/src/test/scala/org/apache/spark/sql/connector/catalog/InMemoryBaseTable.scala:
##########
@@ -85,25 +87,25 @@ abstract class InMemoryBaseTable(
tableColumns = newColumns
}
+ override def displayProperties(): util.Map[String, String] = {
+ Option(tableDisplayProperties).getOrElse(super.displayProperties())
+ }
+
+ def setDisplayProperties(displayProperties: util.Map[String, String]): Unit
= {
+ tableDisplayProperties = new util.HashMap[String,
String](displayProperties)
+ }
+
override def version(): String = tableVersion.toString
def setVersion(version: String): Unit = {
tableVersion = version.toInt
}
/**
- * Copies version and validated version from another table.
- *
- * Some test catalogs (e.g. [[NullColumnIdInMemoryTableCatalog]],
- * [[NullTableIdAndNullColumnIdInMemoryTableCatalog]]) create a new table
object
- * that overrides specific behavior (such as nulling out column IDs). The new
- * object's version counter starts at 0. Without this call, the version
counter
- * resets every time the catalog creates such a replacement table, breaking
the
- * monotonic-version assumption that downstream consumers rely on (e.g.
- * [[InMemoryTable]].copy, validated-version propagation, and the
join-refresh
- * tests in [[DSv2IncrementallyConstructedQueryTests]]).
+ * Copies version, validated version and display-only metadata into a
replacement table.
Review Comment:
**The rename replaced the explanation of why the version must be copied with
a one-line summary.** The previous Scaladoc said that replacement tables start
with a version counter of 0, and that without this call the counter resets and
breaks the monotonic-version assumption that `InMemoryTable.copy`,
validated-version propagation and the join-refresh tests in
`DSv2IncrementallyConstructedQueryTests` rely on. That is still true (the
version-aware `equals`/`hashCode` below uses it), and it isn't documented
anywhere else in the code, so the next person who adds a catalog that rebuilds
tables may skip this call. Also, 5 of the 11 callers are `copy()` snapshots
rather than replacement tables. Could we keep the old paragraph and just add
the display-only metadata to the first sentence?
##########
sql/core/src/test/scala/org/apache/spark/sql/execution/command/v2/ShowTblPropertiesSuite.scala:
##########
@@ -17,10 +17,35 @@
package org.apache.spark.sql.execution.command.v2
+import scala.jdk.CollectionConverters._
+
+import org.apache.spark.sql.Row
+import org.apache.spark.sql.connector.catalog.TableCatalog
import org.apache.spark.sql.execution.command
/**
* The class contains tests for the `SHOW TBLPROPERTIES` command to check V2
table catalogs.
*/
class ShowTblPropertiesSuite extends command.ShowTblPropertiesSuiteBase with
CommandSuiteBase {
+ test("display properties supplement current properties and exclude reserved
keys") {
+ withNamespaceAndTable("ns", "table") { tbl =>
+ sql(s"CREATE TABLE $tbl (id bigint) $defaultUsing " +
+ "TBLPROPERTIES ('persisted' = 'stored')")
+ loadTable(catalog, "ns", "table").setDisplayProperties(Map(
+ "catalog-label" -> "catalog-value",
+ "persisted" -> "display-value",
+ TableCatalog.PROP_EXTERNAL -> "true",
+ TableCatalog.PROP_LOCATION -> "file:/display-location").asJava)
+
+ checkAnswer(sql(s"SHOW TBLPROPERTIES $tbl"),
+ Seq(Row("catalog-label", "catalog-value"), Row("persisted", "stored")))
+ checkAnswer(sql(s"SHOW TBLPROPERTIES $tbl ('persisted')"),
Row("persisted", "stored"))
+ checkAnswer(sql(s"SHOW TBLPROPERTIES $tbl ('catalog-label')"),
+ Row("catalog-label", "catalog-value"))
+
+ sql(s"ALTER TABLE $tbl SET TBLPROPERTIES ('persisted' = 'updated')")
Review Comment:
**This holds for `InMemoryTableCatalog`, but the session-catalog test
catalog still drops display state on `ALTER TABLE`.**
`InMemoryTableSessionCatalog.alterTable`
(`DataSourceV2DataFrameSessionCatalogSuite.scala` L160-181) rebuilds the table
as `new InMemoryTable(table.name, schema, table.partitioning, properties)`
without copying the display properties. That catalog is how the tests model a
session-catalog extension that returns V2 tables (`DatasourceV2SQLBase`
registers it as `spark_catalog`), which is the case the PR description says
`DESCRIBE TABLE EXTENDED` and `SHOW TBLPROPERTIES` support. There, after
`setDisplayProperties` on the live table and `ALTER TABLE t SET TBLPROPERTIES
('k' = 'v')`, `SHOW TBLPROPERTIES t` no longer lists the display entry.
No current test hits this, and that rebuild already reset the version before
this PR. But since
https://github.com/apache/spark/pull/59036#discussion_r4139217065 says table
reconstruction during `ALTER TABLE` preserves the entries, could it also call
`setDisplayProperties(table.displayProperties())`?
##########
sql/catalyst/src/test/scala/org/apache/spark/sql/connector/catalog/InMemoryTableCatalog.scala:
##########
@@ -276,6 +279,7 @@ class BasicInMemoryTableCatalog extends TableCatalog {
s"Unsupported InMemoryBaseTable subclass: ${other.getClass.getName}")
}
newTable.setVersion(currentVersion)
+ newTable.setDisplayProperties(table.displayProperties())
Review Comment:
nit: this copies the table state by hand instead of calling
`copyTableStateFrom`. Now that the helper is the generic "copy table state"
hook, the most common `ALTER TABLE` path (`test_catalog`,
`InMemoryTableCatalog`, `InMemoryCatalog`) still keeps its own list:
`setVersion` + `setDisplayProperties`, and `validatedVersion` only for
`AddConstraint`. `InMemoryRowLevelOperationTableCatalog.alterTable` carries
`validatedVersion` over through the helper instead. If a field is later added
only to `copyTableStateFrom`, this path resets it, and the catalogs that wrap
`super.alterTable` (`NullColumnIdInMemoryTableCatalog`,
`MixedColumnIdTableCatalog`, `TypeChangeResetsColIdTableCatalog`, ...) copy
from the already-reset table. Could this call
`newTable.copyTableStateFrom(table)` and keep the `AddConstraint` loop, or at
least carry a comment to keep the two in sync?
##########
python/pyspark/sql/catalog.py:
##########
@@ -383,6 +383,12 @@ def getTableProperties(self, tableName: str) -> Dict[str,
str]:
.. versionadded:: 4.2.0
+ Notes
Review Comment:
nit: this `Notes` section comes before `Parameters`. The other `Notes`
sections in this file (e.g. `listViews`, `functionExists` and `listColumns`)
come after `Returns` and before `Examples`, which is also the numpydoc order.
The rendered API docs reorder it, but `help(spark.catalog.getTableProperties)`
and IDEs show the raw order. Could it move below `Returns`?
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: [email protected]
For queries about this service, please contact Infrastructure at:
[email protected]
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]