peter-toth commented on PR #57582:
URL: https://github.com/apache/spark/pull/57582#issuecomment-5103091206

   Heads-up on an overlap: @anuragmantri has #57508 (SPARK-58330) open, and it 
changes the same lines this PR touches.
   
   #57508 fixes the case where the same table appears more than once in a 
statement with different `WITH (...)` options. The per-query relation cache is 
keyed on `(catalog, identifier, timeTravelSpec)` only, so the second reference 
gets a cache hit and silently inherits the first reference's options. Its fix 
re-applies each reference's own options to the cached relation on a hit.
   
   Mechanically the two will conflict: #57508 restructures the `.orElse { val 
finalOptions = ... }` block that this PR reads `finalOptions` from. (Minor and 
separate: this PR and #57585 conflict with each other on the import line in 
`DataSourceV2OptionSuite`, though their `RelationResolution` edits merge fine.)
   
   **The design question.** A cache hit never calls the catalog. So once 
`loadTable` can produce a different `Table` depending on the options, a second 
reference with different options keeps the `Table` built for the first one — 
its schema, partitioning, capabilities, and whatever the catalog selected from 
those options. #57508 can correct the `options` field on the cached relation, 
but it cannot re-derive the `Table`. Which reference wins is analyzer traversal 
order — for `INSERT` the target resolves after its query.
   
   Spark already has a worked example of an option that affects the load, and 
it's instructive: time travel can be given purely as an option, `versionAsOf` / 
`timestampAsOf` (key names configurable via `spark.sql.timeTravelVersionKey` / 
`timeTravelTimestampKey`). `TimeTravelSpec.fromOptions` parses it out of the 
read options, and then Spark does three things with it — puts it in the 
relation cache key, dispatches at load time to `loadTable(ident, version)`, and 
forbids it on a write target. It deliberately does *not* leave it to 
`newScanBuilder(options)`, and it can't: `DataSourceV2Relation.output` comes 
from the `Table` returned at load time, so a snapshot selector that only 
reached the scan builder would have the query analyzed against the current 
schema and scanned at the old one.
   
   So the question for this API is which kind of option it is meant to carry:
   
   - **Options that change how the table is read** (split size and the like): 
`DataSourceV2Relation.options` is the right home, that is already 
per-reference, and #57508 is the complete fix — nothing needed here.
   - **Options that change what the table is** (a snapshot, a branch, a 
schema): they need the `versionAsOf` treatment — cache key, load-time dispatch, 
rejected on write targets. Today a connector's own selector of this kind is 
invisible to `fromOptions`, so it gets none of the three, and this PR is what 
makes it actionable at load time.
   
   The javadoc currently says both — "take them into account when producing the 
`Table`" and "customize the scan" — which is what leaves it ambiguous.
   
   The two treatments are not interchangeable, either. Putting options in the 
cache key means one load per distinct option bag, which gives up 
resolve-once-per-query: `MERGE INTO t WITH (a) USING t WITH (b)` would call 
`loadTable` twice, and each call resolves the table's state independently, so 
whether the target and the source observe the same state becomes the 
connector's problem rather than Spark's guarantee. #57508's approach keeps one 
`Table` for the statement and varies only the options. Worth picking 
deliberately.
   
   **Separate point on the same refactor.** `CatalogV2Util.getTable` currently 
has:
   
   ```scala
   assert(writePrivilegesString.isEmpty, "Should not write to a table with time 
travel")
   ```
   
   This PR removes it, and the new default overload replaces it with silent 
precedence:
   
   ```java
   if (context.timeTravel().isPresent()) { ... }
   else if (!context.writePrivileges().isEmpty()) { ... }
   ```
   
   If both are ever set, time travel wins and the write privileges are dropped, 
so the catalog is never asked to authorize the write — the same shape as 
SPARK-58370. I couldn't find an earlier analyzer guard, so as far as I can tell 
that assert was the enforcement. It may be unreachable from SQL via the grammar 
today, in which case keeping the assert seems better than silently choosing 
one. A connector overriding the new method also receives a `TableContext` that 
can hold both, with no word on what the combination means.
   
   Last thing: neither this PR nor #57585 has a test with the same table 
referenced twice, and the `lastLoadTableOptions` fixture records only the last 
call, so it could not express one.
   
   The same argument applies to #57585 (`RelationCatalog.loadRelation`) — no 
need to repeat it there.
   
   cc @anuragmantri
   


-- 
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]

Reply via email to