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


##########
sql/core/src/main/scala/org/apache/spark/sql/execution/CacheManager.scala:
##########
@@ -348,6 +349,47 @@ class CacheManager extends Logging with 
AdaptiveSparkPlanHelper {
     recacheByCondition(spark, _.plan.exists(_.sameResult(normalized)))
   }
 
+  /**
+   * Re-caches every entry whose plan contains a [[LogicalRelation]] for 
`relation`.
+   * Unlike [[recacheByPlan]], this ignores CHAR/VARCHAR scan-mode identity so 
a V1 write
+   * invalidates preserve-only, standard, and unbound cache entries for that 
BaseRelation.
+   */
+  def recacheByV1Relation(spark: SparkSession, relation: BaseRelation): Unit = 
{
+    recacheByCondition(spark, cd => cd.plan.exists {
+      case logical: LogicalRelation => logical.relation == relation
+      case _ => false
+    })
+  }
+
+  /**
+   * Re-caches every entry whose plan contains the given catalog-less 
[[DataSourceV2Relation]].
+   * The scan mode is ignored only for this mutation-specific match so an 
unbound write target
+   * invalidates preserve-native and standard cache entries without weakening 
normal cache identity.
+   */
+  def recacheByV2Relation(spark: SparkSession, relation: 
DataSourceV2Relation): Unit = {
+    val unboundRelation = relation.copy(charVarcharScanMode = None)
+    recacheByCondition(spark, cd => cd.plan.exists {
+      case cached: DataSourceV2Relation =>
+        cached.copy(charVarcharScanMode = None).sameResult(unboundRelation)
+      case _ => false
+    })
+  }
+
+  /**
+   * Looks up direct cache entries for a V2 table mutation while ignoring only 
their analyzed
+   * CHAR/VARCHAR scan mode. Normal cache substitution remains mode-sensitive.
+   */
+  def lookupCachedDataByV2Relation(relation: DataSourceV2Relation): 
Seq[CachedData] = {
+    val unboundRelation = relation.copy(charVarcharScanMode = None)
+    cachedData.filter { cd =>
+      EliminateSubqueryAliases(cd.plan) match {

Review Comment:
   **Non-blocking (P2):** This still misses direct caches of tables that 
actually contain CHAR/VARCHAR columns. ApplyCharTypePadding puts a Project 
above the cleaned DataSourceV2Relation, and EliminateSubqueryAliases leaves 
that Project in place, so this match falls through; the later name-based 
invalidation removes the old cache and rename restores no variant or 
StorageLevel. The new multi-mode rename test remains green because its 
INT/STRING schema never creates that Project. Could the direct-cache matcher 
recognize only the analyzer-owned CHAR/VARCHAR wrapper, while retaining the 
dependent/time-travel exclusions, and exercise it with a real CHAR or VARCHAR 
column?
   
   **Recommended change:** Teach the direct V2 cache matcher to unwrap only the 
analyzer-owned CHAR/VARCHAR projection shape around the target relation, 
preserve mode-insensitive relation matching, and add V2 cache-rename coverage 
using real CHAR/VARCHAR columns for every restored scan mode and storage level.
   
   **Why this works:** Recognize the exact ApplyCharTypePadding projection over 
a single DataSourceV2Relation as a direct cache key, compare its underlying 
relation after clearing charVarcharScanMode, and continue rejecting arbitrary 
projections, dependent queries, and time-travel relations. Snapshot and restore 
the descriptors already associated with every matched cache.
   
   **Scope:** sql/core/src/main/scala/org/apache/spark/sql/execution, 
sql/core/src/test/scala/org/apache/spark/sql
   
   **Compatibility:** Bare-relation direct caches continue to survive V2 
rename, while ordinary cache lookup remains mode-sensitive and rename does not 
restore dependent or time-travel entries.
   
   **Risks:** An overly broad Project match can promote dependent query caches 
to table caches after rename. An overly shape-specific match can miss 
legitimate preserve-native or standard-semantics variants as analyzer 
expressions evolve.
   
   **Constraints:** Normal cache substitution must remain scan-mode-sensitive. 
Dependent query and time-travel cache entries must remain excluded from rename 
restoration. Each restored cache must retain its original StorageLevel and 
CHAR/VARCHAR scan mode.
   
   **Success:** Direct V2 table caches whose key is wrapped by CHAR/VARCHAR 
padding are discovered before rename invalidation. Every preserve-native and 
standard-semantics variant is cached under the destination identifier with its 
original storage level. Dependent query and time-travel caches are not restored 
as direct caches of the destination table.



##########
sql/core/src/main/scala/org/apache/spark/sql/execution/datasources/v2/RenameTableExec.scala:
##########
@@ -49,9 +50,11 @@ case class RenameTableExec(
     } else newIdent
     catalog.renameTable(oldIdent, qualifiedNewIdent)
 

Review Comment:
   **Non-blocking (P2):** This reloads the same destination table once per 
cache variant after renameTable has already committed. With PreserveNative and 
SparkStandard caches, that adds avoidable remote catalog latency; if a later 
load fails, the command reports failure with the table already renamed and only 
a prefix of the caches restored. Could we load qualifiedNewIdent once when 
oldCaches is non-empty and reuse that Table for every mode/StorageLevel 
descriptor?



##########
sql/core/src/main/scala/org/apache/spark/sql/execution/datasources/orc/OrcUtils.scala:
##########
@@ -437,6 +437,9 @@ object OrcUtils extends Logging {
       s"array<${getOrcSchemaString(a.elementType)}>"
     case m: MapType =>
       
s"map<${getOrcSchemaString(m.keyType)},${getOrcSchemaString(m.valueType)}>"
+    // Keep Spark responsible for CHAR/VARCHAR assignment and scan checks. 
Native ORC
+    // CHAR/VARCHAR would truncate or pad before Spark can validate the 
original value.
+    case _: CharType | _: VarcharType => StringType.catalogString

Review Comment:
   Confirmed: the row decoder now accepts all StringType subtypes recursively 
and the V1/V2 row and vector coverage exercises the requested paths. Thanks.
   
   <!-- SPARK_DEV_REVIEW_REPLY 
{"feedback_id":"inline:3882881464","thread_id":"inline:3882881464","verdict_sha256":"9e8299073fe3a3fd317d35d14ddb819ddbbaf0c580a01f233a538f0836de3d9e"}
 -->



##########
sql/core/src/test/scala/org/apache/spark/sql/CharVarcharTestSuite.scala:
##########
@@ -1697,33 +1697,94 @@ class BasicCharVarcharTestSuite extends 
SharedSparkSession {
         sql("DROP TEMPORARY FUNCTION IF EXISTS std_char_param")
         sql("DROP TEMPORARY FUNCTION IF EXISTS std_varchar_param")
       }
+    }
+  }
 
-      // ORC catalog tables stamp the catalyst type so typeof survives 
write/read.
-      withTable("std_orc") {
-        sql("CREATE TABLE std_orc (c CHAR(5), v VARCHAR(5)) USING orc")
-        sql("INSERT INTO std_orc VALUES ('ab', 'cd')")
-        assert(spark.table("std_orc").schema.map(_.dataType) ===
-          Seq(CharType(5), VarcharType(5)))
-        checkAnswer(
-          sql("SELECT concat('<', c, '>'), concat('<', v, '>') FROM std_orc"),
-          Row("<ab   >", "<cd>"))
+  test("SPARK-58814: major formats preserve CHAR/VARCHAR schemas and values") {
+    withSQLConf(SQLConf.CHAR_VARCHAR_STANDARD_SEMANTICS.key -> "true") {
+      Seq("parquet", "orc").foreach { format =>
+        Seq("v1" -> format, "v2" -> "").foreach { case (sourceVersion, 
useV1List) =>
+          withSQLConf(SQLConf.USE_V1_SOURCE_LIST.key -> useV1List) {
+            withTempPath { dir =>
+              val path = dir.getCanonicalPath
+              val input = spark.range(1).selectExpr(
+                "cast('ab' AS CHAR(4)) AS c",
+                "cast('xy' AS VARCHAR(3)) AS v",
+                "named_struct('c', cast('z' AS CHAR(2))) AS s",
+                "array(cast('q' AS VARCHAR(2))) AS a",
+                "map(cast('k' AS CHAR(2)), cast('v' AS VARCHAR(2))) AS m")
+              input.write.mode("overwrite").format(format).save(path)
+
+              val readBack = spark.read.format(format).load(path)
+              assert(DataType.equalsIgnoreNullability(readBack.schema, 
input.schema),
+                s"$format $sourceVersion lost CHAR/VARCHAR schema")
+              checkAnswer(
+                readBack.selectExpr("concat('<', c, '>')", "v", "concat('<', 
s.c, '>')"),

Review Comment:
   Confirmed: the ORC assertions now materialize the collection fields and 
cover oversized struct, array, map-key, and map-value positions. Thanks.
   
   <!-- SPARK_DEV_REVIEW_REPLY 
{"feedback_id":"inline:3882881471","thread_id":"inline:3882881471","verdict_sha256":"9e8299073fe3a3fd317d35d14ddb819ddbbaf0c580a01f233a538f0836de3d9e"}
 -->



##########
sql/core/src/main/scala/org/apache/spark/sql/execution/datasources/orc/OrcUtils.scala:
##########
@@ -437,6 +437,11 @@ object OrcUtils extends Logging {
       s"array<${getOrcSchemaString(a.elementType)}>"
     case m: MapType =>
       
s"map<${getOrcSchemaString(m.keyType)},${getOrcSchemaString(m.valueType)}>"
+    // Under standard semantics, keep Spark responsible for CHAR/VARCHAR 
assignment and scan
+    // checks. Native ORC would truncate or pad before Spark can validate the 
original value.
+    // Preserve-only mode retains the native constrained schema and its legacy 
enforcement.
+    case _: CharType | _: VarcharType if 
SQLConf.get.charVarcharStandardSemantics =>

Review Comment:
   Confirmed: persisted views now retain the analysis-bound scan mode across 
caller configuration changes. Thanks.
   
   <!-- SPARK_DEV_REVIEW_REPLY 
{"feedback_id":"inline:3892187814","thread_id":"inline:3892187814","verdict_sha256":"9e8299073fe3a3fd317d35d14ddb819ddbbaf0c580a01f233a538f0836de3d9e"}
 -->



##########
connector/avro/src/test/scala/org/apache/spark/sql/avro/AvroSuite.scala:
##########
@@ -3724,6 +3724,82 @@ abstract class AvroSuite
     }
   }
 
+  test("SPARK-58814: Avro infers nested CHAR/VARCHAR schema and values") {
+    withSQLConf(SQLConf.CHAR_VARCHAR_STANDARD_SEMANTICS.key -> "true") {
+      withTempPath { dir =>
+        val path = dir.getCanonicalPath
+        val input = spark.range(1).selectExpr(
+          "cast('ab' AS CHAR(4)) AS c",
+          "cast('xy' AS VARCHAR(3)) AS v",
+          "named_struct('c', cast('z' AS CHAR(2))) AS s",
+          "array(cast('q' AS VARCHAR(2))) AS a",
+          "map(cast('k' AS CHAR(2)), cast('v' AS VARCHAR(2))) AS m")
+        input.write.mode("overwrite").format("avro").save(path)
+
+        val readBack = spark.read.format("avro").load(path)
+        assert(DataType.equalsIgnoreNullability(readBack.schema, input.schema))
+        checkAnswer(
+          readBack.selectExpr("concat('<', c, '>')", "v", "concat('<', s.c, 
'>')"),

Review Comment:
   Confirmed: the Avro round trip now materializes and asserts both collection 
fields. Thanks.
   
   <!-- SPARK_DEV_REVIEW_REPLY 
{"feedback_id":"inline:3892187822","thread_id":"inline:3892187822","verdict_sha256":"9e8299073fe3a3fd317d35d14ddb819ddbbaf0c580a01f233a538f0836de3d9e"}
 -->



##########
sql/core/src/main/scala/org/apache/spark/sql/execution/datasources/orc/OrcUtils.scala:
##########
@@ -427,16 +427,29 @@ object OrcUtils extends Logging {
    * Given a `StructType` object, this methods converts it to corresponding 
string representation
    * in ORC.
    */
-  def getOrcSchemaString(dt: DataType): String = dt match {
+  def getOrcSchemaString(dt: DataType): String = {
+    getOrcSchemaString(dt, SQLConf.get.charVarcharStandardSemantics)
+  }
+
+  private def getOrcSchemaString(
+      dt: DataType,
+      charVarcharStandardSemantics: Boolean): String = dt match {
     case s: StructType =>
       val fieldTypes = s.fields.map { f =>
-        s"${quoteIdentifier(f.name)}:${getOrcSchemaString(f.dataType)}"
+        s"${quoteIdentifier(f.name)}:" +
+          s"${getOrcSchemaString(f.dataType, charVarcharStandardSemantics)}"
       }
       s"struct<${fieldTypes.mkString(",")}>"
     case a: ArrayType =>
-      s"array<${getOrcSchemaString(a.elementType)}>"
+      s"array<${getOrcSchemaString(a.elementType, 
charVarcharStandardSemantics)}>"
     case m: MapType =>
-      
s"map<${getOrcSchemaString(m.keyType)},${getOrcSchemaString(m.valueType)}>"
+      s"map<${getOrcSchemaString(m.keyType, charVarcharStandardSemantics)}," +
+        s"${getOrcSchemaString(m.valueType, charVarcharStandardSemantics)}>"
+    // Under standard semantics, keep Spark responsible for CHAR/VARCHAR 
assignment and scan
+    // checks. Native ORC would truncate or pad before Spark can validate the 
original value.
+    // Preserve-only mode retains the native constrained schema and its legacy 
enforcement.
+    case _: CharType | _: VarcharType if charVarcharStandardSemantics =>

Review Comment:
   Confirmed: the preserve-native ORC round trip writes first-class constrained 
types and verifies inferred types plus native padding and truncation in V1 and 
V2. Thanks.
   
   <!-- SPARK_DEV_REVIEW_REPLY 
{"feedback_id":"inline:3898132494","thread_id":"inline:3898132494","verdict_sha256":"9e8299073fe3a3fd317d35d14ddb819ddbbaf0c580a01f233a538f0836de3d9e"}
 -->



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