spark-test commented on code in PR #60059:
URL: https://github.com/apache/spark/pull/60059#discussion_r4471060884


##########
sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/expressions/namedExpressions.scala:
##########
@@ -463,6 +463,13 @@ case class OuterReference(e: NamedExpression)
   extends LeafExpression with NamedExpression with Unevaluable {
   override def dataType: DataType = e.dataType
   override def nullable: Boolean = e.nullable
+  // Forward only the raw CHAR/VARCHAR type when aliased. Other keys are not 
forwarded; some,
+  // such as the SQL UDF parameter and auto-generated alias markers, must not 
leak.
+  override def metadata: Metadata = 
CharVarcharUtils.getRawTypeString(e.metadata) match {

Review Comment:
   **Non-blocking (P2):** The forwarded type only takes effect when a query is 
analyzed, so permanent views created before the upgrade don't pick it up. 
`SessionCatalog.castColToType` aliases every view column with the stored 
field's metadata (`explicitMetadata = Some(toField.metadata)`), and only `WITH 
SCHEMA EVOLUTION` views expose the re-analyzed query. 
`ViewSyncSchemaToMetaStore` compares only type and nullability, so it never 
restores the missing key.
   
   For example, a view created by an earlier version as `CREATE VIEW v AS 
SELECT t.s, l.k FROM t, LATERAL (SELECT t.c AS k) l` over `c CHAR(5)` stored 
`k` as plain STRING. After the upgrade, `SELECT s FROM v WHERE k = 'ab'` still 
returns no rows, while the same query without the view now returns `x`. This 
holds for all three forms (`t.c AS k`, `t.c`, `t.*`), because inside a LATERAL 
subquery each of them lost the CHAR type in earlier versions. Only CHAR columns 
are affected, since VARCHAR comparisons aren't padded.
   
   The migration note says these comparisons now return the correct rows, but 
doesn't say that such views must be recreated with `CREATE OR REPLACE VIEW` or 
`ALTER VIEW ... AS`. Fixing them automatically would need a stored-schema 
migration. Instead, the note should name this exemption and the recreate step, 
with a TODO at the view column projection in `castColToType`, where the stored 
metadata wins.
   
   **Verification:**
   - **Behavior:** A permanent view over `SELECT t.s, l.k FROM t, LATERAL 
(SELECT t.c AS k) l`, with c CHAR(5) holding 'ab', records k as char(5) in its 
stored schema, and `SELECT s FROM v WHERE k = 'ab'` returns the matching row.
   - **Inspection:** The migration-guide entry and the TODO both name the 
exemption for views created by earlier versions, and the step that recreates 
them.



##########
docs/sql-migration-guide.md:
##########
@@ -52,6 +52,7 @@ license: |
 - Since Spark 4.4, the Oracle JDBC datasource maps bare `NUMBER` columns 
(declared without explicit precision or scale) to `DecimalType(38, 18)` instead 
of `DecimalType(38, 10)`. This uses `DecimalType.DEFAULT_SCALE` and preserves 
up to 18 fractional digits instead of 10, reducing silent precision loss. 
However, the integer range shrinks from 28 to 20 digits; bare `NUMBER` values 
with more than 20 integer digits that previously read correctly will raise 
`NUMERIC_VALUE_OUT_OF_RANGE`. The same change applies to Oracle `FLOAT` 
columns, which JDBC reports as `NUMERIC` with `scale = -127`. Columns with 
explicit `NUMBER(p,s)` are not affected. To restore the previous 
`DecimalType(38, 10)` mapping, set 
`spark.sql.legacy.oracle.numberMapping.enabled` to `true`.
 - Since Spark 4.4, when `spark.sql.charVarchar.standardSemantics.enabled` is 
false and `spark.sql.readSideCharPadding` is true (the default), JDBC Data 
Source V2 catalog tables on H2, PostgreSQL, SQL Server, and Oracle push CHAR 
equality, inequality, null-safe equality, and `IN`/`NOT IN`. In 4.3 the default 
read-side padding wrap kept those filters in Spark. Pushed predicates use the 
remote collation, so a case-insensitive SQL Server collation can match a stored 
`'ABC'` with `flag = 'abc'` while a Spark-local comparison would not. The V1 
path (`spark.read.jdbc` and `USING jdbc`) still evaluates these CHAR filters in 
Spark. PostgreSQL's internal `"char"` type (JDBC type name `char`) is mapped to 
`STRING` instead of `CHAR(1)`, so Spark no longer blank-pads its empty value.
 - Since Spark 4.4, when `spark.sql.charVarchar.standardSemantics.enabled` is 
false and `spark.sql.readSideCharPadding` is true (the default), `IS [NOT] 
NULL` over a CHAR scan column is unwrapped to the raw column for every Data 
Source V2 source, including every JDBC dialect. Previously the padding wrap 
kept that null check in Spark.
+- Since Spark 4.4, a CHAR/VARCHAR column of the outer query that a LATERAL or 
correlated subquery returns keeps its CHAR/VARCHAR type, as an inner column 
does. Previously, comparisons against it were not padded and could silently 
return no rows, and CTAS and views over it stored `STRING`. This affected `t.c 
AS k` in all earlier versions, `t.*` since Spark 4.2, and `t.c` since Spark 
4.3. To keep storing `STRING`, set `spark.sql.legacy.charVarcharAsString` to 
`true`, which treats all CHAR/VARCHAR columns as `STRING`.

Review Comment:
   **Nit (P3):** The version history is right for correlated subqueries outside 
LATERAL, but not inside a LATERAL subquery, the first case this entry names. 
SPARK-55794 (4.2.0) started aliasing outer star expansion, and SPARK-58171 
(4.3.0) bare outer references. Before that, a LATERAL `SELECT t1.c` or `SELECT 
t1.*` exposed the outer attribute's own ExprId, which collided with the left 
side. `DeduplicateRelations` resolved the collision by wrapping the reference 
in `Alias(o, a.name)()` (`DeduplicateRelations.scala:399-409`). Before this PR 
`OuterReference` had empty metadata, so that alias dropped the CHAR type too. 
`ResolveSubquerySuite`'s `deduplicate lateral subquery` test has asserted that 
alias since 2021, and neither ticket touched `DeduplicateRelations`.
   
   So on 4.2.0, `SELECT t1.s FROM t1, LATERAL (SELECT t1.c) l WHERE l.c = 'ab'` 
already returns no rows, and `LATERAL (SELECT t1.*)` behaves the same before 
4.2. The sentence should say that in a LATERAL subquery all three forms were 
affected in all earlier versions, and keep the 4.2 and 4.3 dates for other 
correlated subqueries.
   
   **Verification:**
   - **Inspection:** The entry no longer dates the breakage of the bare and 
star forms in a LATERAL subquery to Spark 4.3 and 4.2, and still gives those 
dates for other correlated subqueries.



##########
sql/core/src/test/scala/org/apache/spark/sql/CharVarcharTestSuite.scala:
##########
@@ -866,6 +866,53 @@ trait CharVarcharTestSuite extends QueryTest {
     }
   }
 
+  test("SPARK-XXXXX: char type comparison: outer reference returned by a 
LATERAL subquery") {

Review Comment:
   **Nit (P3):** This test name still carries the `SPARK-XXXXX` placeholder 
instead of this PR's key, `SPARK-60876`. So do the test at line 893 and the new 
`PersistedViewTestSuite` test at `SQLViewTestSuite.scala:630`. Neighboring 
regression tests carry their real keys (for example `SPARK-34833` here). With 
the placeholder, these tests can't be traced back to their ticket or selected 
by its ID after merge.
   
   **Verification:**
   - **Inspection:** Every test name this change adds begins with SPARK-60876, 
and none contains the SPARK-XXXXX placeholder.



##########
docs/sql-migration-guide.md:
##########
@@ -52,6 +52,7 @@ license: |
 - Since Spark 4.4, the Oracle JDBC datasource maps bare `NUMBER` columns 
(declared without explicit precision or scale) to `DecimalType(38, 18)` instead 
of `DecimalType(38, 10)`. This uses `DecimalType.DEFAULT_SCALE` and preserves 
up to 18 fractional digits instead of 10, reducing silent precision loss. 
However, the integer range shrinks from 28 to 20 digits; bare `NUMBER` values 
with more than 20 integer digits that previously read correctly will raise 
`NUMERIC_VALUE_OUT_OF_RANGE`. The same change applies to Oracle `FLOAT` 
columns, which JDBC reports as `NUMERIC` with `scale = -127`. Columns with 
explicit `NUMBER(p,s)` are not affected. To restore the previous 
`DecimalType(38, 10)` mapping, set 
`spark.sql.legacy.oracle.numberMapping.enabled` to `true`.
 - Since Spark 4.4, when `spark.sql.charVarchar.standardSemantics.enabled` is 
false and `spark.sql.readSideCharPadding` is true (the default), JDBC Data 
Source V2 catalog tables on H2, PostgreSQL, SQL Server, and Oracle push CHAR 
equality, inequality, null-safe equality, and `IN`/`NOT IN`. In 4.3 the default 
read-side padding wrap kept those filters in Spark. Pushed predicates use the 
remote collation, so a case-insensitive SQL Server collation can match a stored 
`'ABC'` with `flag = 'abc'` while a Spark-local comparison would not. The V1 
path (`spark.read.jdbc` and `USING jdbc`) still evaluates these CHAR filters in 
Spark. PostgreSQL's internal `"char"` type (JDBC type name `char`) is mapped to 
`STRING` instead of `CHAR(1)`, so Spark no longer blank-pads its empty value.
 - Since Spark 4.4, when `spark.sql.charVarchar.standardSemantics.enabled` is 
false and `spark.sql.readSideCharPadding` is true (the default), `IS [NOT] 
NULL` over a CHAR scan column is unwrapped to the raw column for every Data 
Source V2 source, including every JDBC dialect. Previously the padding wrap 
kept that null check in Spark.
+- Since Spark 4.4, a CHAR/VARCHAR column of the outer query that a LATERAL or 
correlated subquery returns keeps its CHAR/VARCHAR type, as an inner column 
does. Previously, comparisons against it were not padded and could silently 
return no rows, and CTAS and views over it stored `STRING`. This affected `t.c 
AS k` in all earlier versions, `t.*` since Spark 4.2, and `t.c` since Spark 
4.3. To keep storing `STRING`, set `spark.sql.legacy.charVarcharAsString` to 
`true`, which treats all CHAR/VARCHAR columns as `STRING`.

Review Comment:
   **Non-blocking (P2):** The rollback sentence doesn't hold: 
`spark.sql.legacy.charVarcharAsString=true` doesn't keep STRING storage. 
Relation attributes carry the CHAR/VARCHAR type key whether or not the flag is 
set. Under the flag, `ApplyCharTypePadding` returns the plan unchanged, so 
nothing strips the key, and the new `OuterReference.metadata` forwards it into 
the alias. Then `CharVarcharUtils.getRawSchema`, which never reads this flag, 
rebuilds `CHAR(n)`/`VARCHAR(n)` for data-source, Hive and v2 CTAS and for views.
   
   So `SET spark.sql.legacy.charVarcharAsString=true; CREATE TABLE t2 USING 
parquet AS SELECT l.k FROM t, LATERAL (SELECT t.c AS k) l` still stores `t2.k` 
as `char(5)`, while the flag turns off CHAR padding and length checks for every 
column in the session. Inner-column CTAS already behaves the same way, so the 
flag was never a storage rollback.
   
   What does keep STRING is a cast in the query, for example `CAST(t.c AS 
STRING) AS k`: an `Alias` over a `Cast` inherits no metadata, and 
`SimplifyCasts` keeps that cast. The sentence could point to that instead.
   
   **Verification:**
   - **Behavior:** A CTAS table and a permanent view built over a LATERAL 
subquery that returns an outer CHAR(5) column through an explicit CAST to 
STRING store that column as STRING, while the same statements without the cast 
store char(5).
   - **Inspection:** The migration-guide entry no longer presents 
spark.sql.legacy.charVarcharAsString as a way to keep STRING storage.



##########
sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/expressions/namedExpressions.scala:
##########
@@ -463,6 +463,13 @@ case class OuterReference(e: NamedExpression)
   extends LeafExpression with NamedExpression with Unevaluable {
   override def dataType: DataType = e.dataType
   override def nullable: Boolean = e.nullable
+  // Forward only the raw CHAR/VARCHAR type when aliased. Other keys are not 
forwarded; some,
+  // such as the SQL UDF parameter and auto-generated alias markers, must not 
leak.
+  override def metadata: Metadata = 
CharVarcharUtils.getRawTypeString(e.metadata) match {

Review Comment:
   **Nit (P3):** Since this is a `def`, every call builds a new 
`MetadataBuilder` and a one-entry `Metadata` when the outer column is 
CHAR/VARCHAR. `Alias.metadata` reads it on each `Alias.toAttribute`, and 
`Project.output`, also a `def`, calls that on every evaluation. So the same 
value is rebuilt many times during analysis and optimization, until 
decorrelation removes the outer reference. Before this PR, the call returned 
the shared `Metadata.empty`. Earlier in this file, the alias metadata path is 
documented as performance-sensitive: `removeNonInheritableMetadata` avoids 
`MetadataBuilder` for that reason. The value depends only on the immutable `e`, 
so `override lazy val metadata` would compute it once with the same result. The 
cost is planning-time only and probably small.
   
   **Verification:**
   - **Behavior:** Comparisons, result types and marker behavior for outer CHAR 
and VARCHAR columns returned by LATERAL and correlated subqueries are unchanged 
with the cached value.
   - **Inspection:** Repeated metadata reads on one OuterReference instance no 
longer construct a new MetadataBuilder.



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