sunchao commented on code in PR #5130:
URL: https://github.com/apache/datafusion-comet/pull/5130#discussion_r4104259257


##########
native/spark-expr/src/conversion_funcs/string.rs:
##########
@@ -1634,87 +1589,322 @@ fn extract_offset_suffix(value: &str) -> Option<(&str, 
Tz)> {
     None
 }
 
-type TimestampParsePattern<T> = (&'static Regex, fn(&str, &T) -> 
SparkResult<Option<i64>>);
-
-// RE_YEAR allows only 4-6 digits (not 7) because a bare 7-digit string like 
"0119704"
-// is ambiguous and Spark rejects it. The other patterns (RE_MONTH, RE_DAY, 
etc.) keep
-// \d{4,7} because the `-` separator disambiguates the year portion, so 
"0002020-01-01"
-// is validly year 2020 with leading zeros. date_parser's is_valid_digits also 
allows up
-// to 7 year digits for the same reason.
-static RE_YEAR: LazyLock<Regex> = LazyLock::new(|| 
Regex::new(r"^-?\d{4,6}$").unwrap());
-static RE_MONTH: LazyLock<Regex> = LazyLock::new(|| 
Regex::new(r"^-?\d{4,7}-\d{2}$").unwrap());
-static RE_DAY: LazyLock<Regex> = LazyLock::new(|| 
Regex::new(r"^-?\d{4,7}-\d{2}-\d{2}$").unwrap());
-static RE_HOUR: LazyLock<Regex> =
-    LazyLock::new(|| Regex::new(r"^-?\d{4,7}-\d{2}-\d{2}[T 
]\d{1,2}$").unwrap());
-static RE_MINUTE: LazyLock<Regex> =
-    LazyLock::new(|| Regex::new(r"^-?\d{4,7}-\d{2}-\d{2}[T 
]\d{2}:\d{2}$").unwrap());
-static RE_SECOND: LazyLock<Regex> =
-    LazyLock::new(|| Regex::new(r"^-?\d{4,7}-\d{2}-\d{2}[T 
]\d{2}:\d{2}:\d{2}$").unwrap());
-static RE_MICROSECOND: LazyLock<Regex> =
-    LazyLock::new(|| Regex::new(r"^-?\d{4,7}-\d{2}-\d{2}[T 
]\d{2}:\d{2}:\d{2}\.\d+$").unwrap());
-static RE_TIME_ONLY_H: LazyLock<Regex> = LazyLock::new(|| 
Regex::new(r"^T\d{1,2}$").unwrap());
-static RE_TIME_ONLY_HM: LazyLock<Regex> =
-    LazyLock::new(|| Regex::new(r"^T\d{1,2}:\d{1,2}$").unwrap());
-static RE_TIME_ONLY_HMS: LazyLock<Regex> =
-    LazyLock::new(|| Regex::new(r"^T\d{1,2}:\d{1,2}:\d{1,2}$").unwrap());
-static RE_TIME_ONLY_HMSU: LazyLock<Regex> =
-    LazyLock::new(|| Regex::new(r"^T\d{1,2}:\d{1,2}:\d{1,2}\.\d+$").unwrap());
-static RE_BARE_HM: LazyLock<Regex> = LazyLock::new(|| 
Regex::new(r"^\d{1,2}:\d{1,2}$").unwrap());
-static RE_BARE_HMS: LazyLock<Regex> =
-    LazyLock::new(|| Regex::new(r"^\d{1,2}:\d{1,2}:\d{1,2}$").unwrap());
-static RE_BARE_HMSU: LazyLock<Regex> =
-    LazyLock::new(|| Regex::new(r"^\d{1,2}:\d{1,2}:\d{1,2}\.\d+$").unwrap());
+/// The timestamp string shapes the parser recognises, listed in the order 
they are matched.
+/// The shapes are mutually exclusive, so at most one can apply to any given 
string.
+#[derive(Clone, Copy, PartialEq, Eq, Debug)]
+enum TimestampPattern {
+    Year,
+    Month,
+    Day,
+    Hour,
+    Minute,
+    Second,
+    Microsecond,
+    TimeOnlyH,
+    TimeOnlyHm,
+    TimeOnlyHms,
+    TimeOnlyHmsu,
+    BareHm,
+    BareHms,
+    BareHmsu,
+}
+
+impl TimestampPattern {
+    /// Every shape, in the order they are matched. First match wins, so the 
order matters.
+    const ALL: [TimestampPattern; 14] = [
+        Self::Year,
+        Self::Month,
+        Self::Day,
+        Self::Hour,
+        Self::Minute,
+        Self::Second,
+        Self::Microsecond,
+        Self::TimeOnlyH,
+        Self::TimeOnlyHm,
+        Self::TimeOnlyHms,
+        Self::TimeOnlyHmsu,
+        Self::BareHm,
+        Self::BareHms,
+        Self::BareHmsu,
+    ];
+
+    /// The equivalent regular expression for this shape.
+    ///
+    /// Only used for the rare non-ASCII input, where the Unicode-aware `\d` 
class accepts
+    /// digits (e.g. Arabic-Indic) that the ASCII classifier below does not.
+    ///
+    /// `Year` allows only 4-6 digits (not 7) because a bare 7-digit string 
like "0119704" is
+    /// ambiguous and Spark rejects it. The others keep `\d{4,7}` because the 
`-` separator
+    /// disambiguates the year portion, so "0002020-01-01" is validly year 
2020 with leading
+    /// zeros. `date_parser`'s `is_valid_digits` allows up to 7 year digits 
for the same reason.
+    fn regex_str(self) -> &'static str {
+        match self {
+            Self::Year => r"^-?\d{4,6}$",
+            Self::Month => r"^-?\d{4,7}-\d{2}$",
+            Self::Day => r"^-?\d{4,7}-\d{2}-\d{2}$",
+            Self::Hour => r"^-?\d{4,7}-\d{2}-\d{2}[T ]\d{1,2}$",
+            Self::Minute => r"^-?\d{4,7}-\d{2}-\d{2}[T ]\d{2}:\d{2}$",
+            Self::Second => r"^-?\d{4,7}-\d{2}-\d{2}[T ]\d{2}:\d{2}:\d{2}$",
+            Self::Microsecond => r"^-?\d{4,7}-\d{2}-\d{2}[T 
]\d{2}:\d{2}:\d{2}\.\d+$",
+            Self::TimeOnlyH => r"^T\d{1,2}$",
+            Self::TimeOnlyHm => r"^T\d{1,2}:\d{1,2}$",
+            Self::TimeOnlyHms => r"^T\d{1,2}:\d{1,2}:\d{1,2}$",
+            Self::TimeOnlyHmsu => r"^T\d{1,2}:\d{1,2}:\d{1,2}\.\d+$",
+            Self::BareHm => r"^\d{1,2}:\d{1,2}$",
+            Self::BareHms => r"^\d{1,2}:\d{1,2}:\d{1,2}$",
+            Self::BareHmsu => r"^\d{1,2}:\d{1,2}:\d{1,2}\.\d+$",
+        }
+    }
+
+    /// True for the shapes that carry no date component: `T12`, `T12:34`, 
`12:34`, ...
+    fn is_time_only(self) -> bool {
+        !matches!(
+            self,
+            Self::Year
+                | Self::Month
+                | Self::Day
+                | Self::Hour
+                | Self::Minute
+                | Self::Second
+                | Self::Microsecond
+        )
+    }
+
+    /// True for the `T`-prefixed time-only shapes only, which Spark 4.0+ 
rejects when the
+    /// raw value has leading whitespace.
+    fn is_t_time_only(self) -> bool {
+        matches!(
+            self,
+            Self::TimeOnlyH | Self::TimeOnlyHm | Self::TimeOnlyHms | 
Self::TimeOnlyHmsu
+        )
+    }
+}
+
+static TIMESTAMP_PATTERN_SET: LazyLock<RegexSet> = LazyLock::new(|| {
+    
RegexSet::new(TimestampPattern::ALL.map(TimestampPattern::regex_str)).unwrap()
+});
+
+/// Returns the shape `value` has, or `None` when it matches none of them.
+///
+/// ASCII input - effectively all real data - is classified by a single 
left-to-right byte
+/// scan. Non-ASCII input falls back to a single `RegexSet` pass, which 
reports every
+/// matching pattern in one search of the haystack; the lowest matching index 
is taken so
+/// that the result is the same first-match-wins answer the ASCII scan gives.
+fn classify_timestamp_pattern(value: &str) -> Option<TimestampPattern> {
+    if value.is_ascii() {
+        classify_ascii_timestamp_pattern(value.as_bytes())
+    } else {
+        TIMESTAMP_PATTERN_SET
+            .matches(value)
+            .iter()
+            .next()
+            .map(|i| TimestampPattern::ALL[i])
+    }
+}
+
+/// Number of leading ASCII digits in `bytes`.
+fn digit_run(bytes: &[u8]) -> usize {
+    bytes
+        .iter()
+        .position(|b| !b.is_ascii_digit())

Review Comment:
   [P2] Could we bound digit scans for fixed-width fields? For a string column 
containing `repeat('1', 8192) || 'Z'`, a legacy timestamp cast should reject 
the oversized year promptly and return NULL. The result remains NULL, but 
`digit_run` now traverses all 8,192 digits both before and after stripping `Z`. 
The base regexes reject after a bounded prefix. Release-mode measurements 
reproduce a 12.5–12.9× parser slowdown, reducing throughput for batches 
containing these malformed values. Stop after the maximum field width plus one 
for year/month/day/hour/minute/second, retain unbounded scanning for fractional 
digits, and include this shape in the benchmark.
   
   Evidence: A disposable Rust harness copied the timestamp parser bodies from 
the requested base and head and built both with optimized dependencies (`regex` 
1.13.1, `chrono` 0.4.45, Arrow 59.3.0). It called `timestamp_parser` with UTC, 
Legacy mode, and `is_spark4_plus=false`, using 20 warm-up calls and 10,000 
timed calls per sample. Across two independent processes with three samples 
each, 8,192 ASCII digits followed by `Z` took approximately 431–444 ns/call at 
base versus 5,533–5,578 ns/call at head. With 1,024 digits, the regression was 
approximately 1.85–1.95×. A disposable variant bounding fixed-field scans to 
eight bytes while preserving unrestricted fraction validation reduced the 
8,192-digit case to approximately 374–414 ns/call. Reproduction source and logs 
are under `/tmp/comet-5130-review/perf/`, `/tmp/comet-5130-review/perf.log`, 
and `/tmp/comet-5130-review/perf-second.log`.



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