srielau commented on code in PR #58033:
URL: https://github.com/apache/spark/pull/58033#discussion_r3800859874


##########
sql/core/src/test/resources/sql-tests/inputs/charvarchar-standard-semantics.sql:
##########
@@ -0,0 +1,90 @@
+--SET spark.sql.charVarchar.standardSemantics.enabled=true
+
+-- R3: CAST introduces CHAR/VARCHAR
+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
+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));
+
+-- R2: least common type (COALESCE / CASE)
+SELECT typeof(coalesce(cast('hello' AS VARCHAR(5)), cast('world' AS 
VARCHAR(10))));
+SELECT typeof(coalesce(cast('hello' AS VARCHAR(5)), cast('world!' AS 
CHAR(6))));
+SELECT typeof(coalesce(cast('hello' AS CHAR(5)), cast('world!' AS CHAR(6))));
+SELECT typeof(coalesce(cast('hello' AS VARCHAR(5)), 'world'));
+SELECT typeof(coalesce(cast('hello' AS CHAR(5)), NULL));
+SELECT typeof(
+  CASE WHEN true THEN cast('a' AS CHAR(2)) ELSE cast('bb' AS CHAR(4)) END);
+
+-- R2: least common type for IN lists
+SELECT cast('a' AS CHAR(2)) IN (cast('a ' AS CHAR(2)), cast('bbb' AS 
VARCHAR(3)));
+SELECT typeof(c) FROM (SELECT cast('a' AS CHAR(2)) AS c) t WHERE c IN ('a ', 
'b');
+
+-- R1: transforming functions return STRING
+SELECT typeof(upper(cast('ab' AS CHAR(2))));
+SELECT typeof(lower(cast('AB' AS VARCHAR(2))));
+SELECT typeof(cast('a' AS CHAR(1)) || cast('b' AS VARCHAR(1)));
+SELECT typeof(substr(cast('hello' AS VARCHAR(5)), 1, 2));
+SELECT typeof(upper(coalesce(cast('a' AS CHAR(2)), cast('b' AS CHAR(4)))));
+SELECT typeof(concat(cast('a' AS CHAR(2)), cast('b' AS CHAR(3))));
+SELECT typeof(trim(cast('ab  ' AS CHAR(4))));
+SELECT typeof(lpad(cast('ab' AS CHAR(2)), 5, 'x'));
+
+-- R1: regexp / mask / split family
+SELECT typeof(regexp_replace(cast('ab' AS CHAR(2)), 'a', 'x'));
+SELECT typeof(regexp_extract(cast('ab' AS VARCHAR(2)), '(a)', 1));
+SELECT typeof(regexp_extract_all(cast('aab' AS VARCHAR(3)), '(a)', 1));
+SELECT typeof(split(cast('a,b' AS CHAR(3)), ','));
+SELECT typeof(mask(cast('ab' AS CHAR(2))));
+
+-- R1: CHAR/VARCHAR promote to STRING where a plain string is expected, so 
expressions that
+-- require all their string inputs to share one type accept them alongside a 
STRING argument.
+-- Values are wrapped in sentinels because the golden format trims trailing 
blanks, which would
+-- otherwise hide the CHAR padding these expressions operate on.
+SELECT typeof(overlay(cast('ab' AS CHAR(5)) PLACING 'x' FROM 1));
+SELECT concat('<', overlay(cast('ab' AS CHAR(5)) PLACING 'x' FROM 1), '>');
+SELECT typeof(elt(1, cast('ab' AS CHAR(5)), 'x'));
+SELECT typeof(right(cast('ab' AS CHAR(5)), 2));
+SELECT concat('<', right(cast('ab' AS CHAR(5)), 2), '>');
+SELECT typeof(left(cast('ab' AS CHAR(5)), 2));
+
+-- R1: transforms whose result length differs from the input must not inherit 
the constraint.
+SELECT typeof(reverse(cast('ab' AS CHAR(5))));
+SELECT typeof(hex(cast('ab' AS CHAR(5))));
+SELECT hex(cast('ab' AS CHAR(5)));
+SELECT typeof(array_join(array(cast('ab' AS CHAR(5)), cast('cd' AS CHAR(5))), 
'-'));
+SELECT concat('<', array_join(array(cast('ab' AS CHAR(5)), cast('cd' AS 
CHAR(5))), '-'), '>');
+-- reverse() on a non-string input is unaffected.
+SELECT typeof(reverse(array(1, 2)));
+
+-- R2 with collation.
+-- The recorded "string collate null" is a pre-existing gap, not a 
standardSemantics behavior:

Review Comment:
   **High (Golden/Tests):** Please don’t golden `string collate null` for 
collated CHAR coalesce. The LCT helper unit test already expects `CharType(4, 
UTF8_LCASE)`, so this SQL/`typeof` result is a real E2E gap in the foundation — 
not just documentation of a known quirk.
   
   Prefer removing/ignoring the query until fixed, or regenerating against the 
