andygrove opened a new pull request, #4945:
URL: https://github.com/apache/datafusion-comet/pull/4945

   ## Which issue does this PR close?
   
   Part of #4764 (EPIC: consistent handling of invalid UTF-8 in native 
StringType). This PR implements **Gap B** only, decoding invalid UTF-8 at the 
JVM to native Arrow FFI import boundary. It does not close the EPIC, which 
still tracks Gap A (the native scan rejecting invalid UTF-8).
   
   ## Rationale for this change
   
   Spark's `StringType` (`UTF8String`) can hold arbitrary bytes, including 
sequences that are not valid UTF-8. When a JVM side source hands string columns 
to native code over the Arrow C Data Interface, arrow-rs imports them with 
`from_ffi` / `from_ffi_and_data_type`, which build the array via 
`ArrayData::new_unchecked` and do not validate UTF-8. The imported Arrow `Utf8` 
/ `LargeUtf8` array then lies about its validity, and any downstream native 
string kernel that reads `&str` through arrow-rs's unchecked 
`StringArray::value()` (`from_utf8_unchecked`) exercises undefined behaviour: 
iterating chars, slicing on char boundaries, and similar operations can 
misbehave, panic, or be miscompiled. This is a latent, default configuration 
soundness hazard.
   
   The string producing sites already decode invalid bytes the way Spark 
renders them: `CAST(binary AS string)` (#4763) and native shuffle `get_string` 
(#4521) both use `decode_utf8_spark_lossy`. This PR applies the same policy to 
the string ingress side, so the whole native pipeline agrees on a single 
invariant: native string data is always valid UTF-8.
   
   ## What changes are included in this PR?
   
   - A new `decode_string_arrays` walker in `datafusion-comet-common`, beside 
the existing `decode_utf8_spark_lossy`. It ensures every `Utf8` / `LargeUtf8` 
array reachable from an imported column holds valid UTF-8, decoding invalid 
bytes to Spark's rendered form. It is zero copy for the valid common case (a 
single `from_utf8` validation pass plus an O(number of strings) boundary check, 
returning the same `Arc`), and rebuilds element by element only when bytes are 
genuinely invalid. It recurses through `Dictionary`, `Struct`, `List`, 
`LargeList`, `FixedSizeList`, and `Map`.
   - The walker is called at the three JVM to native FFI import sites that 
carry string data:
     - `ScanExec::pull_next`, which handles all native query input (the 
native_comet JVM reader, Spark columnar handoff, shuffle reads, mapInArrow). 
Decoding runs before dictionary unpack so compact dictionary values are 
validated rather than the expanded ones.
     - `columnarToRow` conversion.
     - JVM UDF result.
   - The fast path validates the used byte range and also confirms no element 
boundary splits a codepoint. This second check is required for soundness: a 
whole buffer valid `"é"` (bytes `C3 A9`) split across two offsets would 
otherwise hand each element an invalid slice, and `value()` decodes those 
unchecked.
   - A criterion benchmark for the valid fast path.
   
   No configuration flag gates this. It fixes undefined behaviour in the 
default configuration and matches the always on behaviour of the sibling cast 
and shuffle fixes.
   
   The only observable divergence from Spark is the previously documented one: 
decoding rather than preserving raw bytes differs only under byte level round 
trips (for example `CAST(CAST(X'FF' AS STRING) AS BINARY)`), already noted in 
the compatibility guide.
   
   ## How are these changes tested?
   
   - Rust unit tests for the walker covering: valid input returned zero copy 
(pointer identical buffers); invalid bytes decoding to U+FFFD matching the JVM; 
the split codepoint boundary case; nested `Dictionary` / `Struct` / `List` / 
`FixedSizeList` / `Map` (decode and zero copy paths); null preservation; sliced 
arrays with a non zero starting offset; and trailing empty strings.
   - A Rust test at the `ScanExec` import site proving an invalid UTF-8 column 
is decoded through the production per column path.
   - A criterion benchmark measuring the valid fast path (about 120 
microseconds for an 8192 row batch, a single validation pass with a zero copy 
return).
   


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