szehon-ho commented on code in PR #57865:
URL: https://github.com/apache/spark/pull/57865#discussion_r3788083696
##########
sql/core/src/main/scala/org/apache/spark/sql/classic/DataFrameWriter.scala:
##########
@@ -354,13 +358,13 @@ final class DataFrameWriter[T] private[sql](ds:
Dataset[T]) extends sql.DataFram
}
private def insertIntoCommand(catalog: CatalogPlugin, ident: Identifier):
LogicalPlan = {
- import org.apache.spark.sql.connector.catalog.CatalogV2Implicits._
-
- val table = catalog.asTableCatalog.loadTable(ident,
getWritePrivileges.toSet.asJava) match {
+ val tableOptions = new CaseInsensitiveStringMap(extraOptions.toMap.asJava)
+ val table = CatalogV2Util.getTableForWrite(
Review Comment:
Non-blocking, and I have traced this rather than run it, so worth a check
before acting on it.
`getTableForWrite` rejects the time-travel options as its first step, before
the returned table is inspected, so the reject lands ahead of the `V1Table`
dispatch on the line below. `saveAsTableCommand` has the same ordering --
`getTableForWrite` at `:495`, `case (_, Some(_: V1Table))` at `:501`.
That matters for `saveAsTable` in particular, because `canUseV2` there is
also true when a custom V2 session catalog is configured, regardless of format.
So `df.write.option("versionAsOf", "1").saveAsTable("hive_table")` against a V1
target would now raise `UNSUPPORTED_FEATURE.TIME_TRAVEL`, where previously
every option including that one was simply dropped on the V1 path.
Two knock-on effects if it holds. The migration-guide bullet scopes the
change to "catalog-backed Data Source V2 writes", which would not cover this.
And the `insertInto` scaladoc clause added for finding 12 says "for V1 tables
the options are ignored", which becomes slightly optimistic once one option can
raise an error instead.
Either widening the wording or moving the reject after the `V1Table`
dispatch would resolve it; the latter also keeps the V1 path behaviourally
untouched, which seems closer to the PR's stated scope.
##########
sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/analysis/RelationResolution.scala:
##########
@@ -236,15 +246,18 @@ class RelationResolution(
val planId = u.getTagValue(LogicalPlan.PLAN_ID_TAG)
val writePrivileges =
u.options.get(UnresolvedRelation.REQUIRED_WRITE_PRIVILEGES)
val finalOptions = u.clearWritePrivileges.options
- // Time travel applies to reads only; reject it on a write target
(reachable via the option
- // form, e.g. `INSERT INTO t WITH ('versionAsOf' = ...)`) with a
user-facing error.
+ if (writePrivileges != null) {
+ CatalogV2Util.rejectTimeTravelOptionsForWrite(catalog, ident,
finalOptions)
+ }
+ // Time travel applies to reads only; reject an explicit time-travel
specification on a
+ // write target with a user-facing error.
if (finalTimeTravelSpec.nonEmpty && writePrivileges != null) {
throw
QueryCompilationErrors.timeTravelUnsupportedError(toSQLId(identifier))
Review Comment:
Non-blocking, and a tail of @peter-toth's finding 10 rather than a new issue.
The option-form paths all agree now, but this check and the one three lines
above it disagree on qualification. `rejectTimeTravelOptionsForWrite` at `:250`
renders `toSQLId(ident.toQualifiedNameParts(catalog))`, while this line renders
`toSQLId(identifier)` -- the name as written. Both are checks on a persistent
write target, in the same method, with `catalog` and `ident` in scope for
either. (The third site, `resolveTempView:505`, is legitimately as-written: a
temp view has no catalog to qualify against, and the new test correctly expects
`` `temp_view` `` there.)
Worth noting the divergence came out of the fix suggested on finding 10, not
from anything that was wrong before.
Separately, I could not find a path that reaches this branch at all for a
write target. `RelationTimeTravel` is only built from the relation-primary
temporal clause (`AstBuilder:2901`, `:2994`), and the INSERT / MERGE / DELETE /
UPDATE targets plus both DataFrame writers all resolve their names through
`parseMultipartIdentifier`, so `finalTimeTravelSpec` should always be empty
when `writePrivileges != null`. If that holds, deleting the branch is cleaner
than aligning it, since `getTableForWrite` and the `:250` check now cover
everything reachable. Either way, leaving two renderings three lines apart
invites the next reader to preserve both.
##########
sql/core/src/test/scala/org/apache/spark/sql/connector/DataSourceV2OptionSuite.scala:
##########
@@ -60,9 +68,44 @@ class StateAwareInMemoryCatalog extends
LoadCountingInMemoryCatalog {
util.Set.of("snapshot", UnresolvedRelation.REQUIRED_WRITE_PRIVILEGES)
}
+class NullReturningInMemoryCatalog extends InMemoryCatalog {
+ override def createTable(ident: Identifier, tableInfo: TableInfo): Table = {
+ super.createTable(ident, tableInfo)
+ null
+ }
+}
+
+class WriteStateAwareInMemoryCatalog extends InMemoryCatalog {
+ override def tableStateOptionKeys(): util.Set[String] =
util.Set.of("load-option")
+}
+
+class NullReturningStagingInMemoryCatalog extends InMemoryCatalog with
StagingTableCatalog {
+ override def stageCreate(ident: Identifier, tableInfo: TableInfo):
StagedTable = {
+ createTable(ident, tableInfo)
+ null
+ }
+
+ override def stageReplace(ident: Identifier, tableInfo: TableInfo):
StagedTable = {
+ dropTable(ident)
+ createTable(ident, tableInfo)
+ null
+ }
+
+ override def stageCreateOrReplace(ident: Identifier, tableInfo: TableInfo):
StagedTable = {
+ if (tableExists(ident)) {
+ dropTable(ident)
+ }
+ createTable(ident, tableInfo)
+ null
+ }
+}
+
class DataSourceV2OptionSuite extends DatasourceV2SQLBase {
import testImplicits._
+ override protected def testCatalogClass: Class[_ <: InMemoryCatalog] =
Review Comment:
Non-blocking, fixture wiring. There are now two ways to declare state option
keys for the in-memory catalogs, and this suite uses the less flexible one.
`BasicInMemoryTableCatalog` gained a `tableStateOptionKeys` init option in
this PR, and `SupportsCatalogOptionsSuite` drives it through conf:
```scala
spark.conf.set(s"spark.sql.catalog.$catalogName.tableStateOptionKeys",
"load-option")
```
Here the same thing is done by subclassing
(`WriteStateAwareInMemoryCatalog`, `:78`) and hardcoding the override, which
also means the init option is silently ignored for `testcat` in this suite.
Two consequences worth weighing. `testCatalogClass` swaps `testcat` for the
whole file, so `load-option` is a declared state key for every test here,
including the read tests that predate this PR -- they pass because they use
`split-size`, but the next read test that happens to pick `load-option` will
behave differently for a non-obvious reason. And `registerCatalog` plus the
conf would give the same coverage without a new class or the suite-wide swap,
keeping both suites on one mechanism.
If the subclass is preferred for another reason, a one-line comment noting
that `testcat` deliberately declares `load-option` for the whole suite would
save the next reader the lookup.
##########
sql/core/src/main/scala/org/apache/spark/sql/classic/DataFrameWriter.scala:
##########
@@ -486,7 +490,10 @@ final class DataFrameWriter[T] private[sql](ds:
Dataset[T]) extends sql.DataFram
v2ProviderOpt: Option[TableProvider],
ident: Identifier,
nameParts: Seq[String]): LogicalPlan = {
- val tableOpt = try Option(catalog.loadTable(ident,
getWritePrivileges.toSet.asJava)) catch {
+ val tableOptions = new CaseInsensitiveStringMap(extraOptions.toMap.asJava)
+ val tableOpt = try {
+ Option(CatalogV2Util.getTableForWrite(catalog, ident,
getWritePrivileges, tableOptions))
Review Comment:
Following up here -- the widening covers the `writeOptions` channel, and
SQL's CTAS fills the other one, so the reworded description still reads as
broader than the check.
`CreateTableAsSelect` has two option channels and the two front ends fill
opposite ones. `DataFrameWriterV2.createCommand` puts `.option()` calls in
`writeOptions` and leaves the spec's list empty (`buildTableSpec`:
`optionExpression = OptionList(Seq.empty)`), while the parser does the reverse
-- `AstBuilder:6262` passes `Map.empty` for `writeOptions` and routes `OPTIONS
(...)` into `tableSpec`. Since the new `ResolveCatalogs` cases destructure only
`writeOptions`, `writeTo(t).option("versionAsOf", ...).create()` is rejected
and `CREATE TABLE t OPTIONS ('versionAsOf' = '1') AS SELECT ...` is not. Same
for `REPLACE TABLE ... AS SELECT`.
I think keeping the boundary at the write-option channel is defensible:
`tableSpec.options` is not a per-write directive, it becomes persisted table
metadata (`CatalogV2Util.convertTableProperties` stores it both bare and under
`option.`), so policing names there would be a new policy rather than an
extension of this one. But then "including table creation and replacement" in
the migration guide reads as covering SQL CTAS. Either scope the wording to the
DataFrame writer APIs, or add a case to the new `DataSourceV2OptionSuite` test
pinning that SQL CTAS is deliberately out of scope -- as it stands that test
enumerates seven entry points and reads as an exhaustive matrix.
##########
sql/api/src/main/scala/org/apache/spark/sql/DataFrameWriter.scala:
##########
@@ -267,7 +267,9 @@ abstract class DataFrameWriter[T] {
* +---+---+
* }}}
*
- * Because it inserts data to an existing table, format or options will be
ignored.
+ * Because it inserts data to an existing table, the format is ignored. For
data source V2
Review Comment:
This landed in `sql/api`, but the override in `sql/core` carries the same
scaladoc and did not get the clause.
`sql/api/.../DataFrameWriter.scala:270`
> ...all options are forwarded to the write; for V1 tables the options are
ignored.
`sql/core/.../classic/DataFrameWriter.scala:320`
> ...all options are forwarded to the write.
Both carry the full doc rather than `@inheritdoc`, so the concrete class
still leaves a V1 reader without a statement. Worth adding there too -- and
while you are in it, "the options are ignored" is now slightly optimistic for
V1 as well, since the time-travel reject in `getTableForWrite` runs ahead of
the `V1Table` dispatch (I left a note on `DataFrameWriter.scala:362`), so one
option can raise an error rather than be ignored.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: [email protected]
For queries about this service, please contact Infrastructure at:
[email protected]
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]