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


##########
native/spark-expr/src/conversion_funcs/cast.rs:
##########
@@ -250,6 +250,7 @@ pub(crate) fn cast_array(
         return Ok(Arc::new(array));
     }
 
+    let array = prepare_nested_cast_input(array, to_type)?;

Review Comment:
   This runs on every cast of a struct, list or map. It copies the input with 
`take` whenever a struct has a null row, a list or map is sliced, or more than 
a quarter of its rows are null. Only signed integer widening and identity casts 
on primitives skip it, so most casts that can't fail pay for the copy too.
   
   Against the merge base, at 8,192 rows, `struct<int, string>` to 
`struct<bigint, string>` went from 1.6 µs to 70 µs with 10% null rows, because 
the string child is now copied instead of reused. At 50% null rows, `list<int>` 
to `list<bigint>` went from 19.6 µs to 66 µs and `map<int, int>` to `map<int, 
bigint>` from 19.8 µs to 112 µs. A sliced `list<int>` with no nulls went from 
19.5 µs to 111 µs. ANSI gave the same numbers, and shapes with no nulls and no 
slice didn't change.
   
   The copy only matters when a hidden child value could fail the cast. That's 
DATE to TIMESTAMP in every mode, plus the leaves that can fail under ANSI or 
TRY. For a cast that can't fail, `main` already reuses the children, so the 
copy is all cost. Could the gate be whether some leaf of the cast can fail in 
this eval mode, rather than whether the leaves are fixed width?



##########
spark/src/main/scala/org/apache/comet/serde/operator/CometNativeScan.scala:
##########
@@ -163,6 +175,15 @@ object CometNativeScan extends 
CometOperatorSerde[CometScanExec] with CometTypeS
       withFallbackReason(scanExec, "Full native scan disabled because 
ignoreMissingFiles enabled")
     }
 
