-
Notifications
You must be signed in to change notification settings - Fork 29.3k
[SPARK-58330][SQL] Prevent silent dropping of dynamic options on same table references in DML #57508
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
[SPARK-58330][SQL] Prevent silent dropping of dynamic options on same table references in DML #57508
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -121,6 +121,44 @@ class DataSourceV2OptionSuite extends DatasourceV2SQLBase { | |
| } | ||
| } | ||
|
|
||
| test("SPARK-58330: dynamic options are not lost when INSERT selects from the same table") { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Finding 3. All three new tests pair a write reference with a read reference. The plainest instance of the bug - two read references in one query - isn't covered, and that's the case where the fix changes behavior in both directions (neither scan inherits the other's options). On master both scans come out with test("SPARK-58330: each reference in a self-join keeps its own dynamic options") {
val t1 = s"${catalogAndNamespace}table"
withTable(t1) {
sql(s"CREATE TABLE $t1 (id bigint, data string)")
sql(s"INSERT INTO $t1 VALUES (1, 'a'), (2, 'b')")
val df = sql(s"SELECT a.id FROM $t1 WITH (`split-size` = 5) a " +
s"JOIN $t1 WITH (`split-size` = 9) b ON a.id = b.id")
val splitSizes = df.queryExecution.optimizedPlan.collect {
case s: DataSourceV2ScanRelation => s.relation.options.get("split-size")
}
assert(splitSizes.sorted === Seq("5", "9"))
}
} |
||
| val t1 = s"${catalogAndNamespace}table" | ||
| withTable(t1) { | ||
| sql(s"CREATE TABLE $t1 (id bigint, data string)") | ||
| sql(s"INSERT INTO $t1 VALUES (1, 'a'), (2, 'b')") | ||
|
|
||
| // The target and the source are the same table, so they share one per-query relation-cache | ||
| // entry. Each reference must keep its own options: the target's write options must not be | ||
| // dropped by the source reference, and the source's read options must not leak to the target. | ||
| val df = sql(s"INSERT INTO $t1 WITH (`write.split-size` = 10) " + | ||
| s"SELECT id, data FROM $t1 WITH (`split-size` = 5)") | ||
|
|
||
| val appendData = df.queryExecution.optimizedPlan.collectFirst { | ||
| case CommandResult(_, a: AppendData, _, _) => a | ||
| }.getOrElse(fail("expected an AppendData in the optimized plan")) | ||
|
|
||
| // target: the write relation keeps its own option and does not pick up the source's | ||
| val targetOptions = appendData.table.asInstanceOf[DataSourceV2Relation].options | ||
| assert(targetOptions.get("write.split-size") === "10", "target write option") | ||
| assert(!targetOptions.containsKey("split-size"), "target must not see source option") | ||
|
|
||
| // source: the read relation under the query keeps its own option and does not pick up | ||
| // the target's | ||
| val sourceOptions = appendData.query.collect { | ||
| case r: DataSourceV2Relation => r.options | ||
| case s: DataSourceV2ScanRelation => s.relation.options | ||
| }.filter(_.containsKey("split-size")) | ||
| assert(sourceOptions.nonEmpty, "source relation carrying the option was not found") | ||
| sourceOptions.foreach { opts => | ||
| assert(opts.get("split-size") === "5", "source read option") | ||
| assert(!opts.containsKey("write.split-size"), "source must not see target option") | ||
| } | ||
|
|
||
| checkAnswer(sql(s"SELECT * FROM $t1"), | ||
| Seq(Row(1, "a"), Row(2, "b"), Row(1, "a"), Row(2, "b"))) | ||
| } | ||
| } | ||
|
|
||
| test("SPARK-50286: Propagate options for DataFrameWriter Append") { | ||
| val t1 = s"${catalogAndNamespace}table" | ||
| withTable(t1) { | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Finding 2. This only rewrites
DataSourceV2Relation, so the same bug survives for v1 relations. A v1 table (session-catalog data-source or Hive table) resolves throughcreateDataSourceV1Scan->SessionCatalog.getRelation(v1Table, options)->SubqueryAlias(_, UnresolvedCatalogRelation(metadata, options))(SessionCatalog.scala:1140), that plan goes into the samerelationCache, and the options on it are live:FindDataSourceTablehands them toreadDataSourceTable->DataSourceUtils.generateDatasourceOptions(DataSourceStrategy.scala:382,:259), which even rebuilds a cachedHadoopFsRelationwhen the options differ. v1 dynamic options aren't theoretical -DDLSuite.scala:1394("SPARK-51747: Data source cached plan respects options if ignore conf disabled") asserts thatSELECT * FROM t WITH ('delimiter' = ';')on a CSV table changes the returned rows. SoINSERT INTO v1t WITH (a) SELECT * FROM v1t WITH (b), or a plain self-join on a v1 table, still ends up with one set of options for both references.UnresolvedCatalogRelationis already imported here, so it's a one-liner:If you'd rather keep this PR DSv2-only, could you say that in the description and leave a short note here? This PR removes the
AstBuildercomment that was the only record of the limitation.