Repository navigation
Conversation
There was a problem hiding this comment.
Thank you for the PR, @szehon-ho . The motivation of keeping catalog-owned transient metadata out of SHOW CREATE TABLE makes sense. I left inline comments; here is a summary.
API behavior and contract
spark.catalog.getTableProperties(Scala/Java, PySpark, Spark Connect; released in 4.2.0) is implemented viaShowTableProperties. It now returns display-only keys as table properties, whilegetCreateTableStringdoesn't. The PR description doesn't mention this and no test covers it.- The
Type(EXTERNAL/MANAGED) row and the reserved rows (Location, Provider, Owner, Comment, ...) are also derived fromdisplayProperties(), but the Javadoc doesn't require it to be a superset ofproperties(). A connector returning only its labels would get wrong or missing metadata inDESCRIBE TABLE EXTENDEDandSHOW TABLE EXTENDED. - For
spark_catalog(builtin orCatalogExtension),SHOW TABLE EXTENDEDis always rewritten to the v1ShowTablesCommand. For the same table, the display-only keys appear inDESCRIBE TABLE EXTENDEDandSHOW TBLPROPERTIESbut not inSHOW TABLE EXTENDED. - (Minor) Mockito mocks that stub only
properties()now get an empty map from the new default method.
API design
- Could this be an additive map of extras (default empty) that Spark merges, instead of a second full map that defaults to
properties()? - Is it intentional to add this only to
Tableand not toRelation? v2 views and theTableInfo/DelegatingTablepath currently have no way to use it.
Tests (mostly nits)
- The test fixture can't detect whether the
Type/reserved rows come fromdisplayProperties(). - The in-memory test catalogs lose
tableDisplayPropertiesincopy(),alterTableandpinTable. - The
SHOWassertions live in theDESCRIBE TABLEsuite. checkAnswerwould be more precise than the hand-rolled assertions.- The new
InMemoryBaseTablemembers split the version section. - The fallback could use
super.displayProperties().
|
|
||
| // The reserved properties are accessible through DESCRIBE | ||
| val properties = conf.redactOptions(catalogTable.properties.asScala.toMap) | ||
| val properties = conf.redactOptions(catalogTable.displayProperties.asScala.toMap) |
There was a problem hiding this comment.
This also changes the public API spark.catalog.getTableProperties, because classic.Catalog.getTableProperties is implemented by executing a ShowTableProperties plan. PySpark and Spark Connect (SparkConnectPlanner.transformGetTableProperties) go through the same path. The API was released in 4.2.0 and is documented as "Returns all table properties as a map (same as SHOW TBLPROPERTIES)".
Take a connector whose displayProperties() returns properties() + {"catalog-label" -> "catalog-value"}:
spark.catalog.getTableProperties("cat.ns.t")containscatalog-label.spark.catalog.getCreateTableString("cat.ns.t")doesn't.
User code that copies table properties via getTableProperties(src) into CREATE TABLE ... TBLPROPERTIES, ALTER TABLE ... SET TBLPROPERTIES or DataFrameWriterV2.tableProperty would then persist the transient label on the target table. That is the leak this PR prevents for SHOW CREATE TABLE. The existing getTableProperties tests only use V1 tables, so they don't cover this.
Could you either keep getTableProperties on properties(), or document this behavior change in the PR description and the API doc, with a test?
There was a problem hiding this comment.
Kept getTableProperties() consistent with SHOW TBLPROPERTIES, including the additional display-only entries. The Scala and Python API docs and PR description now explain that these entries may be transient and unsuitable for copying into DDL, and point to getCreateTableString(). Added a V2 test in org.apache.spark.sql.internal.CatalogSuite covering the returned properties, unchanged table type/description, and exclusion of the display-only entry from generated DDL. Existing connectors retain their behavior because the new method defaults to an empty map.
| addIdentifierRows(rows, catalogName, identifier, entityLabel = "Table") | ||
|
|
||
| val tableType = if (table.properties().containsKey(TableCatalog.PROP_EXTERNAL)) { | ||
| val displayProperties = table.displayProperties() |
There was a problem hiding this comment.
This switches not only the free-form Table Properties list but also the Type row (EXTERNAL/MANAGED via PROP_EXTERNAL) and the reserved-property rows (Comment, Collation, Location, Provider, Owner, ...) to displayProperties(). ShowTablesExtendedExec does the same.
However, the new Javadoc doesn't say that displayProperties() must be a superset of properties(). Suppose a connector takes "display-only properties" literally and returns only its labels, e.g. Map.of("label", "x"). For an external table:
DESCRIBE TABLE EXTENDEDandSHOW TABLE EXTENDEDshowType MANAGEDand drop the Location/Provider/Owner rows.SHOW TBLPROPERTIES t ('k')returns "does not have property: k" for a persisted keyk.
Meanwhile SHOW CREATE TABLE and spark.catalog.getTable(...).tableType/description still read properties(), so they disagree. Also, a display key named like a reserved key (owner, location, external, ...) silently overrides that row. Because the check is containsKey(PROP_EXTERNAL), even external=false shows EXTERNAL.
Could we keep Type and the reserved rows on table.properties() and use displayProperties() only for the Table Properties list? If not, please document the superset and reserved-key contract in the Javadoc.
There was a problem hiding this comment.
Changed both commands so Type and all reserved metadata rows continue to read properties(). The shared CatalogV2Util.tablePropertiesForDisplay() helper merges the additional entries with properties() (existing values win), then removes reserved keys from the free-form property list. The Javadoc now defines that contract, and the command tests cover managed/external tables and conflicting reserved display keys.
| results.put("Namespace", identifier.namespace().quoted) | ||
| results.put("Table", identifier.name()) | ||
| val tableType = if (table.properties().containsKey(TableCatalog.PROP_EXTERNAL)) { | ||
| val displayProperties = table.displayProperties() |
There was a problem hiding this comment.
FYI, the session catalog never reaches this exec. For spark_catalog (builtin, or a CatalogExtension such as Delta/Iceberg-style session catalogs), ResolveSessionCatalog always rewrites ShowTablesExtended(ResolvedV1Database(db), ...) to the v1 ShowTablesCommand; there is no useV1Command guard. That command prints CatalogTable.simpleString and never calls loadTable() or displayProperties().
DESCRIBE TABLE EXTENDED and SHOW TBLPROPERTIES, by contrast, fall back to v1 only for V1Table. When the extension returns its own Table implementation, those two use the v2 execs. So for the same session-catalog table, the display-only keys appear in DESCRIBE TABLE EXTENDED and SHOW TBLPROPERTIES but not in SHOW TABLE EXTENDED.
The PR description says the metadata appears in SHOW TABLE EXTENDED with no conditions. Could you document the scope, or add a session-catalog test that pins the current behavior?
There was a problem hiding this comment.
Documented this scope in the PR description. The new properties are used only by the V2 table execution paths. SHOW TABLE EXTENDED for spark_catalog, including session-catalog extensions, still uses the V1 command. The description also calls out that DESCRIBE TABLE EXTENDED and SHOW TBLPROPERTIES can use the new API when an extension returns a V2 table.
| * @since 4.4.0 | ||
| */ | ||
| default Map<String, String> displayProperties() { | ||
| return properties(); |
There was a problem hiding this comment.
Minor: the display commands now call this new default method instead of properties(). Under Mockito's default answer (RETURNS_DEFAULTS), a mock doesn't run default methods, so a Table mock returns an empty HashMap here. Take a mock that only stubs when(t.properties()).thenReturn(Map("external" -> "true", "k" -> "v").asJava):
DescribeTableExecnow givesType MANAGEDandTable Properties [].SHOW TBLPROPERTIESreturns 0 rows.
Spark's own tests don't do this. Downstream connector tests may change without warning, though, and would need thenCallRealMethod(). A note in the PR description may be worth it.
There was a problem hiding this comment.
The additive API addresses this: an empty display map now contributes no extra entries, while Spark still reads properties(). Added a CatalogV2UtilSuite regression test with a Mockito mock that stubs only properties() and leaves displayProperties() unstubbed. No thenCallRealMethod() is needed for the existing properties to remain visible; the PR description also notes this.
| * <p>The default implementation returns {@link #properties()}. Implementations may include | ||
| * transient metadata that should not be treated as persisted table properties. |
There was a problem hiding this comment.
Design question: this models display-only metadata as a second full property map that defaults to properties().
- An implementation has to copy the persisted properties just to add one label, as the test does with
new util.HashMap[String, String](table.properties). - There are two sources of truth for the same keys. A cached display map can go stale after
ALTER TABLE ... SET TBLPROPERTIESwhileSHOW CREATE TABLEshows the new value.
Have you considered an additive form instead? For example, a map of catalog-owned extras that defaults to empty, which Spark merges as properties() ++ extras (persisted/reserved keys win) or renders as separate rows. That matches existing additive patterns such as SupportsMetadataColumns and the Statistics/Metadata Columns/Constraints sections of DESCRIBE, and it removes the superset-contract question. Since this is a new public API (@since 4.4.0), it would be good to settle the shape before it is released.
There was a problem hiding this comment.
Adopted the additive form. displayProperties() now returns only extra entries and defaults to an empty map. Spark merges them with the current properties(), gives existing values precedence, and filters reserved keys in one shared helper. The SHOW TBLPROPERTIES test includes a colliding key and verifies that an ALTER TABLE ... SET TBLPROPERTIES update is reflected while the additional entry is preserved.
| // Stores the table version validated during the last `ALTER TABLE ... ADD CONSTRAINT` operation. | ||
| private var validatedTableVersion: String = null | ||
|
|
||
| private var tableDisplayProperties: util.Map[String, String] = null |
There was a problem hiding this comment.
This per-instance state doesn't survive when the in-memory test infra copies or rebuilds a table:
InMemoryTable.copy()and the othercopy()implementations only callsetVersionAndValidatedVersionFrom.InMemoryTableCatalog.alterTablerebuilds the table vianewInMemoryTable.pinTablecopies the table.
setDisplayProperties is then silently lost and displayProperties() falls back to properties. That happens with:
copyOnLoad=truecatalogs, e.g.CachedTableSuiteandDataSourceV2DataFrameSuite.row_level_op_catalog, which always returnsrlot.copy()and is registered in the same v2CommandSuiteBase.- Any
ALTER TABLE.
The new test passes only because test_catalog returns the live instance. Could you document this limitation, or store only the extra entries and propagate them on copy?
There was a problem hiding this comment.
The fixture now stores only the additional entries and copies them defensively. Renamed the shared state-copy helper to copyTableStateFrom() and updated its callers to propagate display metadata alongside version state; table reconstruction during ALTER TABLE also preserves it. Added tests for copyOnLoad, the row-level operation catalog, pinning, and alteration, including checks that pinned properties retain their original values while the current table reflects the update.
| } | ||
| } | ||
|
|
||
| test("display commands use display properties without exposing them to SHOW CREATE TABLE") { |
There was a problem hiding this comment.
nit: this test checks SHOW TBLPROPERTIES, SHOW TABLE EXTENDED and SHOW CREATE TABLE inside the DESCRIBE TABLE suite ("contains tests for the DESCRIBE TABLE command"). It runs as "DESCRIBE TABLE using V2 catalog V2 command: ...", so people working on ShowTblPropertiesSuite, ShowTablesSuite or ShowCreateTableSuite can easily miss it. loadTable is available in all v2 command suites, so could we split this per command? The SHOW CREATE TABLE part could then reuse getShowCreateDDL.
There was a problem hiding this comment.
Split the coverage across DescribeTableSuite, ShowTblPropertiesSuite, ShowTablesSuite, and ShowCreateTableSuite. The SHOW CREATE TABLE test now uses getShowCreateDDL() and compares the full expected DDL.
| val describeRows = sql(s"DESCRIBE TABLE EXTENDED $tbl").collect() | ||
| val properties = describeRows.find(_.getString(0) == "Table Properties") | ||
| assert(properties.exists(_.getString(1) == | ||
| "[catalog-label=catalog-value,persisted=stored]")) | ||
|
|
||
| val shownProperties = sql(s"SHOW TBLPROPERTIES $tbl").collect() | ||
| .map(row => row.getString(0) -> row.getString(1)).toMap | ||
| assert(shownProperties("catalog-label") == "catalog-value") | ||
| assert(shownProperties("persisted") == "stored") |
There was a problem hiding this comment.
nit: checkAnswer gives an exact comparison and a diff on failure. assert(properties.exists(...)) only reports "... was false", and the toMap + per-key asserts can't detect unexpected extra rows such as a leaked reserved key. For example:
checkAnswer(
sql(s"DESCRIBE TABLE EXTENDED $tbl").where("col_name = 'Table Properties'"),
Row("Table Properties", "[catalog-label=catalog-value,persisted=stored]", ""))
checkAnswer(
sql(s"SHOW TBLPROPERTIES $tbl"),
Seq(Row("catalog-label", "catalog-value"), Row("persisted", "stored")))There was a problem hiding this comment.
Replaced the hand-rolled checks with checkAnswer for the selected DESCRIBE rows and the full SHOW TBLPROPERTIES result, plus keyed-property checks. SHOW TABLE EXTENDED and SHOW CREATE TABLE now compare the complete expected output. The fixtures also include reserved keys and a collision with an existing property.
| override def displayProperties(): util.Map[String, String] = { | ||
| Option(tableDisplayProperties).getOrElse(properties) | ||
| } | ||
|
|
||
| def setDisplayProperties(displayProperties: util.Map[String, String]): Unit = { | ||
| tableDisplayProperties = displayProperties | ||
| } |
There was a problem hiding this comment.
nit: these two members are inserted between version() and setVersion(), which splits the version-related members (version, setVersion, setVersionAndValidatedVersionFrom, ..., setValidatedVersion). AGENTS.md also asks us to follow the file's existing sectioning when adding members. Could we move them before version() (after updateColumns, following the columns/properties/displayProperties order in Table), or after setValidatedVersion?
There was a problem hiding this comment.
Moved displayProperties() and setDisplayProperties() immediately after updateColumns(), before version(), so the version-related methods stay together.
| override def version(): String = tableVersion.toString | ||
|
|
||
| override def displayProperties(): util.Map[String, String] = { | ||
| Option(tableDisplayProperties).getOrElse(properties) |
There was a problem hiding this comment.
nit: delegating to the interface default makes the intent clearer: behave like a regular connector when the display properties are unset.
| Option(tableDisplayProperties).getOrElse(properties) | |
| Option(tableDisplayProperties).getOrElse(super.displayProperties()) |
There was a problem hiding this comment.
Applied: the fixture now falls back to super.displayProperties(). With the additive contract, that default is an empty map.
|
Thanks @dongjoon-hyun for review! can you take another look? |
|
Btw i also chatted with @aokolnychyi offline, he is also ok in concept on the new API |
dongjoon-hyun
left a comment
There was a problem hiding this comment.
Thank you for addressing the previous comments, @szehon-ho. I left 13 inline comments on the latest commit. Since displayProperties() defaults to an empty map, none of them changes the behavior of existing connectors. Here is a summary, ordered by priority.
API behavior and contract
CatalogV2UtilL682: display-only keys share the key namespace ofALTER TABLE ... SET/UNSET TBLPROPERTIES, which only changeproperties().UNSETcannot remove a listed display key, and unsetting a colliding stored key makes the display value show up under that key.TableInfoL70:TableInfois also the input ofcreateTable,createTableLikeandstage*, wheredisplayPropertieshas no documented meaning. Spark never sets it, the defaults drop it, and some test catalogs persist it.
Test coverage
DescribeTableSuiteL235: apart fromexternal, every reserved key in the display maps is also stored, so no test covers a display-onlylocation,collation,table_typeoris_managed_location.DelegatingTableL78: theTableInfo.Builder.withDisplayProperties+DelegatingTablepath has no SQL-levelDESCRIBE/SHOWtest.ShowTblPropertiesSuiteL30: no test checks that display-only entries are redacted.
Documentation
Catalog(sql/api) L849: the Spark Connect Scala client'sgetTablePropertieskeeps its own Scaladoc without this note.TableL101: the Javadoc doesn't say thatSHOW TABLE EXTENDEDon the session catalog omits these entries.
Test infrastructure
InMemoryTableCatalogL174: only two in-memory catalogs honorTableInfo.displayProperties()at create time, andNullTableIdInMemoryTableCatalogdrops it.ShowTblPropertiesSuiteL46:InMemoryTableSessionCatalog.alterTablestill drops display state onALTER TABLE.InMemoryBaseTableL105: the rename dropped the rationale for copying the version.InMemoryTableCatalogL282:alterTablecopies the table state by hand instead of callingcopyTableStateFrom.
Nits
catalog.pyL386: the newNotessection comes beforeParameters.CatalogSuiteL1205: the new test registersdisplay_cataloginstead of reusingtestcat.
Items 1 and 2 are the most important ones, since they settle the contract of the new public API before it is released.
|
|
||
| /** Returns non-reserved table properties with additional display-only metadata. */ | ||
| def tablePropertiesForDisplay(table: Table): Map[String, String] = { | ||
| (table.displayProperties().asScala.toMap ++ table.properties().asScala) |
There was a problem hiding this 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 thenSHOW TBLPROPERTIES t ('persisted')returnsdisplay-valueinstead of "Table ... does not have property: persisted".getTablePropertiesstill containspersisted, whileSHOW CREATE TABLEno longer does.ALTER TABLE t UNSET TBLPROPERTIES ('catalog-label')succeeds, because V2UnsetTablePropertiesonly emitsTableChange.removeProperty(a no-op for a missing key, perTableChange.javaL63), but the key is still listed afterwards. No SQL can remove it, so a tool that syncs properties by diffinggetTablePropertiesagainst a desired set never converges.SET TBLPROPERTIES ('catalog-label' = 'x')followed byUNSETbringscatalog-valueback.
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 #59036 (comment)) would avoid the shared namespace.
There was a problem hiding this comment.
Documented that SET/UNSET only changes properties() and cannot remove catalog-controlled display entries. Extended ShowTblPropertiesSuite to check unsetting a colliding stored key, unsetting a display-only key, and SET followed by UNSET. It also checks that getTableProperties returns the display value after UNSET while the key is absent from getCreateTableString.
| * | ||
| * @since 4.4.0 | ||
| */ | ||
| public Map<String, String> displayProperties() { |
There was a problem hiding this 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),CreateTableLikeExecand the pipelinesDatasetManagerbuildTableInfowithout it. - The default
TableCatalog.createTable(ident, tableInfo)(TableCatalog.javaL359-362) and theStagingTableCatalog.stage*defaults flattenTableInfoto(columns, partitions, properties)and drop it.V2SessionCatalogandJDBCTableCataloguse that default, andDelegatingCatalogExtensiondoesn't override it, so it never reaches the delegate. - The test catalogs in this PR persist it at create time (
BasicInMemoryTableCatalog.createTableandInMemoryRowLevelOperationTableCatalog.createTable), andInMemoryRelationCatalogstores the wholeTableInfo.
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?
There was a problem hiding this comment.
Clarified both Javadocs: display properties describe metadata returned by a catalog, and Spark leaves them empty when calling createTable, createTableLike, stageCreate, stageReplace, or stageCreateOrReplace. Removed the create-time display-property setters from the two in-memory catalogs; the preservation test now sets the entries on the live table returned by createTable.
| if (external) { | ||
| table.properties.put(TableCatalog.PROP_EXTERNAL, "true") | ||
| } | ||
| table.setDisplayProperties(Map( |
There was a problem hiding this 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 #59036 (comment).
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.
There was a problem hiding this comment.
Added a shared fixture in CommandSuiteBase that supplies every key in TABLE_RESERVED_PROPERTIES. Both DESCRIBE and SHOW TABLE EXTENDED now test managed/external tables with and without a stored LOCATION. The DESCRIBE assertion includes all reserved rows, and SHOW TABLE EXTENDED checks the complete output.
| } | ||
|
|
||
| @Override | ||
| public Map<String, String> displayProperties() { |
There was a problem hiding this 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.
There was a problem hiding this comment.
Added an end-to-end test in DataSourceV2MetadataTableSuite. Its catalog builds TableInfo with display properties in loadTable and returns a DelegatingTable. The test exercises DESCRIBE TABLE EXTENDED, SHOW TBLPROPERTIES, SHOW TABLE EXTENDED, and getTableProperties, and verifies that getCreateTableString excludes the display-only entry.
| * 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") { |
There was a problem hiding this 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)?
There was a problem hiding this comment.
Added a display-only password to the shared fixture and asserted *********(redacted) in DESCRIBE TABLE EXTENDED, SHOW TBLPROPERTIES (including single-key lookup), and SHOW TABLE EXTENDED. The catalog API test also verifies redaction in getTableProperties.
| checkAnswer(sql(s"SHOW TBLPROPERTIES $tbl ('catalog-label')"), | ||
| Row("catalog-label", "catalog-value")) | ||
|
|
||
| sql(s"ALTER TABLE $tbl SET TBLPROPERTIES ('persisted' = 'updated')") |
There was a problem hiding this 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 #59036 (comment) says table reconstruction during ALTER TABLE preserves the entries, could it also call setDisplayProperties(table.displayProperties())?
There was a problem hiding this comment.
Added setDisplayProperties(table.displayProperties()) when the session-catalog fixture reconstructs a table. A new regression test checks SHOW TBLPROPERTIES and DESCRIBE TABLE EXTENDED after SET, and that the display entry survives a subsequent UNSET.
| * 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. |
There was a problem hiding this 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?
There was a problem hiding this comment.
Restored the paragraph explaining why the version must be copied, and changed the summary to cover version, validated version, and display-only metadata copied from another table.
| s"Unsupported InMemoryBaseTable subclass: ${other.getClass.getName}") | ||
| } | ||
| newTable.setVersion(currentVersion) | ||
| newTable.setDisplayProperties(table.displayProperties()) |
There was a problem hiding this 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?
There was a problem hiding this comment.
Changed this path to call copyTableStateFrom and kept the AddConstraint loop. The preservation test also checks version copying and that validatedVersion survives a property alteration.
|
|
||
| .. versionadded:: 4.2.0 | ||
|
|
||
| Notes |
There was a problem hiding this 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?
There was a problem hiding this comment.
Moved Notes below Returns and before Examples.
| } | ||
|
|
||
| test("catalog API: getTableProperties includes V2 display-only properties") { | ||
| val catalogName = "display_catalog" |
There was a problem hiding this 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?
There was a problem hiding this comment.
Switched the test to the existing testcat and removed the withSQLConf wrapper.
|
Thank you, @dongjoon-hyun when you have a chance can you take another look? |
|
Sure, @szehon-ho . |
|
BTW, it would be great if you can help mine too. |
|
Ah seems @HyukjinKwon already approved |
dongjoon-hyun
left a comment
There was a problem hiding this comment.
Thank you for addressing round 2, @szehon-ho. I reviewed the latest commit, 7b9429e, and left 10 inline comments. All 13 round-2 comments are addressed except the second halves of R2-1 and R2-2 (items 2 and 3 below). Since displayProperties() still defaults to an empty map, none of these changes the behavior of existing connectors. Here is a summary, ordered by priority.
API contract
CatalogV2UtilL682:displayProperties()has no null contract. A null value makesSHOW TBLPROPERTIESandgetTablePropertiesfail whileDESCRIBE TABLE EXTENDEDprintsk=null, and a null map fails all three commands with an NPE.TableL107: the SET/UNSET sentences describe the catalog'salterTablebehavior as Spark's, and the R2-1 note that a display value shows through after unsetting a colliding stored key is still missing.TableInfoL68: the "catalogs may ignore it there" half of R2-2 is missing, and the five-method list is repeated in the setter.
Pre-existing
DescribeTableExecL192:Typeignorestable_type, so aDelegatingTablebuilt withwithTableType(EXTERNAL)shows bothType MANAGEDandTable_type EXTERNAL. A follow-up JIRA seems better than changing it here.
Performance and cleanup
DescribeTableExecL206:properties()is still read twice per table, because the helper reads it again.CatalogV2UtilL681: the display helper returns unredacted values, and all three callers redact them the same way.
Tests
ShowCreateTableSuiteL43: the shared display fixture inCommandSuiteBaseisn't reused here.DescribeTableSuiteL223: the 4-way grid and the table setup are copied inShowTablesSuite, and two of the four cases add no coverage.CatalogSuite(catalyst) L99: the gridTest mode names don't describe what differs between the two cases.
Nits
InMemoryTableCatalogL171: a whitespace-only re-wrap left over from R2-8.
Items 1 and 2 are the most important ones, since they define the contract of the new public API before it is released.
|
|
||
| /** Returns non-reserved table properties with additional display-only metadata. */ | ||
| def tablePropertiesForDisplay(table: Table): Map[String, String] = { | ||
| (table.displayProperties().asScala.toMap ++ table.properties().asScala) |
There was a problem hiding this comment.
The new API has no null contract, and this merge passes null values through. TableInfo.Builder.withDisplayProperties copies the map with new HashMap<>(...), which keeps null values, and Utils.redact passes them through. Take a connector that returns a DelegatingTable whose display map has "last_refresh" -> null for a value it doesn't know yet:
SHOW TBLPROPERTIES t,SHOW TBLPROPERTIES t ('last_refresh')andspark.catalog.getTablePropertiesfail, becauseShowTablePropertiesdeclareskeyandvalueas non-nullable (v2Commands.scalaL1583-1584) andtoCatalystRowrejects the null.DESCRIBE TABLE EXTENDEDandSHOW TABLE EXTENDEDprintlast_refresh=null.
If displayProperties() itself returns null, asScala returns null and .toMap throws an NPE in all three commands. ShowTablesExtendedExec has no per-table error handling, so one such table fails SHOW TABLE EXTENDED IN cat.ns LIKE '*' for the whole namespace.
properties() has the same exposure, but the SQL path rejects null property values, while these entries are computed by the catalog and may be transient. Could the Table#displayProperties() Javadoc say that the map and its keys and values must not be null, as TableCatalog#tableStateOptionKeys() does with "a non-null set"? Alternatively, this helper could drop null entries.
There was a problem hiding this comment.
Added an explicit non-null map/keys/values contract to Table.displayProperties() and the TableInfo builder parameter. This uses the documented-contract approach rather than silently dropping invalid entries.
| * {@link TableCatalog#PROP_LOCATION} are ignored in this map. Table type and other reserved | ||
| * metadata are read from {@code properties()}. | ||
| * | ||
| * <p>{@code ALTER TABLE ... SET/UNSET TBLPROPERTIES} only changes {@code properties()}. |
There was a problem hiding this comment.
These two sentences describe the catalog's behavior as Spark's. On this path Spark never reads the display entries: SetTableProperties and UnsetTableProperties only produce TableChange.setProperty and TableChange.removeProperty, and AlterTableExec.scala L41 passes them to TableCatalog.alterTable. Whether a display entry survives SET, or disappears on UNSET, is up to the catalog:
InMemoryTableSessionCatalog.alterTabledropped the display state on everyALTER TABLEuntil R2-9 was fixed.- A catalog that handles
removeProperty("label")by deleting its label contradicts "cannot be removed with UNSET TBLPROPERTIES".
Also, the second half of R2-1, that a display value shows through once a colliding stored key is unset, is only in the PR description. Could this paragraph be phrased as guidance for implementors? For example:
<p>Spark passes {@code ALTER TABLE ... SET/UNSET TBLPROPERTIES} to
{@link TableCatalog#alterTable} as {@link TableChange#setProperty} and
{@link TableChange#removeProperty} without consulting this map. Implementations should apply
them to {@code properties()} only and keep display-only entries unchanged. Then
{@code UNSET TBLPROPERTIES} of a display-only key succeeds but the key is still listed, and
unsetting a stored key that collides with a display-only key exposes the display-only value.There was a problem hiding this comment.
Reworded this as guidance for catalog implementations: Spark forwards SET/UNSET to alterTable without consulting the display map. The precedence paragraph now also states that, after a stored property is removed, Spark displays the entry for that key if it remains in the display map.
| /** | ||
| * Returns the additional display-only properties described by {@link Table#displayProperties()}. | ||
| * | ||
| * <p>These properties describe metadata returned by a catalog. Spark leaves them empty when |
There was a problem hiding this comment.
Minor: R2-2 also asked to say that catalogs may ignore this field on the create path, and that half is missing here and in the setter (L96-99). The default TableCatalog.createTable(Identifier, TableInfo) and the StagingTableCatalog.stage* defaults drop it, but a catalog that stores the whole TableInfo, such as InMemoryRelationCatalog, persists whatever a non-Spark caller put there, e.g. catB.createTable(ident, delegatingTable.getTableInfo()). Without that sentence, implementors can't tell whether to ignore it or store it. Could we add something like "Catalogs may ignore them in these methods."? The setter could then just refer to {@link TableInfo#displayProperties()}, so the five-method list lives in one place.
There was a problem hiding this comment.
Removed the duplicated creation-method list from the builder documentation; it now lives only on the getter. I kept the scope to the current Spark behavior: these are returned metadata, and Spark leaves them empty on creation/staging calls. Since TableInfo is internal for now, I would prefer not to add a contract for non-Spark callers supplying this field on those paths.
|
|
||
| val tableType = if (table.properties().containsKey(TableCatalog.PROP_EXTERNAL)) { | ||
| val tableProperties = table.properties() | ||
| val tableType = if (tableProperties.containsKey(TableCatalog.PROP_EXTERNAL)) { |
There was a problem hiding this comment.
Minor, pre-existing: this still decides Type by containsKey(PROP_EXTERNAL) alone and ignores PROP_TABLE_TYPE, although withTableType is the only type setter on TableInfo.Builder. For a DelegatingTable built with .withTableType(TableSummary.EXTERNAL_TABLE_TYPE), like the existing test_json fixture in DataSourceV2MetadataTableSuite:
DESCRIBE TABLE EXTENDEDprints bothType | MANAGEDandTable_type | EXTERNAL.SHOW TABLE EXTENDEDprintsType: MANAGED(ShowTablesExtendedExec.scalaL86).SHOW TABLE EXTENDED ... AS JSON(throughlistTableSummaries) andV1Table.toCatalogTabletreat it asEXTERNAL.
This is the external=false case from R1-2 seen from another side. It isn't introduced by this PR, and fixing it changes pinned outputs, so could we file a follow-up JIRA to derive Type from table_type first and fall back to external?
There was a problem hiding this comment.
Agreed this is pre-existing and should be fixed separately by honoring table_type before the external fallback. Leaving that behavior unchanged in this PR. I have prepared the follow-up JIRA draft, but have not filed it yet.
| val properties = | ||
| conf.redactOptions(table.properties.asScala.toMap).toList | ||
| .filter(kv => !CatalogV2Util.TABLE_RESERVED_PROPERTIES.contains(kv._1)) | ||
| conf.redactOptions(CatalogV2Util.tablePropertiesForDisplay(table)).toList |
There was a problem hiding this comment.
nit: L191 hoists table.properties(), but tablePropertiesForDisplay(table) reads table.properties() again, so this command still reads it twice per table, and so does ShowTablesExtendedExec.scala (L85 and L102). Could the helper take the map the caller already has, e.g. tablePropertiesForDisplay(tableProperties, table.displayProperties())? Then the Type and reserved rows also come from the same snapshot as the Table Properties row.
There was a problem hiding this comment.
Changed the helper to accept the stored and display property maps. DESCRIBE TABLE EXTENDED and SHOW TABLE EXTENDED now pass the stored map already fetched for the reserved metadata rows, so each reads properties() only once.
| } | ||
|
|
||
| /** Returns non-reserved table properties with additional display-only metadata. */ | ||
| def tablePropertiesForDisplay(table: Table): Map[String, String] = { |
There was a problem hiding this comment.
nit: all three callers wrap this helper in the same conf.redactOptions(...): DescribeTableExec.scala L206, ShowTablePropertiesExec.scala L35 and ShowTablesExtendedExec.scala L102. Since the name says "for display", a future caller could print the result as is, and these entries can hold secrets, as the display-only password in the new fixture shows. Could the helper take a conf: SQLConf, like lookupCachedRelationWithStateOptions in this object, and redact inside? If you prefer to keep redaction at the call sites, could the Scaladoc say that the values are not redacted?
There was a problem hiding this comment.
Moved redaction into tablePropertiesForDisplay with an explicit SQLConf parameter and removed the three caller-side wrappers. Added a helper-level test for stored and display-only secrets, the SQL redaction pattern, stored-value precedence, and unchanged input maps.
| sql(s"CREATE TABLE $t (id bigint) $defaultUsing " + | ||
| "COMMENT 'table comment' LOCATION 'file:/tmp/display-properties' " + | ||
| "TBLPROPERTIES ('persisted' = 'stored')") | ||
| loadTable(catalog, ns, table).setDisplayProperties(Map( |
There was a problem hiding this comment.
nit: the other v2 suites use the shared setDisplayProperties helper from CommandSuiteBase, which this suite also mixes in. ShowCreateTableExec reads only table.properties, so setDisplayProperties(loadTable(catalog, ns, table)) keeps the expected DDL, also covers display-only provider, collation, owner and password, and lets us drop scala.jdk.CollectionConverters._ (L20) and TableCatalog (L25). Separately, could the helper's Scaladoc mention that external = true writes external into the stored properties? It shares its name with InMemoryBaseTable.setDisplayProperties, which only sets display entries.
There was a problem hiding this comment.
SHOW CREATE TABLE now uses the shared fixture, including all of its reserved display keys and password entry, and the unused imports are removed. The setter documentation also states that external = true updates the stored properties.
| } | ||
| } | ||
|
|
||
| gridTest("display properties preserve table type and reserved metadata")( |
There was a problem hiding this comment.
nit: this gridTest and the one at ShowTablesSuite.scala L72-80 differ only in the table name. The grid, the LOCATION literal, the CREATE TABLE statement and the setDisplayProperties(loadTable(...), external) call are copies, so the shared helper from R2-3 keeps only the display map in sync. Also, the two axes are independent (Type depends only on the stored external, and Location only on the stored location), so (false, true) and (true, false) add no coverage, while (true, true) still checks that a display external=false and a display location don't override the stored values. Could the table creation move into the CommandSuiteBase helper, and the grid become Seq((false, false), (true, true))?
There was a problem hiding this comment.
Moved table creation into createTableWithDisplayProperties in CommandSuiteBase and reused it across DESCRIBE, SHOW TABLE EXTENDED, and SHOW CREATE TABLE. Reduced both grids to (false, false) and (true, true).
| assert(defaultTable.displayProperties().isEmpty) | ||
| } | ||
|
|
||
| gridTest("display properties survive copies, pinning and alterations")( |
There was a problem hiding this comment.
nit: the two modes are named after different things, an option and a catalog class. L106 passes copyOnLoad=true to both, but InMemoryRowLevelOperationTableCatalog.loadTable always returns rlot.copy(), so the option has no effect in the rowLevel case, and the names don't say what actually differs. Could the modes be named after the catalogs, e.g. Seq("InMemoryTableCatalog", "InMemoryRowLevelOperationTableCatalog"), with a short comment on the option?
There was a problem hiding this comment.
Renamed the grid cases after the two catalog classes and added a comment explaining that the row-level catalog always copies on load, while the option enables this for InMemoryTableCatalog.
|
|
||
| override def createTable(ident: Identifier, tableInfo: TableInfo): Table = { | ||
| createTable(ident, tableInfo.columns(), tableInfo.partitions(), tableInfo.properties(), | ||
| createTable( |
There was a problem hiding this comment.
nit: after the create-time setDisplayProperties was removed for R2-8, this re-wrap is the only change left in this method, and the arguments are unchanged. Could we restore the original single line to keep the diff and git blame clean?
There was a problem hiding this comment.
Restored the original argument wrapping in this method.
dongjoon-hyun
left a comment
There was a problem hiding this comment.
Thank you for addressing round 3, @szehon-ho. All 10 round-3 comments are answered, and you kept the "catalogs may ignore" part of R3-3 and the table_type change of R3-4 out of this PR. I reviewed the same head, f25203b, again on top of the current master (the merge is clean) and left 15 inline comments. I found no correctness issue in the main code, and since displayProperties() still defaults to an empty map, none of these changes the behavior of existing connectors. Here is a summary, ordered by priority.
API documentation
Table.javaL106: the show-through sentence doesn't hold forcomment, the one ignored key thatSET/UNSET TBLPROPERTIESaccept.Table.javaL116:CREATE TABLE ... LIKEpasses the source table, with these entries, tocreateTableLike, whose Javadoc invites connectors to clone its custom state.Table.javaL110: the SQL reference pages forALTER TABLE ... UNSET TBLPROPERTIESandSHOW TBLPROPERTIESdon't mention display-only entries.TableInfo.javaL100: nothing says that Spark reads these entries only to display metadata, so the builder path computes them on every load.Table.javaL101: the Javadoc doesn't say that these entries are redacted like stored properties.TableInfo.javaL97: the setter's@seedoesn't lead to the create/stage note, and the five method references are unchecked{@code}spans.
Tests
DataSourceV2DataFrameSessionCatalogSuite.scalaL69: no test pins the documented session-catalogSHOW TABLE EXTENDEDlimitation, and one assertion here would.CatalogV2UtilSuite.scalaL77: some assertions in the new helper tests can't fail, including the two "unchanged input maps" checks.ShowTblPropertiesSuite.scalaL36: the single-key path ofSHOW TBLPROPERTIESis never queried with a reserved key.DataSourceV2MetadataTableSuite.scalaL195:Predef.assertreplaced the "shouldn't be called" guard, so an unexpected listing now fails asINTERNAL_ERROR.
Test fixtures
CommandSuiteBase.scalaL92: the fixture mutates whateverloadTablereturns, which works only becausetest_catalogreturns the live table.CommandSuiteBase.scalaL100:CREATE EXTERNAL TABLEshould work on this catalog, so the storedexternaldoesn't need to be written by hand.ShowTblPropertiesSuite.scalaL29: this is the only display-property test that doesn't usecreateTableWithDisplayProperties.InMemoryBaseTable.scalaL91:displayProperties()changes from a read-only map to the internal mutable map after the first copy orALTER TABLE.
Nits
CatalogV2UtilSuite.scalaL20:import java.utilmixes two styles in this file and inDataSourceV2DataFrameSessionCatalogSuite.
Item 1 is the most important one, since it corrects a sentence of the new public API contract, and my R3-2 wording had the same gap. Items 2 to 6 are documentation only, and all the test items are non-blocking.
| * precedence, and reserved keys such as {@link TableCatalog#PROP_EXTERNAL} and | ||
| * {@link TableCatalog#PROP_LOCATION} are ignored in this map. Table type and other reserved | ||
| * metadata are read from {@code properties()}. | ||
| * After a stored property is removed, Spark displays the entry for that key if it remains in |
There was a problem hiding this comment.
This sentence doesn't hold for comment, the one ignored key that SET/UNSET TBLPROPERTIES accept. AstBuilder.cleanTableProperties keeps comment (AstBuilder.scala L6058-6059: "we don't make it a reserved table property"), so ALTER TABLE t UNSET TBLPROPERTIES ('comment') reaches alterTable as TableChange.removeProperty("comment"). However, tablePropertiesForDisplay and the reserved-row loops still treat comment as one of TABLE_RESERVED_PROPERTIES. Take a table created with COMMENT 'c' whose displayProperties() contains comment=d. After that UNSET:
DESCRIBE TABLE EXTENDEDhas noCommentrow and nocommententry inTable Properties.SHOW TBLPROPERTIES t ('comment')returns "does not have property: comment".
So the stored property was removed and comment remains in this map, but nothing is displayed. The two examples on L103-104 (external, location) are keys that the parser rejects in TBLPROPERTIES, so they don't hint at this exception, and the wording I suggested in R3-2 had the same gap. Could we name {@link TableCatalog#PROP_COMMENT} among the ignored keys and scope this sentence to non-reserved keys, e.g. "After a stored non-reserved property is removed, ..."?
There was a problem hiding this comment.
Scoped the show-through sentence to stored non-reserved properties and named PROP_COMMENT alongside the ignored reserved keys.
| * <p>For now, {@code SHOW TABLE EXTENDED} omits these entries for the built-in session catalog | ||
| * and its {@link CatalogExtension}s. | ||
| * | ||
| * <p>These entries may be transient and are excluded from {@code SHOW CREATE TABLE}. The |
There was a problem hiding this comment.
CREATE TABLE ... LIKE hands the source table, including these entries, to the target catalog. DataSourceV2Strategy L294 passes the resolved source Table as is, and CreateTableLikeExec L71 calls createTableLike(targetIdent, tableInfo, sourceTable). The createTableLike Javadoc says that connectors "may read format-specific properties or other custom state from this object to clone additional metadata" (TableCatalog.java L382-383, see also L372-373). With this PR, that object also exposes displayProperties(), and a DelegatingTable also exposes getTableInfo().displayProperties(). Nothing tells the connector not to clone them, so a catalog that does would persist the source catalog's transient labels on the new table, which is what this PR keeps out of SHOW CREATE TABLE. Unlike R2-2 and R3-3, which were about non-Spark callers filling TableInfo, here Spark itself makes the call. Could this sentence also cover it, e.g. "These entries may be transient. Spark excludes them from SHOW CREATE TABLE, and TableCatalog#createTableLike implementations should not copy them from the source table."? InMemoryTableCatalog.createTableLike (L324) already copies only sourceTable.properties().
There was a problem hiding this comment.
Added guidance that TableCatalog.createTableLike implementations should not copy display-only entries from the source table, with a link to the full method signature.
| * this map. | ||
| * | ||
| * <p>Spark passes {@code ALTER TABLE ... SET/UNSET TBLPROPERTIES} to | ||
| * {@link TableCatalog#alterTable} without consulting this map. Catalogs should apply these |
There was a problem hiding this comment.
The SQL reference pages don't mention these entries, so SQL users only see the effect. This paragraph explains the SET/UNSET behavior to connector authors, but docs/sql-ref-syntax-ddl-alter-table.md L279 still says that ALTER TABLE UNSET "is used to drop the table property", and docs/sql-ref-syntax-aux-show-tblproperties.md L24-25 says that all the properties are returned. For a V2 table with display-only entries:
ALTER TABLE t UNSET TBLPROPERTIES ('catalog-label')succeeds, butcatalog-labelis still listed.- Unsetting a stored key that collides with a display-only key switches the listed value to the display-only value instead of removing the key, as the new
ShowTblPropertiesSuitetest pins (L47-61).
Neither page becomes wrong, but the explanation now exists only in this Javadoc and in the PR description. Could we add a short note to the SHOW TBLPROPERTIES page, and optionally to the UNSET section of the ALTER TABLE page, e.g. "For V2 tables, the output may also include display-only properties supplied by the catalog. ALTER TABLE ... UNSET TBLPROPERTIES does not remove them, and SHOW CREATE TABLE does not include them."?
There was a problem hiding this comment.
Added notes to both the SHOW TBLPROPERTIES reference and the ALTER TABLE ... UNSET TBLPROPERTIES section. They explain that UNSET leaves display-only entries intact and can expose a display value after removing a colliding stored non-reserved property. The SHOW page also notes their exclusion from SHOW CREATE TABLE.
| * @see Table#displayProperties() | ||
| * @since 4.4.0 | ||
| */ | ||
| public Builder withDisplayProperties(Map<String, String> displayProperties) { |
There was a problem hiding this comment.
Spark reads these entries only in display commands, but this builder makes a catalog compute them on every load. The only readers are DescribeTableExec L207, ShowTablePropertiesExec L36 and ShowTablesExtendedExec L103. This setter copies the map when the TableInfo is built (L101), and loadTable/loadRelation don't know which command is running. So a catalog that follows the new TestingDataSourceTableCatalog fixture computes the entries for every query that resolves the table, although RelationResolution L467 turns a DelegatingTable into a CatalogTable through V1Table.toCatalogTable, which reads only info.properties (V1Table.scala L123). That costs nothing when the labels come with the table metadata. A catalog that fetches them from a separate service, such as a tagging API, pays one extra call per query, and nothing says that it can defer them by overriding displayProperties(), e.g. in a DelegatingTable subclass. Could the Table#displayProperties() Javadoc, or this setter, say something like "Spark reads these entries only to display table metadata, so implementations may compute them on demand."?
There was a problem hiding this comment.
Added to Table.displayProperties() that Spark reads these entries only to display table metadata, so implementations may compute them on demand.
| /** | ||
| * Returns additional, display-only table properties. | ||
| * | ||
| * <p>Spark adds these entries to the non-reserved properties displayed by V2 table metadata |
There was a problem hiding this comment.
Could this paragraph also say that these entries are redacted like stored properties? tablePropertiesForDisplay applies conf.redactOptions to the merged map (CatalogV2Util L687), which uses both spark.sql.redaction.options.regex (default (?i)url) and spark.redaction.regex, and Utils.redact replaces a value when either the key or the value matches. With the default configs, a catalog that returns docs_url -> https://wiki/t and team -> credentials-platform gets *********(redacted) for both entries in every output, including getTableProperties. Redacting them is the right behavior, but only the Scaladoc of the private[sql] helper mentions it, so a connector author can't tell from the API doc why an ordinary label is masked. A sentence such as "Spark redacts these entries in the same way as stored properties." would be enough.
There was a problem hiding this comment.
Added the sentence that Spark redacts these entries in the same way as stored properties.
| sql(s"CREATE TABLE $catalog.$namespace.$table (id bigint) $defaultUsing " + | ||
| s"COMMENT 'table comment' $location" + | ||
| "TBLPROPERTIES ('persisted' = 'stored')") | ||
| setDisplayProperties(loadTable(catalog, namespace, table), external) |
There was a problem hiding this comment.
Minor, latent: the fixture writes into whatever loadTable returns. On test_catalog that is the live table, because copyOnLoad defaults to false, so the current tests are fine. However, row_level_op_catalog, registered in the same sparkConf (L50-51), returns rlot.copy() on every load, and the copy shares properties with the live table (InMemoryRowLevelOperationTable.copy) but gets its own display map (InMemoryBaseTable.setDisplayProperties). With that catalog, external = true would reach the live table while every display entry is lost on the next load, so the "excludes display-only properties" checks would pass without checking anything. Could the fixture take the table from BasicInMemoryTableCatalog.liveTable, as RowLevelOperationSuiteBase.table does for the same reason?
There was a problem hiding this comment.
The shared fixture now obtains the underlying table through BasicInMemoryTableCatalog.liveTable before setting display properties, so the setup also works for catalogs that copy on load.
| */ | ||
| def setDisplayProperties(table: InMemoryTable, external: Boolean = false): Unit = { | ||
| if (external) { | ||
| table.properties.put(TableCatalog.PROP_EXTERNAL, "true") |
There was a problem hiding this comment.
nit: CREATE EXTERNAL TABLE ... LOCATION should work on this V2 catalog, so the fixture doesn't need to write external into the stored map by hand. For a non-session catalog, the parser keeps the EXTERNAL flag, the LOCATION requirement exists only in the V1 SessionCatalog.createTable, and CreateTableExec stores external=true through CatalogV2Util.convertTableProperties. That is the same path that already stores external=true for spark.catalog.createTable with a path on testcat, and the reserved-key error points users to it ("please use CREATE EXTERNAL TABLE"). Could createTableWithDisplayProperties use CREATE ${if (external) "EXTERNAL " else ""}TABLE ... and drop L99-101? Then setDisplayProperties only sets display entries, like InMemoryBaseTable.setDisplayProperties, and the caveat in its Scaladoc (L96) is no longer needed. The conflicting display external value can still come from the argument.
There was a problem hiding this comment.
The fixture now uses CREATE EXTERNAL TABLE when requested. Removed the direct write to stored external; setDisplayProperties now sets only display entries, and its Scaladoc reflects that.
| 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 " + |
There was a problem hiding this comment.
nit: this is the only one of the four V2 display-property tests that still creates its table by hand. createTableWithDisplayProperties("ns", "table") creates the same table with persisted = stored and the same display fixture. Its extra COMMENT 'table comment' doesn't change any assertion here, because comment is a reserved key that tablePropertiesForDisplay drops, and getCreateTableString still contains no persisted. Could L29-31 call the helper, so that the shared table definition lives in one place?
There was a problem hiding this comment.
Reused createTableWithDisplayProperties("ns", "table") in the SHOW TBLPROPERTIES test, consolidating its setup with the other display-property tests.
| } | ||
|
|
||
| override def displayProperties(): util.Map[String, String] = { | ||
| Option(tableDisplayProperties).getOrElse(super.displayProperties()) |
There was a problem hiding this comment.
nit: until display properties are set, this returns the interface default Collections.emptyMap(), which rejects put. After setDisplayProperties or any copyTableStateFrom call (every copy(), alterTable and pinTable path), it returns the internal HashMap itself, so t.displayProperties().put(...) silently changes the table's state. No current test mutates it, so this is latent, but DelegatingTable returns a read-only view, which the new catalyst CatalogSuite test pins. Returning Collections.unmodifiableMap(tableDisplayProperties) here would make both states read-only and leave setDisplayProperties as the only way to change them.
There was a problem hiding this comment.
Wrapped the configured display map in Collections.unmodifiableMap, keeping the accessor read-only after setting or copying display properties as well as in its default state.
|
|
||
| package org.apache.spark.sql.connector.catalog | ||
|
|
||
| import java.util |
There was a problem hiding this comment.
nit: the rest of this file writes JDK types fully qualified (e.g. L81 java.util.Set[String] and L161 java.util.Map.of), but the new tests add import java.util for util.Map.of and util.HashMap (L42-69). Could they use java.util.Map.of and new java.util.HashMap and drop the import? The same applies to DataSourceV2DataFrameSessionCatalogSuite L20 and L63, where the only util.Map.of sits next to the existing java.util.Map[String, String] at L151.
There was a problem hiding this comment.
Removed import java.util from both files and qualified the new Map and HashMap usages as java.util.Map and java.util.HashMap to match the surrounding code.
|
thanks for the review @dongjoon-hyun, can you take another look when you get a chance? |
|
Sure, @szehon-ho . |
dongjoon-hyun
left a comment
There was a problem hiding this comment.
+1, LGTM (Pending CIs).
| a property key. If no key is specified then all the properties are returned. | ||
|
|
||
| For V2 tables, the output may also include display-only properties supplied by the catalog. | ||
| `ALTER TABLE ... UNSET TBLPROPERTIES` does not remove these entries, and `SHOW CREATE TABLE` |
There was a problem hiding this comment.
nit: this sentence and "unsetting a stored non-reserved property exposes the display-only value" on L30 read as guarantees from Spark. However, the API contract in Table.displayProperties() only says that catalogs should apply SET/UNSET TBLPROPERTIES to properties() and keep display-only entries unchanged, and it now also allows computing them on demand. For example, a catalog that derives a display label from a stored property, or one that removes the key from both maps in alterTable, would no longer list the entry after UNSET. This is the same distinction that R3-2 made for the Javadoc. Could we say what Spark does and leave the rest to the catalog, e.g. "Spark itself does not remove these entries on ALTER TABLE ... UNSET TBLPROPERTIES; whether they remain depends on the catalog."? The same applies to sql-ref-syntax-ddl-alter-table.md L282-283.
There was a problem hiding this comment.
Updated both SQL reference pages in 7a57149 to distinguish Spark's behavior from the catalog's: Spark itself does not remove display-only entries on UNSET, whether they remain depends on the catalog, and a display value appears after unsetting a stored key only if the catalog still supplies it.
| For V2 tables, the output may also include display-only properties supplied by the catalog. | ||
| `ALTER TABLE ... UNSET TBLPROPERTIES` does not remove these entries, and `SHOW CREATE TABLE` | ||
| does not include them. Stored properties take precedence over display-only entries with the | ||
| same key; unsetting a stored non-reserved property exposes the display-only value if present. |
There was a problem hiding this comment.
nit: "non-reserved" isn't defined anywhere in the SQL reference, and the Note at the end of this page lists the V1 exclusions (spark.sql*, EXTERNAL, comment, Hive statistics), not the V2 reserved keys. In practice, comment is the only reserved key that UNSET TBLPROPERTIES accepts, because AstBuilder.cleanTableProperties keeps it and rejects or drops the other reserved keys. So SQL users can't tell that UNSET TBLPROPERTIES ('comment') doesn't expose a display-only comment. Could we name the key instead, e.g. "unsetting a stored property other than comment exposes ..."? The same applies to sql-ref-syntax-ddl-alter-table.md L283.
There was a problem hiding this comment.
Updated both pages in 7a57149 to say "other than comment" instead of "non-reserved", and explicitly state that display-only comment entries are ignored even after unsetting the stored comment.
| This statement returns the value of a table property given an optional value for | ||
| a property key. If no key is specified then all the properties are returned. | ||
|
|
||
| For V2 tables, the output may also include display-only properties supplied by the catalog. |
There was a problem hiding this comment.
nit: within docs/sql-ref*.md, "V2 tables" appears only here and in sql-ref-syntax-ddl-alter-table.md L281, so SQL users can't tell which tables it covers. For example, parquet or Hive tables in spark_catalog are not covered. Could we link the existing Data Source V2 page, e.g. For tables in [Data Source V2](sql-v2-data-sources.html) catalogs, ...?
There was a problem hiding this comment.
Updated both pages in 7a57149 to use "For tables in Data Source V2 catalogs" with a link to the existing Data Source V2 page.
7a57149 to
63f73a7
Compare
|
re-trigger CI |
Generated-by: Codex (GPT-5)
63f73a7 to
d1f14d3
Compare
|
Build pass but build status not updated due to #58579, merging |
### What changes were proposed in this pull request? Add `Table.displayProperties()` for additional display-only table properties, defaulting to an empty map. The map and its keys and values must be non-null. Spark merges these entries with non-reserved stored properties for V2 `DESCRIBE TABLE EXTENDED`, `SHOW TBLPROPERTIES`, and `SHOW TABLE EXTENDED`. Stored values take precedence, and reserved keys in the display map are ignored. Table type, location, provider, owner, and other reserved metadata continue to come from `properties()`. Support metadata-only tables through `TableInfo.Builder.withDisplayProperties()` and `DelegatingTable`. Keep `SHOW CREATE TABLE` on `properties()`. The API documentation also guides `createTableLike` implementations to exclude display-only entries when copying source metadata, notes that implementations may compute them on demand, and explains that Spark redacts them like stored properties. ### Why are the changes needed? Catalogs may expose transient metadata such as labels that should be visible when inspecting a table without appearing in the DDL used to recreate it. The additive API lets connectors supply these entries separately from stored properties. JIRA: https://issues.apache.org/jira/browse/SPARK-59778 ### Does this PR introduce _any_ user-facing change? Yes. Table implementations can opt in to display-only properties. Existing connectors retain their behavior because the new method defaults to an empty map. Mockito mocks that only stub `properties()` also retain their existing results. `spark.catalog.getTableProperties()` remains equivalent to `SHOW TBLPROPERTIES`, including in PySpark and Spark Connect, so it also returns display-only entries. The Scala, Connect Scala, and Python API documentation explains that these entries may be transient and are excluded from the DDL returned by `getCreateTableString()`. `ALTER TABLE ... SET/UNSET TBLPROPERTIES` forwards changes to the catalog without consulting display properties. The API documentation guides catalogs to apply these changes to stored properties and keep display-only entries unchanged. After a colliding stored non-reserved property is unset, the display value is shown if it remains in the display map. Reserved display keys, including `comment`, remain ignored. The shared display helper applies the existing redaction rules and reuses the stored property map fetched for reserved metadata. `TableInfo` carries display properties as returned metadata. Spark leaves them empty when calling the table creation and staging methods. V2 views retain their existing behavior. The SQL references for `SHOW TBLPROPERTIES` and `ALTER TABLE ... UNSET TBLPROPERTIES` explain display-only entries and stored-property precedence. For now, `SHOW TABLE EXTENDED` omits display-only entries for the built-in session catalog and its `CatalogExtension`s. `DESCRIBE TABLE EXTENDED` and `SHOW TBLPROPERTIES` include them when an extension returns a V2 table. ### How was this patch tested? For this revision, all 34 selected tests passed using `SPARK_LOCAL_IP=localhost build/sbt`: - Catalyst `CatalogV2UtilSuite`: 24 tests. - Catalyst `CatalogSuite -- -z "display properties"`: 2 tests. - V2 `DescribeTableSuite`, `ShowTablesSuite`, `ShowTblPropertiesSuite`, `DataSourceV2MetadataTableSuite`, and `DataSourceV2DataFrameSessionCatalogSuite`, filtered by `"display properties"`: 7 tests. - V2 `ShowCreateTableSuite -- -z "display-only properties"`: 1 test. The added assertions cover reserved-key lookup and the session-catalog `SHOW TABLE EXTENDED` limitation. The helper redaction test checks that mutable input maps remain unchanged. External-table fixtures now use `CREATE EXTERNAL TABLE`, and display fixture setup uses the underlying live table. Catalyst Java Checkstyle, Catalyst/SQL test Scalastyle, and `git diff --check` also passed for this revision. The previous revision passed all 214 selected tests; one pre-existing test was ignored: - Catalyst `CatalogSuite` and `CatalogV2UtilSuite`: 103 tests. - V2 `DescribeTableSuite`, `ShowTblPropertiesSuite`, `ShowTablesSuite`, and `ShowCreateTableSuite`, plus `DataSourceV2MetadataTableSuite`: 100 tests. - `DataSourceV2DataFrameSessionCatalogSuite -- -z "display properties"`: 1 test. - `org.apache.spark.sql.internal.CatalogSuite -- -z "catalog API"`: 10 tests. Ran with `SPARK_LOCAL_IP=localhost build/sbt`, using `catalyst/testOnly` and `sql/testOnly` for the suites above. Coverage includes stored/display collisions, SET and UNSET, every reserved display key with and without a stored location, redaction, metadata-only tables, exclusion from generated DDL, and preservation across copies, pinning, and ALTER TABLE in both V2 and session-catalog fixtures. The previous revision also passed Catalyst Java Checkstyle, Catalyst/SQL main and test Scalastyle, and `git diff --check`. SQL API/Connect Common scalafmt validation and Python syntax checks passed in an earlier revision; those files are unchanged in this update. ### Was this patch authored or co-authored using generative AI tooling? Generated-by: Codex (GPT-6) Closes #59036 from szehon-ho/codex/table-display-properties. Authored-by: Szehon Ho <[email protected]> Signed-off-by: Szehon Ho <[email protected]> (cherry picked from commit 918645b) Signed-off-by: Szehon Ho <[email protected]>
What changes were proposed in this pull request?
Add
Table.displayProperties()for additional display-only table properties, defaulting to an empty map. The map and its keys and values must be non-null. Spark merges these entries with non-reserved stored properties for V2DESCRIBE TABLE EXTENDED,SHOW TBLPROPERTIES, andSHOW TABLE EXTENDED. Stored values take precedence, and reserved keys in the display map are ignored. Table type, location, provider, owner, and other reserved metadata continue to come fromproperties().Support metadata-only tables through
TableInfo.Builder.withDisplayProperties()andDelegatingTable. KeepSHOW CREATE TABLEonproperties(). The API documentation also guidescreateTableLikeimplementations to exclude display-only entries when copying source metadata, notes that implementations may compute them on demand, and explains that Spark redacts them like stored properties.Why are the changes needed?
Catalogs may expose transient metadata such as labels that should be visible when inspecting a table without appearing in the DDL used to recreate it. The additive API lets connectors supply these entries separately from stored properties.
JIRA: https://issues.apache.org/jira/browse/SPARK-59778
Does this PR introduce any user-facing change?
Yes. Table implementations can opt in to display-only properties. Existing connectors retain their behavior because the new method defaults to an empty map. Mockito mocks that only stub
properties()also retain their existing results.spark.catalog.getTableProperties()remains equivalent toSHOW TBLPROPERTIES, including in PySpark and Spark Connect, so it also returns display-only entries. The Scala, Connect Scala, and Python API documentation explains that these entries may be transient and are excluded from the DDL returned bygetCreateTableString().ALTER TABLE ... SET/UNSET TBLPROPERTIESforwards changes to the catalog without consulting display properties. The API documentation guides catalogs to apply these changes to stored properties and keep display-only entries unchanged. After a colliding stored non-reserved property is unset, the display value is shown if it remains in the display map. Reserved display keys, includingcomment, remain ignored. The shared display helper applies the existing redaction rules and reuses the stored property map fetched for reserved metadata.TableInfocarries display properties as returned metadata. Spark leaves them empty when calling the table creation and staging methods. V2 views retain their existing behavior. The SQL references forSHOW TBLPROPERTIESandALTER TABLE ... UNSET TBLPROPERTIESexplain display-only entries and stored-property precedence.For now,
SHOW TABLE EXTENDEDomits display-only entries for the built-in session catalog and itsCatalogExtensions.DESCRIBE TABLE EXTENDEDandSHOW TBLPROPERTIESinclude them when an extension returns a V2 table.How was this patch tested?
For this revision, all 34 selected tests passed using
SPARK_LOCAL_IP=localhost build/sbt:CatalogV2UtilSuite: 24 tests.CatalogSuite -- -z "display properties": 2 tests.DescribeTableSuite,ShowTablesSuite,ShowTblPropertiesSuite,DataSourceV2MetadataTableSuite, andDataSourceV2DataFrameSessionCatalogSuite, filtered by"display properties": 7 tests.ShowCreateTableSuite -- -z "display-only properties": 1 test.The added assertions cover reserved-key lookup and the session-catalog
SHOW TABLE EXTENDEDlimitation. The helper redaction test checks that mutable input maps remain unchanged. External-table fixtures now useCREATE EXTERNAL TABLE, and display fixture setup uses the underlying live table.Catalyst Java Checkstyle, Catalyst/SQL test Scalastyle, and
git diff --checkalso passed for this revision.The previous revision passed all 214 selected tests; one pre-existing test was ignored:
CatalogSuiteandCatalogV2UtilSuite: 103 tests.DescribeTableSuite,ShowTblPropertiesSuite,ShowTablesSuite, andShowCreateTableSuite, plusDataSourceV2MetadataTableSuite: 100 tests.DataSourceV2DataFrameSessionCatalogSuite -- -z "display properties": 1 test.org.apache.spark.sql.internal.CatalogSuite -- -z "catalog API": 10 tests.Ran with
SPARK_LOCAL_IP=localhost build/sbt, usingcatalyst/testOnlyandsql/testOnlyfor the suites above. Coverage includes stored/display collisions, SET and UNSET, every reserved display key with and without a stored location, redaction, metadata-only tables, exclusion from generated DDL, and preservation across copies, pinning, and ALTER TABLE in both V2 and session-catalog fixtures.The previous revision also passed Catalyst Java Checkstyle, Catalyst/SQL main and test Scalastyle, and
git diff --check. SQL API/Connect Common scalafmt validation and Python syntax checks passed in an earlier revision; those files are unchanged in this update.Was this patch authored or co-authored using generative AI tooling?
Generated-by: Codex (GPT-6)