andygrove opened a new issue, #5353:
URL: https://github.com/apache/datafusion-comet/issues/5353

   ## Summary
   
   `upper` / `lower` (and their `ucase` / `lcase` aliases) have a native path 
already, gated off by
   default behind `spark.comet.caseConversion.enabled=false`. On the face of it 
they look like the same
   opportunity as #5351: existing kernel, blanket `Incompatible`, flip the 
default for a safe subset.
   
   They are not, and the reason is worth recording rather than rediscovering. 
**The only subset where the
   engines provably agree is all-ASCII input, which is a per-batch runtime 
property, and Comet chooses
   between the native path and the dispatcher at plan time during serde with no 
mechanism to defer a
   single batch.** On top of that, the non-ASCII behaviour is not one behaviour 
but three across the
   supported version matrix, one of which depends on the host JVM's default 
locale.
   
   Assessed with the `suggest-native-expression` skill. Verdict: **Deferred**, 
upside High, compatibility
   Low for the general case. Filing so the blocker is on record, because it is 
shared by a whole class of
   candidates rather than being specific to case conversion.
   
   ## How it runs today
   
   `class CometCaseConversionBase[T] extends CometScalarFunction[T](function) 
with NativeOptInAvailable`
   (`spark/src/main/scala/org/apache/comet/serde/strings.scala:50`), with
   `nativeOptInConfigKeyOverride = 
Some(CometConf.COMET_CASE_CONVERSION_ENABLED.key)`.
   
   - `getIncompatibleReasons()` is the blanket string "Results can vary 
depending on locale and character
     set".
   - The gate is the **global** `spark.comet.caseConversion.enabled` (default 
`false`), not a
     per-expression `allowIncompatible`. Its doc says "Java uses 
locale-specific rules when converting
     strings to upper or lower case and Rust does not, so we disable upper and 
lower by default."
   - Knock-on: `ilike` is `RuntimeReplaceable` and rewrites to `Like(Lower(l), 
Lower(r))`, so `ilike`
     inherits this path too.
   
   ## Native upside: High
   
   Dumped the real kernel for `Upper(BoundReference(0, StringType))` over a 
nullable `VarCharVector` via
   `CometBatchKernelCodegen.generateSource`. The emitted hot loop, on a Spark 
4.1 build:
   
   ```java
   if (!isNull_1) {
     value_0 = CollationSupport.Upper.execBinaryICU(value_1);
   }
   if (isNull_0) {
     output.setNull(i);
   } else {
     Object utfBase_0 = value_0.getBaseObject();
     int utfLen_0 = value_0.numBytes();
     if (utfBase_0 instanceof byte[]) {
       output.setSafe(i, (byte[]) utfBase_0, ..., utfLen_0);
     } else {
       byte[] utfArr_0 = value_0.getBytes();
       output.setSafe(i, utfArr_0, 0, utfArr_0.length);
     }
   }
   ```
   
   Per non-null row, on the ASCII path, `UTF8String.toUpperCaseAscii()` is 
`convertAscii(Character::toUpperCase)`,
   which allocates a fresh `byte[]` and wraps it in a new `UTF8String`: **2 
allocations per row,
   unconditionally**, about 16k per 8192-row batch. Unlike `replace` there is 
no short-circuit that
   returns the input unchanged, so every row pays.
   
   Being precise about what is **not** a cost: the output write takes the 
`instanceof byte[]` branch (the
   result is byte[]-backed), so there is no extra copy on the way into Arrow, 
and the input read is
   zero-copy.
   
   `upper` and `lower` are among the most common string expressions in real SQL 
(normalisation,
   case-insensitive matching), and `ilike` rides on `Lower`. So the upside 
rating is High. It is simply
   not collectable, see below.
   
   ## Compatibility: Low for the general case
   
   ### There are three different non-ASCII behaviours, not one
   
   | Spark | Path under UTF8_BINARY | Non-ASCII behaviour |
   | --- | --- | --- |
   | 3.4.3, 3.5.9 | `UTF8String.toUpperCase()` | ASCII fast path, else 
`fromString(toString().toUpperCase())` → **JVM default locale** |
   | 4.0.4, 4.1.3, 4.2.0 (default) | `CollationSupport.Upper.execBinaryICU` → 
`CollationAwareUTF8String.toUpperCase` | `isFullAscii()` fast path, else 
`UCharacter.toUpperCase(...)` → **ICU root locale** |
   | 4.0+ with `spark.sql.icu.caseMappings.enabled=false` | 
