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