[SPARK-58460][SQL] Do not leak generated column expressions into DataFrame schemas and derived tables - #57662
Closed
szehon-ho wants to merge 1 commit into
Closed
Conversation
…Frame schemas and derived tables A generated column's generation expression was carried as GENERATION_EXPRESSION metadata on a relation's output, so it surfaced in a DataFrame's schema and was promoted back into a real generated column whenever a table was created from a query: CREATE TABLE AS SELECT and writeStream.toTable both produced a target table with a generated column copied from the source. In the streaming case that made the write itself fail, since streaming writes to tables with generated columns are unsupported. Treat the key as internal metadata so it is stripped when a relation is created and when a table is created from a query. The write path attaches it from the table's columns, which are the source of truth, before handing the expected output to TableOutputResolver, and marks the generated columns it auto-filled so that constraint resolution validates only the values that did not come from the generation expression.
cloud-fan
approved these changes
Jul 31, 2026
cloud-fan
left a comment
Contributor
There was a problem hiding this comment.
0 blocking, 0 non-blocking, 0 nits.
The metadata lifetime is now bounded to write analysis, and the read, derived-table, auto-fill, and validation paths remain internally consistent.
Verification
I traced the metadata from V2 table columns through relation construction, temporary write-time attachment, output resolution, and constraint selection. Read outputs remove both internal generated-column keys, while write resolution restores the persisted expression only on its expected attributes; auto-filled target attributes are marked after character/varchar metadata cleanup, and unmarked generated columns still receive invariants.
cloud-fan
pushed a commit
that referenced
this pull request
Jul 31, 2026
…Frame schemas and derived tables
### What changes were proposed in this pull request?
A generated column's generation expression was carried as `GENERATION_EXPRESSION` metadata on a relation's output attributes. This PR treats that key as internal metadata, so it never reaches a relation's output, and reworks the write path to take the expression from the table's V2 columns instead:
- `GENERATION_EXPRESSION` is added to `INTERNAL_METADATA_KEYS`, so `removeInternalMetadata` strips it. `DataSourceV2Relation.create` already ran the table schema through that, so batch relations are clean; the `StreamingRelationV2` paths built attributes directly from `table.columns.toAttributes` and now use a new `toOutputAttributes`, which applies the same strip (keeping field IDs, which the column-ID feature deliberately exposes).
- `ResolveOutputRelation` attaches the expressions to the expected output from `table.columns()` (the persisted source of truth) via `GeneratedColumn.attachGenerationExpressions`, only for as long as resolving the write takes, so `TableOutputResolver` can still auto-fill missing generated columns.
- Instead of removing metadata from auto-filled columns to suppress validation, the columns Spark computed are now marked with a new internal key, `__auto_filled_generated_column`, and `ResolveTableConstraints` skips those. Validation is therefore the default: any value that did not come from the generation expression gets a `CheckInvariant`, as before.
### Why are the changes needed?
Because the expression rode along on the relation's output, it leaked in two user-visible ways.
It showed up in a DataFrame's schema:
```scala
sql("CREATE TABLE testcat.t (id INT, doubled INT GENERATED ALWAYS AS (id * 2)) USING foo")
spark.table("testcat.t").schema("doubled").metadata
// {"GENERATION_EXPRESSION":"id * 2"}
```
And, since a table created from a query derives its columns from that schema, the generated column was promoted back into a real generated column on the new table:
```scala
sql("CREATE TABLE testcat.dst USING foo AS SELECT * FROM testcat.t")
// dst.doubled is a generated column copied from the source
spark.readStream.table("testcat.t").writeStream.toTable("testcat.dst2")
// fails: the target is created with a generated column, and streaming writes to
// tables with generated columns are unsupported
```
Generated column values on writes were added recently (SPARK-57644) and are not in any release, so this corrects the behavior of an unreleased feature.
### Does this PR introduce _any_ user-facing change?
Yes, within unreleased branches only. `GENERATION_EXPRESSION` no longer appears in the metadata of a DataFrame's schema (batch reads, `readStream`, and `SELECT`), and `CREATE TABLE AS SELECT` / `writeStream.toTable` now create ordinary columns rather than inheriting the source's generated columns. Auto-filling and validation of generated columns on writes to an existing table are unchanged.
### How was this patch tested?
`GeneratedColumnWriteSuite` (72 tests, all passing), with three new cases:
- generation expression is not exposed in the read schema, covering `spark.table`, `spark.read.table`, `SELECT`, and `readStream.table`
- CTAS from a table with generated columns does not create generated columns, and the resulting column behaves as an ordinary one
- streaming write to a new table does not create generated columns
### Was this patch authored or co-authored using generative AI tooling?
Generated-by: Cursor 2.5, Opus 5
Closes #57662 from szehon-ho/gencol-metadata-not-propagated.
Authored-by: Szehon Ho <szehon.apache@gmail.com>
Signed-off-by: Wenchen Fan <wenchen@databricks.com>
(cherry picked from commit 9586a2a)
Signed-off-by: Wenchen Fan <wenchen@databricks.com>
Contributor
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What changes were proposed in this pull request?
A generated column's generation expression was carried as
GENERATION_EXPRESSIONmetadata on a relation's output attributes. This PR treats that key as internal metadata, so it never reaches a relation's output, and reworks the write path to take the expression from the table's V2 columns instead:GENERATION_EXPRESSIONis added toINTERNAL_METADATA_KEYS, soremoveInternalMetadatastrips it.DataSourceV2Relation.createalready ran the table schema through that, so batch relations are clean; theStreamingRelationV2paths built attributes directly fromtable.columns.toAttributesand now use a newtoOutputAttributes, which applies the same strip (keeping field IDs, which the column-ID feature deliberately exposes).ResolveOutputRelationattaches the expressions to the expected output fromtable.columns()(the persisted source of truth) viaGeneratedColumn.attachGenerationExpressions, only for as long as resolving the write takes, soTableOutputResolvercan still auto-fill missing generated columns.__auto_filled_generated_column, andResolveTableConstraintsskips those. Validation is therefore the default: any value that did not come from the generation expression gets aCheckInvariant, as before.Why are the changes needed?
Because the expression rode along on the relation's output, it leaked in two user-visible ways.
It showed up in a DataFrame's schema:
And, since a table created from a query derives its columns from that schema, the generated column was promoted back into a real generated column on the new table:
Generated column values on writes were added recently (SPARK-57644) and are not in any release, so this corrects the behavior of an unreleased feature.
Does this PR introduce any user-facing change?
Yes, within unreleased branches only.
GENERATION_EXPRESSIONno longer appears in the metadata of a DataFrame's schema (batch reads,readStream, andSELECT), andCREATE TABLE AS SELECT/writeStream.toTablenow create ordinary columns rather than inheriting the source's generated columns. Auto-filling and validation of generated columns on writes to an existing table are unchanged.How was this patch tested?
GeneratedColumnWriteSuite(72 tests, all passing), with three new cases:spark.table,spark.read.table,SELECT, andreadStream.tableWas this patch authored or co-authored using generative AI tooling?
Generated-by: Cursor 2.5, Opus 5