`CollationSupport.Upper.execBinary` → `UTF8String.toUpperCase()` | as 3.x, 
**JVM default locale** |
   
   `spark.sql.icu.caseMappings.enabled` defaults to **true** from Spark 4.0 
(`SQLConf.scala`), which is
   why the dumped 4.1 kernel calls `execBinaryICU`.
   
   A native kernel would have to reproduce ICU root-locale full case mapping on 
one version, the JDK's
   default-locale mapping on another, and pick between them from a config. The 
JDK branch is
   **host-dependent**: the same query on a JVM with `-Duser.language=tr` 
produces different output, and
   nothing in the serialised plan captures that.
   
   ### The agreed subset exists but cannot be gated
   
   Both sides agree on all-ASCII input, and notably **Spark itself branches on 
exactly that property**
   (`UTF8String.isFullAscii()`, and `CollationAwareUTF8String.toUpperCase` 
checks it first). So the subset
   is real and even has a name in Spark's own code.
   
   The problem is where the check can live:
   
   - It is not knowable at plan time. Nothing in the schema or the expression 
tells you whether a column
     is ASCII.
   - It is knowable per batch, cheaply, by scanning the values buffer.
   - But `getSupportLevel` runs once during serde, and the 
native-versus-dispatcher decision is baked
     into the serialised plan. A native kernel that discovers non-ASCII bytes 
mid-batch has nowhere to
     go: it cannot hand that batch to the JVM dispatcher.
   
   So the ASCII guard, which is what makes this look tractable, is not 
expressible in the current
   architecture. That is the blocker.
   
   ### Hazard checklist
   
   | Hazard | Applies? |
   | --- | --- |
   | JVM formatting / library APIs | **Yes.** ICU root-locale mapping on 4.x, 
JDK default-locale mapping on 3.x. Reimplementing either is the `Low` 
criterion. |
   | Dependence on host state beyond the plan | **Yes.** The JDK path reads the 
JVM default locale, which is not in the plan. |
   | Collation on Spark 4.0+ | **Yes.** Non-UTF8_BINARY collations route to 
`execLowercase` / `execICU` 
([#2190](https://github.com/apache/datafusion-comet/issues/2190), 
[#4496](https://github.com/apache/datafusion-comet/issues/4496)). |
   | Cross-version behavioural differences | **Yes.** Three behaviours across 
the matrix, config-selected. |
   | Backreferences / regex, decimal, timezone, invalid UTF-8, lambdas, 
nondeterminism | No |
   
   ## What would change the answer
   
   Either of these unblocks it. Both are bigger than this expression.
   
   1. **A per-batch defer-to-dispatcher mechanism.** A native kernel that can 
decline a batch (here: "not
      all ASCII") and have the operator route that batch through the JVM 
codegen dispatcher instead. This
      is adjacent to 
[#4825](https://github.com/apache/datafusion-comet/issues/4825) (partial project
      fallback) but per batch rather than per plan. With it, `upper` / `lower` 
become straightforward: the
      native kernel handles ASCII, everything else falls to Spark's own code, 
and the result stays exact.
   2. **A proof, with pinned Unicode versions, that Rust's `str::to_uppercase` 
/ `to_lowercase` agrees
      with ICU root-locale mapping for all inputs**, plus a decision that the 
3.x JDK-locale path is out
      of scope (for example by requiring 
`spark.sql.icu.caseMappings.enabled=true`). This is a narrower
      claim than "Rust matches Java" and might be checkable by brute force over 
the codepoint range, but
      it is real work and Unicode-version-sensitive.
   
   Until one of those lands, the current default (dispatcher, exact) is correct 
and this issue should stay
   open as the record of why.
   
   ### Note for the wider effort
   
   The same blocker applies to every candidate whose only safe guard is a data 
property rather than an
   expression property: `initcap` (hyphen-as-word-separator, data-dependent) 
and the JSON family
   (`get_json_object`, `from_json`, `to_json`, `length_of_json_array`, whose 
divergences are
   single-quoted JSON, unescaped control characters, and trailing content, all 
data-dependent). Landing
   mechanism 1 above would unlock that whole class at once, which is likely a 
better investment than
   attacking them one at a time.
   
   ---
   
   Filed by the `suggest-native-expression` skill. Motivation:
   [Native Coverage for Codegen-Dispatched 
Expressions](https://github.com/apache/datafusion-comet/blob/main/docs/source/contributor-guide/roadmap.md#native-coverage-for-codegen-dispatched-expressions).
   Assessment recorded in 
`docs/source/contributor-guide/expression-audits/string_funcs.md` under
   `## upper` and `## lower`. Earlier runs of the same skill produced #5347, 
#5349, and #5351.
   


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