andygrove commented on code in PR #4763:
URL: https://github.com/apache/datafusion-comet/pull/4763#discussion_r3579953777


##########
native/spark-expr/src/conversion_funcs/cast.rs:
##########
@@ -853,20 +859,78 @@ fn spark_binary_formatter(value: &[u8], 
binary_output_style: BinaryOutputStyle)
     }
 }
 
-fn cast_binary_formatter(value: &[u8]) -> String {
-    match String::from_utf8(value.to_vec()) {
-        Ok(value) => value,
-        Err(_) => unsafe { String::from_utf8_unchecked(value.to_vec()) },
-    }
+fn cast_binary_formatter(value: &[u8]) -> Cow<'_, str> {
+    // CAST(binary AS string) reinterprets the bytes as UTF-8, like Spark's 
UTF8String.fromBytes.
+    // Spark keeps the raw bytes, but Arrow's Utf8 type requires valid UTF-8, 
and building a String
+    // from non-UTF-8 bytes is undefined behaviour (#4488). Decode 
JVM-compatibly-lossily instead:
+    // `decode_utf8_spark_lossy` replaces ill-formed sequences with U+FFFD 
exactly as Spark's
+    // `new String(bytes, UTF_8)` does (the same decoder Comet's native 
shuffle uses, #4521). The
+    // result is memory-safe valid UTF-8, never feeds invalid bytes into 
downstream native string
+    // kernels, and matches Spark's rendered output byte-for-byte. It diverges 
from Spark only under
+    // byte-level round-trips such as CAST(CAST(x AS string) AS binary), where 
Spark still has the
+    // original bytes and Comet has the U+FFFD replacements. Returning `Cow` 
lets the valid path
+    // borrow so the caller appends without an intermediate allocation.
+    decode_utf8_spark_lossy(value)

Review Comment:
   You are right, and the docs in this PR understated the divergence: they only 
called out byte-level round-trips. Distinct ill-formed sequences all decode to 
the same U+FFFD, so `CAST(b AS STRING) = CAST(X'EFBFBD' AS STRING)` is `false` 
in Spark (UTF8String compares the raw bytes) and `true` in Comet, and the same 
collapse can affect joins, grouping, ordering, and byte-based functions such as 
`contains`. fb126fdf8 documents that class explicitly in the compatibility 
guide, the cast expression-audit note, and the comment on 
`cast_binary_formatter`.
   
   I have kept the cast `Compatible` rather than downgrading it, for two 
reasons:
   
   1. Comet already behaves this way independently of the cast. The native 
columnar shuffle decodes with the same `decode_utf8_spark_lossy` (#4521), so an 
invalid-UTF-8 string that goes through a Comet shuffle already collapses to 
U+FFFD regardless of how it was produced. Making just this one cast fall back 
would not restore byte-exact string identity in a Comet plan.
   
   2. Falling back would trade this divergence for the UB the PR removes. If 
the cast runs on the JVM, Spark produces a raw-byte `UTF8String`, and that 
value then enters the native pipeline through the unchecked Arrow FFI import 
(`from_ffi` does not validate UTF-8), where any downstream native string kernel 
reading it via `StringArray::value` materializes an invalid `&str`. That is Gap 
B in #4764.
   
   The real fix is a single ingress policy (decode, do not reinterpret, at 
every boundary), tracked by the epic #4764, which the compatibility guide now 
links from this section. Happy to be argued out of this if you think the 
value-identity divergence is severe enough to warrant gating the cast in the 
meantime.



##########
docs/source/contributor-guide/adding_a_new_expression.md:
##########
@@ -478,6 +478,21 @@ pub fn spark_ceil(
 }
 ```
 
+#### Producing strings from arbitrary bytes
+
+Spark's `StringType` may contain bytes that are not valid UTF-8 (Spark stores 
them verbatim), but
+Arrow's string arrays must be valid UTF-8. Any native code that turns 
arbitrary bytes into a string
+must therefore **decode** them, not reinterpret them. Building a Rust 
`String`/`&str` from non-UTF-8
+bytes is undefined behaviour, and an Arrow string array that holds invalid 
UTF-8 is unsound for the
+downstream kernels that read it via `StringArray::value` (which assumes the 
UTF-8 invariant).
+
+Use `crate::utils::decode_utf8_spark_lossy`. It replaces each ill-formed 
sequence with `U+FFFD`

Review Comment:
   Fixed in fb126fdf8: the reference now points at 
`datafusion_comet_common::decode_utf8_spark_lossy` 
(`native/common/src/utils.rs`), which is where it moved in the previous round.



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