andygrove commented on PR #5682:
URL: 
https://github.com/apache/datafusion-comet/pull/5682#issuecomment-5561675333

   I re-probed and re-benchmarked at `89e5ff5cc`. Everything I asked for last 
time is in, and the `starts_with('T')` rewrite is better than what I had in 
mind. I traced `parseTimestampString` at v4.0.4 and v4.1.3 to check it: once 
`getTrimmedStart(bytes) > 0` and `bytes(j) == 'T'`, every branch falls to 
`return (Array.empty, None, false)`, so there is no leading-whitespace 
T-prefixed input Spark accepts and your predicate is a strict superset of the 
four regexes it replaced. I probed `\x01T2:30`, `\x7FT2:30`, `\x0BT2:30` and `" 
Tgarbage"` under both version flags and Comet agrees with Spark on all of them.
   
   The `trim_end()` I asked you to add turned out to carry an edge case I had 
not thought through when I suggested it. Spark builds the zone string as `new 
String(bytes, j, strEndTrimmed - j)` and then resolves it with 
`getZoneId(zoneName.trim)`. Java's `String.trim` only strips chars `<= U+0020`, 
so Spark keeps `U+00A0` and the other non-ASCII spaces, which makes `ZoneId.of` 
throw and the whole cast return NULL, and it strips `0x01` through `0x1F`, 
which makes the value parse. Rust's `trim_end` gets both of those backwards. I 
ran seven forms against Spark 4.1.3 through Parquet-backed columns and they all 
diverge:
   
   | input (`WS` is the character named) | Spark | Comet |
   | --- | --- | --- |
   | `2020-01-01 12:34:56` `WS=U+00A0` `+08:00` | `null` | `2019-12-31 
20:34:56` |
   | `2020-01-01 12:34:56` `WS=U+3000` `+08:00` | `null` | `2019-12-31 
20:34:56` |
   | `2020-01-01 12:34:56` `WS=U+2009` `+08:00` | `null` | `2019-12-31 
20:34:56` |
   | `2020-01-01 12:34:56` `WS=U+00A0` `UTC` | `null` | `2020-01-01 04:34:56` |
   | `2020-01-01 12:34:56` `WS=U+00A0` `Z` | `null` | `2020-01-01 04:34:56` |
   | `2020-01-01 12:34:56.123` `WS=U+00A0` `+08:00` | `null` | `2019-12-31 
20:34:56.123` |
   | `2020-01-01 12:34:56` `WS=0x01` `+08:00` | `2019-12-31 20:34:56` | `null` |
   
   The first six are silent wrong values, and under ANSI Comet returns a row 
where Spark raises. They are new here, because before this PR there was no 
`trim_end()` on the TIMESTAMP path and all six fell through to NULL. The NTZ 
path already had one, so those are older.
   
   I know @sunchao routed the whitespace-set mismatch to #5149, and that is a 
fair call in general. The epic does cover the class and the compatibility guide 
already documents it for the padded case. What tips it for me here is that the 
fix is one line on a line this PR is adding, and that the PR moves the 
divergence from a NULL to a wrong value. Would you be willing to take:
   
   ```rust
   let stripped = stripped.trim_end_matches(|c: char| c <= '\u{20}');
   ```
   
   at `string.rs:1471`, and the same at the two NTZ sites at `:1823` and 
`:1835`? I applied exactly that and all eleven inputs I probed then match 
Spark, including `0x7F` staying rejected and `0x0B` and `0x0C` staying 
accepted, which is what Spark's split between `getTrimmedEnd` and `String.trim` 
produces. 662 spark-expr tests and all 26 `CometNativeCastSuite` timestamp 
tests still pass, so it does not disturb anything you added.
   
   While you are in there, `2021-11-22 10:54:27 +08:00` is currently the only 
