fix: align string to timestamp parsing with Spark's segment rules - #5682
fix: align string to timestamp parsing with Spark's segment rules#5682peterxcli wants to merge 5 commits into
Conversation
Spark's SparkDateTimeUtils.parseTimestampString validates each segment with isValidDigits: month, day, hour, minute and second take 1-2 digits, the fraction after '.' may be empty, a timestamp year takes at most 6 digits (only stringToDate allows 7), and a zone id is only captured when the scanner is inside the seconds or fraction segment. Comet's regex table required exactly 2 digits and a non-empty fraction, allowed 7-digit years, and stripped a zone suffix from any shape, so '2020-1-1', '2020-01-01 12:34:5' and '2020-01-01 12:34:56.' returned NULL (or raised under ANSI) while '2020-10-01Z' and '0002020-01-01 00:00:00' were accepted. Relax the segment quantifiers, cap the year at 6 digits for timestamp shapes, allow an empty fraction, and only honour a stripped suffix when the remainder ends in a seconds or fraction segment, for both TIMESTAMP and TIMESTAMP_NTZ. Previously accepted inputs keep their exact values; CAST(... AS DATE) keeps its 7-digit years because date_parser is a separate port of stringToDate. Closes apache#5674 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
sunchao
left a comment
There was a problem hiding this comment.
Correctness
This change brings the timestamp grammar closer to Spark 3.5 and 4.0. It permits one-digit month, day and time segments and an empty fractional segment, restricts years to four through six digits, and accepts a timezone suffix only after seconds or fractional seconds. The separate DATE parser is unchanged. I compared the TIMESTAMP and TIMESTAMP_NTZ paths, the Legacy/ANSI/Try wrappers, null propagation, timezone handling and the native array dispatch against Spark's parser and cast implementation.
There is one new [P2] correctness issue in the broadened digit classes, detailed inline. A newly accepted non-ASCII digit can pass the regex and then be replaced by a default component when integer parsing fails. That turns malformed column input into a real timestamp, including in ANSI mode, instead of following Spark's rejection semantics. I separated this regression from the older two-digit Unicode behavior and did not report the older behavior as introduced here.
Validation and CI
The focused Rust probe compiled the exact head/base production parser blocks, helper graph, error definitions and Arrow array entry points. It covered 117 main inputs plus separate Unicode and empty/error sets across Legacy, ANSI and Try and both Spark-version flags. The 2,040 main/Unicode evidence assertions and 60 empty/error checks include assertions confirming the reported defect. They are not a claim that every input matched Spark. Column-array checks are important here because literal casts can be folded by Spark before native execution.
At the final CI/discussion refresh, there were 65 successful and nine skipped checks. I also verified the tested merge's exact parents for the inspected Rust and Spark jobs. Those logs report 1,114 Rust tests passed with four skipped, 1,296 Spark 3.5 tests passed with 11 cancelled and 12 ignored, and 1,303 Spark 4.0 tests passed with three cancelled and 12 ignored. No failures were reported in those inspected test summaries. The custom Unicode input was exercised through native Arrow entry points, not through a local Spark/JNI run. I did not run a local full Comet workspace suite or a distributed query.
Performance
The new suffix predicate does not add a heap allocation, but zoned inputs can now be classified once by that predicate and again by the main regex dispatch. I checked this with two alternating-order component benchmark runs using 8,192-row arrays, nulls, repeated kernel calls and nine samples. Ordinary date and ISO cases were roughly unchanged. Zulu-suffix cases measured about 1.16–1.21 times the previous time, and numeric offsets about 1.07–1.10 times. Named-zone results varied more.
These are shared-host parser measurements without CPU pinning, not a Spark-versus-Comet or end-to-end throughput result. They identify the extra classification cost but do not establish a query-level regression. The existing regex-dispatch design remains the main source of repeated work. I did not infer a performance improvement from the expanded correctness coverage.
Design
Keeping the timezone-suffix rule in one predicate makes the intended Spark rule easier to inspect across TIMESTAMP and TIMESTAMP_NTZ. Preserving the separate DATE path also limits the behavioral scope. The year bound, empty-fraction behavior and suffix placement are understandable local changes rather than a new parsing architecture.
The important design invariant is that the shape recognizer and numeric decoder must accept the same alphabet. The inline fix can restore that invariant locally while retaining the intended one-digit support. A broader parser rewrite is not needed to address the demonstrated regression.
Abstraction & complexity
The patch reuses the existing parser table, conversion helpers and cast dispatch rather than adding a new public interface, dependency or configuration. The shared suffix helper represents a concrete rule used by both timestamp paths, so its level of abstraction is appropriate.
The remaining complexity comes from having regex recognition and component decoding as separate stages. I traced both stages instead of treating a regex match as proof that decoding succeeds. The existing fallback defaults make that separation observable for malformed digits, which is why the new grammar needs the ASCII constraint described inline.
sunchao
left a comment
There was a problem hiding this comment.
Correctness
Follow-up on bcd3803717ec3e6d24d7a30637251d2669307591 against base 81d637b9bf40a5be6f4f0c65ad6f497b34746e69. The four reported month/day/minute/second examples are fixed. The added cases use Parquet-backed columns, with each malformed input evaluated separately under ANSI mode.
[P2] The existing Unicode-digit finding remains partially unresolved. In the newly accepted short-date forms, 2020-1-1T٢ still becomes 2020-01-01 00:00:00, and 2020-1-1T1:2:3.٢ becomes 2020-01-01 01:02:03 (UTC; ٢ is U+0662). Both produce values for TIMESTAMP and TIMESTAMP_NTZ in Legacy, TRY and ANSI modes. The authoritative base and Spark return NULL in Legacy/TRY and reject them in ANSI. This continues the existing finding, rather than adding a duplicate inline comment.
The hour class in RE_HOUR and the fraction class in RE_MICROSECOND still use Unicode-aware \d; failed numeric conversion then defaults to zero. The maintained Spark 3.5 and 4.0 scanners accept only ASCII digits in these segments. Please make the remaining numeric classes consistent with that rule and add these mixed short-date cases to the column regressions.
Validation
Exact production parser/Arrow cast blocks from the base, previous head and current head reproduced both failures. Spark 4.0.4 Cast(BoundReference) and generated projections agreed on rejection. The 180-input probe produced 9,720 native result records and 1,080 Spark evaluations through each path; 2,250 ASCII/null comparisons were unchanged by this update. This was component validation; the broader corpus also contains existing mismatches and is not an all-pass compatibility claim. No local full Comet JNI suite or Spark 3.4/3.5/4.1 runtime validation was performed. Canonical Spark 3.4/4.1 source refs were unavailable.
CI logs confirm the Rust segment tests and both timestamp segment suites in the Linux Spark 3.5/4.0 expression jobs passed. They executed merge 48846aee, combining this head with 719cba11, not the authoritative base above. The fresh head-check read at 21:37 UTC reports 64 successful and nine skipped checks, with no failing or unfinished check in that list. That does not change the merge/base qualification above.
Performance
The update narrows existing regex character classes without adding another per-row scan, allocation or parser pass. No new performance concern was identified in this delta. The correctness probe is not a benchmark; no throughput improvement is claimed.
Design
Restricting the numeric grammar is a small, appropriate fix, but it needs to cover whole accepted shapes: shortening an ASCII month/day can currently expose an unchanged Unicode hour or fraction. Applying the same digit rule throughout those patterns closes this gap without changing timezone handling or adding a separate parser.
Abstraction & complexity
No new production abstraction is introduced. The existing shared malformed-input fixtures remain suitable for the additional cases, and separate ANSI evaluations ensure a failure in the first input does not hide later regressions.
sunchao
left a comment
There was a problem hiding this comment.
Correctness
Follow-up on 0a1fc76a02ae14228a6c521d6dda1949f8afc312 against base 81d637b9bf40a5be6f4f0c65ad6f497b34746e69. The remaining cases in the existing P2 are fixed. Both 2020-1-1T٢ and 2020-1-1T1:2:3.٢ now follow the malformed-input path for TIMESTAMP and TIMESTAMP_NTZ. No remaining or new P1/P2 was found in this update.
All 14 timestamp patterns now use ASCII digits, matching the maintained Spark 3.5 and 4.0 byte scanners. I traced the year, hour, fraction and time-only classes through direct matching, timezone-suffix classification and both inner dispatch tables. Numeric Unicode cannot pass those gates and then become a default component. The date parser, numeric decoders, dispatch and dependencies are unchanged. For ASCII input, the regex languages are equivalent to the previous head, preserving the existing widths, empty fractions, timezone handling and microsecond calculation.
Validation
The expanded Rust parser fixtures and Parquet-backed Scala fixtures include the remaining examples, Unicode years, zoned fractions and time-only forms. Each malformed Scala input is evaluated separately for both timestamp types, so an earlier ANSI failure cannot hide later inputs. The helper explicitly checks native operators for Legacy and compares TRY results and ANSI exceptions.
CI logs confirm the three Rust segment tests and both timestamp segment suites passed on Spark 3.5 and Spark 4.1. Those jobs tested merge 7db8334c, combining this head with 719cba11. The relevant parser, dependency and timestamp-test sections match the head; this is not a claim that CI tested the authoritative base above.
The local Spark 4.0.4 component probe covered 457 inputs with 2,742 evaluations through each of the interpreted and generated paths. All 266 focused Unicode inputs returned NULL in Legacy/TRY and CAST_INVALID_INPUT in ANSI for both types. Source comparison additionally verified all 14 ASCII grammars. No new local native parser build or full Comet JNI suite was run. The earlier native corpus remains historical and includes known pre-existing mismatches; maintained Spark 3.4/4.1 source gaps remain recorded.
At 2026-09-05T02:39:14.744Z, current-head checks reported 53 successful, 11 running and 7 skipped, with no failures. Full CI was still running at that cutoff.
Performance
The update narrows the existing regex classes without adding a scan, allocation or parser pass. ASCII inputs retain the same classification decisions. No new performance concern was found in this delta, and no throughput improvement is claimed; the correctness checks were not benchmarks.
Design
Applying the same digit alphabet to every accepted timestamp shape closes the gap between recognition and numeric decoding. The shared predicates now enforce that rule consistently across direct and zoned input, while preserving the separate DATE grammar. This is an appropriate local fix.
Abstraction & complexity
No new production abstraction, dependency or configuration is introduced. Extending the shared malformed-input fixtures keeps the change small, and the existing per-input ANSI queries provide useful regression coverage without another testing framework.
|
I dug into this one because the regex edits looked like they might cost something, and it turns out the opposite is true. Benchmarking The description also undersells the fix. On main, Related, and also worth putting in the description: on main On the For what it is worth, I checked Last thing, and I think it is the real story here. Your note about the fuzz alphabet is the important one: |
|
Addressed the follow-up:
Validation: 662 Rust tests passed; |
sunchao
left a comment
There was a problem hiding this comment.
Correctness
Follow-up at 7c449fd9 against 81d637b9, covering the changes since 0a1fc76a. One new P2 remains: trimming the extracted suffix prefix newly admits a leading-whitespace T-time-only input on Spark 4 without reapplying its existing rejection guard. The inline comment traces the exact input and Legacy/TRY/ANSI behavior.
The two fraction get(..) guards are safe, and all 14 ASCII digit patterns remain unchanged, so the previous Unicode-digit P2 stays fixed. The added month10/12-day1, negative-year and spaced-offset fixtures improve coverage. Null handling, fractional truncation, component bounds and overflow paths are unchanged. The known positive bare-year mismatch is explicitly tracked by #5716; the narrow fuzz exclusion and compatibility entry do not fix it. Broader whitespace-set differences, including Unicode separators before a zone, remain covered by #5149; they are not duplicated as another inline.
I compared the maintained Spark 3.5 and 4.0 sources: 3.5 trims bytes before scanning, while 4.0 retains the original byte index and accepts a leading T only at index zero. NTZ rejects time-only input entirely. Maintained Spark 3.4/4.1 source refs were unavailable, which remains a coverage limitation.
Validation
The local Spark 4.0.4 oracle exercised 251 non-foldable column inputs through both Cast evaluation and generated projections: 1,506 results per route, covering both timestamp types and all three modes. The primary counterexample returns NULL in Legacy/TRY and CAST_INVALID_INPUT in ANSI. Comet's changed behavior is established by the source path; I did not run a new local Comet native/JNI build. A separate Rust standard-library trim probe is only whitespace-component evidence.
Current-head Rust CI and Spark 3.5/4.0/4.1 expression jobs passed the segment-rule tests. CI tested merge 6f3dc8a5 (parents 7190df63 and this head), not the authoritative base pair. The parser, dispatch and benchmark blobs, plus relevant Scala timestamp fixtures/helpers, match the head; the merge also contains unrelated numeric tests, package dependency edges and benchmark registrations. At 2026-09-05 21:23:07 UTC, the refreshed head checks report 8 skipped, 65 successful, 1 failed. All checks had completed at that cutoff. The failed macOS scans job records a JVM SIGSEGV; its cause and relationship to this change are unproven.
Performance
The benchmark adds single-digit segments, an empty fraction and an invalid date-only zone. It evaluates columns in prebuilt 8,192-row batches, retains null coverage, and appropriately skips invalid batches under ANSI. The safe fraction access adds no allocation, and the new trim runs only after successful suffix extraction.
The peer benchmark report measured base 81d637b9 versus the previous head 0a1fc76a: roughly 37% lower canonical time, 44% lower microseconds time and 25% lower offset-suffix time, with similar NTZ/non-UTC results. These are historical peer measurements; I did not independently benchmark 7c449fd9, and benchmark compilation/smoke success is not a timing result. No additional material performance finding in this follow-up.
Design
The local defensive fraction guards and expanded fixtures fit the existing parser design. The suffix normalization correctly enables legal space-before-offset datetimes, but that path must also preserve the wrapper's version-specific admission checks. Reapplying the Spark 4 leading-whitespace restriction to the extracted T-time-only prefix, with a column regression fixture, is a narrow fix.
Abstraction & complexity
No new abstraction, dependency or serialization layer is introduced. The existing regex recognition and component decoders remain easy to follow, and the benchmark extends the existing batch generator. No separate complexity finding.
sunchao
left a comment
There was a problem hiding this comment.
Correctness
Follow-up at 89e5ff5c against 81d637b9, reviewing the delta from 7c449fd9. The leading-whitespace P2 is fixed. The wrapper now rejects a leading-whitespace T prefix before any suffix extraction, so a timezone cannot hide the restriction. The reported TAB-prefixed input and fractional variant produce NULL in Legacy/TRY and an error in ANSI on Spark 4. Spark 3.5 retains its trim-first behavior, and NTZ still rejects time-only input.
The maintained Spark 3.5 and 4.0 sources support this distinction: 3.5 starts scanning after trimming the bytes, while 4.0 recognizes a leading T only at original byte index zero. The new guard cannot admit anything rejected by the old guard; it broadens rejection only to leading-whitespace T forms that Spark 4 rejects. I verified that all other production parser bytes are unchanged, including the 14 ASCII patterns, fraction guards, offsets, null handling and overflow paths. No new or remaining P1/P2 was found in this update.
The added Parquet-backed tests evaluate the reported input, a fractional case and a valid control separately for both timestamp types. The helper checks the native operator in Legacy and compares TRY results and ANSI errors. Current CI logs confirm the new test passed on Spark 3.5, Spark 4.0 and Spark 4.1; the native whitespace regression also passed.
Those jobs tested merge d9b1ef12, combining this head with c348f775, rather than the authoritative base above. Its complete parser blob, timestamp dispatch arms and Scala timestamp fixtures/helpers match the head. Its dependency graph is newer, including Arrow 59.3 versus 58.4 and DataFusion 55 versus 54.1; the regex, chrono and chrono-tz package blocks match. This qualifies the CI evidence.
Locally, 232 Spark 4.0.4 inputs produced 1,392 matching results through each of Cast evaluation and generated projections. A separate standard-library guard probe rejected 105 inputs on the Spark-4 flag, all consistent with Spark's rejection, and rejected none on the Spark-3 flag. These are component checks; I did not run a new local Comet native/JNI build. Maintained Spark 3.4/4.1 source refs remain unavailable. The final current-head CI refresh at 2026-09-05T22:49:34Z reported 62 successful, three running and seven skipped checks, with no failed check at that cutoff.
Performance
This delta replaces up to four anchored regex checks with a prefix check on the existing leading-whitespace path. It adds no allocation or parser pass. The benchmark code is unchanged from the previous review. No material performance concern was found; no new timing or end-to-end speedup is claimed.
Design
Checking the raw-input restriction in the outer wrapper is appropriate: it covers direct and zoned time-only forms before either parsing route can bypass the rule. The existing version flag keeps Spark 3.5 behavior intact, and the paired valid control checks that the fix does not reject ordinary zoned time-only input.
Abstraction & complexity
The prefix predicate simplifies the existing condition without introducing an abstraction, dependency or configuration. Reusing the column-cast helper and extending the native fixture provides focused coverage for the actual bypass, including separate invalid ANSI evaluations.
Which issue does this PR close?
Closes #5674.
Rationale for this change
Comet's string-to-timestamp regexes disagreed with Spark's segment scanner for both TIMESTAMP and TIMESTAMP_NTZ. Besides rejecting valid inputs, the fallback could silently return the wrong timestamp: in UTC,
2020-10-1and2020-12-1were interpreted with a trailing -01:00 offset, producing 01:00 instead of midnight. Unicode fractions such as2020-01-01 12:34:56.1٢٢٢andT1:2:3.1٢٢٢could panic when truncated at a non-character byte boundary.The ASCII digit classes also improve throughput. In andygrove's review benchmarks comparing base
81d637b9bwith0a1fc76a0, canonical, microseconds, and offset_suffix times decreased by 37%, 44%, and 25%, respectively, with comparable NTZ/non-UTC improvements. The canonical/microseconds base-versus-base noise floor was below 1%. These are reviewer-reported measurements:[0-9]avoids the more expensive UnicodeNdclass compiled by\dwith regex's default features.What changes are included in this PR?
2021-11-22 10:54:27 +08:00while still rejecting date-only zones.-,.,+, andZ.+[0-9]{4,6}years from this fuzz comparison until fixed.How are these changes tested?
CometNativeCastSuite: 169 passed, 1 failed, exposing String-to-timestamp cast rejects explicit positive years accepted by Spark #5716. After the narrow known-case exclusion, all 7 string-to-timestamp tests passed, including both segment-rule suites and the widened fuzz test.make core, benchmark compilation, all 77 benchmark smoke cases, package-wide Clippy with-D warnings, cargo formatting, Maven Spotless/scalastyle, andgit diff --checkpassed. The benchmark smoke run checks execution, not performance; the throughput figures above are the reviewer's A/B results.