shivadarshan-devadiga opened a new pull request, #58553:
URL: https://github.com/apache/spark/pull/58553
### What changes were proposed in this pull request?
This PR fixes five bugs in the CHAR type comparison rewrite path.
1. **`ApplyCharTypePaddingHelper` — IN list elements are misaligned with
their lengths.**
In the `In` branch, `literalCharLengths` was computed from the *non-null*
subset of the
list but then `zip`ped against the *original* list. Since `zip` truncates
to the shorter
sequence, a NULL element ahead of real literals shifted every later
length by one and
dropped trailing literals entirely. Lengths are now computed per element
as `Option[Int]`
so each stays paired with its own element, and NULL elements are left
untouched in place
(they can never match, so they need no padding).
Keeping the original element also fixes a second symptom: the old code
rebuilt NULLs as
`Literal.create(null, StringType)` with the *default* collation, while
`createStringRPad`
gives padded elements the column's collation. On a collated CHAR column
that mixture made
`In.checkInputDataTypes` fail, so the query did not return a wrong answer
— it did not run
at all.
2. **`TypeCoercionHelper.InTypeCoercion` — a redundant `Cast` hides the CHAR
column.**
When any list element's type differed from the value's, the rule cast
*every* child
unconditionally, including children already of the common type. An
untyped `NULL` in the
list therefore wrapped the value in `cast(c as string)`, and
`ApplyCharTypePadding`'s
`AttrOrOuterRef` extractor no longer matched it, so no padding was
applied at all. Note
`SimplifyCasts` deliberately preserves `Cast(charAttr, StringType)`, so
this cast survived
the whole optimizer rather than being a passing analysis-time artifact.
Now uses the
existing `castIfNotSameType` helper, and the guard moves to
`!haveSameType(...)` so that
guard and action agree — matching the `CreateArray` / `Concat` /
`MapConcat` / `Coalesce`
cases, which already pair `haveSameType` with `castIfNotSameType`.
3. **`CharVarcharUtils.padCharToTargetLength` — struct padding is
discarded.**
`needPadding = padded.isDefined` overwrote the flag on every field
instead of accumulating
it, so only the *last* field decided whether the struct was rebuilt. A
trailing field that
needs no padding threw away the padding computed for the fields before
it, at any nesting
depth. Changed to `needPadding |= padded.isDefined`.
4. **`CharVarcharUtils.padCharToTargetLength` — struct nullability is lost.**
Rebuilding a struct with `CreateNamedStruct(GetStructField(expr, i),
...)` turns a NULL
struct into a non-NULL struct of NULL fields. Guarded with
`If(IsNull(expr), Literal(null, struct.dataType), struct)`, mirroring
what the scan-side
rewrite in `processStringForCharVarchar` already does a few lines above.
Without this,
fix (3) would have extended the problem to multi-field structs; with it,
the long-standing
single-field case is fixed too.
5. **`CharVarcharUtils.addPaddingInStringComparison` — non-orderable
operands.**
A struct holding a MAP is not comparable, and `CheckAnalysis` rejects it.
Once fix (3)
made such a struct eligible for rebuilding, the error started naming the
rewritten
expression instead of the user's. Comparisons on non-orderable types are
now skipped, so
the message names the original attribute again.
### Why are the changes needed?
All of these produce wrong results or spurious failures, not diagnosable
errors.
**Bugs 1 and 2 together** (`c` is `CHAR(2)` holding `'a'`):
```sql
CREATE TABLE t(c CHAR(2)) USING parquet;
INSERT INTO t VALUES ('a');
SELECT c IN (null, 'a') FROM t;
```
`'a'` matches, and `NULL OR TRUE` is `TRUE` in SQL three-valued logic, so
this must return
`true`. It returns `null`. The analyzed plan before this PR shows the
literal dropped
outright:
```
Filter c#5 IN (null,null) -- 'a' is gone
```
and where a longer literal widens the comparison, the surviving literal is
left unpadded:
```
Filter rpad(c#5, 3, ) IN (rpad(cast(null as string), 3, ), a, null)
```
On a collated column the same bug fails the query instead:
```sql
CREATE TABLE t(c CHAR(2) COLLATE UTF8_LCASE) USING parquet;
SELECT c IN (null, 'A') FROM t;
-- [DATATYPE_MISMATCH.DATA_DIFF_TYPES] ... Input to `in` should all be the
same type,
-- but it's ["STRING COLLATE UTF8_LCASE", "STRING COLLATE UTF8_LCASE",
"STRING"]
```
**Bug 3:**
```sql
CREATE TABLE t(c1 STRUCT<c: CHAR(2), i: INT>, c2 STRUCT<c: CHAR(5), i: INT>)
USING parquet;
INSERT INTO t VALUES (struct('a', 1), struct('a', 1));
SELECT c1 = c2, c1 < c2 FROM t;
```
Both structs hold the same logical value, so this must return `true, false`.
It returns
`false, true`, because `c1.c` is compared as `'a '` against `'a '`.
Moving the `INT` field
to the front of the struct hides the bug, which is why the existing
single-field
`STRUCT<c: CHAR(2)>` coverage never caught it.
**Bug 4** (present since SPARK-33480 for single-field structs):
```sql
CREATE TABLE t(s1 STRUCT<c: CHAR(2)>, s2 STRUCT<c: CHAR(5)>) USING parquet;
INSERT INTO t VALUES (null, null);
SELECT s1 <=> s2 FROM t; -- returns false; NULL <=> NULL must be true
```
Bugs 1, 3 and 4 date back to SPARK-33480 / SPARK-34233 (Spark 3.1) and are
independent of
`spark.sql.charVarchar.standardSemantics.enabled` — they reproduce with the
flag off. Under
that flag the padding rewrite is not the comparison mechanism at all (CHAR
is promoted to
STRING, and `c = 'a'` is already `false` there), so `IN` stays consistent
with `=`.
### Does this PR introduce _any_ user-facing change?
Yes. Queries that returned wrong results, or failed, now behave correctly:
| Query | Before | After |
|---|---|---|
| `c IN (null, 'A')` on `CHAR(2) COLLATE UTF8_LCASE` |
`DATATYPE_MISMATCH.DATA_DIFF_TYPES` | `true` |
| `c IN (null, 'a')` on `CHAR(2)` = `'a'` | `null` | `true` |
| `c IN (null, 'a', 'bcd')` | `null` | `true` |
| `STRUCT<c: CHAR(2), i: INT> = STRUCT<c: CHAR(5), i: INT>`, equal values |
`false` | `true` |
| `s1 <=> s2`, both NULL `STRUCT<c: CHAR(2)>` | `false` | `true` |
| `s1 = s2` where `s1` is NULL | `false` | `null` |
One error message improves: comparing `STRUCT<c: CHAR(2), m:
MAP<STRING,STRING>>` reports
`Cannot resolve "(s1 = s2)"` rather than the rewritten `named_struct(...)`
expression.
Apart from those results and that one message, the only other change is
analyzed-plan text:
fix (2) removes redundant same-type `Cast` nodes from any `In` with
heterogeneous element
types, e.g. `cast(a as int) IN (cast(null as int))` becomes `a IN (cast(null
as int))`. That
is why six golden `analyzer-results` files are regenerated. No golden
*result* file changed
anywhere in the repo.
### How was this patch tested?
Four new tests in `CharVarcharTestSuite`, in the shared trait so they run
under the
file-source, DSV2 and Hive suites:
- `char type IN list with a NULL ahead of the matching literal` — both
spellings of NULL
(`null` and `cast(null as string)`), NULL before / after / interleaved,
matching and
non-matching literals, literals that widen the comparison length,
partitioned and
non-partitioned tables, `spark.sql.readSideCharPadding=false` (the
predicate-padding path),
`NOT IN`, a correlated subquery where the value is an `OuterReference`,
and a collated
CHAR column.
- `char type comparison: multi-field struct` — five shapes: CHAR field first
with a non-CHAR
field last, the reverse order as a control, an all-CHAR struct where only
the first field
needs padding, a multi-field struct nested inside another struct, and one
inside an array.
- `char type comparison: struct nullability is preserved` — NULL structs on
either or both
sides for `=` and `<=>`, single-field and multi-field asserted to agree,
plus NULL arrays
and arrays containing NULL structs.
- `char type comparison: non-orderable struct keeps its original error` —
asserts the
reported `sqlExpr` is the user's expression.
Each was confirmed to fail before the corresponding fix and pass after.
Existing suites, all green:
| Run | Result |
|---|---|
| `sql/testOnly *CharVarchar*` | 719 passed |
| `hive/testOnly *CharVarchar*` | 75 passed |
| `catalyst/testOnly *TypeCoercionSuite *AnsiTypeCoercionSuite
*AnalysisSuite *FilterPushdownSuite *OptimizeInSuite` | 707 passed |
| `sql/testOnly org.apache.spark.sql.SQLQueryTestSuite` | 788 passed |
| `sql/testOnly *PlanStability*` (incl. TPCDS) | 322 passed, no plan churn |
`scalastyle` clean for `catalyst` and `sql` (main and test).
### Was this patch authored or co-authored using generative AI tooling?
No
--
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]