You are viewing a plain text version of this content. The canonical link for it is here.
Posted to github@datafusion.apache.org by "andygrove (via GitHub)" <gi...@apache.org> on 2026/07/30 17:42:50 UTC

[PR] fix: match Spark's whitespace trim semantics for casts from string to boolean, integral, float/double and decimal [datafusion-comet]

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

   ## Which issue does this PR close?
   
   Part of #5149. This PR covers the first five subtasks of that epic; the timestamp targets and the latent `to_time.rs` trim are left for follow-ups.
   
   ## Rationale for this change
   
   Comet's cast-from-string kernels used four different trim sets, three of which diverge from Spark. Spark has exactly two trim regimes, and neither of them trims any non-ASCII whitespace:
   
   | Regime | Trimmed bytes | Cast targets |
   | --- | --- | --- |
   | `UTF8String.trimAll` | `0x00`-`0x20` and `0x7F` | boolean, byte, short, int, long, date, timestamp, timestamp_ntz |
   | `java.lang.String.trim` | `0x00`-`0x20` | float, double, decimal |
   
   Every affected cast is `Compatible()` in `CometCast.canCastFromString`, so all of them run natively by default. The divergence went in both directions: spurious failures where Spark parses fine, and silent wrong results (with the ANSI error swallowed) where Spark returns `NULL` or raises.
   
   ```sql
   -- before this PR
   SELECT CAST(concat(char(1), 'true') AS boolean);  -- Spark: true,  Comet: NULL (throws under ANSI)
   SELECT CAST(concat(char(1), '123')  AS int);      -- Spark: 123,   Comet: NULL (throws under ANSI)
   SELECT CAST(concat(' ', 'true') AS boolean);     -- Spark: NULL,  Comet: true
   SELECT CAST(concat(' ', '1.5')  AS double);      -- Spark: NULL,  Comet: 1.5
   ```
   
   ## What changes are included in this PR?
   
   - New `native/spark-expr/src/conversion_funcs/trim.rs` with the two regimes as explicit, documented helpers (`trim_all` / `trim_all_bytes` and `trim_java_string`), unit-tested against the byte sets themselves rather than against cast results.
   - `CAST(string AS boolean)` uses the `trimAll` set. The keyword comparison is now allocation-free (`eq_ignore_ascii_case`) instead of `to_ascii_lowercase()` per row.
   - `CAST(string AS byte/short/int/long)` uses the `trimAll` set in all three eval modes. `<[u8]>::trim_ascii` was excluding `0x0B`, which Spark trims.
   - `CAST(string AS float/double)` and `CAST(string AS decimal)` use the `String.trim` set, leaving `0x7F` untrimmed and preserving the existing NUL handling (NUL is in that set).
   - `date_parser` was already correct; it now shares the predicate instead of recomputing it, so there is one definition of the `trimAll` set.
   - Docs: the compatibility page documents both regimes; the boolean entry in the known-divergences list is replaced by an entry for the remaining timestamp divergence.
   
   Not in this PR (tracked on #5149): `CAST(string AS timestamp)` / `timestamp_ntz`, including `timestamp_parser`'s Spark-4 leading-whitespace gate, and the latent `to_time.rs` trim.
   
   ## How are these changes tested?
   
   - Rust: the trim helpers are tested byte-by-byte against Java's `Character.isWhitespace`/`isISOControl` definitions, plus a parity matrix (leading / trailing / both / interior position × all three eval modes) for every fixed cast target. Reverting any one of the four kernel changes fails the corresponding matrix test.
   - Scala: three new `CometCastSuite` tests run the same codepoint matrix through `castTest`, so real Spark is the oracle for values, `NULL`s and ANSI error messages.
   - Full `CometCastSuite` and the `cast` SQL-file tests pass on Spark 3.5.
   


-- 
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: github-unsubscribe@datafusion.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


---------------------------------------------------------------------
To unsubscribe, e-mail: github-unsubscribe@datafusion.apache.org
For additional commands, e-mail: github-help@datafusion.apache.org


Re: [PR] fix: match Spark's whitespace trim semantics for casts from string to boolean, integral, float/double and decimal [datafusion-comet]

Posted by "andygrove (via GitHub)" <gi...@apache.org>.
andygrove commented on code in PR #5150:
URL: https://github.com/apache/datafusion-comet/pull/5150#discussion_r3693361681


##########
native/spark-expr/src/conversion_funcs/trim.rs:
##########
@@ -0,0 +1,196 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+//! Whitespace trimming for parsing from string.
+//!
+//! Spark's string casts do not all agree on what "whitespace" means. There are exactly two
+//! regimes, and neither of them matches Rust's `str::trim` (which trims Unicode whitespace) or
+//! `<[u8]>::trim_ascii` (which omits `0x0B`):
+//!
+//! | Regime               | Trimmed bytes            | Cast targets                                          |
+//! |----------------------|--------------------------|-------------------------------------------------------|
+//! | [`trim_all`]         | `0x00`-`0x20` and `0x7F` | boolean, byte, short, int, long, date, timestamp \*    |
+//! | [`trim_java_string`] | `0x00`-`0x20`            | float, double, decimal                                |
+//!
+//! Crucially, **neither regime trims any non-ASCII whitespace**. `U+0085`, `U+00A0`, `U+1680`,
+//! `U+2000`-`U+200A`, `U+2028`, `U+2029`, `U+202F`, `U+205F` and `U+3000` all leave Spark
+//! returning NULL (or raising under ANSI) for every cast target, so using `str::trim` here
+//! silently produces a value where Spark produces none.
+//!
+//! The two regimes differ only in `0x7F` (DELETE), which the `trimAll` set removes and the
+//! `String.trim` set does not. That single byte is why a shared helper cannot be applied
+//! uniformly: trimming it in the float/double/decimal paths would introduce a new divergence.
+//!
+//! \* `timestamp` and `timestamp_ntz` are listed for what Spark does; the Comet parsers for
+//! those two targets still use `str::trim` and have not been migrated to these helpers

Review Comment:
   Addressed in 434efec — sorry for answering this only in a summary comment rather than here.
   
   The `trim.rs` module doc keeps the regime table and the footnote; both paragraphs that restated the table are gone, replaced by one line. `is_whitespace_or_iso_control`, `trim_all` and `trim_java_string` are now pointers at that table rather than re-listing byte ranges and cast targets, so the mapping has a single place to go stale. Same treatment for `trimPadding`'s Scaladoc in `CometCastSuite`.
   
   I kept only the two things not derivable from the code: the signed-byte widening note, and the JDK call paths (`Double.parseDouble` / `Decimal.stringToJavaBigDecimal`) that justify the `String.trim` regime — the latter moved into the module doc so it is not duplicated in a function doc.



##########
native/spark-expr/src/conversion_funcs/trim.rs:
##########
@@ -0,0 +1,196 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+//! Whitespace trimming for parsing from string.
+//!
+//! Spark's string casts do not all agree on what "whitespace" means. There are exactly two
+//! regimes, and neither of them matches Rust's `str::trim` (which trims Unicode whitespace) or
+//! `<[u8]>::trim_ascii` (which omits `0x0B`):
+//!
+//! | Regime               | Trimmed bytes            | Cast targets                                          |
+//! |----------------------|--------------------------|-------------------------------------------------------|
+//! | [`trim_all`]         | `0x00`-`0x20` and `0x7F` | boolean, byte, short, int, long, date, timestamp \*    |
+//! | [`trim_java_string`] | `0x00`-`0x20`            | float, double, decimal                                |
+//!
+//! Crucially, **neither regime trims any non-ASCII whitespace**. `U+0085`, `U+00A0`, `U+1680`,
+//! `U+2000`-`U+200A`, `U+2028`, `U+2029`, `U+202F`, `U+205F` and `U+3000` all leave Spark
+//! returning NULL (or raising under ANSI) for every cast target, so using `str::trim` here
+//! silently produces a value where Spark produces none.
+//!
+//! The two regimes differ only in `0x7F` (DELETE), which the `trimAll` set removes and the
+//! `String.trim` set does not. That single byte is why a shared helper cannot be applied
+//! uniformly: trimming it in the float/double/decimal paths would introduce a new divergence.

Review Comment:
   Fixed in 434efec: both duplicated paragraphs (the non-ASCII-whitespace one and the regime restatement) are gone, replaced by a single line, with the regime table and footnote left as the one authoritative place.



##########
native/spark-expr/src/conversion_funcs/trim.rs:
##########
@@ -0,0 +1,196 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+//! Whitespace trimming for parsing from string.
+//!
+//! Spark's string casts do not all agree on what "whitespace" means. There are exactly two
+//! regimes, and neither of them matches Rust's `str::trim` (which trims Unicode whitespace) or
+//! `<[u8]>::trim_ascii` (which omits `0x0B`):
+//!
+//! | Regime               | Trimmed bytes            | Cast targets                                          |
+//! |----------------------|--------------------------|-------------------------------------------------------|
+//! | [`trim_all`]         | `0x00`-`0x20` and `0x7F` | boolean, byte, short, int, long, date, timestamp \*    |
+//! | [`trim_java_string`] | `0x00`-`0x20`            | float, double, decimal                                |
+//!
+//! Crucially, **neither regime trims any non-ASCII whitespace**. `U+0085`, `U+00A0`, `U+1680`,
+//! `U+2000`-`U+200A`, `U+2028`, `U+2029`, `U+202F`, `U+205F` and `U+3000` all leave Spark
+//! returning NULL (or raising under ANSI) for every cast target, so using `str::trim` here
+//! silently produces a value where Spark produces none.
+//!
+//! The two regimes differ only in `0x7F` (DELETE), which the `trimAll` set removes and the
+//! `String.trim` set does not. That single byte is why a shared helper cannot be applied
+//! uniformly: trimming it in the float/double/decimal paths would introduce a new divergence.
+//!
+//! \* `timestamp` and `timestamp_ntz` are listed for what Spark does; the Comet parsers for
+//! those two targets still use `str::trim` and have not been migrated to these helpers
+//! (<https://github.com/apache/datafusion-comet/issues/5149>).
+
+/// True for the bytes trimmed by `org.apache.spark.unsafe.types.UTF8String.trimAll`, i.e. the
+/// bytes `b` for which `Character.isWhitespace(b) || Character.isISOControl(b)` holds.
+///
+/// `isWhitespace` covers `0x09`-`0x0D`, `0x1C`-`0x1F` and `0x20`; `isISOControl` covers
+/// `0x00`-`0x1F` and `0x7F`; the union is `0x00`-`0x20` plus `0x7F`. Spark widens a *signed*
+/// `byte` into the `int` overload, so bytes `0x80`-`0xFF` arrive negative and are never trimmed.

Review Comment:
   Fixed in 434efec: `is_whitespace_or_iso_control`'s doc no longer spells out either byte range or their union. It now points at the module table, with only the signed-byte widening note kept, since that part is not obvious from the expression.



##########
native/spark-expr/src/conversion_funcs/trim.rs:
##########
@@ -0,0 +1,196 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+//! Whitespace trimming for parsing from string.
+//!
+//! Spark's string casts do not all agree on what "whitespace" means. There are exactly two
+//! regimes, and neither of them matches Rust's `str::trim` (which trims Unicode whitespace) or
+//! `<[u8]>::trim_ascii` (which omits `0x0B`):
+//!
+//! | Regime               | Trimmed bytes            | Cast targets                                          |
+//! |----------------------|--------------------------|-------------------------------------------------------|
+//! | [`trim_all`]         | `0x00`-`0x20` and `0x7F` | boolean, byte, short, int, long, date, timestamp \*    |
+//! | [`trim_java_string`] | `0x00`-`0x20`            | float, double, decimal                                |
+//!
+//! Crucially, **neither regime trims any non-ASCII whitespace**. `U+0085`, `U+00A0`, `U+1680`,
+//! `U+2000`-`U+200A`, `U+2028`, `U+2029`, `U+202F`, `U+205F` and `U+3000` all leave Spark
+//! returning NULL (or raising under ANSI) for every cast target, so using `str::trim` here
+//! silently produces a value where Spark produces none.
+//!
+//! The two regimes differ only in `0x7F` (DELETE), which the `trimAll` set removes and the
+//! `String.trim` set does not. That single byte is why a shared helper cannot be applied
+//! uniformly: trimming it in the float/double/decimal paths would introduce a new divergence.
+//!
+//! \* `timestamp` and `timestamp_ntz` are listed for what Spark does; the Comet parsers for
+//! those two targets still use `str::trim` and have not been migrated to these helpers
+//! (<https://github.com/apache/datafusion-comet/issues/5149>).
+
+/// True for the bytes trimmed by `org.apache.spark.unsafe.types.UTF8String.trimAll`, i.e. the
+/// bytes `b` for which `Character.isWhitespace(b) || Character.isISOControl(b)` holds.
+///
+/// `isWhitespace` covers `0x09`-`0x0D`, `0x1C`-`0x1F` and `0x20`; `isISOControl` covers
+/// `0x00`-`0x1F` and `0x7F`; the union is `0x00`-`0x20` plus `0x7F`. Spark widens a *signed*
+/// `byte` into the `int` overload, so bytes `0x80`-`0xFF` arrive negative and are never trimmed.
+#[inline]
+const fn is_whitespace_or_iso_control(b: u8) -> bool {
+    b <= 0x20 || b == 0x7F
+}
+
+/// True for the bytes trimmed by `java.lang.String.trim`, which drops any char `<= U+0020`.
+///
+/// A char above `U+0020` always encodes to bytes `>= 0x80` in UTF-8, so testing bytes rather
+/// than chars gives the same answer.
+#[inline]
+const fn is_java_trim_byte(b: u8) -> bool {
+    b <= 0x20
+}
+
+/// Trims the `UTF8String.trimAll` byte set (`0x00`-`0x20` and `0x7F`) from both ends.
+///
+/// This is the trim used by `CAST(string AS boolean)`, the integral casts and `date_parser`.
+/// See the [module docs](self) for why the other targets need [`trim_java_string`].

Review Comment:
   Fixed in 434efec — `trim_all`'s doc is now just:
   
   ```
   /// See the [module docs](self) for which cast targets use this regime and which use
   /// [`trim_java_string`].
   ```



##########
native/spark-expr/src/conversion_funcs/trim.rs:
##########
@@ -0,0 +1,196 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+//! Whitespace trimming for parsing from string.
+//!
+//! Spark's string casts do not all agree on what "whitespace" means. There are exactly two
+//! regimes, and neither of them matches Rust's `str::trim` (which trims Unicode whitespace) or
+//! `<[u8]>::trim_ascii` (which omits `0x0B`):
+//!
+//! | Regime               | Trimmed bytes            | Cast targets                                          |
+//! |----------------------|--------------------------|-------------------------------------------------------|
+//! | [`trim_all`]         | `0x00`-`0x20` and `0x7F` | boolean, byte, short, int, long, date, timestamp \*    |
+//! | [`trim_java_string`] | `0x00`-`0x20`            | float, double, decimal                                |
+//!
+//! Crucially, **neither regime trims any non-ASCII whitespace**. `U+0085`, `U+00A0`, `U+1680`,
+//! `U+2000`-`U+200A`, `U+2028`, `U+2029`, `U+202F`, `U+205F` and `U+3000` all leave Spark
+//! returning NULL (or raising under ANSI) for every cast target, so using `str::trim` here
+//! silently produces a value where Spark produces none.
+//!
+//! The two regimes differ only in `0x7F` (DELETE), which the `trimAll` set removes and the
+//! `String.trim` set does not. That single byte is why a shared helper cannot be applied
+//! uniformly: trimming it in the float/double/decimal paths would introduce a new divergence.
+//!
+//! \* `timestamp` and `timestamp_ntz` are listed for what Spark does; the Comet parsers for
+//! those two targets still use `str::trim` and have not been migrated to these helpers
+//! (<https://github.com/apache/datafusion-comet/issues/5149>).
+
+/// True for the bytes trimmed by `org.apache.spark.unsafe.types.UTF8String.trimAll`, i.e. the
+/// bytes `b` for which `Character.isWhitespace(b) || Character.isISOControl(b)` holds.
+///
+/// `isWhitespace` covers `0x09`-`0x0D`, `0x1C`-`0x1F` and `0x20`; `isISOControl` covers
+/// `0x00`-`0x1F` and `0x7F`; the union is `0x00`-`0x20` plus `0x7F`. Spark widens a *signed*
+/// `byte` into the `int` overload, so bytes `0x80`-`0xFF` arrive negative and are never trimmed.
+#[inline]
+const fn is_whitespace_or_iso_control(b: u8) -> bool {
+    b <= 0x20 || b == 0x7F
+}
+
+/// True for the bytes trimmed by `java.lang.String.trim`, which drops any char `<= U+0020`.
+///
+/// A char above `U+0020` always encodes to bytes `>= 0x80` in UTF-8, so testing bytes rather
+/// than chars gives the same answer.
+#[inline]
+const fn is_java_trim_byte(b: u8) -> bool {
+    b <= 0x20
+}
+
+/// Trims the `UTF8String.trimAll` byte set (`0x00`-`0x20` and `0x7F`) from both ends.
+///
+/// This is the trim used by `CAST(string AS boolean)`, the integral casts and `date_parser`.
+/// See the [module docs](self) for why the other targets need [`trim_java_string`].
+#[inline]
+pub(crate) fn trim_all(s: &str) -> &str {
+    let (start, end) = trim_all_range(s.as_bytes());
+    &s[start..end]
+}
+
+/// [`trim_all`] over a byte slice, for parsers that already work on bytes.
+#[inline]
+pub(crate) fn trim_all_bytes(bytes: &[u8]) -> &[u8] {
+    trim_bytes(bytes, is_whitespace_or_iso_control).0
+}
+
+/// The byte offsets [`trim_all`] would slice at, for parsers that need to keep a cursor into
+/// the untrimmed input.
+#[inline]
+pub(crate) fn trim_all_range(bytes: &[u8]) -> (usize, usize) {
+    let (trimmed, start) = trim_bytes(bytes, is_whitespace_or_iso_control);
+    (start, start + trimmed.len())
+}
+
+/// Trims the `java.lang.String.trim` byte set (`0x00`-`0x20`, keeping `0x7F`) from both ends.
+///
+/// This is the trim used by `CAST(string AS float/double)` (via `Double.parseDouble`, which
+/// calls `String.trim` before parsing) and by `CAST(string AS decimal)` (via
+/// `Decimal.stringToJavaBigDecimal`, which does `str.toString.trim`).

Review Comment:
   Fixed in 434efec the same way: `trim_java_string`'s doc points at the module table instead of re-listing float/double/decimal. The JDK call paths moved up into the module doc, so they are stated once rather than in both places.



##########
spark/src/test/scala/org/apache/comet/CometCastSuite.scala:
##########
@@ -811,6 +811,43 @@ class CometCastSuite extends CometTestBase with AdaptiveSparkPlanHelper {
     castTest(testValues, DataTypes.BooleanType)
   }
 
+  /**
+   * Padding used to check that Comet trims exactly the byte set that each Spark cast trims. Spark
+   * has two trim regimes and neither of them trims any non-ASCII whitespace:
+   *   - `UTF8String.trimAll` (bytes `0x00`-`0x20` and `0x7F`) for boolean, integral and datetime
+   *   - `java.lang.String.trim` (bytes `0x00`-`0x20` only) for float, double and decimal
+   *
+   * Spark itself is the oracle here, so the expectations do not need to be spelled out. See
+   * https://github.com/apache/datafusion-comet/issues/5149.

Review Comment:
   Fixed in 434efec: `trimPadding`'s Scaladoc now points at `conversion_funcs::trim` instead of re-deriving both regimes in Scala, so the Rust table is the only copy.



-- 
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: github-unsubscribe@datafusion.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


---------------------------------------------------------------------
To unsubscribe, e-mail: github-unsubscribe@datafusion.apache.org
For additional commands, e-mail: github-help@datafusion.apache.org


Re: [PR] fix: match Spark's whitespace trim semantics for casts from string to boolean, integral, float/double and decimal [datafusion-comet]

Posted by "andygrove (via GitHub)" <gi...@apache.org>.
andygrove merged PR #5150:
URL: https://github.com/apache/datafusion-comet/pull/5150


-- 
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: github-unsubscribe@datafusion.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


---------------------------------------------------------------------
To unsubscribe, e-mail: github-unsubscribe@datafusion.apache.org
For additional commands, e-mail: github-help@datafusion.apache.org


Re: [PR] fix: match Spark's whitespace trim semantics for casts from string to boolean, integral, float/double and decimal [datafusion-comet]

Posted by "andygrove (via GitHub)" <gi...@apache.org>.
andygrove commented on code in PR #5150:
URL: https://github.com/apache/datafusion-comet/pull/5150#discussion_r3693364347


##########
native/spark-expr/benches/cast_string_to_timestamp.rs:
##########
@@ -0,0 +1,210 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+use arrow::array::{builder::StringBuilder, RecordBatch};
+use arrow::datatypes::{DataType, Field, Schema, TimeUnit};
+use criterion::{criterion_group, criterion_main, Criterion};
+use datafusion::physical_expr::{expressions::Column, PhysicalExpr};
+use datafusion_comet_spark_expr::{Cast, EvalMode, SparkCastOptions};
+use std::sync::Arc;
+
+const BATCH_SIZE: usize = 8192;
+
+fn criterion_benchmark(c: &mut Criterion) {
+    let expr = Arc::new(Column::new("a", 0)) as Arc<dyn PhysicalExpr>;
+
+    // Input shapes, chosen to cover each branch `timestamp_parser` can take: the canonical
+    // form, the fractional-second form, an offset suffix (which takes the extract-offset
+    // path), a date-only string, whitespace padding (the trim), and a mix that includes
+    // invalid values so the null path is measured too.
+    let batches = [
+        (
+            "canonical",
+            create_batch(|i| {
+                format!(
+                    "{:04}-{:02}-{:02} {:02}:{:02}:{:02}",
+                    1970 + i % 60,
+                    i % 12 + 1,
+                    i % 28 + 1,
+                    i % 24,
+                    i % 60,
+                    i % 60
+                )
+            }),
+        ),
+        (
+            "microseconds",
+            create_batch(|i| {
+                format!(
+                    "{:04}-{:02}-{:02}T{:02}:{:02}:{:02}.{:06}",
+                    1970 + i % 60,
+                    i % 12 + 1,
+                    i % 28 + 1,
+                    i % 24,
+                    i % 60,
+                    i % 60,
+                    i % 1_000_000
+                )
+            }),
+        ),
+        (
+            "offset_suffix",
+            create_batch(|i| {
+                format!(
+                    "{:04}-{:02}-{:02}T{:02}:{:02}:{:02}+05:30",
+                    1970 + i % 60,
+                    i % 12 + 1,
+                    i % 28 + 1,
+                    i % 24,
+                    i % 60,
+                    i % 60
+                )
+            }),
+        ),
+        (
+            "date_only",
+            create_batch(|i| format!("{:04}-{:02}-{:02}", 1970 + i % 60, i % 12 + 1, i % 28 + 1)),
+        ),
+        (
+            "padded",
+            create_batch(|i| {
+                format!(
+                    "  {:04}-{:02}-{:02} {:02}:00:00  ",
+                    1970 + i % 60,
+                    i % 12 + 1,
+                    i % 28 + 1,
+                    i % 24
+                )
+            }),
+        ),
+        (
+            "mixed",
+            create_batch(|i| match i % 5 {
+                0 => format!(
+                    "{:04}-{:02}-{:02} 12:34:56",
+                    1970 + i % 60,
+                    i % 12 + 1,
+                    i % 28 + 1
+                ),
+                1 => format!(
+                    "{:04}-{:02}-{:02}T12:34:56.123456Z",
+                    1970 + i % 60,
+                    i % 12 + 1,
+                    i % 28 + 1
+                ),
+                2 => format!(
+                    "  {:04}-{:02}-{:02}  ",
+                    1900 + i % 200,
+                    i % 12 + 1,
+                    i % 28 + 1
+                ),
+                3 => "T12:34:56".to_string(),
+                _ => "not a timestamp".to_string(),
+            }),
+        ),
+    ];
+
+    // Timezone-aware and NTZ go through different parsers (`timestamp_parser` vs
+    // `timestamp_ntz_parser`), and a non-UTC session timezone exercises the offset lookup that
+    // UTC short-circuits, so all three are measured.
+    for (target_name, to_type, timezone) in [
+        (
+            "timestamp",
+            DataType::Timestamp(TimeUnit::Microsecond, Some("UTC".into())),
+            "UTC",
+        ),
+        (
+            "timestamp_non_utc",
+            DataType::Timestamp(TimeUnit::Microsecond, Some("America/Los_Angeles".into())),
+            "America/Los_Angeles",
+        ),
+        (
+            "timestamp_ntz",
+            DataType::Timestamp(TimeUnit::Microsecond, None),
+            "UTC",
+        ),
+    ] {
+        for (mode, mode_name) in [
+            (EvalMode::Legacy, "legacy"),
+            (EvalMode::Ansi, "ansi"),
+            (EvalMode::Try, "try"),
+        ] {
+            let mut group =
+                c.benchmark_group(format!("cast_string_to_{}/{}", target_name, mode_name));
+            for (name, batch) in &batches {
+                // ANSI raises on the first invalid value, so timing it against a batch that is
+                // mostly invalid would measure the error path rather than the parser.
+                if mode == EvalMode::Ansi && *name == "mixed" {
+                    continue;
+                }
+                let cast = Cast::new(
+                    Arc::clone(&expr),
+                    to_type.clone(),
+                    SparkCastOptions::new(mode, timezone, false),
+                    None,
+                    None,
+                );
+                group.bench_function(*name, |b| {
+                    b.iter(|| cast.evaluate(batch).unwrap());
+                });
+            }
+            group.finish();
+        }
+    }
+
+    // The Spark 4 path adds a leading-whitespace check for T-prefixed time-only strings, so it
+    // is measured separately on the inputs where that check can fire.
+    let mut group = c.benchmark_group("cast_string_to_timestamp/spark4_legacy");
+    for name in ["padded", "mixed"] {
+        let batch = &batches.iter().find(|(n, _)| *n == name).unwrap().1;
+        let cast = Cast::new(
+            Arc::clone(&expr),
+            DataType::Timestamp(TimeUnit::Microsecond, Some("UTC".into())),
+            SparkCastOptions::new_with_version(EvalMode::Legacy, "UTC", false, true),
+            None,
+            None,
+        );
+        group.bench_function(name, |b| {
+            b.iter(|| cast.evaluate(batch).unwrap());
+        });
+    }
+    group.finish();
+}
+
+fn create_batch(f: impl Fn(usize) -> String) -> RecordBatch {
+    let schema = Arc::new(Schema::new(vec![Field::new("a", DataType::Utf8, true)]));
+    let mut builder = StringBuilder::new();
+    for i in 0..BATCH_SIZE {
+        if i % 17 == 0 {
+            builder.append_null();
+        } else {
+            builder.append_value(f(i));
+        }
+    }
+    RecordBatch::try_new(schema, vec![Arc::new(builder.finish())]).unwrap()
+}

Review Comment:
   Fixed in 434efec. There is a new `benches/common/mod.rs` with one parameterized `string_batch(rows, null_modulus, value_fn)`, pulled into all three cast-from-string benches with `#[path]`. `create_batch` here is now a one-line delegate:
   
   ```rust
   fn create_batch(f: impl Fn(usize) -> String) -> RecordBatch {
       common::string_batch(BATCH_SIZE, 17, f)
   }
   ```
   
   and `cast_string_to_date.rs` uses the same helper, so the byte-for-byte copy is gone. The helper lives in a subdirectory deliberately: Cargo's bench auto-discovery only scans `benches/*.rs`, so a top-level `common.rs` would be built as its own bench target.



##########
native/spark-expr/benches/cast_from_string.rs:
##########


Review Comment:
   Fixed in 434efec: `EVAL_MODES` is now a single const used at all three sites. `grep -c` for the inline array in that file is down from 3 to 1 (the const itself).



##########
native/spark-expr/benches/cast_from_string.rs:
##########
@@ -133,6 +133,80 @@ fn criterion_benchmark(c: &mut Criterion) {
         }
         group.finish();
     }
+
+    // str -> boolean and str -> float benchmarks, with and without the leading/trailing
+    // whitespace that exercises the trim helpers in `conversion_funcs::trim`
+    let bool_batch = create_boolean_string_batch(false);
+    let bool_padded_batch = create_boolean_string_batch(true);
+    let float_batch = create_float_string_batch(false);
+    let float_padded_batch = create_float_string_batch(true);
+    for (mode, mode_name) in [
+        (EvalMode::Legacy, "legacy"),
+        (EvalMode::Ansi, "ansi"),
+        (EvalMode::Try, "try"),
+    ] {
+        let spark_cast_options = SparkCastOptions::new(mode, "", false);
+        let mut group = c.benchmark_group(format!("cast_string_to_bool_and_float/{}", mode_name));
+        for (data_type, name, batch) in [
+            (DataType::Boolean, "boolean", &bool_batch),
+            (DataType::Boolean, "boolean_padded", &bool_padded_batch),
+            (DataType::Float32, "float", &float_batch),
+            (DataType::Float64, "double", &float_batch),
+            (DataType::Float64, "double_padded", &float_padded_batch),
+        ] {
+            let cast = Cast::new(
+                expr.clone(),
+                data_type,
+                spark_cast_options.clone(),
+                None,
+                None,
+            );
+            group.bench_function(name, |b| {
+                b.iter(|| cast.evaluate(batch).unwrap());
+            });
+        }
+        group.finish();
+    }
+}
+
+/// Create batch with the boolean spellings Spark accepts, optionally space-padded
+fn create_boolean_string_batch(padded: bool) -> RecordBatch {
+    let words = ["true", "FALSE", "t", "n", "yes", "0", "TRUE", "no"];
+    create_string_batch(|i| {
+        let word = words[i % words.len()];
+        if padded {
+            format!("  {}  ", word)
+        } else {
+            word.to_string()
+        }
+    })
+}
+
+/// Create batch with floating point strings, optionally space-padded
+fn create_float_string_batch(padded: bool) -> RecordBatch {
+    let mut rng = StdRng::seed_from_u64(42);
+    create_string_batch(move |_| {
+        let value = rng.random_range(-1_000_000.0..1_000_000.0f64);
+        if padded {
+            format!("  {}  ", value)
+        } else {
+            format!("{}", value)
+        }
+    })
+}
+
+/// Create a single-column Utf8 batch of 8192 rows, every tenth one null
+fn create_string_batch(mut value: impl FnMut(usize) -> String) -> RecordBatch {
+    let schema = Arc::new(Schema::new(vec![Field::new("a", DataType::Utf8, true)]));
+    let mut b = StringBuilder::new();
+    for i in 0..8192 {
+        if i % 10 == 0 {
+            b.append_null();
+        } else {
+            b.append_value(value(i));
+        }
+    }
+    RecordBatch::try_new(schema, vec![Arc::new(b.finish())]).unwrap()
 }

Review Comment:
   Fixed in 434efec: all five near-copies (`create_small_int_string_batch`, `create_int_string_batch`, `create_string_batch`, and the other two) collapse into `common::string_batch`. `cast_from_string.rs` is 185 lines lighter as a result. All three benches smoke-tested with `--test`.



-- 
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: github-unsubscribe@datafusion.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


---------------------------------------------------------------------
To unsubscribe, e-mail: github-unsubscribe@datafusion.apache.org
For additional commands, e-mail: github-help@datafusion.apache.org


Re: [PR] fix: match Spark's whitespace trim semantics for casts from string to boolean, integral, float/double and decimal [datafusion-comet]

Posted by "andygrove (via GitHub)" <gi...@apache.org>.
andygrove commented on PR #5150:
URL: https://github.com/apache/datafusion-comet/pull/5150#issuecomment-5143478809

   @peterxcli could you help review?


-- 
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: github-unsubscribe@datafusion.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


---------------------------------------------------------------------
To unsubscribe, e-mail: github-unsubscribe@datafusion.apache.org
For additional commands, e-mail: github-help@datafusion.apache.org


Re: [PR] fix: match Spark's whitespace trim semantics for casts from string to boolean, integral, float/double and decimal [datafusion-comet]

Posted by "andygrove (via GitHub)" <gi...@apache.org>.
andygrove commented on PR #5150:
URL: https://github.com/apache/datafusion-comet/pull/5150#issuecomment-5145951681

   Thanks @mbutrovich @comphead — all of this is addressed in 434efec. Point by point:
   
   **Verbose docs (@comphead, @mbutrovich).** The `trim.rs` module doc keeps the regime table and the footnote; both paragraphs that restated it are gone, replaced by one line. `is_whitespace_or_iso_control`, `trim_all` and `trim_java_string` are now pointers at that table rather than re-listing the byte ranges and cast targets, so the mapping has one place to go stale. I did keep the two things that aren't derivable from the code: the signed-byte widening note, and the JDK call paths that justify the `String.trim` regime (`Double.parseDouble` / `Decimal.stringToJavaBigDecimal`) — the latter moved up into the module doc so it isn't duplicated in a function doc. Same treatment for `trimPadding`'s Scaladoc in `CometCastSuite`, which now points at `conversion_funcs::trim` instead of restating the regimes in Scala.
   
   **`is_java_trim_byte` docstring (@comphead).** You're right, and the correct invariant is the reverse direction. Reworded to: "In valid UTF-8 a byte `<= 0x20` is always a single-byte codepoint, since the lead and continuation bytes of a multi-byte sequence are all `>= 0x80`."
   
   **`to_time` / `try_to_time` (@mbutrovich).** Added to the not-yet-migrated footnote in `trim.rs`, so the boundary is in the code and not just in the PR description. I also added them to the user-facing divergence bullet in `compatibility/index.md`, which was only naming timestamp.
   
   **Bench duplication (@mbutrovich).** New `benches/common/mod.rs`, pulled in with `#[path]` from all three cast-from-string benches. It lives in a subdirectory so Cargo's bench auto-discovery (which only scans `benches/*.rs`) doesn't try to build it as a target. One parameterized `string_batch(rows, null_modulus, value_fn)` replaces the `create_batch` copy shared by `cast_string_to_date.rs` / `cast_string_to_timestamp.rs` **and** all five near-copies in `cast_from_string.rs` — that file is 185 lines lighter. `EVAL_MODES` is hoisted to a single const and used at all three sites. All three benches smoke-tested with `--test`.
   
   **Pure-padding / empty input (@comphead).** Both `assert_trim_parity` (Rust) and `trimPaddedValues` (Scala) now generate `input == pad` and `input == ""` in addition to the seeded cases, so the empty-slice / `j == str_end_trimmed` branches are hit directly rather than incidentally.
   
   **Timestamp divergence untested (@comphead).** Agreed this was a real gap, but an `ignore`d Scala test wouldn't close it, and `castTest` can't either — Spark is the oracle there and Comet doesn't fall back, it silently returns a value, so the test would just fail. Pinned it as a Rust test instead (`test_cast_string_to_timestamp_unicode_whitespace_divergence`), asserting the *current* divergent behaviour for `U+0085` / `U+00A0` / `U+2028` / `U+3000` across both timestamp and timestamp_ntz. The failure message says what to do when it flips: delete the test and extend `assert_trim_parity` to the timestamp targets. That makes #5149's remaining work impossible to land silently.
   
   **SQL file tests (@comphead).** Added `expressions/cast/cast_string_trim.sql`. The padding is materialized into the Parquet table via `chr()` / `decode(X'…', 'utf-8')` at insert time so the file stays pure ASCII and the cast operands stay inside expressions Comet runs natively — the coverage assertion confirms they do. It covers both regimes (DEL trimmed for boolean/integral/date but not float/double/decimal, `U+00A0` / `U+3000` trimmed for nothing) plus the padding-only and interior-padding rows, across the dictionary on/off matrix.
   
   **Divergence in both directions (@comphead).** The description already covers this ("The divergence went in both directions: spurious failures where Spark parses fine, and silent wrong results…") with the four-line SQL repro, so I left it as is — happy to expand if you'd like the `UTF8String.java:983-999` reference in there explicitly.
   
   Verified: full `CometCastSuite` (165 passed), the new SQL fixture on both dictionary settings, all 554 `datafusion-comet-spark-expr` unit tests, and `cargo clippy --all-targets -D warnings` clean, on Spark 3.5.


-- 
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: github-unsubscribe@datafusion.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


---------------------------------------------------------------------
To unsubscribe, e-mail: github-unsubscribe@datafusion.apache.org
For additional commands, e-mail: github-help@datafusion.apache.org


Re: [PR] fix: match Spark's whitespace trim semantics for casts from string to boolean, integral, float/double and decimal [datafusion-comet]

Posted by "comphead (via GitHub)" <gi...@apache.org>.
comphead commented on code in PR #5150:
URL: https://github.com/apache/datafusion-comet/pull/5150#discussion_r3686789250


##########
native/spark-expr/src/conversion_funcs/trim.rs:
##########
@@ -0,0 +1,196 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+//! Whitespace trimming for parsing from string.
+//!
+//! Spark's string casts do not all agree on what "whitespace" means. There are exactly two
+//! regimes, and neither of them matches Rust's `str::trim` (which trims Unicode whitespace) or
+//! `<[u8]>::trim_ascii` (which omits `0x0B`):
+//!
+//! | Regime               | Trimmed bytes            | Cast targets                                          |
+//! |----------------------|--------------------------|-------------------------------------------------------|
+//! | [`trim_all`]         | `0x00`-`0x20` and `0x7F` | boolean, byte, short, int, long, date, timestamp \*    |
+//! | [`trim_java_string`] | `0x00`-`0x20`            | float, double, decimal                                |
+//!
+//! Crucially, **neither regime trims any non-ASCII whitespace**. `U+0085`, `U+00A0`, `U+1680`,
+//! `U+2000`-`U+200A`, `U+2028`, `U+2029`, `U+202F`, `U+205F` and `U+3000` all leave Spark
+//! returning NULL (or raising under ANSI) for every cast target, so using `str::trim` here
+//! silently produces a value where Spark produces none.
+//!
+//! The two regimes differ only in `0x7F` (DELETE), which the `trimAll` set removes and the
+//! `String.trim` set does not. That single byte is why a shared helper cannot be applied
+//! uniformly: trimming it in the float/double/decimal paths would introduce a new divergence.
+//!
+//! \* `timestamp` and `timestamp_ntz` are listed for what Spark does; the Comet parsers for
+//! those two targets still use `str::trim` and have not been migrated to these helpers

Review Comment:
   I feel the comments are too verbose 🤔 



-- 
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: github-unsubscribe@datafusion.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


---------------------------------------------------------------------
To unsubscribe, e-mail: github-unsubscribe@datafusion.apache.org
For additional commands, e-mail: github-help@datafusion.apache.org


Re: [PR] fix: match Spark's whitespace trim semantics for casts from string to boolean, integral, float/double and decimal [datafusion-comet]

Posted by "mbutrovich (via GitHub)" <gi...@apache.org>.
mbutrovich commented on code in PR #5150:
URL: https://github.com/apache/datafusion-comet/pull/5150#discussion_r3691581059


##########
native/spark-expr/src/conversion_funcs/trim.rs:
##########
@@ -0,0 +1,196 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+//! Whitespace trimming for parsing from string.
+//!
+//! Spark's string casts do not all agree on what "whitespace" means. There are exactly two
+//! regimes, and neither of them matches Rust's `str::trim` (which trims Unicode whitespace) or
+//! `<[u8]>::trim_ascii` (which omits `0x0B`):
+//!
+//! | Regime               | Trimmed bytes            | Cast targets                                          |
+//! |----------------------|--------------------------|-------------------------------------------------------|
+//! | [`trim_all`]         | `0x00`-`0x20` and `0x7F` | boolean, byte, short, int, long, date, timestamp \*    |
+//! | [`trim_java_string`] | `0x00`-`0x20`            | float, double, decimal                                |
+//!
+//! Crucially, **neither regime trims any non-ASCII whitespace**. `U+0085`, `U+00A0`, `U+1680`,
+//! `U+2000`-`U+200A`, `U+2028`, `U+2029`, `U+202F`, `U+205F` and `U+3000` all leave Spark
+//! returning NULL (or raising under ANSI) for every cast target, so using `str::trim` here
+//! silently produces a value where Spark produces none.
+//!
+//! The two regimes differ only in `0x7F` (DELETE), which the `trimAll` set removes and the
+//! `String.trim` set does not. That single byte is why a shared helper cannot be applied
+//! uniformly: trimming it in the float/double/decimal paths would introduce a new divergence.
+//!
+//! \* `timestamp` and `timestamp_ntz` are listed for what Spark does; the Comet parsers for
+//! those two targets still use `str::trim` and have not been migrated to these helpers
+//! (<https://github.com/apache/datafusion-comet/issues/5149>).
+
+/// True for the bytes trimmed by `org.apache.spark.unsafe.types.UTF8String.trimAll`, i.e. the
+/// bytes `b` for which `Character.isWhitespace(b) || Character.isISOControl(b)` holds.
+///
+/// `isWhitespace` covers `0x09`-`0x0D`, `0x1C`-`0x1F` and `0x20`; `isISOControl` covers
+/// `0x00`-`0x1F` and `0x7F`; the union is `0x00`-`0x20` plus `0x7F`. Spark widens a *signed*
+/// `byte` into the `int` overload, so bytes `0x80`-`0xFF` arrive negative and are never trimmed.

Review Comment:
   `is_whitespace_or_iso_control`'s doc spells out `isWhitespace`'s byte range, `isISOControl`'s byte range, and their union, to justify a one-line boolean expression that is already the `trim_all` row of the module table. Shorten to: "`Character.isWhitespace(b) || Character.isISOControl(b)`, which reduces to `b <= 0x20 || b == 0x7F`." Keep the sentence about signed-byte widening (`0x80`-`0xFF` never trimmed); that one is not derivable from the code.



##########
native/spark-expr/src/conversion_funcs/trim.rs:
##########
@@ -0,0 +1,196 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+//! Whitespace trimming for parsing from string.
+//!
+//! Spark's string casts do not all agree on what "whitespace" means. There are exactly two
+//! regimes, and neither of them matches Rust's `str::trim` (which trims Unicode whitespace) or
+//! `<[u8]>::trim_ascii` (which omits `0x0B`):
+//!
+//! | Regime               | Trimmed bytes            | Cast targets                                          |
+//! |----------------------|--------------------------|-------------------------------------------------------|
+//! | [`trim_all`]         | `0x00`-`0x20` and `0x7F` | boolean, byte, short, int, long, date, timestamp \*    |
+//! | [`trim_java_string`] | `0x00`-`0x20`            | float, double, decimal                                |
+//!
+//! Crucially, **neither regime trims any non-ASCII whitespace**. `U+0085`, `U+00A0`, `U+1680`,
+//! `U+2000`-`U+200A`, `U+2028`, `U+2029`, `U+202F`, `U+205F` and `U+3000` all leave Spark
+//! returning NULL (or raising under ANSI) for every cast target, so using `str::trim` here
+//! silently produces a value where Spark produces none.
+//!
+//! The two regimes differ only in `0x7F` (DELETE), which the `trimAll` set removes and the
+//! `String.trim` set does not. That single byte is why a shared helper cannot be applied
+//! uniformly: trimming it in the float/double/decimal paths would introduce a new divergence.

Review Comment:
   This is still open from the earlier review comment that the module doc is too verbose. Two things are said twice each: the non-ASCII-whitespace paragraph (lines 29-32) restates the module table's implication in nine spelled-out codepoints that never appear again in this file, and the "the regimes differ only in 0x7F" paragraph (lines 34-36) is also proven by `trim_sets_match_spark`'s `assert_eq!(differing, vec![0x7F])` and restated a third time in `is_java_trim_byte`'s own doc ("keeping 0x7F"). Cut both paragraphs to one line: "Neither regime trims non-ASCII whitespace (`str::trim` does, which is why it can't be reused here)." Keep the table and the trailing footnote about timestamp/timestamp_ntz.



##########
native/spark-expr/src/conversion_funcs/trim.rs:
##########
@@ -0,0 +1,196 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+//! Whitespace trimming for parsing from string.
+//!
+//! Spark's string casts do not all agree on what "whitespace" means. There are exactly two
+//! regimes, and neither of them matches Rust's `str::trim` (which trims Unicode whitespace) or
+//! `<[u8]>::trim_ascii` (which omits `0x0B`):
+//!
+//! | Regime               | Trimmed bytes            | Cast targets                                          |
+//! |----------------------|--------------------------|-------------------------------------------------------|
+//! | [`trim_all`]         | `0x00`-`0x20` and `0x7F` | boolean, byte, short, int, long, date, timestamp \*    |
+//! | [`trim_java_string`] | `0x00`-`0x20`            | float, double, decimal                                |
+//!
+//! Crucially, **neither regime trims any non-ASCII whitespace**. `U+0085`, `U+00A0`, `U+1680`,
+//! `U+2000`-`U+200A`, `U+2028`, `U+2029`, `U+202F`, `U+205F` and `U+3000` all leave Spark
+//! returning NULL (or raising under ANSI) for every cast target, so using `str::trim` here
+//! silently produces a value where Spark produces none.
+//!
+//! The two regimes differ only in `0x7F` (DELETE), which the `trimAll` set removes and the
+//! `String.trim` set does not. That single byte is why a shared helper cannot be applied
+//! uniformly: trimming it in the float/double/decimal paths would introduce a new divergence.
+//!
+//! \* `timestamp` and `timestamp_ntz` are listed for what Spark does; the Comet parsers for
+//! those two targets still use `str::trim` and have not been migrated to these helpers
+//! (<https://github.com/apache/datafusion-comet/issues/5149>).
+
+/// True for the bytes trimmed by `org.apache.spark.unsafe.types.UTF8String.trimAll`, i.e. the
+/// bytes `b` for which `Character.isWhitespace(b) || Character.isISOControl(b)` holds.
+///
+/// `isWhitespace` covers `0x09`-`0x0D`, `0x1C`-`0x1F` and `0x20`; `isISOControl` covers
+/// `0x00`-`0x1F` and `0x7F`; the union is `0x00`-`0x20` plus `0x7F`. Spark widens a *signed*
+/// `byte` into the `int` overload, so bytes `0x80`-`0xFF` arrive negative and are never trimmed.
+#[inline]
+const fn is_whitespace_or_iso_control(b: u8) -> bool {
+    b <= 0x20 || b == 0x7F
+}
+
+/// True for the bytes trimmed by `java.lang.String.trim`, which drops any char `<= U+0020`.
+///
+/// A char above `U+0020` always encodes to bytes `>= 0x80` in UTF-8, so testing bytes rather
+/// than chars gives the same answer.
+#[inline]
+const fn is_java_trim_byte(b: u8) -> bool {
+    b <= 0x20
+}
+
+/// Trims the `UTF8String.trimAll` byte set (`0x00`-`0x20` and `0x7F`) from both ends.
+///
+/// This is the trim used by `CAST(string AS boolean)`, the integral casts and `date_parser`.
+/// See the [module docs](self) for why the other targets need [`trim_java_string`].
+#[inline]
+pub(crate) fn trim_all(s: &str) -> &str {
+    let (start, end) = trim_all_range(s.as_bytes());
+    &s[start..end]
+}
+
+/// [`trim_all`] over a byte slice, for parsers that already work on bytes.
+#[inline]
+pub(crate) fn trim_all_bytes(bytes: &[u8]) -> &[u8] {
+    trim_bytes(bytes, is_whitespace_or_iso_control).0
+}
+
+/// The byte offsets [`trim_all`] would slice at, for parsers that need to keep a cursor into
+/// the untrimmed input.
+#[inline]
+pub(crate) fn trim_all_range(bytes: &[u8]) -> (usize, usize) {
+    let (trimmed, start) = trim_bytes(bytes, is_whitespace_or_iso_control);
+    (start, start + trimmed.len())
+}
+
+/// Trims the `java.lang.String.trim` byte set (`0x00`-`0x20`, keeping `0x7F`) from both ends.
+///
+/// This is the trim used by `CAST(string AS float/double)` (via `Double.parseDouble`, which
+/// calls `String.trim` before parsing) and by `CAST(string AS decimal)` (via
+/// `Decimal.stringToJavaBigDecimal`, which does `str.toString.trim`).

Review Comment:
   Same issue as `trim_all`: `trim_java_string`'s doc re-lists float/double/decimal and the JDK call paths that justify them, duplicating the module table. Replace with a pointer to the module docs.



##########
native/spark-expr/src/conversion_funcs/trim.rs:
##########
@@ -0,0 +1,196 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+//! Whitespace trimming for parsing from string.
+//!
+//! Spark's string casts do not all agree on what "whitespace" means. There are exactly two
+//! regimes, and neither of them matches Rust's `str::trim` (which trims Unicode whitespace) or
+//! `<[u8]>::trim_ascii` (which omits `0x0B`):
+//!
+//! | Regime               | Trimmed bytes            | Cast targets                                          |
+//! |----------------------|--------------------------|-------------------------------------------------------|
+//! | [`trim_all`]         | `0x00`-`0x20` and `0x7F` | boolean, byte, short, int, long, date, timestamp \*    |
+//! | [`trim_java_string`] | `0x00`-`0x20`            | float, double, decimal                                |
+//!
+//! Crucially, **neither regime trims any non-ASCII whitespace**. `U+0085`, `U+00A0`, `U+1680`,
+//! `U+2000`-`U+200A`, `U+2028`, `U+2029`, `U+202F`, `U+205F` and `U+3000` all leave Spark
+//! returning NULL (or raising under ANSI) for every cast target, so using `str::trim` here
+//! silently produces a value where Spark produces none.
+//!
+//! The two regimes differ only in `0x7F` (DELETE), which the `trimAll` set removes and the
+//! `String.trim` set does not. That single byte is why a shared helper cannot be applied
+//! uniformly: trimming it in the float/double/decimal paths would introduce a new divergence.
+//!
+//! \* `timestamp` and `timestamp_ntz` are listed for what Spark does; the Comet parsers for
+//! those two targets still use `str::trim` and have not been migrated to these helpers
+//! (<https://github.com/apache/datafusion-comet/issues/5149>).
+
+/// True for the bytes trimmed by `org.apache.spark.unsafe.types.UTF8String.trimAll`, i.e. the
+/// bytes `b` for which `Character.isWhitespace(b) || Character.isISOControl(b)` holds.
+///
+/// `isWhitespace` covers `0x09`-`0x0D`, `0x1C`-`0x1F` and `0x20`; `isISOControl` covers
+/// `0x00`-`0x1F` and `0x7F`; the union is `0x00`-`0x20` plus `0x7F`. Spark widens a *signed*
+/// `byte` into the `int` overload, so bytes `0x80`-`0xFF` arrive negative and are never trimmed.
+#[inline]
+const fn is_whitespace_or_iso_control(b: u8) -> bool {
+    b <= 0x20 || b == 0x7F
+}
+
+/// True for the bytes trimmed by `java.lang.String.trim`, which drops any char `<= U+0020`.
+///
+/// A char above `U+0020` always encodes to bytes `>= 0x80` in UTF-8, so testing bytes rather
+/// than chars gives the same answer.
+#[inline]
+const fn is_java_trim_byte(b: u8) -> bool {
+    b <= 0x20
+}
+
+/// Trims the `UTF8String.trimAll` byte set (`0x00`-`0x20` and `0x7F`) from both ends.
+///
+/// This is the trim used by `CAST(string AS boolean)`, the integral casts and `date_parser`.
+/// See the [module docs](self) for why the other targets need [`trim_java_string`].

Review Comment:
   `trim_all`'s doc comment ("This is the trim used by `CAST(string AS boolean)`, the integral casts and `date_parser`") restates the "Cast targets" column of the module table verbatim. Replace with a pointer to the module docs instead of re-listing the targets, so the mapping has one place to go stale instead of two.



##########
spark/src/test/scala/org/apache/comet/CometCastSuite.scala:
##########
@@ -811,6 +811,43 @@ class CometCastSuite extends CometTestBase with AdaptiveSparkPlanHelper {
     castTest(testValues, DataTypes.BooleanType)
   }
 
+  /**
+   * Padding used to check that Comet trims exactly the byte set that each Spark cast trims. Spark
+   * has two trim regimes and neither of them trims any non-ASCII whitespace:
+   *   - `UTF8String.trimAll` (bytes `0x00`-`0x20` and `0x7F`) for boolean, integral and datetime
+   *   - `java.lang.String.trim` (bytes `0x00`-`0x20` only) for float, double and decimal
+   *
+   * Spark itself is the oracle here, so the expectations do not need to be spelled out. See
+   * https://github.com/apache/datafusion-comet/issues/5149.

Review Comment:
   The Scaladoc on `trimPadding` re-derives the two trim regimes ("`UTF8String.trimAll` (bytes `0x00`-`0x20` and `0x7F`) for boolean, integral and datetime" / "`java.lang.String.trim` (bytes `0x00`-`0x20` only) for float, double and decimal") in different words than `conversion_funcs::trim`'s module doc, in a different language, so the two definitions can drift independently. Shorten to a one-line pointer at `conversion_funcs::trim` and issue #5149 instead of re-explaining the regimes here.



##########
native/spark-expr/benches/cast_string_to_timestamp.rs:
##########
@@ -0,0 +1,210 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+use arrow::array::{builder::StringBuilder, RecordBatch};
+use arrow::datatypes::{DataType, Field, Schema, TimeUnit};
+use criterion::{criterion_group, criterion_main, Criterion};
+use datafusion::physical_expr::{expressions::Column, PhysicalExpr};
+use datafusion_comet_spark_expr::{Cast, EvalMode, SparkCastOptions};
+use std::sync::Arc;
+
+const BATCH_SIZE: usize = 8192;
+
+fn criterion_benchmark(c: &mut Criterion) {
+    let expr = Arc::new(Column::new("a", 0)) as Arc<dyn PhysicalExpr>;
+
+    // Input shapes, chosen to cover each branch `timestamp_parser` can take: the canonical
+    // form, the fractional-second form, an offset suffix (which takes the extract-offset
+    // path), a date-only string, whitespace padding (the trim), and a mix that includes
+    // invalid values so the null path is measured too.
+    let batches = [
+        (
+            "canonical",
+            create_batch(|i| {
+                format!(
+                    "{:04}-{:02}-{:02} {:02}:{:02}:{:02}",
+                    1970 + i % 60,
+                    i % 12 + 1,
+                    i % 28 + 1,
+                    i % 24,
+                    i % 60,
+                    i % 60
+                )
+            }),
+        ),
+        (
+            "microseconds",
+            create_batch(|i| {
+                format!(
+                    "{:04}-{:02}-{:02}T{:02}:{:02}:{:02}.{:06}",
+                    1970 + i % 60,
+                    i % 12 + 1,
+                    i % 28 + 1,
+                    i % 24,
+                    i % 60,
+                    i % 60,
+                    i % 1_000_000
+                )
+            }),
+        ),
+        (
+            "offset_suffix",
+            create_batch(|i| {
+                format!(
+                    "{:04}-{:02}-{:02}T{:02}:{:02}:{:02}+05:30",
+                    1970 + i % 60,
+                    i % 12 + 1,
+                    i % 28 + 1,
+                    i % 24,
+                    i % 60,
+                    i % 60
+                )
+            }),
+        ),
+        (
+            "date_only",
+            create_batch(|i| format!("{:04}-{:02}-{:02}", 1970 + i % 60, i % 12 + 1, i % 28 + 1)),
+        ),
+        (
+            "padded",
+            create_batch(|i| {
+                format!(
+                    "  {:04}-{:02}-{:02} {:02}:00:00  ",
+                    1970 + i % 60,
+                    i % 12 + 1,
+                    i % 28 + 1,
+                    i % 24
+                )
+            }),
+        ),
+        (
+            "mixed",
+            create_batch(|i| match i % 5 {
+                0 => format!(
+                    "{:04}-{:02}-{:02} 12:34:56",
+                    1970 + i % 60,
+                    i % 12 + 1,
+                    i % 28 + 1
+                ),
+                1 => format!(
+                    "{:04}-{:02}-{:02}T12:34:56.123456Z",
+                    1970 + i % 60,
+                    i % 12 + 1,
+                    i % 28 + 1
+                ),
+                2 => format!(
+                    "  {:04}-{:02}-{:02}  ",
+                    1900 + i % 200,
+                    i % 12 + 1,
+                    i % 28 + 1
+                ),
+                3 => "T12:34:56".to_string(),
+                _ => "not a timestamp".to_string(),
+            }),
+        ),
+    ];
+
+    // Timezone-aware and NTZ go through different parsers (`timestamp_parser` vs
+    // `timestamp_ntz_parser`), and a non-UTC session timezone exercises the offset lookup that
+    // UTC short-circuits, so all three are measured.
+    for (target_name, to_type, timezone) in [
+        (
+            "timestamp",
+            DataType::Timestamp(TimeUnit::Microsecond, Some("UTC".into())),
+            "UTC",
+        ),
+        (
+            "timestamp_non_utc",
+            DataType::Timestamp(TimeUnit::Microsecond, Some("America/Los_Angeles".into())),
+            "America/Los_Angeles",
+        ),
+        (
+            "timestamp_ntz",
+            DataType::Timestamp(TimeUnit::Microsecond, None),
+            "UTC",
+        ),
+    ] {
+        for (mode, mode_name) in [
+            (EvalMode::Legacy, "legacy"),
+            (EvalMode::Ansi, "ansi"),
+            (EvalMode::Try, "try"),
+        ] {
+            let mut group =
+                c.benchmark_group(format!("cast_string_to_{}/{}", target_name, mode_name));
+            for (name, batch) in &batches {
+                // ANSI raises on the first invalid value, so timing it against a batch that is
+                // mostly invalid would measure the error path rather than the parser.
+                if mode == EvalMode::Ansi && *name == "mixed" {
+                    continue;
+                }
+                let cast = Cast::new(
+                    Arc::clone(&expr),
+                    to_type.clone(),
+                    SparkCastOptions::new(mode, timezone, false),
+                    None,
+                    None,
+                );
+                group.bench_function(*name, |b| {
+                    b.iter(|| cast.evaluate(batch).unwrap());
+                });
+            }
+            group.finish();
+        }
+    }
+
+    // The Spark 4 path adds a leading-whitespace check for T-prefixed time-only strings, so it
+    // is measured separately on the inputs where that check can fire.
+    let mut group = c.benchmark_group("cast_string_to_timestamp/spark4_legacy");
+    for name in ["padded", "mixed"] {
+        let batch = &batches.iter().find(|(n, _)| *n == name).unwrap().1;
+        let cast = Cast::new(
+            Arc::clone(&expr),
+            DataType::Timestamp(TimeUnit::Microsecond, Some("UTC".into())),
+            SparkCastOptions::new_with_version(EvalMode::Legacy, "UTC", false, true),
+            None,
+            None,
+        );
+        group.bench_function(name, |b| {
+            b.iter(|| cast.evaluate(batch).unwrap());
+        });
+    }
+    group.finish();
+}
+
+fn create_batch(f: impl Fn(usize) -> String) -> RecordBatch {
+    let schema = Arc::new(Schema::new(vec![Field::new("a", DataType::Utf8, true)]));
+    let mut builder = StringBuilder::new();
+    for i in 0..BATCH_SIZE {
+        if i % 17 == 0 {
+            builder.append_null();
+        } else {
+            builder.append_value(f(i));
+        }
+    }
+    RecordBatch::try_new(schema, vec![Arc::new(builder.finish())]).unwrap()
+}

Review Comment:
   `create_batch` here is a byte-for-byte duplicate of `create_batch` in native/spark-expr/benches/cast_string_to_date.rs:62-73 (same `BATCH_SIZE = 8192`, same `i % 17 == 0` null pattern, same `StringBuilder`/`Schema` setup). Move it into a shared bench-support module included via `#[path]` from both files instead of retyping it.



##########
native/spark-expr/benches/cast_from_string.rs:
##########


Review Comment:
   The `[(EvalMode::Legacy, "legacy"), (EvalMode::Ansi, "ansi"), (EvalMode::Try, "try")]` array is written out three times in this file (here, at 112-116, and at 143-147, the one this PR adds). Hoist a single `const EVAL_MODES: [(EvalMode, &str); 3]` at the top of `criterion_benchmark` and reuse it at all three sites instead of adding a third copy.



##########
native/spark-expr/benches/cast_from_string.rs:
##########
@@ -133,6 +133,80 @@ fn criterion_benchmark(c: &mut Criterion) {
         }
         group.finish();
     }
+
+    // str -> boolean and str -> float benchmarks, with and without the leading/trailing
+    // whitespace that exercises the trim helpers in `conversion_funcs::trim`
+    let bool_batch = create_boolean_string_batch(false);
+    let bool_padded_batch = create_boolean_string_batch(true);
+    let float_batch = create_float_string_batch(false);
+    let float_padded_batch = create_float_string_batch(true);
+    for (mode, mode_name) in [
+        (EvalMode::Legacy, "legacy"),
+        (EvalMode::Ansi, "ansi"),
+        (EvalMode::Try, "try"),
+    ] {
+        let spark_cast_options = SparkCastOptions::new(mode, "", false);
+        let mut group = c.benchmark_group(format!("cast_string_to_bool_and_float/{}", mode_name));
+        for (data_type, name, batch) in [
+            (DataType::Boolean, "boolean", &bool_batch),
+            (DataType::Boolean, "boolean_padded", &bool_padded_batch),
+            (DataType::Float32, "float", &float_batch),
+            (DataType::Float64, "double", &float_batch),
+            (DataType::Float64, "double_padded", &float_padded_batch),
+        ] {
+            let cast = Cast::new(
+                expr.clone(),
+                data_type,
+                spark_cast_options.clone(),
+                None,
+                None,
+            );
+            group.bench_function(name, |b| {
+                b.iter(|| cast.evaluate(batch).unwrap());
+            });
+        }
+        group.finish();
+    }
+}
+
+/// Create batch with the boolean spellings Spark accepts, optionally space-padded
+fn create_boolean_string_batch(padded: bool) -> RecordBatch {
+    let words = ["true", "FALSE", "t", "n", "yes", "0", "TRUE", "no"];
+    create_string_batch(|i| {
+        let word = words[i % words.len()];
+        if padded {
+            format!("  {}  ", word)
+        } else {
+            word.to_string()
+        }
+    })
+}
+
+/// Create batch with floating point strings, optionally space-padded
+fn create_float_string_batch(padded: bool) -> RecordBatch {
+    let mut rng = StdRng::seed_from_u64(42);
+    create_string_batch(move |_| {
+        let value = rng.random_range(-1_000_000.0..1_000_000.0f64);
+        if padded {
+            format!("  {}  ", value)
+        } else {
+            format!("{}", value)
+        }
+    })
+}
+
+/// Create a single-column Utf8 batch of 8192 rows, every tenth one null
+fn create_string_batch(mut value: impl FnMut(usize) -> String) -> RecordBatch {
+    let schema = Arc::new(Schema::new(vec![Field::new("a", DataType::Utf8, true)]));
+    let mut b = StringBuilder::new();
+    for i in 0..8192 {
+        if i % 10 == 0 {
+            b.append_null();
+        } else {
+            b.append_value(value(i));
+        }
+    }
+    RecordBatch::try_new(schema, vec![Arc::new(b.finish())]).unwrap()
 }

Review Comment:
   `create_string_batch` is a fifth variant of the "Utf8 batch with periodic nulls" builder already present four times in this file (`create_small_int_string_batch`, `create_int_string_batch`, `create_decimal_string_batch`, `create_decimal_cast_string_batch`), differing only in the null-row modulus and the value-closure signature (`FnMut` vs `Fn`). Parameterize one builder on `(null_modulus, value_fn)` and call it from all five sites instead of adding a fifth near-copy.



-- 
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: github-unsubscribe@datafusion.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


---------------------------------------------------------------------
To unsubscribe, e-mail: github-unsubscribe@datafusion.apache.org
For additional commands, e-mail: github-help@datafusion.apache.org


Re: [PR] fix: match Spark's whitespace trim semantics for casts from string to boolean, integral, float/double and decimal [datafusion-comet]

Posted by "andygrove (via GitHub)" <gi...@apache.org>.
andygrove commented on PR #5150:
URL: https://github.com/apache/datafusion-comet/pull/5150#issuecomment-5134802629

   Follow-up commit with benchmark numbers, since "does this change performance?" is the obvious question for a change to per-row cast kernels.
   
   Measured with `cast_from_string` (criterion, this branch vs `apache/main`, same machine):
   
   | Cast | Change |
   | --- | --- |
   | string → boolean | **63-67% faster** |
   | string → boolean (space-padded) | **65-67% faster** |
   | string → double | **12-13% faster** |
   | string → double (space-padded) | **20-21% faster** |
   | string → decimal | **3-7% faster** |
   | string → byte/short/int/long | unchanged (within ±3% run-to-run noise) |
   
   The boolean win is from dropping the per-row `to_ascii_lowercase()` allocation; the float/decimal wins are from replacing `str::trim` (Unicode-aware, decodes chars) with a byte scan.
   
   The integral casts needed care: my first version of the scan used indexed loops and cost them 20-30% versus the `<[u8]>::trim_ascii` they used before. Rewriting the scan with slice patterns — the same shape std uses for `trim_ascii` — recovers that. There's a comment on `trim_bytes` recording why, so it doesn't get "simplified" back.
   
   Also added `cast_string_to_bool_and_float` groups to `cast_from_string.rs`; string → boolean and string → float had no benchmark coverage at all, so the 63% win was invisible to the bench suite.


-- 
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: github-unsubscribe@datafusion.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


---------------------------------------------------------------------
To unsubscribe, e-mail: github-unsubscribe@datafusion.apache.org
For additional commands, e-mail: github-help@datafusion.apache.org