+    // A requested NTZ field can be backed by a physical DATE column. Filtered 
native scans
+    // currently use a safe cast that turns DATE overflow into NULL. Checking 
eagerly instead
+    // would fail on rows Spark can skip through pruning or an early LIMIT. 
The physical schema
+    // is only available when each file opens, so conservatively retain 
Spark's reader for this
+    // projected type, including genuine NTZ files, until conversion follows 
Spark's row demand.
+    if (scanExec.dataFilters.nonEmpty && 
containsTimestampNTZ(scanExec.requiredSchema)) {

Review Comment:
   This sends every filtered Parquet scan that reads a `TIMESTAMP_NTZ` field to 
Spark's reader, and almost all of those are genuine NTZ files with no DATE 
column anywhere. Spark adds filters by itself too. An inner join's `isnotnull` 
key filter or a `window()` is enough. That's the six red tests on every 
profile. `window.sql` keeps 2 of 8 operators native, `unix_timestamp.sql` keeps 
0 of 3, and the fuzz `join` test finds 0 native scans instead of 2.
   
   I don't think it buys the row-demand guarantee either. With no filter the 
guard doesn't apply, so `read().limit(1)` on the same 6,000-row file runs 
natively and throws. Comet's 8,192-row batch converts rows that Spark's 
4,096-row batch never reaches. That's the batch-evaluation difference the new 
cast docs already accept. `main` and `branch-1.1` already throw for a top-level 
DATE to NTZ overflow in a filtered scan, because that column goes through the 
native `Cast` in legacy mode, whose NTZ branch calls Arrow's cast with `safe: 
false`. Spark can't prune on the converted column either, apart from `IS NULL` 
and `IS NOT NULL`. `ParquetFilters.valueCanMakeFilterOn` only builds a filter 
on a DATE column for a `Date`, a `LocalDate` or a null, and an NTZ literal is a 
`LocalDateTime`.
   
   Could the DATE to NTZ arm in `parquet_convert_array_impl` stay checked 
whether or not the scan has filters, and this guard and its compatibility 
paragraph go? That keeps genuine NTZ scans native, and a selected overflowing 
row still gets the plain `ArithmeticException`. It also fixes filtered nested 
fields, which `main` silently nulls. If you want Spark's `IS NULL` pruning 
covered exactly, a null check through the checked conversion is the same as a 
null check on the DATE column, so it can be unwrapped. For the test, a file 
whose overflowing rows sit in a row group that both engines prune on `id` 
statistics would cover the pruning case without asserting a fallback.



##########
spark/src/main/scala/org/apache/comet/expressions/CometCast.scala:
##########
@@ -520,6 +568,18 @@ object CometCast
       case _ => Unsupported(Some(s"Cast from DateType to $toType is not 
supported"))
     }
 
+  /**
+   * Resolve fixed-zone aliases with Spark's rules, then serialize only the 
subset understood by
+   * the native fixed-offset parser. Checking `isFixedOffset` alone would 
admit UTC aliases that
+   * chrono rejects and offsets with seconds that chrono can silently truncate 
to whole minutes.
+   */
+  private[comet] def nativeDateTimestampTimezone(timezone: String): 
Option[String] =

Review Comment:
   This duplicates `CometTimeZone.nativeId`. Both normalize with 
`DateTimeUtils.getZoneId(...).normalized()`, map a zero offset to `UTC`, keep 
whole-minute offsets as `+HH:MM` and reject offsets with seconds. `castToProto` 
already runs the result through `nativeId`, so the remap in `convert` doesn't 
change anything. Could `canCastFromDate` ask `CometTimeZone` instead, for 
example through an `isFixedOffset` helper next to `nativeId`, and the remap go? 
`timezones.md` asks for timezones to reach native code only through 
`CometTimeZone`, and keeping one parser for these IDs was the point of the 
alias change in the first round.



##########
native/spark-expr/src/conversion_funcs/temporal.rs:
##########
@@ -42,50 +38,47 @@ pub(crate) fn cast_date_to_timestamp(
     cast_options: &SparkCastOptions,
     target_tz: &Option<Arc<str>>,
 ) -> SparkResult<ArrayRef> {
-    if target_tz.is_none() {
-        // TIMESTAMP_NTZ ignores session TZ. Only TRY_CAST turns overflow into 
null.
-        return Ok(cast_with_options(
-            array_ref.as_ref(),
-            &DataType::Timestamp(TimeUnit::Microsecond, None),
-            &CastOptions {
-                safe: cast_options.eval_mode == EvalMode::Try,
-                ..DEFAULT_CAST_OPTIONS
-            },
-        )?);
-    }
-
+    // Retain checked arithmetic instead of Arrow's generic NTZ cast so 
failures preserve
+    // Spark's plain ArithmeticException("long overflow") in both ANSI and 
legacy modes.
     let date_array = array_ref.as_primitive::<Date32Type>();
     let mut builder = 
TimestampMicrosecondBuilder::with_capacity(date_array.len());
-    // TIMESTAMP: midnight in session TZ → UTC epoch μs
-    let tz_str = if cast_options.timezone.is_empty() {
-        "UTC"
+
+    // Date32 has a wider range than both chrono's calendar and an i64 
microsecond timestamp.
+    // Use day arithmetic for NTZ and fixed-offset zones. Region-zone casts 
run through Spark's
+    // JVM codegen dispatcher (see CometCast.canCastFromDate), which owns 
their full-range rules.
+    let offset_seconds = if target_tz.is_none()
+        || cast_options.timezone.is_empty()
+        || cast_options.timezone == "UTC"
+    {
+        0
     } else {
-        cast_options.timezone.as_str()
+        // Reject any unexpected region-zone call safely instead of 
constructing a chrono date.
+        // CometCast canonicalizes fixed-zone aliases to UTC or +/-HH:MM 
before serialization.
+        cast_options
+            .timezone
+            .parse::<FixedOffset>()
+            .map_err(|_| {
+                ArrowError::ParseError(format!(
+                    "Invalid timezone \"{}\": expected a fixed offset",
+                    cast_options.timezone
+                ))
+            })?
+            .local_minus_utc()
     };
-    // safe to unwrap since we are falling back to UTC above
-    let tz = Tz::from_str(tz_str)?;
-    let epoch = NaiveDate::from_ymd_opt(1970, 1, 1).unwrap();
     for date in date_array.iter() {

Review Comment:
   This loop is about 2.7x slower than the Arrow cast the merge base uses for 
NTZ, about 8 µs there against 21.6 µs here for 8,192 dates with 1% nulls, in 
both legacy and TRY mode. The fixed-offset `TIMESTAMP` case went the other way, 
from about 165 µs to 22 µs in UTC, because chrono is gone.
   
   You can keep the plain `ArithmeticException` and still get the vectorized 
loop for both. I tried `try_unary` with a closure that returns 
`to_micros(d).ok_or(())`, then `.map_err(|_| SparkError::LongOverflow)`, plus 
`unary_opt` for TRY. That's the same shape as your new Parquet arm. It ran at 
about 9 µs for NTZ, UTC and `+05:30`, and the 125 `conversion_funcs` tests 
still pass. Returning `SparkError` straight from the closure only got to 15.6 
µs, because the wide `Result` slows the hot loop. Could the cast use that form?



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