cloud-fan commented on code in PR #58317:
URL: https://github.com/apache/spark/pull/58317#discussion_r3960197735


##########
sql/catalyst/src/main/scala/org/apache/spark/sql/execution/datasources/v2/DataSourceV2Relation.scala:
##########
@@ -116,7 +116,10 @@ case class DataSourceV2Relation(
     catalog: Option[CatalogPlugin],
     identifier: Option[Identifier],
     options: CaseInsensitiveStringMap,
-    timeTravelSpec: Option[TimeTravelSpec] = None)
+    timeTravelSpec: Option[TimeTravelSpec] = None,
+    // Bound at analysis so sameResult / cache reuse distinguish preserve-only 
vs standard
+    // CHAR/VARCHAR scans. None means the relation was not analyzed under 
first-class types.
+    charVarcharScanMode: Option[CharVarcharScanMode] = None)

Review Comment:
   **Blocking (P1):** Adding this mode to case-class state fixes cross-mode 
cache reuse, but it also makes `sameResult` distinguish the unbound target used 
after a catalog-less V2 write from every bound read of the same table. 
`DataSourceV2Strategy.refreshCache` still calls `recacheByPlan(session, r)` for 
this path, and `V2WriteCommand.table` is a non-child slot, so analysis leaves 
the write target at `None` while cached reads carry `Some(PreserveNative)` or 
`Some(SparkStandard)`. A successful append can therefore leave a materialized 
path read stale. Please add a V2 write-invalidation path that matches the 
written TableProvider relation while ignoring only this mode, and cover 
append/recache under both bound modes without weakening ordinary cross-mode 
cache identity.
   
   **Recommended change:** Add a dedicated catalog-less V2 
mutation-invalidation path that deliberately ignores only charVarcharScanMode 
when locating cache entries for the written relation.
   
   **Why this works:** Have CacheManager match cached DataSourceV2Relation 
nodes to the written TableProvider relation using the existing table identity 
while excluding charVarcharScanMode from this mutation-only comparison, then 
use that path from DataSourceV2Strategy.refreshCache.
   
   **Scope:** CacheManager, the catalog-less TableProvider branch of 
DataSourceV2Strategy.refreshCache, and focused V2 append/recache coverage.
   
   **Compatibility:** Keep charVarcharScanMode in ordinary sameResult identity 
so preserve-native and standard reads cannot reuse each other's cached results.
   
   **Risks:** A matcher broader than the written TableProvider relation could 
refresh unrelated cache entries. Covering only one bound mode could leave the 
symmetric mismatch undiscovered.
   
   **Constraints:** Ignore the mode only for mutation invalidation, not for 
normal cache substitution. Invalidate every cached descendant that reads the 
successfully mutated relation.
   
   **Success:** After a catalog-less V2 append or overwrite, materialized 
cached reads observe the written rows under PreserveNative and SparkStandard, 
while cross-mode read plans remain distinct for normal cache reuse.



