felipepessoto opened a new issue, #12784:
URL: https://github.com/apache/gluten/issues/12784

   ### Backend
   
   VL (Velox)
   
   ### Bug description
   
   Gluten discards the **expression-level** `EvalMode` of `Cast` and instead 
lets Velox decide cast failure/overflow behaviour from the **session-level** 
`spark.sql.ansi.enabled`.
   
   Consequently, a `Cast` whose own `evalMode` is `LEGACY` — which in Spark 
must return `NULL` on failure — throws whenever the session flag happens to be 
`true`.
   
   This matters because Spark and Delta legitimately build such nodes: 
`Cast(..., evalMode = LEGACY)` is created by `TableOutputResolver` (and by 
Delta's `UpdateExpressionsSupport.castIfNeeded`) when 
`spark.sql.storeAssignmentPolicy=LEGACY`, **independently** of 
`spark.sql.ansi.enabled`. So a session can legitimately have 
`spark.sql.ansi.enabled=true` while an individual `Cast` node is `LEGACY`.
   
   ### Repro
   
   No Delta, no table, no write — static values only. Verified on Spark 3.5.5 
with the Gluten Velox bundle.
   
   ```scala
   import org.apache.spark.sql.Column
   import org.apache.spark.sql.catalyst.expressions.{Cast, EvalMode}
   import org.apache.spark.sql.functions.lit
   import org.apache.spark.sql.types.IntegerType
   
   // The session says ANSI ...
   spark.conf.set("spark.sql.ansi.enabled", "true")
   
   // ... but this Cast node explicitly says LEGACY, so Spark must return NULL.
   val c = new Column(Cast(lit("abc").expr, IntegerType, None, EvalMode.LEGACY))
   spark.range(1).select(c).show()
   ```
   
   Session configuration used:
   
   ```
   --conf spark.sql.ansi.enabled=true
   --conf spark.gluten.sql.ansiFallback.enabled=false
   --conf 
spark.sql.optimizer.excludedRules=org.apache.spark.sql.catalyst.optimizer.ConvertToLocalRelation,org.apache.spark.sql.catalyst.optimizer.ConstantFolding
   ```
   
   Notes on those two extra confs:
   * `spark.gluten.sql.ansiFallback.enabled=false` is exactly what Gluten's own 
Delta test harness sets 
(`backends-velox/src-delta40/test/scala/org/apache/spark/sql/delta/test/DeltaSQLCommandTest.scala`).
 With the default `true` the query falls back to vanilla and the bug is masked.
   * The excluded optimizer rules only prevent the literal from being 
folded/evaluated on the driver, so the cast is actually executed by Velox. They 
are not part of the bug.
   
   ### Actual behaviour
   
   | Engine | Result |
   | --- | --- |
   | Vanilla Spark 3.5.5 | `null` |
   | Gluten (Velox) | **throws** |
   
   ```
   org.apache.gluten.exception.GlutenException: Exception: VeloxUserError
   Error Source: USER
   Reason: Cannot cast VARCHAR 'abc' to INTEGER. Invalid leading character: ""
   ```
   
   ### Expected behaviour
   
   `null` on both engines. The `Cast` node's `evalMode` is `LEGACY`, so the 
session-level ANSI flag must not apply to it.
   
   ### Which casts are affected today
   
   Same harness, all with expression-level `evalMode = LEGACY` and session 
`spark.sql.ansi.enabled=true`. Every row should be a non-throwing result:
   
   | Cast | Vanilla | Gluten |
   | --- | --- | --- |
   | `string 'abc'` → `int` | `null` | **throws** |
   | `string '9999999999'` → `int` (overflow) | `null` | **throws** |
   | `string 'xyz'` → `date` | `null` | **throws** |
   | `decimal(4,1)` → `decimal(3,2)` (overflow) | `null` | `null` (see below) |
   | `decimal(4,1)` → `tinyint` (overflow) | `-25` | `-25` |
   | `bigint` → `int` (overflow) | `1410065407` | `1410065407` |
   | `double` → `int` (overflow) | `2147483647` | `2147483647` |
   
   The rows that currently agree do so only because Velox does not (yet) 
enforce ANSI for those particular conversions — not because Gluten handles 
`evalMode` correctly. As Velox adds ANSI coverage, more rows flip to "throws". 
That is precisely what just happened for `decimal → decimal`, see *Impact* 
below.
   
   ### Root cause
   
   1. 
`gluten-substrait/src/main/scala/org/apache/gluten/expression/UnaryExpressionTransformer.scala`
 (`CastTransformer`) forwards only whether the mode is `TRY`:
   
      ```scala
      ExpressionBuilder.makeCast(
        typeNode,
        child.doTransform(context),
        SparkShimLoader.getSparkShims.withTryEvalMode(original))
      ```
   
      `EvalMode.LEGACY` and `EvalMode.ANSI` therefore produce **identical** 
Substrait nodes — the distinction is lost before it ever reaches native code.
   
   2. 
`gluten-substrait/src/main/scala/org/apache/gluten/config/GlutenConfig.scala` 
copies the session-level flag into the native conf map:
   
      ```scala
      (SQLConf.ANSI_ENABLED.key, SQLConf.ANSI_ENABLED.defaultValueString)
      ```
   
   3. `cpp/core/config/GlutenConfig.h` reads it natively:
   
      ```cpp
      const std::string kAnsiEnabled = "spark.sql.ansi.enabled";
      ```
   
   So Velox applies one query-wide ANSI setting to **every** cast, rather than 
honouring each `Cast`'s own `evalMode`.
   
   This is already acknowledged in Gluten's own test code — 
`gluten-ut/spark40/src/test/scala/org/apache/spark/sql/catalyst/expressions/GlutenCastWithAnsiOnSuite.scala`:
   
   > `CastWithAnsiOnSuite` creates `Cast` expressions with `EvalMode.ANSI` but 
does not set the session-level ANSI config. Velox reads ANSI mode from session 
config to decide cast behavior [...]. We must sync session config with the 
expression-level `evalMode` [...]
   
   ### Impact
   
   This is the root cause of the Delta CI regressions tracked in #12779.
   
   Delta's `ImplicitMergeCastingSuite` / `TypeWideningInsertSchemaEvolution*` 
tests run with `spark.sql.storeAssignmentPolicy=LEGACY` and 
`spark.sql.ansi.enabled=true`, so Delta emits `Cast(evalMode = LEGACY)` and the 
tests expect **no exception**. Gluten reports ANSI to Velox and Velox throws.
   
   Those tests only started failing after the `2026_08_14` daily Velox bump 
(#12769), which pulled in upstream Velox `a96d0415c` *"Enable ANSI-compliant 
cast from DECIMAL to DECIMAL"*. Before that bump Velox simply had no ANSI 
enforcement for `decimal → decimal`, which masked the defect for that 
conversion — on a pre-bump build even a genuine `EvalMode.ANSI` `decimal → 
decimal` cast returns `null`. The Velox change is correct; it merely exposed 
the pre-existing `evalMode` handling gap in Gluten.
   
   ### Suggested fix
   
   Propagate the per-expression `EvalMode` (`LEGACY` / `ANSI` / `TRY`) through 
Substrait to Velox and evaluate each cast accordingly, instead of relying on 
the session-wide `spark.sql.ansi.enabled`.
   
   ### Spark version
   
   Reproduced on Spark 3.5.5 (Gluten `1.8.0-SNAPSHOT`). The mechanism is 
version independent and applies to Spark 3.4+ where `Cast` carries `evalMode`.
   
   ### Spark configurations
   
   ```
   spark.sql.ansi.enabled=true
   spark.gluten.sql.ansiFallback.enabled=false
   ```
   
   ### Are you willing to submit a PR?
   
   - [ ] Yes, I am willing to submit a PR!
   


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