You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@spark.apache.org by ge...@apache.org on 2026/10/20 04:39:45 UTC
(spark) branch master updated: [SPARK-60111][SQL] Preserve tags when rebuilding DataSource V2 relations
This is an automated email from the ASF dual-hosted git repository.
gengliangwang pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/spark.git
The following commit(s) were added to refs/heads/master by this push:
new 35686db92365 [SPARK-60111][SQL] Preserve tags when rebuilding DataSource V2 relations
35686db92365 is described below
commit 35686db92365eb5aa74686f65176ff1a24dcd33f
Author: yyanyy <yy...@gmail.com>
AuthorDate: Mon Oct 19 21:39:33 2026 -0700
[SPARK-60111][SQL] Preserve tags when rebuilding DataSource V2 relations
### What changes were proposed in this pull request?
This pull request preserves `TreeNode` tags when a `DataSourceV2Relation` is rebuilt in two places:
* `DataSourceV2Relation.withMetadataColumns`
* `Analyzer.ResolveOutputRelation` when it cleans and marks a DataSource V2 write target
It also adds regression tests for both rebuild paths.
### Why are the changes needed?
These paths use the Scala case-class `copy` method directly. Unlike `TreeNode.makeCopy` and the tree transformation helpers, the generated `copy` method does not call `copyTagsFrom`. As a result, tags attached to the original relation are silently absent from the rebuilt relation.
Preserving the tags is consistent with the existing `LogicalRelation.withMetadataColumns` behavior and with the `TreeNode` contract for semantically equivalent replacements.
### Does this PR introduce _any_ user-facing change?
No. This change preserves internal logical-plan metadata.
### How was this patch tested?
Added the following regression tests:
* `DataSourceV2RelationSuite: withMetadataColumns preserves tags`
* `V2ResolveOutputRelationSuite: ResolveOutputRelation preserves tags on a rebuilt DSv2 write target`
Before the production changes, each new test failed because the rebuilt relation had no tag. After the changes, the two relevant suites completed with 30 tests succeeded and 0 failed:
`build/sbt 'catalyst/testOnly org.apache.spark.sql.execution.datasources.v2.DataSourceV2RelationSuite org.apache.spark.sql.catalyst.analysis.V2ResolveOutputRelationSuite'`
The Catalyst compile and test scalastyle checks also passed with 0 errors:
`build/sbt 'catalyst/scalastyle' 'catalyst/Test/scalastyle'`
### Was this patch authored or co-authored using generative AI tooling?
Generated-by: OpenAI Codex
Closes #59330 from yyanyy/spark-preserve-dsv2-tags-20261008.
Authored-by: yyanyy <yy...@gmail.com>
Signed-off-by: Gengliang Wang <ge...@apache.org>
---
.../spark/sql/catalyst/analysis/Analyzer.scala | 4 ++-
.../datasources/v2/DataSourceV2Relation.scala | 4 ++-
.../catalyst/analysis/V2WriteAnalysisSuite.scala | 30 +++++++++++++++++++++-
.../datasources/v2/DataSourceV2RelationSuite.scala | 30 +++++++++++++++++++---
4 files changed, 62 insertions(+), 6 deletions(-)
diff --git a/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/analysis/Analyzer.scala b/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/analysis/Analyzer.scala
index 1c24d24c6653..b1f7eded0b76 100644
--- a/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/analysis/Analyzer.scala
+++ b/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/analysis/Analyzer.scala
@@ -4095,8 +4095,10 @@ class Analyzer(
val cleanedTable = v2Write.table match {
case r: DataSourceV2Relation =>
val cleaned = r.output.map(CharVarcharUtils.cleanAttrMetadata)
- r.copy(output =
+ val cleanedRelation = r.copy(output =
GeneratedColumn.markAutoFilledGeneratedColumns(cleaned, autoFilledGenCols))
+ cleanedRelation.copyTagsFrom(r)
+ cleanedRelation
case other => other
}
v2Write.withNewQuery(projection).withNewTable(cleanedTable)
diff --git a/sql/catalyst/src/main/scala/org/apache/spark/sql/execution/datasources/v2/DataSourceV2Relation.scala b/sql/catalyst/src/main/scala/org/apache/spark/sql/execution/datasources/v2/DataSourceV2Relation.scala
index da080055bbd0..7ca3dea43ffc 100644
--- a/sql/catalyst/src/main/scala/org/apache/spark/sql/execution/datasources/v2/DataSourceV2Relation.scala
+++ b/sql/catalyst/src/main/scala/org/apache/spark/sql/execution/datasources/v2/DataSourceV2Relation.scala
@@ -137,7 +137,9 @@ case class DataSourceV2Relation(
def withMetadataColumns(): DataSourceV2Relation = {
val newMetadata = metadataOutput.filterNot(outputSet.contains)
if (newMetadata.nonEmpty) {
- copy(output = output ++ newMetadata)
+ val newRelation = copy(output = output ++ newMetadata)
+ newRelation.copyTagsFrom(this)
+ newRelation
} else {
this
}
diff --git a/sql/catalyst/src/test/scala/org/apache/spark/sql/catalyst/analysis/V2WriteAnalysisSuite.scala b/sql/catalyst/src/test/scala/org/apache/spark/sql/catalyst/analysis/V2WriteAnalysisSuite.scala
index 3b4da14c7b15..24cdbd0cf0df 100644
--- a/sql/catalyst/src/test/scala/org/apache/spark/sql/catalyst/analysis/V2WriteAnalysisSuite.scala
+++ b/sql/catalyst/src/test/scala/org/apache/spark/sql/catalyst/analysis/V2WriteAnalysisSuite.scala
@@ -17,7 +17,7 @@
package org.apache.spark.sql.catalyst.analysis
-import java.util.Locale
+import java.util.{Locale, Set => JSet}
import org.apache.spark.sql.catalyst.dsl.expressions._
import org.apache.spark.sql.catalyst.dsl.plans._
@@ -25,10 +25,38 @@ import org.apache.spark.sql.catalyst.expressions.{Alias, ArrayTransform, Attribu
import org.apache.spark.sql.catalyst.expressions.objects.AssertNotNull
import org.apache.spark.sql.catalyst.plans.logical._
import org.apache.spark.sql.catalyst.rules.Rule
+import org.apache.spark.sql.catalyst.trees.TreeNodeTag
import org.apache.spark.sql.catalyst.types.DataTypeUtils
+import org.apache.spark.sql.connector.catalog.{Column, Table, TableCapability}
+import org.apache.spark.sql.execution.datasources.v2.DataSourceV2Relation
import org.apache.spark.sql.internal.SQLConf
import org.apache.spark.sql.internal.SQLConf.StoreAssignmentPolicy
import org.apache.spark.sql.types._
+import org.apache.spark.sql.util.CaseInsensitiveStringMap
+
+class V2ResolveOutputRelationSuite extends AnalysisTest {
+
+ test("ResolveOutputRelation preserves tags on a rebuilt DSv2 write target") {
+ val table = new Table {
+ override def name(): String = "table-name"
+ override def columns(): Array[Column] = Array(Column.create("id", LongType))
+ override def capabilities(): JSet[TableCapability] =
+ JSet.of(TableCapability.BATCH_WRITE)
+ }
+ val target =
+ DataSourceV2Relation.create(table, None, None, CaseInsensitiveStringMap.empty())
+ val tag = TreeNodeTag[Unit]("resolve-output-relation-test")
+ target.setTagValue(tag, ())
+ val query = LocalRelation(AttributeReference("id", IntegerType)())
+ val write = AppendData.byName(target, query)
+
+ val resolved = getAnalyzer.ResolveOutputRelation(write).asInstanceOf[AppendData]
+ val resolvedTarget = resolved.table.asInstanceOf[DataSourceV2Relation]
+
+ assert(resolvedTarget ne target)
+ assert(resolvedTarget.getTagValue(tag).contains(()))
+ }
+}
class V2AppendDataANSIAnalysisSuite extends V2ANSIWriteAnalysisSuiteBase {
override def byName(table: NamedRelation, query: LogicalPlan): LogicalPlan = {
diff --git a/sql/catalyst/src/test/scala/org/apache/spark/sql/execution/datasources/v2/DataSourceV2RelationSuite.scala b/sql/catalyst/src/test/scala/org/apache/spark/sql/execution/datasources/v2/DataSourceV2RelationSuite.scala
index 2d0272ff14ef..8f4d300be61d 100644
--- a/sql/catalyst/src/test/scala/org/apache/spark/sql/execution/datasources/v2/DataSourceV2RelationSuite.scala
+++ b/sql/catalyst/src/test/scala/org/apache/spark/sql/execution/datasources/v2/DataSourceV2RelationSuite.scala
@@ -26,15 +26,15 @@ import org.apache.spark.sql.catalyst.expressions._
import org.apache.spark.sql.catalyst.plans.SQLHelper
import org.apache.spark.sql.catalyst.plans.logical.{Filter, Histogram, HistogramBin, Project}
import org.apache.spark.sql.catalyst.plans.logical.statsEstimation.EstimationUtils
-import org.apache.spark.sql.catalyst.trees.TreePattern
+import org.apache.spark.sql.catalyst.trees.{TreeNodeTag, TreePattern}
import org.apache.spark.sql.catalyst.util.FieldMetadataUtils.FIELD_ID_METADATA_KEY
import org.apache.spark.sql.catalyst.util.INTERNAL_METADATA_KEYS
-import org.apache.spark.sql.connector.catalog.{Column, Table, TableCapability}
+import org.apache.spark.sql.connector.catalog.{Column, MetadataColumn, SupportsMetadataColumns, Table, TableCapability}
import org.apache.spark.sql.connector.expressions.{FieldReference, NamedReference}
import org.apache.spark.sql.connector.read.{Scan, Statistics => V2Statistics, SupportsReportStatistics}
import org.apache.spark.sql.connector.read.colstats.ColumnStatistics
import org.apache.spark.sql.internal.SQLConf
-import org.apache.spark.sql.types.{IntegerType, MetadataBuilder, StringType, StructField, StructType}
+import org.apache.spark.sql.types.{DataType, IntegerType, MetadataBuilder, StringType, StructField, StructType}
import org.apache.spark.sql.util.CaseInsensitiveStringMap
class DataSourceV2RelationSuite extends SparkFunSuite with SQLHelper {
@@ -724,6 +724,30 @@ class DataSourceV2RelationSuite extends SparkFunSuite with SQLHelper {
assert(field.metadata.contains(FIELD_ID_METADATA_KEY))
}
+ test("withMetadataColumns preserves tags") {
+ val table = new Table with SupportsMetadataColumns {
+ override def name(): String = "t"
+ override def columns(): Array[Column] = Array(Column.create("id", IntegerType))
+ override def metadataColumns(): Array[MetadataColumn] =
+ Array(new MetadataColumn {
+ override def name(): String = "index"
+ override def dataType(): DataType = IntegerType
+ override def isNullable(): Boolean = false
+ })
+ override def capabilities(): util.Set[TableCapability] =
+ util.Set.of[TableCapability]()
+ }
+ val relation =
+ DataSourceV2Relation.create(table, None, None, CaseInsensitiveStringMap.empty())
+ val tag = TreeNodeTag[Unit]("metadata-columns-test")
+ relation.setTagValue(tag, ())
+
+ val relationWithMetadata = relation.withMetadataColumns()
+
+ assert(relationWithMetadata ne relation)
+ assert(relationWithMetadata.getTagValue(tag).contains(()))
+ }
+
test("nodePatterns declare the DSv2 relation identity tree patterns") {
val table = new Table {
override def name(): String = "t"
---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@spark.apache.org
For additional commands, e-mail: commits-help@spark.apache.org