correct type. Otherwise we normalize a bug and a later fix looks like a golden 
regression.



##########
sql/api/src/main/scala/org/apache/spark/sql/types/StringType.scala:
##########
@@ -159,25 +159,76 @@ case object StringHelper extends 
PartialOrdering[StringConstraint] {
 
   def isPlainString(s: StringType): Boolean = s.constraint == NoConstraint
 
+  /**
+   * Strip CHAR/VARCHAR length constraints, preserving collation.
+   *
+   * Used by transforming string expressions (upper, substr, concat, ...) so 
their result type is
+   * plain STRING even when inputs are CharType/VarcharType (SQL standard 
CHAR/VARCHAR R1), when
+   * standard semantics are on.
+   */
+  def plainStringType(dt: DataType): DataType = dt match {
+    case c: CharType => c.toStringType
+    case v: VarcharType => v.toStringType
+    case other => other
+  }
+
+  def plainStringType(s: StringType): StringType = s match {
+    case c: CharType => c.toStringType
+    case v: VarcharType => v.toStringType
+    case other => other
+  }
+
+  /**
+   * Result type for transforming string expressions. Under
+   * spark.sql.charVarchar.standardSemantics.enabled, always plain STRING 
(R1). Under
+   * preserveCharVarcharTypeInfo alone, keep child type (legacy leaky path).
+   */
+  def transformingStringResultType(dt: DataType): DataType = {

Review Comment:
   **High (Architecture):** R1 is currently a dual protocol 
(`charVarcharToPlainString` + per-expr `transformingStringResultType`). That 
will keep regressing — anything using `ExpectsInputTypes` (no cast) or 
returning `child`/`first.dataType` without the helper still leaks 
`CharType`/`VarcharType` (see `StringToMap` in the review summary).
   
   Can we centralize “transforming string result → plain STRING under 
standardSemantics” (trait / shared base) and keep an explicit R2/R3 allowlist 
for pass-throughs (`coalesce`, `max`, `element_at`, …)? At minimum, please add 
an inventory test that fails when a string-producing expr still returns 
constrained types under the flag.



##########
sql/hive-thriftserver/src/main/scala/org/apache/spark/sql/hive/thriftserver/RowSetUtils.scala:
##########
@@ -147,8 +147,12 @@ object RowSetUtils {
             // types that reach this branch do not use the `nested` flag in 
`toHiveString`. Now,
             // Geospatial types use it for wrapping EWKT in quotes when nested 
= true, so we need
             // to set `nested` here to false to avoid spurious quotes for 
standalone geo values.
+            // String types need the same treatment: the fast path above 
matches only the
+            // default-collation StringType singleton, so CHAR/VARCHAR and 
collated strings land
+            // here and would otherwise be rendered as "value" instead of 
value.
             val nested = typ match {
               case _: GeometryType | _: GeographyType => false
+              case _: StringType => false

Review Comment:
   **Medium (Compatibility):** The `nested = false` for `_: StringType` also 
fixes collated strings on the Thrift fallback path, ungated by 
`standardSemantics`. That’s probably right, but it’s a broader behavior change 
than the CHAR/VARCHAR flag.
   
   Please document it explicitly in the PR description and add a test (collated 
string + CHAR/VARCHAR) so we don’t regress quoting again. Consider splitting to 
a tiny dedicated bugfix if a cleaner changelog is preferred.



##########
sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/expressions/stringExpressions.scala:
##########
@@ -453,7 +454,8 @@ trait String2StringExpression extends 
ImplicitCastInputTypes {
 
   def convert(v: UTF8String): UTF8String
 
-  override def dataType: DataType = child.dataType
+  override def dataType: DataType =
+    StringHelper.transformingStringResultType(child.dataType)

Review Comment:
   **Medium (Architecture):** Putting R1 on `String2StringExpression` also 
changes `Empty2Null`, which isn’t a transforming function (empty→null is 
pass-through for non-empty values). V1Writes currently only applies it when 
`dataType == StringType`, so we’re safe by accident.
   
   Please override `Empty2Null.dataType` to keep the child type (or don’t share 
that trait).



##########
sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/expressions/collectionOperations.scala:
##########
@@ -1430,7 +1430,10 @@ case class Reverse(child: Expression)
       BinaryType,
       ArrayType))
 
-  override def dataType: DataType = child.dataType
+  // Reversing a string transforms its content, so a CHAR/VARCHAR input yields 
plain STRING (R1).
+  // Array and binary inputs are unaffected. The promotion in 
ImplicitTypeCasts does not reach
+  // here because the expected type is a TypeCollection rather than a plain 
string type.

Review Comment:
   **Low (Style):** Nit: `expectsStringType` already treats `TypeCollection`, 
so ImplicitTypeCasts *does* promote CHAR for `Reverse`’s string branch. Mind 
updating the comment so we don’t teach the wrong invariant? 
(`transformingStringResultType` remains useful as belt-and-suspenders / for 
non-cast paths.)



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