This is an automated email from the ASF dual-hosted git repository.

Yicong-Huang pushed a commit to branch branch-4.x
in repository https://gitbox.apache.org/repos/asf/spark.git


The following commit(s) were added to refs/heads/branch-4.x by this push:
     new 7f73f99c0c65 [SPARK-58798][SQL] Fix collated CHAR LCT and cover 
compare/IN/set-ops
7f73f99c0c65 is described below

commit 7f73f99c0c651d2197911b7c7946446bfa0f183b
Author: Serge Rielau <[email protected]>
AuthorDate: Thu Aug 20 16:58:26 2026 +0000

    [SPARK-58798][SQL] Fix collated CHAR LCT and cover compare/IN/set-ops
    
    ### What changes were proposed in this pull request?
    
    Covers the string-family LCT lattice 
([SPARK-58798](https://issues.apache.org/jira/browse/SPARK-58798)). Parent: 
[SPARK-58794](https://issues.apache.org/jira/browse/SPARK-58794). Also includes 
explicit character CAST truncation 
([SPARK-58797](https://issues.apache.org/jira/browse/SPARK-58797)).
    
    Rebased onto `master` after #58033 merged. Unique commits vs master: 
https://github.com/srielau/spark/compare/master...SPARK-58794-lct-compare
    
    Follow-up that drills into least-common-type, comparison, and `IN` under 
`spark.sql.charVarchar.standardSemantics.enabled`.
    
    `CollationTypeCoercion` is not gated on `standardSemantics` alone 
(`charVarcharFirstClassTypes` is also true under 
`preserveCharVarcharTypeInfo`). The equal-collation length LCT therefore 
applies in preserve-only mode as well; tests cover both flags.
    
    **Bug fix.** `CollationTypeCoercion.getWinningStringType` required 
`sameType` at equal collation strength. Same-collation `CHAR(2)` and `CHAR(4)` 
therefore looked like a collation mismatch and became `IndeterminateStringType` 
(`string collate null`). At equal strength, take 
`StringHelper.tightestCommonString` instead so every child is cast to `max(n, 
m)` (padding, never truncating).
    
    **Explicit CAST vs store assignment (ISO 6.13 / 9.2).** Under the flag:
    
    - Character-to-`CHAR`/`VARCHAR` `CAST` / `TRY_CAST` **truncates** to `n` 
characters (then `CHAR` pads). Example: `CAST('abcdef' AS VARCHAR(2))` is 
`'ab'`, not `EXCEED_LIMIT_LENGTH`.
    - Numeric (and other non-string) `CAST` to `CHAR`/`VARCHAR` still 
**errors** when the formatted literal does not fit (`CAST(12345 AS VARCHAR(4))` 
-> `EXCEED_LIMIT_LENGTH` / 54006). That matches ISO 22001, not silent digit 
clipping.
    - Store assignment (`INSERT`, `Dataset.to`, encoder write-side) is 
unchanged: non-space overflow still errors. Project store-assignment casts to 
unconstrained `STRING` then `stringLengthCheck`, so they do not inherit CAST 
truncation.
    
    **Coverage / pinned behavior.**
    
    - Set ops: `UNION` / `UNION ALL` / `INTERSECT` / `EXCEPT`, plus multi-row 
`VALUES`, all share the string-family LCT.
    - Comparison and `IN`: every participant is cast to that LCT, including the 
`IN` left-hand side (`InTypeCoercion` uses `findWiderCommonType` over `value +: 
list`). Casting to `CHAR` pads, so unequal `CHAR` lengths compare equal after 
widen; casting to `VARCHAR`/`STRING` keeps the `CHAR` pad, so `CHAR 'a'` 
(stored as `'a '`) is not equal to `VARCHAR`/`STRING 'a'` unless the other side 
carries the same trailing blank. Ignoring trailing blanks is a collation 
concern (`RTRIM`), not a type [...]
    - Parameterized lengths (`CHAR(:n)` / `VARCHAR(?)`): markers in length 
position already bind through the parser; under the flag, character CAST 
truncates to the bound `n`, numeric CAST still errors if it does not fit, and 
DDL keeps the bound `n`. Negative / non-integral lengths fail at parse after 
substitution.
    
    ### Why are the changes needed?
    
    #58033 left the collated mixed-length LCT gap as a known issue, and had 
only thin coverage of `UNION ALL` / `INTERSECT` with no compare / `IN`-LHS / 
`VALUES` / `EXCEPT` matrix. Without pinning the LCT-cast rule, CHAR vs CHAR vs 
VARCHAR equality is easy to misread as PAD SPACE.
    
    Explicit character CAST was still using the write-side length check, so 
`CAST('abcdef' AS VARCHAR(2))` failed the same way as `INSERT`. ISO 6.13 
truncates that CAST; 9.2 keeps the INSERT error. Numeric CAST must still fail 
when the literal does not fit (ISO 22001).
    
    ### Does this PR introduce _any_ user-facing change?
    
    Yes, when the flag is on:
    
    - Collated `CHAR`/`VARCHAR` values of different lengths but the same 
collation now widen to `CHAR`/`VARCHAR(max(n,m))` instead of an indeterminate 
collation. Compare / `IN` / set-op results for mixed `CHAR` lengths follow the 
LCT-cast rule above (this matches what the engine already did for non-collated 
paths; the new tests lock it in).
    - Explicit character-to-`CHAR`/`VARCHAR` CAST truncates instead of raising 
`EXCEED_LIMIT_LENGTH`. `INSERT` / encoder overflow and numeric CAST overflow 
still error.
    
    ### How was this patch tested?
    
    - New `BasicCharVarcharTestSuite` cases covering compare, `IN` (including 
analyzed-plan assertion that the LHS widens), `RTRIM`, collated LCT, `UNION` / 
`INTERSECT` / `EXCEPT`, multi-row `VALUES`, parameterized `CHAR`/`VARCHAR` 
lengths, character CAST truncation, and numeric CAST overflow under the flag.
    - Expanded golden file `charvarchar-standard-semantics.sql`.
    - Locally: `BasicCharVarcharTestSuite` and the golden file regeneration/run.
    
    Closes #58080 from srielau/SPARK-58794-lct-compare.
    
    Authored-by: Serge Rielau <[email protected]>
    Signed-off-by: Yicong-Huang <[email protected]>
    (cherry picked from commit ac5bba0dec7c8908b54dfdb0e585d8ce4951a2f7)
    Signed-off-by: Yicong-Huang <[email protected]>
---
 .../sql/catalyst/util/CharVarcharCodegenUtils.java |  24 ++
 .../catalyst/analysis/CollationTypeCoercion.scala  |  66 +++-
 .../catalyst/analysis/TableOutputResolver.scala    |   9 +-
 .../spark/sql/catalyst/expressions/Cast.scala      |   4 +
 .../sql/catalyst/expressions/ToStringBase.scala    |  45 ++-
 .../plans/logical/basicLogicalOperators.scala      |  33 +-
 .../spark/sql/catalyst/util/CharVarcharUtils.scala |   9 +
 .../catalyst/util/ResolveDefaultColumnsUtil.scala  |   3 +-
 .../charvarchar-standard-semantics.sql.out         | 377 ++++++++++++++++++++
 .../inputs/charvarchar-standard-semantics.sql      | 115 +++++-
 .../results/charvarchar-standard-semantics.sql.out | 393 ++++++++++++++++++++-
 .../apache/spark/sql/CharVarcharTestSuite.scala    | 211 ++++++++++-
 12 files changed, 1226 insertions(+), 63 deletions(-)

diff --git 
a/sql/catalyst/src/main/java/org/apache/spark/sql/catalyst/util/CharVarcharCodegenUtils.java
 
b/sql/catalyst/src/main/java/org/apache/spark/sql/catalyst/util/CharVarcharCodegenUtils.java
index 659833851ccd..4c1d1014f5bb 100644
--- 
a/sql/catalyst/src/main/java/org/apache/spark/sql/catalyst/util/CharVarcharCodegenUtils.java
+++ 
b/sql/catalyst/src/main/java/org/apache/spark/sql/catalyst/util/CharVarcharCodegenUtils.java
@@ -54,6 +54,30 @@ public class CharVarcharCodegenUtils {
     }
   }
 
+  /**
+   * Applies the SQL explicit-cast rules for a character string source and 
CHAR target.
+   *
+   * Unlike store assignment, an explicit character-to-character cast 
truncates non-space
+   * characters instead of raising a right-truncation exception.
+   */
+  public static UTF8String charTypeCast(UTF8String inputStr, int limit) {
+    int numChars = inputStr.numChars();
+    if (numChars == limit) {
+      return inputStr;
+    } else if (numChars < limit) {
+      return inputStr.rpad(limit, SPACE);
+    } else {
+      return inputStr.substring(0, limit);
+    }
+  }
+
+  /**
+   * Applies the SQL explicit-cast rules for a character string source and 
VARCHAR target.
+   */
+  public static UTF8String varcharTypeCast(UTF8String inputStr, int limit) {
+    return inputStr.numChars() > limit ? inputStr.substring(0, limit) : 
inputStr;
+  }
+
   public static UTF8String readSidePadding(UTF8String inputStr, int limit) {
     int numChars = inputStr.numChars();
     if (numChars == limit) {
diff --git 
a/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/analysis/CollationTypeCoercion.scala
 
b/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/analysis/CollationTypeCoercion.scala
index f43dfb77a9c0..ea1a1917d8f5 100644
--- 
a/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/analysis/CollationTypeCoercion.scala
+++ 
b/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/analysis/CollationTypeCoercion.scala
@@ -25,7 +25,10 @@ import 
org.apache.spark.sql.catalyst.plans.logical.{Aggregate, LogicalPlan, Proj
 import org.apache.spark.sql.catalyst.trees.TreeNodeTag
 import org.apache.spark.sql.catalyst.util.TypeUtils.toSQLExpr
 import org.apache.spark.sql.errors.QueryCompilationErrors
-import org.apache.spark.sql.types.{ArrayType, DataType, 
IndeterminateStringType, MapType, NullType, StringType, StructType}
+import org.apache.spark.sql.types.{
+  ArrayType, DataType, IndeterminateStringType, MapType, NullType, 
StringHelper,
+  StringType, StructType
+}
 import org.apache.spark.sql.util.SchemaUtils
 
 /**
@@ -107,6 +110,13 @@ object CollationTypeCoercion extends SQLConfHelper {
 
   /**
    * Changes the data type of the expression to the given `newType`.
+   *
+   * Never retarget an existing Cast (`cast.copy(dataType = ...)`). Explicit 
CAST
+   * truncation / overflow (ISO 6.13) and CHAR padding must stay on the inner 
node;
+   * LCT is an outer Cast. Uncollated TypeCoercion already nests.
+   *
+   * Literals: `copy(dataType)` is enough when only collation changes. When a 
string
+   * constraint changes (CHAR(2) to CHAR(4)), wrap in Cast so padding is 
re-applied.
    */
   private def changeType(expr: Expression, newType: DataType): Expression = {
     mergeTypes(expr.dataType, newType) match {
@@ -114,8 +124,11 @@ object CollationTypeCoercion extends SQLConfHelper {
         
assert(!newDataType.existsRecursively(_.isInstanceOf[StringTypeWithContext]))
 
         expr match {
+          case lit: Literal if stringConstraintChanged(lit.dataType, 
newDataType) =>
+            Cast(lit, newDataType, timeZoneId = 
Some(conf.sessionLocalTimeZone))
           case lit: Literal => lit.copy(dataType = newDataType)
-          case cast: Cast => cast.copy(dataType = newDataType)
+          case cast: Cast =>
+            Cast(cast, newDataType, timeZoneId = 
Some(conf.sessionLocalTimeZone))
           case subquery: SubqueryExpression =>
             changeTypeInSubquery(subquery, newType)
 
@@ -127,6 +140,24 @@ object CollationTypeCoercion extends SQLConfHelper {
     }
   }
 
+  /**
+   * True when CHAR/VARCHAR length (or nested length) differs between `from` 
and `to`.
+   * Collation-only differences are not a constraint change.
+   */
+  private def stringConstraintChanged(from: DataType, to: DataType): Boolean = 
{
+    (from, to) match {
+      case (f: StringType, t: StringType) => f.constraint != t.constraint
+      case (ArrayType(fe, _), ArrayType(te, _)) => stringConstraintChanged(fe, 
te)
+      case (MapType(fk, fv, _), MapType(tk, tv, _)) =>
+        stringConstraintChanged(fk, tk) || stringConstraintChanged(fv, tv)
+      case (fs: StructType, ts: StructType) if fs.length == ts.length =>
+        fs.fields.indices.exists { i =>
+          stringConstraintChanged(fs.fields(i).dataType, ts.fields(i).dataType)
+        }
+      case _ => false
+    }
+  }
+
   /**
    * Changes the data type of the expression in the subquery to the given 
`newType`.
    * Currently only supports subqueries with [[Project]] and [[Aggregate]] 
plan.
@@ -414,7 +445,23 @@ object CollationTypeCoercion extends SQLConfHelper {
     }
   }
 
-  /** Determines the winning StringTypeWithContext based on the strength of 
the collation. */
+  /**
+   * Resolves collation strength independently of CHAR/VARCHAR length.
+   *
+   * This rule always runs. First-class CHAR/VARCHAR appear whenever
+   * `charVarcharFirstClassTypes` is true (`standardSemantics` or
+   * `preserveCharVarcharTypeInfo`), not only under `standardSemantics`.
+   *
+   * Same collation, including mixed strength: take the string-family LCT 
`max(n, m)`
+   * (pads, never truncates) and attach the stronger strength. Example:
+   * `coalesce(CAST('a' AS CHAR(2) COLLATE UTF8_LCASE),
+   * CAST(1 AS CHAR(4) COLLATE UTF8_LCASE))` is CHAR(4) COLLATE UTF8_LCASE
+   * (Implicit CHAR(2) from a string CAST vs Default CHAR(4) from a non-string 
CAST).
+   *
+   * Different collations at equal strength: mismatch (error if Explicit, else
+   * indeterminate). Different collations at unequal strength: the stronger 
operand
+   * wins in full, including its length (SQL collation precedence).
+   */
   private def getWinningStringType(
       left: StringTypeWithContext,
       right: StringTypeWithContext): StringTypeWithContext = {
@@ -427,14 +474,13 @@ object CollationTypeCoercion extends SQLConfHelper {
       }
     }
 
-    (left.strength.priority, right.strength.priority) match {
-      case (leftPriority, rightPriority) if leftPriority == rightPriority =>
-        if (left.sameType(right)) left
-        else handleMismatch()
+    val winner =
+      if (left.strength.priority <= right.strength.priority) left else right
 
-      case (leftPriority, rightPriority) =>
-        if (leftPriority < rightPriority) left
-        else right
+    StringHelper.tightestCommonString(left.stringType, right.stringType) match 
{
+      case Some(lct) => StringTypeWithContext(lct, winner.strength)
+      case None if left.strength.priority == right.strength.priority => 
handleMismatch()
+      case None => winner
     }
   }
 
diff --git 
a/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/analysis/TableOutputResolver.scala
 
b/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/analysis/TableOutputResolver.scala
index b50c77eee479..933513654dc1 100644
--- 
a/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/analysis/TableOutputResolver.scala
+++ 
b/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/analysis/TableOutputResolver.scala
@@ -236,7 +236,8 @@ object TableOutputResolver extends SQLConfHelper with 
Logging {
     if (canWriteValue) {
       val nullCheckedValue = checkNullability(value, attr, conf, colPath)
       val casted = cast(nullCheckedValue, attrTypeWithoutCharVarchar, conf, 
colPath.quoted)
-      val exprWithStrLenCheck = if (conf.charVarcharAsString || 
!attrTypeHasCharVarchar) {
+      val exprWithStrLenCheck = if (!attrTypeHasCharVarchar ||
+          !CharVarcharUtils.shouldApplyWriteSideLengthCheck(conf)) {
         casted
       } else {
         CharVarcharUtils.stringLengthCheck(casted, attr.dataType)
@@ -333,7 +334,8 @@ object TableOutputResolver extends SQLConfHelper with 
Logging {
       expectedCol: Attribute,
       conf: SQLConf): NamedExpression = {
     val rawType = 
CharVarcharUtils.getRawType(expectedCol.metadata).getOrElse(expectedCol.dataType)
-    val checked = if (!conf.charVarcharAsString && 
CharVarcharUtils.hasCharVarchar(rawType)) {
+    val checked = if (CharVarcharUtils.hasCharVarchar(rawType) &&
+        CharVarcharUtils.shouldApplyWriteSideLengthCheck(conf)) {
       val value = defaultExpr match {
         case a: Alias => a.child
         case other => other
@@ -854,7 +856,8 @@ object TableOutputResolver extends SQLConfHelper with 
Logging {
         } else {
           val udtUnwrapped = unwrapUDT(queryExpr)
           val casted = cast(udtUnwrapped, attrTypeWithoutCharVarchar, conf, 
colPath.quoted)
-          if (conf.charVarcharAsString || !attrTypeHasCharVarchar) {
+          if (!attrTypeHasCharVarchar ||
+              !CharVarcharUtils.shouldApplyWriteSideLengthCheck(conf)) {
             casted
           } else {
             CharVarcharUtils.stringLengthCheck(casted, tableAttr.dataType)
diff --git 
a/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/expressions/Cast.scala
 
b/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/expressions/Cast.scala
index b1fcee354b83..5e5bb00df6c2 100644
--- 
a/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/expressions/Cast.scala
+++ 
b/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/expressions/Cast.scala
@@ -692,6 +692,10 @@ case class Cast(
   override def withTimeZone(timeZoneId: String): TimeZoneAwareExpression =
     copy(timeZoneId = Option(timeZoneId))
 
+  // Parser, Column.cast, and Connect set USER_SPECIFIED_CAST. 
Analyzer-inserted Casts do not.
+  override protected def truncateCharVarcharOnCast: Boolean =
+    containsTag(Cast.USER_SPECIFIED_CAST)
+
   override protected def withNewChildInternal(newChild: Expression): Cast = 
copy(child = newChild)
 
   // CAST_TO_TIMESTAMP must be set on a superset of the targets accepted by
diff --git 
a/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/expressions/ToStringBase.scala
 
b/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/expressions/ToStringBase.scala
index 97f41b296055..096c1788df63 100644
--- 
a/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/expressions/ToStringBase.scala
+++ 
b/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/expressions/ToStringBase.scala
@@ -35,6 +35,13 @@ import org.apache.spark.util.SparkStringUtils
 
 trait ToStringBase { self: UnaryExpression with TimeZoneAwareExpression =>
 
+  /**
+   * ISO 6.13 truncation applies only to user-written CAST / TRY_CAST. 
Implicit and
+   * store-assignment Casts keep the write-side length check. 
[[ToPrettyString]] is never
+   * a CAST, so it stays false.
+   */
+  protected def truncateCharVarcharOnCast: Boolean = false
+
   private lazy val dateFormatter = DateFormatter()
   private lazy val timeFormatter = new FractionTimeFormatter()
   private lazy val timestampFormatter = 
TimestampFormatter.getFractionFormatter(zoneId)
@@ -57,14 +64,22 @@ trait ToStringBase { self: UnaryExpression with 
TimeZoneAwareExpression =>
 
   // Returns a function to convert a value to pretty string. The function 
assumes input is not null.
   protected final def castToString(
-      from: DataType, to: StringConstraint = NoConstraint): Any => UTF8String =
-    to match {
-      case FixedLength(length) =>
-        s => 
CharVarcharCodegenUtils.charTypeWriteSideCheck(castToString(from)(s), length)
-      case MaxLength(length) =>
-        s => 
CharVarcharCodegenUtils.varcharTypeWriteSideCheck(castToString(from)(s), length)
-      case NoConstraint => castToString(from)
+      from: DataType, to: StringConstraint = NoConstraint): Any => UTF8String 
= {
+    val toUTF8String = castToString(from)
+    (to, from) match {
+      case (FixedLength(length), _: StringType)
+          if SQLConf.get.charVarcharStandardSemantics && 
truncateCharVarcharOnCast =>
+        s => CharVarcharCodegenUtils.charTypeCast(toUTF8String(s), length)
+      case (MaxLength(length), _: StringType)
+          if SQLConf.get.charVarcharStandardSemantics && 
truncateCharVarcharOnCast =>
+        s => CharVarcharCodegenUtils.varcharTypeCast(toUTF8String(s), length)
+      case (FixedLength(length), _) =>
+        s => CharVarcharCodegenUtils.charTypeWriteSideCheck(toUTF8String(s), 
length)
+      case (MaxLength(length), _) =>
+        s => 
CharVarcharCodegenUtils.varcharTypeWriteSideCheck(toUTF8String(s), length)
+      case (NoConstraint, _) => toUTF8String
     }
+  }
 
   // The Types Framework is the single integration point for framework types' 
cast-to-string, via
   // the zone-less formatUTF8. The cast's session zone is threaded into the 
lookup so TIMESTAMP_LTZ
@@ -196,14 +211,22 @@ trait ToStringBase { self: UnaryExpression with 
TimeZoneAwareExpression =>
     (c, evPrim) => {
       val tmpVar = ctx.freshVariable("tmp", classOf[UTF8String])
       val castToString = castToStringCode(from, ctx)(c, tmpVar)
-      val maintainConstraint = to match {
-        case FixedLength(length) =>
+      val maintainConstraint = (to, from) match {
+        case (FixedLength(length), _: StringType)
+            if SQLConf.get.charVarcharStandardSemantics && 
truncateCharVarcharOnCast =>
+          code"""$evPrim = 
org.apache.spark.sql.catalyst.util.CharVarcharCodegenUtils
+                .charTypeCast($tmpVar, $length);""".stripMargin
+        case (MaxLength(length), _: StringType)
+            if SQLConf.get.charVarcharStandardSemantics && 
truncateCharVarcharOnCast =>
+          code"""$evPrim = 
org.apache.spark.sql.catalyst.util.CharVarcharCodegenUtils
+                .varcharTypeCast($tmpVar, $length);""".stripMargin
+        case (FixedLength(length), _) =>
           code"""$evPrim = 
org.apache.spark.sql.catalyst.util.CharVarcharCodegenUtils
                 .charTypeWriteSideCheck($tmpVar, $length);""".stripMargin
-        case MaxLength(length) =>
+        case (MaxLength(length), _) =>
           code"""$evPrim = 
org.apache.spark.sql.catalyst.util.CharVarcharCodegenUtils
                 .varcharTypeWriteSideCheck($tmpVar, $length);""".stripMargin
-        case NoConstraint => code"$evPrim = $tmpVar;"
+        case (NoConstraint, _) => code"$evPrim = $tmpVar;"
       }
       code"""
             UTF8String $tmpVar;
diff --git 
a/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/plans/logical/basicLogicalOperators.scala
 
b/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/plans/logical/basicLogicalOperators.scala
index 3b562308c805..5d56eca1c1f2 100644
--- 
a/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/plans/logical/basicLogicalOperators.scala
+++ 
b/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/plans/logical/basicLogicalOperators.scala
@@ -184,13 +184,44 @@ object Project {
         if (other == target) {
           col
         } else if (Cast.canANSIStoreAssign(other, target)) {
-          Cast(col, target, Option(conf.sessionLocalTimeZone), ansiEnabled = 
true)
+          storeAssignCast(col, other, target, conf)
         } else {
           throw 
QueryCompilationErrors.invalidColumnOrFieldDataTypeError(columnPath, other, 
target)
         }
     }
   }
 
+  /**
+   * Cast `col` for ANSI store assignment without using character-to-character 
CAST
+   * truncation (ISO 6.13).
+   *
+   * For CHAR/VARCHAR targets the plan is Cast to unconstrained STRING, then
+   * `stringLengthCheck` (write-side overflow). Avoid 
replaceCharVarcharWithString
+   * so first-class types stay CHAR/VARCHAR.
+   */
+  private def storeAssignCast(
+      col: Expression,
+      other: DataType,
+      target: DataType,
+      conf: SQLConf): Expression = {
+    val (castTarget, lengthCheckType) = target match {
+      case c: CharType => (c.toStringType, Some(c: DataType))
+      case v: VarcharType => (v.toStringType, Some(v: DataType))
+      case otherType => (otherType, None)
+    }
+    val casted = if (other == castTarget) {
+      col
+    } else {
+      Cast(col, castTarget, Option(conf.sessionLocalTimeZone), ansiEnabled = 
true)
+    }
+    lengthCheckType match {
+      case Some(dt) if CharVarcharUtils.shouldApplyWriteSideLengthCheck(conf) 
=>
+        CharVarcharUtils.stringLengthCheck(casted, dt)
+      case _ =>
+        casted
+    }
+  }
+
   private def reorderFields(
       fields: Seq[(String, Expression)],
       expected: Seq[StructField],
diff --git 
a/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/util/CharVarcharUtils.scala
 
b/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/util/CharVarcharUtils.scala
index 1b8f68175878..00adcfe69bc5 100644
--- 
a/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/util/CharVarcharUtils.scala
+++ 
b/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/util/CharVarcharUtils.scala
@@ -167,6 +167,15 @@ object CharVarcharUtils extends Logging with 
SparkCharVarcharUtils {
     }.getOrElse(expr)
   }
 
+  /**
+   * Write-side CHAR/VARCHAR length checks apply unless the session is on the 
legacy
+   * `charVarcharAsString` path with no first-class types. `standardSemantics` 
and
+   * `preserveCharVarcharTypeInfo` keep first-class types even if the legacy 
flag is also on.
+   */
+  def shouldApplyWriteSideLengthCheck(conf: SQLConf): Boolean = {
+    !conf.charVarcharAsString || conf.charVarcharFirstClassTypes
+  }
+
   def stringLengthCheck(expr: Expression, dt: DataType): Expression = {
     processStringForCharVarchar(
       expr,
diff --git 
a/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/util/ResolveDefaultColumnsUtil.scala
 
b/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/util/ResolveDefaultColumnsUtil.scala
index 68529e41937e..b9c0a0255f31 100644
--- 
a/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/util/ResolveDefaultColumnsUtil.scala
+++ 
b/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/util/ResolveDefaultColumnsUtil.scala
@@ -439,7 +439,8 @@ object ResolveDefaultColumns extends QueryErrorsBase
           throw QueryCompilationErrors.defaultValuesDataTypeError(
             statementType, colName, defaultSQL, dataType, other.dataType))
     }
-    if (!conf.charVarcharAsString && CharVarcharUtils.hasCharVarchar(dataType) 
&& ret.foldable) {
+    if (CharVarcharUtils.hasCharVarchar(dataType) &&
+        CharVarcharUtils.shouldApplyWriteSideLengthCheck(conf) && 
ret.foldable) {
       CharVarcharUtils.stringLengthCheck(ret, dataType).eval(EmptyRow)
     }
     ret
diff --git 
a/sql/core/src/test/resources/sql-tests/analyzer-results/charvarchar-standard-semantics.sql.out
 
b/sql/core/src/test/resources/sql-tests/analyzer-results/charvarchar-standard-semantics.sql.out
index 81fb27a17e34..85688308aa2e 100644
--- 
a/sql/core/src/test/resources/sql-tests/analyzer-results/charvarchar-standard-semantics.sql.out
+++ 
b/sql/core/src/test/resources/sql-tests/analyzer-results/charvarchar-standard-semantics.sql.out
@@ -55,6 +55,71 @@ Project [try_cast(abcdef as varchar(2)) AS TRY_CAST(abcdef 
AS VARCHAR(2))#x]
 +- OneRowRelation
 
 
+-- !query
+SELECT CAST(12345 AS VARCHAR(4))
+-- !query analysis
+Project [cast(12345 as varchar(4)) AS CAST(12345 AS VARCHAR(4))#x]
++- OneRowRelation
+
+
+-- !query
+SELECT CAST(12345 AS VARCHAR(5))
+-- !query analysis
+Project [cast(12345 as varchar(5)) AS CAST(12345 AS VARCHAR(5))#x]
++- OneRowRelation
+
+
+-- !query
+SELECT try_cast(12345 AS VARCHAR(4))
+-- !query analysis
+Project [try_cast(12345 as varchar(4)) AS TRY_CAST(12345 AS VARCHAR(4))#x]
++- OneRowRelation
+
+
+-- !query
+SELECT coalesce(CAST('abcdef' AS VARCHAR(2)), CAST('x' AS VARCHAR(4)))
+-- !query analysis
+Project [coalesce(cast(cast(abcdef as varchar(2)) as varchar(4)), cast(x as 
varchar(4))) AS coalesce(CAST(abcdef AS VARCHAR(2)), CAST(x AS VARCHAR(4)))#x]
++- OneRowRelation
+
+
+-- !query
+SELECT CASE WHEN true THEN CAST('abcdef' AS VARCHAR(2)) ELSE CAST('x' AS 
VARCHAR(4)) END
+-- !query analysis
+Project [CASE WHEN true THEN cast(cast(abcdef as varchar(2)) as varchar(4)) 
ELSE cast(x as varchar(4)) END AS CASE WHEN true THEN CAST(abcdef AS 
VARCHAR(2)) ELSE CAST(x AS VARCHAR(4)) END#x]
++- OneRowRelation
+
+
+-- !query
+SELECT CAST('abcdef' AS VARCHAR(2)) IN (CAST('ab' AS VARCHAR(4)))
+-- !query analysis
+Project [cast(cast(abcdef as varchar(2)) as varchar(4)) IN (cast(cast(ab as 
varchar(4)) as varchar(4))) AS (CAST(abcdef AS VARCHAR(2)) IN (CAST(ab AS 
VARCHAR(4))))#x]
++- OneRowRelation
+
+
+-- !query
+SELECT coalesce(
+  CAST('abcdef' AS VARCHAR(2) COLLATE UTF8_LCASE),
+  CAST('x' AS VARCHAR(4) COLLATE UTF8_LCASE))
+-- !query analysis
+Project [coalesce(cast(cast(abcdef as varchar(2) collate UTF8_LCASE) as 
varchar(4) collate UTF8_LCASE), cast(x as varchar(4) collate UTF8_LCASE)) AS 
coalesce(CAST(abcdef AS VARCHAR(2) COLLATE UTF8_LCASE), CAST(x AS VARCHAR(4) 
COLLATE UTF8_LCASE))#x]
++- OneRowRelation
+
+
+-- !query
+SELECT coalesce(try_cast(12345 AS VARCHAR(4)), CAST('x' AS VARCHAR(5)))
+-- !query analysis
+Project [coalesce(cast(try_cast(12345 as varchar(4)) as varchar(5)), cast(x as 
varchar(5))) AS coalesce(TRY_CAST(12345 AS VARCHAR(4)), CAST(x AS 
VARCHAR(5)))#x]
++- OneRowRelation
+
+
+-- !query
+SELECT coalesce(CAST(12345 AS VARCHAR(4)), CAST('x' AS VARCHAR(5)))
+-- !query analysis
+Project [coalesce(cast(cast(12345 as varchar(4)) as varchar(5)), cast(x as 
varchar(5))) AS coalesce(CAST(12345 AS VARCHAR(4)), CAST(x AS VARCHAR(5)))#x]
++- OneRowRelation
+
+
 -- !query
 SELECT typeof(coalesce(cast('hello' AS VARCHAR(5)), cast('world' AS 
VARCHAR(10))))
 -- !query analysis
@@ -322,6 +387,48 @@ Project [typeof(coalesce(cast(a as char(2) collate 
UTF8_LCASE), cast(bb as char(
 +- OneRowRelation
 
 
+-- !query
+SELECT typeof(coalesce(
+  cast('a' AS CHAR(2) COLLATE UTF8_LCASE), cast('bb' AS CHAR(4) COLLATE 
UTF8_LCASE)))
+-- !query analysis
+Project [typeof(coalesce(cast(cast(a as char(2) collate UTF8_LCASE) as char(4) 
collate UTF8_LCASE), cast(bb as char(4) collate UTF8_LCASE))) AS 
typeof(coalesce(CAST(a AS CHAR(2) COLLATE UTF8_LCASE), CAST(bb AS CHAR(4) 
COLLATE UTF8_LCASE)))#x]
++- OneRowRelation
+
+
+-- !query
+SELECT hex(coalesce(
+  cast('a' AS CHAR(2) COLLATE UTF8_LCASE), cast('bb' AS CHAR(4) COLLATE 
UTF8_LCASE)))
+-- !query analysis
+Project [hex(cast(coalesce(cast(cast(a as char(2) collate UTF8_LCASE) as 
char(4) collate UTF8_LCASE), cast(bb as char(4) collate UTF8_LCASE)) as string 
collate UTF8_LCASE)) AS hex(coalesce(CAST(a AS CHAR(2) COLLATE UTF8_LCASE), 
CAST(bb AS CHAR(4) COLLATE UTF8_LCASE)))#x]
++- OneRowRelation
+
+
+-- !query
+SELECT typeof(coalesce(
+  cast('a' AS CHAR(2) COLLATE UTF8_LCASE), cast('bb' AS VARCHAR(4) COLLATE 
UTF8_LCASE)))
+-- !query analysis
+Project [typeof(coalesce(cast(cast(a as char(2) collate UTF8_LCASE) as 
varchar(4) collate UTF8_LCASE), cast(bb as varchar(4) collate UTF8_LCASE))) AS 
typeof(coalesce(CAST(a AS CHAR(2) COLLATE UTF8_LCASE), CAST(bb AS VARCHAR(4) 
COLLATE UTF8_LCASE)))#x]
++- OneRowRelation
+
+
+-- !query
+SELECT typeof(coalesce(
+  cast('a' AS CHAR(2) COLLATE UTF8_LCASE),
+  cast(1 AS CHAR(4) COLLATE UTF8_LCASE)))
+-- !query analysis
+Project [typeof(coalesce(cast(cast(a as char(2) collate UTF8_LCASE) as char(4) 
collate UTF8_LCASE), cast(1 as char(4) collate UTF8_LCASE))) AS 
typeof(coalesce(CAST(a AS CHAR(2) COLLATE UTF8_LCASE), CAST(1 AS CHAR(4) 
COLLATE UTF8_LCASE)))#x]
++- OneRowRelation
+
+
+-- !query
+SELECT hex(coalesce(
+  cast('a' AS CHAR(2) COLLATE UTF8_LCASE),
+  cast(1 AS CHAR(4) COLLATE UTF8_LCASE)))
+-- !query analysis
+Project [hex(cast(coalesce(cast(cast(a as char(2) collate UTF8_LCASE) as 
char(4) collate UTF8_LCASE), cast(1 as char(4) collate UTF8_LCASE)) as string 
collate UTF8_LCASE)) AS hex(coalesce(CAST(a AS CHAR(2) COLLATE UTF8_LCASE), 
CAST(1 AS CHAR(4) COLLATE UTF8_LCASE)))#x]
++- OneRowRelation
+
+
 -- !query
 SELECT typeof(c) FROM (
   SELECT cast('a' AS VARCHAR(3)) AS c
@@ -341,6 +448,276 @@ GlobalLimit 1
                +- OneRowRelation
 
 
+-- !query
+SELECT typeof(c) FROM (
+  SELECT cast('a' AS CHAR(2)) AS c
+  UNION ALL
+  SELECT cast('bb' AS CHAR(4)) AS c
+) t LIMIT 1
+-- !query analysis
+GlobalLimit 1
++- LocalLimit 1
+   +- Project [typeof(c#x) AS typeof(c)#x]
+      +- SubqueryAlias t
+         +- Union false, false
+            :- Project [cast(c#x as char(4)) AS c#x]
+            :  +- Project [cast(a as char(2)) AS c#x]
+            :     +- OneRowRelation
+            +- Project [cast(bb as char(4)) AS c#x]
+               +- OneRowRelation
+
+
+-- !query
+SELECT concat('<', c, '>') FROM (
+  SELECT cast('a' AS CHAR(2)) AS c
+  UNION ALL
+  SELECT cast('bb' AS CHAR(4)) AS c
+) t
+-- !query analysis
+Project [concat(<, cast(c#x as string), >) AS concat(<, c, >)#x]
++- SubqueryAlias t
+   +- Union false, false
+      :- Project [cast(c#x as char(4)) AS c#x]
+      :  +- Project [cast(a as char(2)) AS c#x]
+      :     +- OneRowRelation
+      +- Project [cast(bb as char(4)) AS c#x]
+         +- OneRowRelation
+
+
+-- !query
+SELECT typeof(c) FROM (
+  SELECT cast('a' AS CHAR(2)) AS c
+  UNION
+  SELECT cast('a' AS CHAR(4)) AS c
+) t
+-- !query analysis
+Project [typeof(c#x) AS typeof(c)#x]
++- SubqueryAlias t
+   +- Distinct
+      +- Union false, false
+         :- Project [cast(c#x as char(4)) AS c#x]
+         :  +- Project [cast(a as char(2)) AS c#x]
+         :     +- OneRowRelation
+         +- Project [cast(a as char(4)) AS c#x]
+            +- OneRowRelation
+
+
+-- !query
+SELECT concat('<', c, '>') FROM (
+  SELECT cast('a' AS CHAR(2)) AS c
+  UNION
+  SELECT cast('a' AS CHAR(4)) AS c
+) t
+-- !query analysis
+Project [concat(<, cast(c#x as string), >) AS concat(<, c, >)#x]
++- SubqueryAlias t
+   +- Distinct
+      +- Union false, false
+         :- Project [cast(c#x as char(4)) AS c#x]
+         :  +- Project [cast(a as char(2)) AS c#x]
+         :     +- OneRowRelation
+         +- Project [cast(a as char(4)) AS c#x]
+            +- OneRowRelation
+
+
+-- !query
+SELECT typeof(c) FROM (
+  SELECT cast('ab' AS CHAR(2)) AS c
+  INTERSECT
+  SELECT cast('ab' AS CHAR(4)) AS c
+) t
+-- !query analysis
+Project [typeof(c#x) AS typeof(c)#x]
++- SubqueryAlias t
+   +- Intersect false
+      :- Project [cast(c#x as char(4)) AS c#x]
+      :  +- Project [cast(ab as char(2)) AS c#x]
+      :     +- OneRowRelation
+      +- Project [cast(ab as char(4)) AS c#x]
+         +- OneRowRelation
+
+
+-- !query
+SELECT concat('<', c, '>') FROM (
+  SELECT cast('ab' AS CHAR(2)) AS c
+  INTERSECT
+  SELECT cast('ab' AS CHAR(4)) AS c
+) t
+-- !query analysis
+Project [concat(<, cast(c#x as string), >) AS concat(<, c, >)#x]
++- SubqueryAlias t
+   +- Intersect false
+      :- Project [cast(c#x as char(4)) AS c#x]
+      :  +- Project [cast(ab as char(2)) AS c#x]
+      :     +- OneRowRelation
+      +- Project [cast(ab as char(4)) AS c#x]
+         +- OneRowRelation
+
+
+-- !query
+SELECT typeof(c) FROM (
+  SELECT cast('ab' AS CHAR(2)) AS c
+  EXCEPT
+  SELECT cast('xy' AS CHAR(4)) AS c
+) t
+-- !query analysis
+Project [typeof(c#x) AS typeof(c)#x]
++- SubqueryAlias t
+   +- Except false
+      :- Project [cast(c#x as char(4)) AS c#x]
+      :  +- Project [cast(ab as char(2)) AS c#x]
+      :     +- OneRowRelation
+      +- Project [cast(xy as char(4)) AS c#x]
+         +- OneRowRelation
+
+
+-- !query
+SELECT concat('<', c, '>') FROM (
+  SELECT cast('ab' AS CHAR(2)) AS c
+  EXCEPT
+  SELECT cast('xy' AS CHAR(4)) AS c
+) t
+-- !query analysis
+Project [concat(<, cast(c#x as string), >) AS concat(<, c, >)#x]
++- SubqueryAlias t
+   +- Except false
+      :- Project [cast(c#x as char(4)) AS c#x]
+      :  +- Project [cast(ab as char(2)) AS c#x]
+      :     +- OneRowRelation
+      +- Project [cast(xy as char(4)) AS c#x]
+         +- OneRowRelation
+
+
+-- !query
+SELECT typeof(c) FROM (VALUES
+  (cast('a' AS CHAR(2))),
+  (cast('bb' AS CHAR(4)))
+) t(c)
+-- !query analysis
+Project [typeof(c#x) AS typeof(c)#x]
++- SubqueryAlias t
+   +- Project [col1#x AS c#x]
+      +- LocalRelation [col1#x]
+
+
+-- !query
+SELECT concat('<', c, '>') FROM (VALUES
+  (cast('a' AS CHAR(2))),
+  (cast('bb' AS CHAR(4)))
+) t(c)
+-- !query analysis
+Project [concat(<, cast(c#x as string), >) AS concat(<, c, >)#x]
++- SubqueryAlias t
+   +- Project [col1#x AS c#x]
+      +- LocalRelation [col1#x]
+
+
+-- !query
+SELECT cast('a' AS CHAR(2)) = cast('a' AS CHAR(4))
+-- !query analysis
+Project [(cast(cast(a as char(2)) as char(4)) = cast(a as char(4))) AS (CAST(a 
AS CHAR(2)) = CAST(a AS CHAR(4)))#x]
++- OneRowRelation
+
+
+-- !query
+SELECT cast('a' AS CHAR(2)) = cast('a' AS VARCHAR(2))
+-- !query analysis
+Project [(cast(cast(a as char(2)) as varchar(2)) = cast(a as varchar(2))) AS 
(CAST(a AS CHAR(2)) = CAST(a AS VARCHAR(2)))#x]
++- OneRowRelation
+
+
+-- !query
+SELECT cast('a' AS CHAR(2)) = cast('a ' AS VARCHAR(2))
+-- !query analysis
+Project [(cast(cast(a as char(2)) as varchar(2)) = cast(a  as varchar(2))) AS 
(CAST(a AS CHAR(2)) = CAST(a  AS VARCHAR(2)))#x]
++- OneRowRelation
+
+
+-- !query
+SELECT cast('a' AS CHAR(2)) = 'a'
+-- !query analysis
+Project [(cast(cast(a as char(2)) as string collate UTF8_BINARY) = a) AS 
(CAST(a AS CHAR(2)) = a)#x]
++- OneRowRelation
+
+
+-- !query
+SELECT cast('a' AS CHAR(2)) = 'a '
+-- !query analysis
+Project [(cast(cast(a as char(2)) as string collate UTF8_BINARY) = a ) AS 
(CAST(a AS CHAR(2)) = a )#x]
++- OneRowRelation
+
+
+-- !query
+SELECT cast('a' AS CHAR(2) COLLATE UTF8_BINARY_RTRIM) = 'a'
+-- !query analysis
+Project [(cast(a as char(2) collate UTF8_BINARY_RTRIM) = cast(a as char(2) 
collate UTF8_BINARY_RTRIM)) AS (CAST(a AS CHAR(2) COLLATE UTF8_BINARY_RTRIM) = 
a)#x]
++- OneRowRelation
+
+
+-- !query
+SELECT cast('a' AS CHAR(2) COLLATE UTF8_BINARY_RTRIM) =
+  cast('a' AS CHAR(4) COLLATE UTF8_BINARY_RTRIM)
+-- !query analysis
+Project [(cast(cast(a as char(2) collate UTF8_BINARY_RTRIM) as char(4) collate 
UTF8_BINARY_RTRIM) = cast(a as char(4) collate UTF8_BINARY_RTRIM)) AS (CAST(a 
AS CHAR(2) COLLATE UTF8_BINARY_RTRIM) = CAST(a AS CHAR(4) COLLATE 
UTF8_BINARY_RTRIM))#x]
++- OneRowRelation
+
+
+-- !query
+SELECT cast('a' AS CHAR(2)) IN (cast('a' AS CHAR(4)))
+-- !query analysis
+Project [cast(cast(a as char(2)) as char(4)) IN (cast(cast(a as char(4)) as 
char(4))) AS (CAST(a AS CHAR(2)) IN (CAST(a AS CHAR(4))))#x]
++- OneRowRelation
+
+
+-- !query
+SELECT cast('a' AS CHAR(2)) IN (cast('a' AS VARCHAR(2)))
+-- !query analysis
+Project [cast(cast(a as char(2)) as varchar(2)) IN (cast(cast(a as varchar(2)) 
as varchar(2))) AS (CAST(a AS CHAR(2)) IN (CAST(a AS VARCHAR(2))))#x]
++- OneRowRelation
+
+
+-- !query
+SELECT cast('a' AS CHAR(2)) IN (cast('a ' AS VARCHAR(2)))
+-- !query analysis
+Project [cast(cast(a as char(2)) as varchar(2)) IN (cast(cast(a  as 
varchar(2)) as varchar(2))) AS (CAST(a AS CHAR(2)) IN (CAST(a  AS 
VARCHAR(2))))#x]
++- OneRowRelation
+
+
+-- !query
+SELECT cast('a' AS CHAR(2)) IN ('a', 'b')
+-- !query analysis
+Project [cast(cast(a as char(2)) as string collate UTF8_BINARY) IN (cast(a as 
string collate UTF8_BINARY),cast(b as string collate UTF8_BINARY)) AS (CAST(a 
AS CHAR(2)) IN (a, b))#x]
++- OneRowRelation
+
+
+-- !query
+SELECT cast('a' AS CHAR(2)) IN ('a ', 'b')
+-- !query analysis
+Project [cast(cast(a as char(2)) as string collate UTF8_BINARY) IN (cast(a  as 
string collate UTF8_BINARY),cast(b as string collate UTF8_BINARY)) AS (CAST(a 
AS CHAR(2)) IN (a , b))#x]
++- OneRowRelation
+
+
+-- !query
+SELECT cast('a' AS CHAR(2)) IN (cast('a' AS CHAR(4)), cast('b' AS VARCHAR(3)))
+-- !query analysis
+Project [cast(cast(a as char(2)) as varchar(4)) IN (cast(cast(a as char(4)) as 
varchar(4)),cast(cast(b as varchar(3)) as varchar(4))) AS (CAST(a AS CHAR(2)) 
IN (CAST(a AS CHAR(4)), CAST(b AS VARCHAR(3))))#x]
++- OneRowRelation
+
+
+-- !query
+SELECT cast('a' AS CHAR(2) COLLATE UTF8_LCASE) = cast('a' AS VARCHAR(2) 
COLLATE UTF8_LCASE)
+-- !query analysis
+Project [(cast(cast(a as char(2) collate UTF8_LCASE) as varchar(2) collate 
UTF8_LCASE) = cast(a as varchar(2) collate UTF8_LCASE)) AS (CAST(a AS CHAR(2) 
COLLATE UTF8_LCASE) = CAST(a AS VARCHAR(2) COLLATE UTF8_LCASE))#x]
++- OneRowRelation
+
+
+-- !query
+SELECT cast('a' AS CHAR(2) COLLATE UTF8_LCASE) IN (cast('a' AS VARCHAR(2) 
COLLATE UTF8_LCASE))
+-- !query analysis
+Project [cast(cast(a as char(2) collate UTF8_LCASE) as varchar(2) collate 
UTF8_LCASE) IN (cast(a as varchar(2) collate UTF8_LCASE)) AS (CAST(a AS CHAR(2) 
COLLATE UTF8_LCASE) IN (CAST(a AS VARCHAR(2) COLLATE UTF8_LCASE)))#x]
++- OneRowRelation
+
+
 -- !query
 SELECT typeof(array(cast('a' AS CHAR(2)), cast('bb' AS CHAR(3))))
 -- !query analysis
diff --git 
a/sql/core/src/test/resources/sql-tests/inputs/charvarchar-standard-semantics.sql
 
b/sql/core/src/test/resources/sql-tests/inputs/charvarchar-standard-semantics.sql
index 6d591bc7e8cf..25a52737bcb2 100644
--- 
a/sql/core/src/test/resources/sql-tests/inputs/charvarchar-standard-semantics.sql
+++ 
b/sql/core/src/test/resources/sql-tests/inputs/charvarchar-standard-semantics.sql
@@ -5,12 +5,25 @@ SELECT typeof(CAST('ab' AS CHAR(5)));
 SELECT typeof(CAST('hello' AS VARCHAR(5)));
 SELECT 'X' || CAST('5' AS CHAR(5)) || 'X';
 
--- CAST length enforcement: trailing spaces are trimmed, real overflow errors
+-- CAST length: character-to-character truncates (ISO 6.13); numeric overflow 
errors
 SELECT CAST('ab   ' AS CHAR(2));
 SELECT CAST('abcdef' AS CHAR(2));
 SELECT CAST('abcdef' AS VARCHAR(2));
 SELECT try_cast('abcdef' AS CHAR(2));
 SELECT try_cast('abcdef' AS VARCHAR(2));
+SELECT CAST(12345 AS VARCHAR(4));
+SELECT CAST(12345 AS VARCHAR(5));
+SELECT try_cast(12345 AS VARCHAR(4));
+
+-- Explicit CAST inside LCT must keep inner truncation / overflow (do not 
retarget).
+SELECT coalesce(CAST('abcdef' AS VARCHAR(2)), CAST('x' AS VARCHAR(4)));
+SELECT CASE WHEN true THEN CAST('abcdef' AS VARCHAR(2)) ELSE CAST('x' AS 
VARCHAR(4)) END;
+SELECT CAST('abcdef' AS VARCHAR(2)) IN (CAST('ab' AS VARCHAR(4)));
+SELECT coalesce(
+  CAST('abcdef' AS VARCHAR(2) COLLATE UTF8_LCASE),
+  CAST('x' AS VARCHAR(4) COLLATE UTF8_LCASE));
+SELECT coalesce(try_cast(12345 AS VARCHAR(4)), CAST('x' AS VARCHAR(5)));
+SELECT coalesce(CAST(12345 AS VARCHAR(4)), CAST('x' AS VARCHAR(5)));
 
 -- R2: least common type (COALESCE / CASE)
 SELECT typeof(coalesce(cast('hello' AS VARCHAR(5)), cast('world' AS 
VARCHAR(10))));
@@ -66,22 +79,108 @@ SELECT typeof(reverse(array(1, 2)));
 SELECT typeof(str_to_map(cast('a:1,b:2' AS CHAR(7))));
 SELECT typeof(c0) FROM (SELECT json_tuple(cast('{"a":"1"}' AS CHAR(9)), 'a') 
AS c0);
 
--- R2 with collation. A declared collation survives the CAST and an LCT over 
equally constrained
--- operands. The mixed-length case (CHAR(2) with CHAR(4), same collation) is 
deliberately not
--- covered here: CollationTypeCoercion reads the differing lengths as a 
collation mismatch and
--- yields an indeterminate collation. That predates this change (it reproduces 
under
--- spark.sql.preserveCharVarcharTypeInfo) and is tracked separately, so 
goldening it would
--- normalize the bug.
+-- Collation survives CAST and LCT. Mixed lengths with the same collation 
widen to max(n, m);
+-- they must not collapse to an indeterminate collation.
 SELECT typeof(cast('a' AS CHAR(2) COLLATE UTF8_LCASE));
 SELECT typeof(coalesce(
   cast('a' AS CHAR(2) COLLATE UTF8_LCASE), cast('bb' AS CHAR(2) COLLATE 
UTF8_LCASE)));
+SELECT typeof(coalesce(
+  cast('a' AS CHAR(2) COLLATE UTF8_LCASE), cast('bb' AS CHAR(4) COLLATE 
UTF8_LCASE)));
+SELECT hex(coalesce(
+  cast('a' AS CHAR(2) COLLATE UTF8_LCASE), cast('bb' AS CHAR(4) COLLATE 
UTF8_LCASE)));
+SELECT typeof(coalesce(
+  cast('a' AS CHAR(2) COLLATE UTF8_LCASE), cast('bb' AS VARCHAR(4) COLLATE 
UTF8_LCASE)));
+-- Mixed strength, same collation: Implicit string CAST CHAR(2) vs Default
+-- non-string CAST CHAR(4). Length still widens to max(n, m); the COLLATE
+-- operator itself is STRING, so it is not used here.
+SELECT typeof(coalesce(
+  cast('a' AS CHAR(2) COLLATE UTF8_LCASE),
+  cast(1 AS CHAR(4) COLLATE UTF8_LCASE)));
+SELECT hex(coalesce(
+  cast('a' AS CHAR(2) COLLATE UTF8_LCASE),
+  cast(1 AS CHAR(4) COLLATE UTF8_LCASE)));
 
--- UNION LCT
+-- Set operations and multi-row VALUES share the same LCT as COALESCE.
 SELECT typeof(c) FROM (
   SELECT cast('a' AS VARCHAR(3)) AS c
   UNION ALL
   SELECT cast('abcd' AS VARCHAR(8)) AS c
 ) t LIMIT 1;
+SELECT typeof(c) FROM (
+  SELECT cast('a' AS CHAR(2)) AS c
+  UNION ALL
+  SELECT cast('bb' AS CHAR(4)) AS c
+) t LIMIT 1;
+SELECT concat('<', c, '>') FROM (
+  SELECT cast('a' AS CHAR(2)) AS c
+  UNION ALL
+  SELECT cast('bb' AS CHAR(4)) AS c
+) t;
+SELECT typeof(c) FROM (
+  SELECT cast('a' AS CHAR(2)) AS c
+  UNION
+  SELECT cast('a' AS CHAR(4)) AS c
+) t;
+SELECT concat('<', c, '>') FROM (
+  SELECT cast('a' AS CHAR(2)) AS c
+  UNION
+  SELECT cast('a' AS CHAR(4)) AS c
+) t;
+SELECT typeof(c) FROM (
+  SELECT cast('ab' AS CHAR(2)) AS c
+  INTERSECT
+  SELECT cast('ab' AS CHAR(4)) AS c
+) t;
+SELECT concat('<', c, '>') FROM (
+  SELECT cast('ab' AS CHAR(2)) AS c
+  INTERSECT
+  SELECT cast('ab' AS CHAR(4)) AS c
+) t;
+-- Non-empty EXCEPT: after widening, 'ab  ' is not 'xy  '.
+SELECT typeof(c) FROM (
+  SELECT cast('ab' AS CHAR(2)) AS c
+  EXCEPT
+  SELECT cast('xy' AS CHAR(4)) AS c
+) t;
+SELECT concat('<', c, '>') FROM (
+  SELECT cast('ab' AS CHAR(2)) AS c
+  EXCEPT
+  SELECT cast('xy' AS CHAR(4)) AS c
+) t;
+SELECT typeof(c) FROM (VALUES
+  (cast('a' AS CHAR(2))),
+  (cast('bb' AS CHAR(4)))
+) t(c);
+SELECT concat('<', c, '>') FROM (VALUES
+  (cast('a' AS CHAR(2))),
+  (cast('bb' AS CHAR(4)))
+) t(c);
+
+-- Comparison and IN: both sides (including the IN left-hand side) are cast to 
the LCT of all
+-- participants. Casting to CHAR pads, so CHAR vs CHAR of different lengths 
compares equal after
+-- widening; casting to VARCHAR/STRING keeps the CHAR pad, so CHAR 'a' (stored 
as 'a ') is not equal
+-- to VARCHAR/STRING 'a' unless the other side carries the same trailing 
blank. Trailing-blank
+-- ignoring is a collation concern (RTRIM), not a type-level PAD SPACE policy.
+SELECT cast('a' AS CHAR(2)) = cast('a' AS CHAR(4));
+SELECT cast('a' AS CHAR(2)) = cast('a' AS VARCHAR(2));
+SELECT cast('a' AS CHAR(2)) = cast('a ' AS VARCHAR(2));
+SELECT cast('a' AS CHAR(2)) = 'a';
+SELECT cast('a' AS CHAR(2)) = 'a ';
+SELECT cast('a' AS CHAR(2) COLLATE UTF8_BINARY_RTRIM) = 'a';
+SELECT cast('a' AS CHAR(2) COLLATE UTF8_BINARY_RTRIM) =
+  cast('a' AS CHAR(4) COLLATE UTF8_BINARY_RTRIM);
+SELECT cast('a' AS CHAR(2)) IN (cast('a' AS CHAR(4)));
+SELECT cast('a' AS CHAR(2)) IN (cast('a' AS VARCHAR(2)));
+SELECT cast('a' AS CHAR(2)) IN (cast('a ' AS VARCHAR(2)));
+SELECT cast('a' AS CHAR(2)) IN ('a', 'b');
+SELECT cast('a' AS CHAR(2)) IN ('a ', 'b');
+-- Three-part IN: LHS CHAR(2) and list CHAR(4)/VARCHAR(3) all widen to 
VARCHAR(4).
+SELECT cast('a' AS CHAR(2)) IN (cast('a' AS CHAR(4)), cast('b' AS VARCHAR(3)));
+-- Same CHAR-pad rule under a non-RTRIM collation: UTF8_LCASE must not make
+-- CHAR 'a' equal VARCHAR 'a'. The analyzer must nest CHAR then VARCHAR, not
+-- retarget CAST('a' AS CHAR(2) COLLATE UTF8_LCASE) to VARCHAR(2).
+SELECT cast('a' AS CHAR(2) COLLATE UTF8_LCASE) = cast('a' AS VARCHAR(2) 
COLLATE UTF8_LCASE);
+SELECT cast('a' AS CHAR(2) COLLATE UTF8_LCASE) IN (cast('a' AS VARCHAR(2) 
COLLATE UTF8_LCASE));
 
 -- Nested types keep CHAR/VARCHAR
 SELECT typeof(array(cast('a' AS CHAR(2)), cast('bb' AS CHAR(3))));
diff --git 
a/sql/core/src/test/resources/sql-tests/results/charvarchar-standard-semantics.sql.out
 
b/sql/core/src/test/resources/sql-tests/results/charvarchar-standard-semantics.sql.out
index 1830a80cac51..168b5ef173be 100644
--- 
a/sql/core/src/test/resources/sql-tests/results/charvarchar-standard-semantics.sql.out
+++ 
b/sql/core/src/test/resources/sql-tests/results/charvarchar-standard-semantics.sql.out
@@ -34,21 +34,38 @@ ab
 -- !query
 SELECT CAST('abcdef' AS CHAR(2))
 -- !query schema
-struct<>
+struct<CAST(abcdef AS CHAR(2)):char(2)>
 -- !query output
-org.apache.spark.SparkRuntimeException
-{
-  "errorClass" : "EXCEED_LIMIT_LENGTH",
-  "sqlState" : "54006",
-  "messageParameters" : {
-    "limit" : "2"
-  }
-}
+ab
 
 
 -- !query
 SELECT CAST('abcdef' AS VARCHAR(2))
 -- !query schema
+struct<CAST(abcdef AS VARCHAR(2)):varchar(2)>
+-- !query output
+ab
+
+
+-- !query
+SELECT try_cast('abcdef' AS CHAR(2))
+-- !query schema
+struct<TRY_CAST(abcdef AS CHAR(2)):char(2)>
+-- !query output
+ab
+
+
+-- !query
+SELECT try_cast('abcdef' AS VARCHAR(2))
+-- !query schema
+struct<TRY_CAST(abcdef AS VARCHAR(2)):varchar(2)>
+-- !query output
+ab
+
+
+-- !query
+SELECT CAST(12345 AS VARCHAR(4))
+-- !query schema
 struct<>
 -- !query output
 org.apache.spark.SparkRuntimeException
@@ -56,27 +73,84 @@ org.apache.spark.SparkRuntimeException
   "errorClass" : "EXCEED_LIMIT_LENGTH",
   "sqlState" : "54006",
   "messageParameters" : {
-    "limit" : "2"
+    "limit" : "4"
   }
 }
 
 
 -- !query
-SELECT try_cast('abcdef' AS CHAR(2))
+SELECT CAST(12345 AS VARCHAR(5))
 -- !query schema
-struct<TRY_CAST(abcdef AS CHAR(2)):char(2)>
+struct<CAST(12345 AS VARCHAR(5)):varchar(5)>
 -- !query output
-NULL
+12345
 
 
 -- !query
-SELECT try_cast('abcdef' AS VARCHAR(2))
+SELECT try_cast(12345 AS VARCHAR(4))
 -- !query schema
-struct<TRY_CAST(abcdef AS VARCHAR(2)):varchar(2)>
+struct<TRY_CAST(12345 AS VARCHAR(4)):varchar(4)>
 -- !query output
 NULL
 
 
+-- !query
+SELECT coalesce(CAST('abcdef' AS VARCHAR(2)), CAST('x' AS VARCHAR(4)))
+-- !query schema
+struct<coalesce(CAST(abcdef AS VARCHAR(2)), CAST(x AS VARCHAR(4))):varchar(4)>
+-- !query output
+ab
+
+
+-- !query
+SELECT CASE WHEN true THEN CAST('abcdef' AS VARCHAR(2)) ELSE CAST('x' AS 
VARCHAR(4)) END
+-- !query schema
+struct<CASE WHEN true THEN CAST(abcdef AS VARCHAR(2)) ELSE CAST(x AS 
VARCHAR(4)) END:varchar(4)>
+-- !query output
+ab
+
+
+-- !query
+SELECT CAST('abcdef' AS VARCHAR(2)) IN (CAST('ab' AS VARCHAR(4)))
+-- !query schema
+struct<(CAST(abcdef AS VARCHAR(2)) IN (CAST(ab AS VARCHAR(4)))):boolean>
+-- !query output
+true
+
+
+-- !query
+SELECT coalesce(
+  CAST('abcdef' AS VARCHAR(2) COLLATE UTF8_LCASE),
+  CAST('x' AS VARCHAR(4) COLLATE UTF8_LCASE))
+-- !query schema
+struct<coalesce(CAST(abcdef AS VARCHAR(2) COLLATE UTF8_LCASE), CAST(x AS 
VARCHAR(4) COLLATE UTF8_LCASE)):varchar(4) collate UTF8_LCASE>
+-- !query output
+ab
+
+
+-- !query
+SELECT coalesce(try_cast(12345 AS VARCHAR(4)), CAST('x' AS VARCHAR(5)))
+-- !query schema
+struct<coalesce(TRY_CAST(12345 AS VARCHAR(4)), CAST(x AS 
VARCHAR(5))):varchar(5)>
+-- !query output
+x
+
+
+-- !query
+SELECT coalesce(CAST(12345 AS VARCHAR(4)), CAST('x' AS VARCHAR(5)))
+-- !query schema
+struct<>
+-- !query output
+org.apache.spark.SparkRuntimeException
+{
+  "errorClass" : "EXCEED_LIMIT_LENGTH",
+  "sqlState" : "54006",
+  "messageParameters" : {
+    "limit" : "4"
+  }
+}
+
+
 -- !query
 SELECT typeof(coalesce(cast('hello' AS VARCHAR(5)), cast('world' AS 
VARCHAR(10))))
 -- !query schema
@@ -375,6 +449,53 @@ struct<typeof(coalesce(CAST(a AS CHAR(2) COLLATE 
UTF8_LCASE), CAST(bb AS CHAR(2)
 char(2) collate UTF8_LCASE
 
 
+-- !query
+SELECT typeof(coalesce(
+  cast('a' AS CHAR(2) COLLATE UTF8_LCASE), cast('bb' AS CHAR(4) COLLATE 
UTF8_LCASE)))
+-- !query schema
+struct<typeof(coalesce(CAST(a AS CHAR(2) COLLATE UTF8_LCASE), CAST(bb AS 
CHAR(4) COLLATE UTF8_LCASE))):string>
+-- !query output
+char(4) collate UTF8_LCASE
+
+
+-- !query
+SELECT hex(coalesce(
+  cast('a' AS CHAR(2) COLLATE UTF8_LCASE), cast('bb' AS CHAR(4) COLLATE 
UTF8_LCASE)))
+-- !query schema
+struct<hex(coalesce(CAST(a AS CHAR(2) COLLATE UTF8_LCASE), CAST(bb AS CHAR(4) 
COLLATE UTF8_LCASE))):string collate UTF8_LCASE>
+-- !query output
+61202020
+
+
+-- !query
+SELECT typeof(coalesce(
+  cast('a' AS CHAR(2) COLLATE UTF8_LCASE), cast('bb' AS VARCHAR(4) COLLATE 
UTF8_LCASE)))
+-- !query schema
+struct<typeof(coalesce(CAST(a AS CHAR(2) COLLATE UTF8_LCASE), CAST(bb AS 
VARCHAR(4) COLLATE UTF8_LCASE))):string>
+-- !query output
+varchar(4) collate UTF8_LCASE
+
+
+-- !query
+SELECT typeof(coalesce(
+  cast('a' AS CHAR(2) COLLATE UTF8_LCASE),
+  cast(1 AS CHAR(4) COLLATE UTF8_LCASE)))
+-- !query schema
+struct<typeof(coalesce(CAST(a AS CHAR(2) COLLATE UTF8_LCASE), CAST(1 AS 
CHAR(4) COLLATE UTF8_LCASE))):string>
+-- !query output
+char(4) collate UTF8_LCASE
+
+
+-- !query
+SELECT hex(coalesce(
+  cast('a' AS CHAR(2) COLLATE UTF8_LCASE),
+  cast(1 AS CHAR(4) COLLATE UTF8_LCASE)))
+-- !query schema
+struct<hex(coalesce(CAST(a AS CHAR(2) COLLATE UTF8_LCASE), CAST(1 AS CHAR(4) 
COLLATE UTF8_LCASE))):string collate UTF8_LCASE>
+-- !query output
+61202020
+
+
 -- !query
 SELECT typeof(c) FROM (
   SELECT cast('a' AS VARCHAR(3)) AS c
@@ -387,6 +508,248 @@ struct<typeof(c):string>
 varchar(8)
 
 
+-- !query
+SELECT typeof(c) FROM (
+  SELECT cast('a' AS CHAR(2)) AS c
+  UNION ALL
+  SELECT cast('bb' AS CHAR(4)) AS c
+) t LIMIT 1
+-- !query schema
+struct<typeof(c):string>
+-- !query output
+char(4)
+
+
+-- !query
+SELECT concat('<', c, '>') FROM (
+  SELECT cast('a' AS CHAR(2)) AS c
+  UNION ALL
+  SELECT cast('bb' AS CHAR(4)) AS c
+) t
+-- !query schema
+struct<concat(<, c, >):string>
+-- !query output
+<a   >
+<bb  >
+
+
+-- !query
+SELECT typeof(c) FROM (
+  SELECT cast('a' AS CHAR(2)) AS c
+  UNION
+  SELECT cast('a' AS CHAR(4)) AS c
+) t
+-- !query schema
+struct<typeof(c):string>
+-- !query output
+char(4)
+
+
+-- !query
+SELECT concat('<', c, '>') FROM (
+  SELECT cast('a' AS CHAR(2)) AS c
+  UNION
+  SELECT cast('a' AS CHAR(4)) AS c
+) t
+-- !query schema
+struct<concat(<, c, >):string>
+-- !query output
+<a   >
+
+
+-- !query
+SELECT typeof(c) FROM (
+  SELECT cast('ab' AS CHAR(2)) AS c
+  INTERSECT
+  SELECT cast('ab' AS CHAR(4)) AS c
+) t
+-- !query schema
+struct<typeof(c):string>
+-- !query output
+char(4)
+
+
+-- !query
+SELECT concat('<', c, '>') FROM (
+  SELECT cast('ab' AS CHAR(2)) AS c
+  INTERSECT
+  SELECT cast('ab' AS CHAR(4)) AS c
+) t
+-- !query schema
+struct<concat(<, c, >):string>
+-- !query output
+<ab  >
+
+
+-- !query
+SELECT typeof(c) FROM (
+  SELECT cast('ab' AS CHAR(2)) AS c
+  EXCEPT
+  SELECT cast('xy' AS CHAR(4)) AS c
+) t
+-- !query schema
+struct<typeof(c):string>
+-- !query output
+char(4)
+
+
+-- !query
+SELECT concat('<', c, '>') FROM (
+  SELECT cast('ab' AS CHAR(2)) AS c
+  EXCEPT
+  SELECT cast('xy' AS CHAR(4)) AS c
+) t
+-- !query schema
+struct<concat(<, c, >):string>
+-- !query output
+<ab  >
+
+
+-- !query
+SELECT typeof(c) FROM (VALUES
+  (cast('a' AS CHAR(2))),
+  (cast('bb' AS CHAR(4)))
+) t(c)
+-- !query schema
+struct<typeof(c):string>
+-- !query output
+char(4)
+char(4)
+
+
+-- !query
+SELECT concat('<', c, '>') FROM (VALUES
+  (cast('a' AS CHAR(2))),
+  (cast('bb' AS CHAR(4)))
+) t(c)
+-- !query schema
+struct<concat(<, c, >):string>
+-- !query output
+<a   >
+<bb  >
+
+
+-- !query
+SELECT cast('a' AS CHAR(2)) = cast('a' AS CHAR(4))
+-- !query schema
+struct<(CAST(a AS CHAR(2)) = CAST(a AS CHAR(4))):boolean>
+-- !query output
+true
+
+
+-- !query
+SELECT cast('a' AS CHAR(2)) = cast('a' AS VARCHAR(2))
+-- !query schema
+struct<(CAST(a AS CHAR(2)) = CAST(a AS VARCHAR(2))):boolean>
+-- !query output
+false
+
+
+-- !query
+SELECT cast('a' AS CHAR(2)) = cast('a ' AS VARCHAR(2))
+-- !query schema
+struct<(CAST(a AS CHAR(2)) = CAST(a  AS VARCHAR(2))):boolean>
+-- !query output
+true
+
+
+-- !query
+SELECT cast('a' AS CHAR(2)) = 'a'
+-- !query schema
+struct<(CAST(a AS CHAR(2)) = a):boolean>
+-- !query output
+false
+
+
+-- !query
+SELECT cast('a' AS CHAR(2)) = 'a '
+-- !query schema
+struct<(CAST(a AS CHAR(2)) = a ):boolean>
+-- !query output
+true
+
+
+-- !query
+SELECT cast('a' AS CHAR(2) COLLATE UTF8_BINARY_RTRIM) = 'a'
+-- !query schema
+struct<(CAST(a AS CHAR(2) COLLATE UTF8_BINARY_RTRIM) = a):boolean>
+-- !query output
+true
+
+
+-- !query
+SELECT cast('a' AS CHAR(2) COLLATE UTF8_BINARY_RTRIM) =
+  cast('a' AS CHAR(4) COLLATE UTF8_BINARY_RTRIM)
+-- !query schema
+struct<(CAST(a AS CHAR(2) COLLATE UTF8_BINARY_RTRIM) = CAST(a AS CHAR(4) 
COLLATE UTF8_BINARY_RTRIM)):boolean>
+-- !query output
+true
+
+
+-- !query
+SELECT cast('a' AS CHAR(2)) IN (cast('a' AS CHAR(4)))
+-- !query schema
+struct<(CAST(a AS CHAR(2)) IN (CAST(a AS CHAR(4)))):boolean>
+-- !query output
+true
+
+
+-- !query
+SELECT cast('a' AS CHAR(2)) IN (cast('a' AS VARCHAR(2)))
+-- !query schema
+struct<(CAST(a AS CHAR(2)) IN (CAST(a AS VARCHAR(2)))):boolean>
+-- !query output
+false
+
+
+-- !query
+SELECT cast('a' AS CHAR(2)) IN (cast('a ' AS VARCHAR(2)))
+-- !query schema
+struct<(CAST(a AS CHAR(2)) IN (CAST(a  AS VARCHAR(2)))):boolean>
+-- !query output
+true
+
+
+-- !query
+SELECT cast('a' AS CHAR(2)) IN ('a', 'b')
+-- !query schema
+struct<(CAST(a AS CHAR(2)) IN (a, b)):boolean>
+-- !query output
+false
+
+
+-- !query
+SELECT cast('a' AS CHAR(2)) IN ('a ', 'b')
+-- !query schema
+struct<(CAST(a AS CHAR(2)) IN (a , b)):boolean>
+-- !query output
+true
+
+
+-- !query
+SELECT cast('a' AS CHAR(2)) IN (cast('a' AS CHAR(4)), cast('b' AS VARCHAR(3)))
+-- !query schema
+struct<(CAST(a AS CHAR(2)) IN (CAST(a AS CHAR(4)), CAST(b AS 
VARCHAR(3)))):boolean>
+-- !query output
+false
+
+
+-- !query
+SELECT cast('a' AS CHAR(2) COLLATE UTF8_LCASE) = cast('a' AS VARCHAR(2) 
COLLATE UTF8_LCASE)
+-- !query schema
+struct<(CAST(a AS CHAR(2) COLLATE UTF8_LCASE) = CAST(a AS VARCHAR(2) COLLATE 
UTF8_LCASE)):boolean>
+-- !query output
+false
+
+
+-- !query
+SELECT cast('a' AS CHAR(2) COLLATE UTF8_LCASE) IN (cast('a' AS VARCHAR(2) 
COLLATE UTF8_LCASE))
+-- !query schema
+struct<(CAST(a AS CHAR(2) COLLATE UTF8_LCASE) IN (CAST(a AS VARCHAR(2) COLLATE 
UTF8_LCASE))):boolean>
+-- !query output
+false
+
+
 -- !query
 SELECT typeof(array(cast('a' AS CHAR(2)), cast('bb' AS CHAR(3))))
 -- !query schema
diff --git 
a/sql/core/src/test/scala/org/apache/spark/sql/CharVarcharTestSuite.scala 
b/sql/core/src/test/scala/org/apache/spark/sql/CharVarcharTestSuite.scala
index 34e268d3ba79..f2905da9a2d8 100644
--- a/sql/core/src/test/scala/org/apache/spark/sql/CharVarcharTestSuite.scala
+++ b/sql/core/src/test/scala/org/apache/spark/sql/CharVarcharTestSuite.scala
@@ -21,9 +21,9 @@ import scala.util.Try
 
 import org.apache.spark.{SparkConf, SparkException, SparkRuntimeException}
 import org.apache.spark.sql.catalyst.analysis.FunctionRegistry
-import org.apache.spark.sql.catalyst.expressions.{Attribute, EqualTo, 
GreaterThan, ScalarSubquery, StringRPad}
+import org.apache.spark.sql.catalyst.expressions.{Attribute, EqualTo, 
GreaterThan, Literal, ScalarSubquery, StringRPad}
 import org.apache.spark.sql.catalyst.expressions.Cast.toSQLId
-import org.apache.spark.sql.catalyst.parser.CatalystSqlParser
+import org.apache.spark.sql.catalyst.parser.{CatalystSqlParser, ParseException}
 import org.apache.spark.sql.catalyst.plans.logical.{Aggregate, Filter, Project}
 import org.apache.spark.sql.catalyst.util.CharVarcharUtils
 import org.apache.spark.sql.connector.SchemaRequiredDataSource
@@ -868,25 +868,80 @@ class BasicCharVarcharTestSuite extends 
SharedSparkSession {
       assert(varcharDf.schema.head.dataType === VarcharType(5))
       checkAnswer(varcharDf, Row("hello"))
 
+      // ISO 6.13: character-to-character CAST truncates rather than erroring.
+      checkAnswer(sql("SELECT CAST('hello!' AS VARCHAR(5)) AS v"), 
Row("hello"))
+      checkAnswer(sql("SELECT CAST('abcdef' AS CHAR(2)) AS c"), Row("ab"))
+      checkAnswer(sql("SELECT CAST('abcdef' AS VARCHAR(2)) AS v"), Row("ab"))
+      checkAnswer(sql("SELECT try_cast('abcdef' AS CHAR(2)) AS c"), Row("ab"))
+      checkAnswer(sql("SELECT try_cast('abcdef' AS VARCHAR(2)) AS v"), 
Row("ab"))
+
+      // Multi-byte characters: length is in characters, not octets.
+      // scalastyle:off nonascii
+      checkAnswer(sql("SELECT CAST('你好' AS VARCHAR(2)) AS v"), Row("你好"))
+      checkAnswer(sql("SELECT CAST('你好啊' AS VARCHAR(2)) AS v"), Row("你好"))
+      // scalastyle:on nonascii
+
+      // ISO 6.13 numeric-to-character CAST still errors when the literal does 
not fit.
       checkError(
         exception = intercept[SparkRuntimeException] {
-          sql("SELECT CAST('hello!' AS VARCHAR(5))").collect()
+          sql("SELECT CAST(12345 AS VARCHAR(4))").collect()
         },
         condition = "EXCEED_LIMIT_LENGTH",
-        parameters = Map("limit" -> "5")
+        parameters = Map("limit" -> "4")
       )
+      checkAnswer(sql("SELECT CAST(12345 AS VARCHAR(5)) AS v"), Row("12345"))
+      checkAnswer(sql("SELECT try_cast(12345 AS VARCHAR(4)) AS v"), Row(null))
 
-      // Multi-byte characters: length is in characters, not octets.
-      // scalastyle:off nonascii
-      checkAnswer(sql("SELECT CAST('你好' AS VARCHAR(2)) AS v"), Row("你好"))
+      // LCT must wrap the inner CAST, not retarget it (truncation / overflow 
stay).
+      checkAnswer(
+        sql("SELECT coalesce(CAST('abcdef' AS VARCHAR(2)), CAST('x' AS 
VARCHAR(4))) AS c"),
+        Row("ab"))
+      checkAnswer(
+        sql("""SELECT CASE WHEN true THEN CAST('abcdef' AS VARCHAR(2))
+          |ELSE CAST('x' AS VARCHAR(4)) END AS c""".stripMargin),
+        Row("ab"))
+      checkAnswer(
+        sql("SELECT CAST('abcdef' AS VARCHAR(2)) IN (CAST('ab' AS 
VARCHAR(4)))"),
+        Row(true))
+      checkAnswer(
+        sql("""SELECT coalesce(
+          |  CAST('abcdef' AS VARCHAR(2) COLLATE UTF8_LCASE),
+          |  CAST('x' AS VARCHAR(4) COLLATE UTF8_LCASE)) AS c""".stripMargin),
+        Row("ab"))
+      checkAnswer(
+        sql("SELECT coalesce(try_cast(12345 AS VARCHAR(4)), CAST('x' AS 
VARCHAR(5))) AS c"),
+        Row("x"))
       checkError(
         exception = intercept[SparkRuntimeException] {
-          sql("SELECT CAST('你好啊' AS VARCHAR(2))").collect()
+          sql("SELECT coalesce(CAST(12345 AS VARCHAR(4)), CAST('x' AS 
VARCHAR(5)))").collect()
         },
         condition = "EXCEED_LIMIT_LENGTH",
-        parameters = Map("limit" -> "2")
+        parameters = Map("limit" -> "4")
       )
-      // scalastyle:on nonascii
+    }
+  }
+
+  test("SPARK-58797: store assignment with standardSemantics and 
charVarcharAsString") {
+    withSQLConf(
+        SQLConf.CHAR_VARCHAR_STANDARD_SEMANTICS.key -> "true",
+        SQLConf.LEGACY_CHAR_VARCHAR_AS_STRING.key -> "true") {
+      val wide = new StructType().add("c", CharType(10))
+      val df = spark.createDataFrame(java.util.Arrays.asList(Row("spark")), 
wide)
+      checkError(
+        exception = intercept[SparkRuntimeException] {
+          df.to(new StructType().add("c", CharType(3))).collect()
+        },
+        condition = "EXCEED_LIMIT_LENGTH",
+        parameters = Map("limit" -> "3"))
+      withTable("std_and_as_string") {
+        sql("CREATE TABLE std_and_as_string (v VARCHAR(2)) USING parquet")
+        checkError(
+          exception = intercept[SparkRuntimeException] {
+            sql("INSERT INTO std_and_as_string VALUES ('abc')")
+          },
+          condition = "EXCEED_LIMIT_LENGTH",
+          parameters = Map("limit" -> "2"))
+      }
     }
   }
 
@@ -1201,6 +1256,91 @@ class BasicCharVarcharTestSuite extends 
SharedSparkSession {
     }
   }
 
+  test("SPARK-58794: collated mixed-length LCT ignores collation strength for 
length") {
+    val mixedLength =
+      """SELECT coalesce(
+        |  cast('a' AS CHAR(2) COLLATE UTF8_LCASE),
+        |  cast('bb' AS CHAR(4) COLLATE UTF8_LCASE)) AS c""".stripMargin
+    val mixedStrength =
+      """SELECT coalesce(
+        |  cast('a' AS CHAR(2) COLLATE UTF8_LCASE),
+        |  cast(1 AS CHAR(4) COLLATE UTF8_LCASE)) AS c""".stripMargin
+
+    Seq(
+      SQLConf.CHAR_VARCHAR_STANDARD_SEMANTICS.key -> "true",
+      SQLConf.PRESERVE_CHAR_VARCHAR_TYPE_INFO.key -> "true").foreach { case 
(key, value) =>
+      withSQLConf(key -> value) {
+        assert(sql(mixedLength).schema.head.dataType === CharType(4, 
"UTF8_LCASE"),
+          s"$key=$value same-strength mixed CHAR lengths")
+        assert(sql(mixedStrength).schema.head.dataType === CharType(4, 
"UTF8_LCASE"),
+          s"$key=$value Implicit CHAR(2) vs Default CHAR(4) must widen, not 
narrow")
+      }
+    }
+  }
+
+  test("SPARK-58794: typed CHAR Literal is re-padded when LCT widens the 
length") {
+    // CollationTypeCoercion.changeType used to `copy(dataType)` on Literal, 
which would
+    // leave CHAR(2) "a " as a CHAR(4) value without the extra pad. SQL CAST 
is a Cast
+    // node so goldens do not cover this; Literal.create does.
+    withSQLConf(SQLConf.CHAR_VARCHAR_STANDARD_SEMANTICS.key -> "true") {
+      val c2 = Column(Literal.create("a", CharType(2, "UTF8_LCASE")))
+      val c4 = Column(Literal.create("bb", CharType(4, "UTF8_LCASE")))
+      val coalesced = functions.coalesce(c2, c4)
+      val df = spark.range(1).select(coalesced.as("c"))
+      assert(df.schema.head.dataType === CharType(4, "UTF8_LCASE"))
+      checkAnswer(df, Row("a   "))
+    }
+  }
+
+  test("SPARK-58794: parameterized CHAR/VARCHAR lengths under 
standardSemantics") {
+    // Length positions accept parameter markers (`integerValue` -> 
`parameterMarker`). Under
+    // standardSemantics the bound type stays first-class: CAST keeps 
CHAR/VARCHAR, pads and
+    // enforces length, and DDL schemas retain the substituted n.
+    withSQLConf(SQLConf.CHAR_VARCHAR_STANDARD_SEMANTICS.key -> "true") {
+      val charDf = spark.sql("SELECT cast('ab' AS CHAR(:n)) AS c", Map("n" -> 
5))
+      assert(charDf.schema.head.dataType === CharType(5))
+      checkAnswer(
+        spark.sql("SELECT concat('<', cast('ab' AS CHAR(:n)), '>')", Map("n" 
-> 5)),
+        Row("<ab   >"))
+
+      val varcharDf = spark.sql("SELECT cast('hello' AS VARCHAR(?)) AS c", 
Array(5))
+      assert(varcharDf.schema.head.dataType === VarcharType(5))
+      checkAnswer(
+        spark.sql("SELECT cast('abcdef' AS VARCHAR(?))", Array(2)),
+        Row("ab"))
+
+      withTable("param_varchar", "param_char") {
+        spark.sql(
+          "CREATE TABLE param_varchar (c VARCHAR(:n)) USING parquet", Map("n" 
-> 7))
+        assert(spark.table("param_varchar").schema.head.dataType === 
VarcharType(7))
+        spark.sql("CREATE TABLE param_char (c CHAR(?)) USING parquet", 
Array(4))
+        assert(spark.table("param_char").schema.head.dataType === CharType(4))
+      }
+
+      // Non-integral / negative lengths fail when substituted into the length 
position.
+      checkError(
+        exception = intercept[ParseException] {
+          spark.sql("SELECT cast('a' AS CHAR(:n))", Map("n" -> -1))
+        },
+        condition = "PARSE_SYNTAX_ERROR",
+        parameters = Map("error" -> "'-'", "hint" -> ""),
+        context = ExpectedContext(
+          fragment = "SELECT cast('a' AS CHAR(:n))",
+          start = 0,
+          stop = 27))
+      checkError(
+        exception = intercept[ParseException] {
+          spark.sql("SELECT cast('a' AS CHAR(:n))", Map("n" -> 1.5))
+        },
+        condition = "PARSE_SYNTAX_ERROR",
+        parameters = Map("error" -> "'1.5D'", "hint" -> ""),
+        context = ExpectedContext(
+          fragment = "SELECT cast('a' AS CHAR(:n))",
+          start = 0,
+          stop = 27))
+    }
+  }
+
   test("SPARK-58802: single-pass resolver agrees with fixed-point under 
standardSemantics") {
     // Dual run defaults to on under tests, but pin it explicitly so this 
coverage cannot be
     // silently lost: the HybridAnalyzer compares output schema and normalized 
plan across the
@@ -1219,7 +1359,18 @@ class BasicCharVarcharTestSuite extends 
SharedSparkSession {
         VarcharType(5))
       assert(sql("SELECT try_cast('abcdef' AS CHAR(2)) AS 
c").schema.head.dataType ===
         CharType(2))
-      checkAnswer(sql("SELECT try_cast('abcdef' AS VARCHAR(2)) AS c"), 
Row(null))
+      checkAnswer(sql("SELECT try_cast('abcdef' AS VARCHAR(2)) AS c"), 
Row("ab"))
+      checkAnswer(
+        sql("SELECT coalesce(CAST('abcdef' AS VARCHAR(2)), CAST('x' AS 
VARCHAR(4))) AS c"),
+        Row("ab"))
+      checkAnswer(
+        sql("SELECT CAST('abcdef' AS VARCHAR(2)) IN (CAST('ab' AS 
VARCHAR(4)))"),
+        Row(true))
+      checkAnswer(
+        sql("""SELECT coalesce(
+          |  CAST('abcdef' AS VARCHAR(2) COLLATE UTF8_LCASE),
+          |  CAST('x' AS VARCHAR(4) COLLATE UTF8_LCASE)) AS c""".stripMargin),
+        Row("ab"))
 
       // Least common type (R2): COALESCE / CASE / NULL / CHAR+VARCHAR / 
CHAR+STRING.
       assert(sql(
@@ -1289,9 +1440,41 @@ class BasicCharVarcharTestSuite extends 
SharedSparkSession {
         .schema.head.dataType ===
         StructType(Seq(StructField("f", CharType(2), nullable = false))))
 
-      // Collated CAST keeps the declared collation under both analyzers.
-      assert(sql("SELECT CAST('ab' AS CHAR(2) COLLATE UTF8_LCASE) AS c")
-        .schema.head.dataType === CharType(2, "UTF8_LCASE"))
+      // Collated mixed-length LCT (the CollationTypeCoercion equal-strength 
and
+      // mixed-strength paths). Set ops have a separate resolver path.
+      assert(sql(
+        """SELECT coalesce(
+          |  CAST('a' AS CHAR(2) COLLATE UTF8_LCASE),
+          |  CAST('bb' AS CHAR(4) COLLATE UTF8_LCASE)) AS c""".stripMargin)
+        .schema.head.dataType === CharType(4, "UTF8_LCASE"))
+      assert(sql(
+        """SELECT coalesce(
+          |  CAST('a' AS CHAR(2) COLLATE UTF8_LCASE),
+          |  CAST(1 AS CHAR(4) COLLATE UTF8_LCASE)) AS c""".stripMargin)
+        .schema.head.dataType === CharType(4, "UTF8_LCASE"))
+      checkAnswer(
+        sql("SELECT CAST('a' AS CHAR(2) COLLATE UTF8_LCASE) = " +
+          "CAST('a' AS CHAR(4) COLLATE UTF8_LCASE)"),
+        Row(true))
+      checkAnswer(
+        sql("SELECT CAST('a' AS CHAR(2) COLLATE UTF8_LCASE) IN " +
+          "(CAST('a' AS CHAR(4) COLLATE UTF8_LCASE))"),
+        Row(true))
+      checkAnswer(
+        sql("SELECT CAST('a' AS CHAR(2) COLLATE UTF8_LCASE) = " +
+          "CAST('a' AS VARCHAR(2) COLLATE UTF8_LCASE)"),
+        Row(false))
+      checkAnswer(
+        sql("SELECT CAST('a' AS CHAR(2) COLLATE UTF8_LCASE) IN " +
+          "(CAST('a' AS VARCHAR(2) COLLATE UTF8_LCASE))"),
+        Row(false))
+
+      val mixedCharUnion = sql(
+        """SELECT CAST('a' AS CHAR(2)) AS c
+          |UNION ALL
+          |SELECT CAST('bb' AS CHAR(4)) AS c""".stripMargin)
+      assert(mixedCharUnion.schema.head.dataType === CharType(4))
+      checkAnswer(mixedCharUnion, Seq(Row("a   "), Row("bb  ")))
 
       // Bare column references keep the declared type (R3) through dual-run 
analysis.
       withTable("char_varchar_dual_run") {


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to