fixture for the whole family that `trim_end()` makes reachable, and there is no 
negative case in the same shape. The one I would most want covered is a zone 
after a non-seconds segment with whitespace in front, since that is precisely 
what `trim_end()` could have broken. `2020-01-01 12:34 UTC` covers the named 
form but not the bare-offset form. I ran these against Spark 4.1.3 and all 
sixteen pass on your head, so they are free regression coverage:
   
   ```scala
   // sparkSegmentRuleTimestamps
   "2020-01-01 12:34:56 Z",
   "2020-01-01 12:34:56\t+08:00",
   "2020-01-01 12:34:56\n+08:00",
   "2020-01-01 12:34:56.123 +08:00",
   "2020-01-01 12:34:56. +08:00",
   "2020-01-01 12:34:56  +08:00",
   "2020-01-01 12:34:56 -08:00",
   "2020-01-01 12:34:56 Europe/Moscow",
   "-0001-01-01T12:34:56 +08:00",
   "-0001-01-01T12:34:56-08:00",
   
   // sparkSegmentRuleMalformedTimestamps
   "2020-01-01 12:34 +08:00",
   "2020-01-01 12 +08:00",
   "2020-01-01 +08:00",
   "2020-01-01 Z",
   "2020-01 +08:00",
   "2020 +08:00",
   ```
   
   The last two valid ones also cover the interaction between a leading `-` 
year sign and `extract_offset_suffix` picking the rightmost sign, which nothing 
tests today.
   
   On the docs, the #5716 bullet landed between the `## Known result-value 
divergences` heading and the "The following native paths silently return values 
that differ from Spark" paragraph that introduces that section's list, so it 
renders as an orphan bullet above its own introduction. Could you move it down 
into the list next to the #5149 entry? That entry already describes a 
NULL-in-Comet direction, so it fits there.
   
   I re-ran `cast_string_to_timestamp` at your head against the merge base, 
three interleaved rounds with run-to-run spread of 1.5% or less on your side. 
`canonical` at -36.6% and `microseconds` at -44.1% still match what I measured 
at `0a1fc76a0`, but `offset_suffix` is now -18.6% rather than -25%, which makes 
sense given `7c449fd9c` added a `trim_end()` and an 
`ends_with_seconds_segment()` call to that exact path. Worth updating that one 
number so the description matches the head. Two things it undersells, though. 
`89e5ff5cc` is a real speedup in its own right: swapping the four anchored 
regex matches for `starts_with('T')` gives -43.4% on `spark4_legacy/padded`. 
And the only shape that got slower is `date_only_zone` at +2.6% on UTC and 
+3.2% on non-UTC, which is the shape that went from returning a wrong value on 
main to returning NULL, so the extra `ends_with_seconds_segment` pass is what 
buys the correctness fix. That is worth saying explicitly, because it is the 
one number a fu
 ture reader might otherwise try to optimise away. Related, 
`ends_with_seconds_segment` also fixes a silent wrong value the description 
does not mention at all: on main `CAST('2020-01-01Z' AS TIMESTAMP)` returns 
`2020-01-01 00:00:00`, and it is NULL now.
   
   Two comments in the benchmark drifted out of date. The one above `batches` 
lists the shapes starting at `canonical`, but the three new ones went in above 
it, so it now describes only part of the array. And the `spark4_legacy` comment 
says it measures "the inputs where that check can fire", but the guard needs 
leading whitespace and a `T` prefix together, and neither `padded` nor `mixed` 
has both, so it measures the check's cost without ever taking its rejection 
branch. The cost is the interesting part given it just moved 43%, so it is 
worth saying that instead.
   
   Last thing, and not something to hold this PR for. While probing your new 
fraction guards I noticed they both return `Ok(None)`, which on the NTZ side 
lands on `timestamp_ntz_parser_inner`'s `None => Ok(None)` arm at 
`string.rs:1871` and returns NULL even under ANSI. So if a future pattern edit 
ever does make those guards reachable, the invariant they exist to protect 
comes back as a silently swallowed ANSI error rather than a panic. That 
asymmetry is already reachable today through the year-range guard at `:1147`, 
where `CAST('294249-01-01' AS TIMESTAMP_NTZ)` under ANSI raises 
`CAST_INVALID_INPUT` in Spark and returns NULL in Comet while the TIMESTAMP 
version raises correctly. It predates this PR so I have filed it separately as 
#5749.
   


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