##########
sql/core/src/main/scala/org/apache/spark/sql/execution/datasources/ApplyCharTypePadding.scala:
##########
@@ -51,34 +51,74 @@ object ApplyCharTypePadding extends Rule[LogicalPlan] {
   }
 
   override def apply(plan: LogicalPlan): LogicalPlan = {
+    val standardSemantics = conf.charVarcharStandardSemantics
+    val scanMode = CharVarcharScanMode(standardSemantics)
+
+    // Bind into case-class state, not a TreeNodeTag: `TreeNode.makeCopy` 
calls `copyTagsFrom`,
+    // so a tag survives canonicalization, but it does not participate in 
structural plan

Review Comment:
   **Nit (P3):** `TreeNode.makeCopy` is not a general guarantee that tags 
survive canonicalization: `LogicalRelation.doCanonicalize` and 
`HiveTableRelation.doCanonicalize` return direct `copy(...)` results, so 
QueryPlan never invokes the tag-copying fallback and the tags are dropped. 
Please avoid the blanket claim here and state the stable reason for using a 
field: `TreeNodeTag` values do not participate in structural equality / 
`sameResult`.



##########
sql/core/src/main/scala/org/apache/spark/sql/execution/datasources/FileFormat.scala:
##########
@@ -266,6 +295,25 @@ object FileFormat {
    */
   val OPTION_RETURNING_BATCH = "returning_batch"
 
+  /**
+   * Engine-private Hadoop configuration entry that transports the analyzed 
CHAR/VARCHAR scan mode
+   * across the legacy [[FileFormat.buildReaderWithPartitionValues]] 
signature. It is written by the
+   * mode-aware overload and read by formats that honor first-class 
CHAR/VARCHAR types. This is not
+   * a public option; the authoritative state is the typed plan field and 
overload parameter.
+   */
+  val CHAR_VARCHAR_SCAN_MODE = "__spark_sql_char_varchar_scan_mode"
+
+  /** Writes the CHAR/VARCHAR scan mode into `conf` under 
[[CHAR_VARCHAR_SCAN_MODE]]. */
+  private[sql] def setCharVarcharScanMode(
+      conf: Configuration, mode: CharVarcharScanMode): Unit = {
+    conf.set(CHAR_VARCHAR_SCAN_MODE, mode.toString)
+  }
+
+  /** Reads the CHAR/VARCHAR scan mode from `conf`, or `None` if no mode was 
bridged in. */
+  private[sql] def charVarcharScanMode(conf: Configuration): 
Option[CharVarcharScanMode] = {
+    Option(conf.get(CHAR_VARCHAR_SCAN_MODE)).map(CharVarcharScanMode.fromName)

Review Comment:
   **Non-blocking (P2):** `FileSourceScanExec` builds this Hadoop configuration 
from relation options. When the relation mode is `None`, the legacy overload 
passes it unchanged, and ORC now calls this strict parser on any value under 
this key. Thus `spark.read.option("__spark_sql_char_varchar_scan_mode", 
"not-a-mode")...` can fail an otherwise ordinary unbound ORC read; a valid 
value can forge analyzed semantics. Please remove the private key before legacy 
dispatch and let only the typed overload add it to its cloned per-call 
configuration, with a collision regression.
   
   **Recommended change:** Ensure that only the mode-aware FileFormat overload 
can author the private CHAR/VARCHAR bridge entry.
   
   **Why this works:** Strip the reserved bridge key from Hadoop configurations 
derived from relation options before legacy dispatch, then add it only to the 
cloned configuration created by the typed overload.
   
   **Scope:** V1 FileSourceScanExec/FileFormat configuration dispatch and an 
unbound ORC read regression with a colliding caller option.
   
   **Compatibility:** Existing callers of the seven-argument overload continue 
to use legacy preserve-native behavior, and arbitrary data-source options 
remain opaque to ORC scan-mode selection.
   
   **Risks:** Removing the key after the typed overload adds it would break 
delegating OrcFileFormat subclasses. Sanitizing only direct DataFrameReader 
options could miss persisted table options.
   
   **Constraints:** Preserve virtual dispatch through the existing 
seven-argument overload. Do not let caller-controlled option maps select or 
invalidate typed plan state.
   
   **Success:** An unbound ORC read ignores both valid and invalid caller 
values under the reserved key, while bound PreserveNative and SparkStandard 
calls still cross delegating legacy overrides with their analyzed mode intact.



##########
sql/core/src/test/scala/org/apache/spark/sql/execution/datasources/SaveIntoDataSourceCommandSuite.scala:
##########
@@ -69,7 +70,29 @@ class SaveIntoDataSourceCommandSuite extends 
SharedSparkSession {
     saveIntoDataSource(2)
     checkAnswer(loadData, Row(0) :: Row(1) :: Nil)
 
+    spark.catalog.clearCache()
     FakeV1DataSource.data = null
+
+    // Bound modes store Some(false)/Some(true) on LogicalRelation, so recache 
must match

Review Comment:
   **Nit (P3):** `charVarcharScanMode` is now `Option[CharVarcharScanMode]`, so 
this comment's `Some(false)`/`Some(true)` representation no longer exists. 
Please name `Some(PreserveNative)` and `Some(SparkStandard)` (or simply say 
"the two bound modes") so the cache-invalidation rationale matches the state 
under test.



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