Support DateTime64 values before 1900 and after 2299#107907
Conversation
Previously `DateTime64` was limited to the range `[1900-01-01, 2299-12-31]`, the exact span covered by the `DateLUTImpl` lookup table. Out-of-range time points were silently clamped to the boundary. `DateLUTImpl` now keeps the loaded cctz time zone and, for time points and day numbers outside `[DATE_LUT_MIN_YEAR, DATE_LUT_MAX_YEAR]`, falls back to cctz instead of clamping. The escape paths are guarded by `unlikely` and live in `DateLUTImpl.cpp`; the in-range fast paths are left byte-for-byte unchanged (the `if constexpr` guard emits the check only for the `Time` and `ExtendedDayNum` arguments that can actually fall out of range). Week and ISO computations reuse the in-range logic via exact 400-year (Gregorian) periodicity. Decomposition is clamped to the representable `[0000, 9999]` window so that the 4-digit year used when formatting can never overflow. The supported range of `DateTime64` is widened to `[0000-01-01, 9999-12-31]`: `MIN_DATETIME64_TIMESTAMP` / `MAX_DATETIME64_TIMESTAMP` are lifted, the text parser's "scaled integer" disambiguation threshold and `parseDateTime`'s year bounds are raised, and a dedicated `MAX_DATE32_TIMESTAMP` keeps `Date32` pinned to `[1900, 2299]`. At precisions 8 and 9 the range stays narrower because the ticks are stored in an `Int64` (nanoseconds reach only `2262-04-11`). The `makeDateTime64` / `YYYYMMDDhhmmssToDateTime64` numeric constructors still clamp to `[1900, 2299]` and can be extended in a follow-up. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
`isOutOfLUTRange` is on the hot path of every `DateTime64` / `Date32` operation. Replace the two-sided comparison `(t < lut_start_time || t >= lut_end_time)` with a single unsigned range check `UInt64(t) - UInt64(lut_start_time) >= lut_time_span`, precomputing `lut_time_span` in the constructor. Operands are cast to unsigned before subtracting to avoid signed overflow for extreme time points. The day-number overload gets the same treatment against `DATE_LUT_SIZE`. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
Workflow [PR], commit [d3d54c6] Summary: ✅
AI ReviewSummaryThis PR fixes most of the earlier Findings❌ Blockers
Final Verdict❌ Request changes. The core |
The per-row out-of-range check on the DateTime64 / Date32 hot path was heavier than the pre-existing in-range checks for two reasons, both now removed: 1. It compared the time point against per-instance, time-zone-dependent members (`lut_start_time` / `lut_end_time`), i.e. two loads per row that the compiler cannot hoist out of the column loop (the data and the members are both Int64, so non-aliasing cannot be proven). The lookup table covers a fixed number of *local* days and the Unix-time boundary cannot shift between time zones by more than the largest UTC offset (well under a day), so the check now uses a compile-time window shrunk by two days on each side — in range for every time zone, no members, no loads. Near-boundary points fall to the cctz path, which is always correct. 2. It duplicated the bounds logic that `findIndex` already performs. Once a value has passed the (now constant) gate, `findIndexInRange` does the day lookup without repeating the clamp; the clamping `findIndex` is kept as the safe default for the un-gated paths (e.g. `DateTime`). Release (-O3) effect: toYear(DateTime64) regression drops from ~+14% to ~+3%, and most date functions return to parity; Date / DateTime are unchanged. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… chrono safety Address review findings on the extended DateTime64 range (PR #107907): - `toStartOfHour` (via `roundDown`) and `toStartOfMinuteInterval` checked the whole-number-offset fast path before the out-of-range escape, so pre-1900 historical sub-hour / sub-minute offsets (e.g. `Europe/Moscow`'s `+02:30:17` LMT) rounded to a UTC boundary instead of the local one. Check `isOutOfLUTRange` first, as `toSecond` / `toMinute` already do. This also initialises the local `date`, fixing the `clang-tidy` `cppcoreguidelines-init-variables` build error. - `toStartOfDayInterval` used truncating division for negative day numbers, which rounds a pre-epoch value forward (past the start of the interval); use floor division, matching the month branch. - `toYearWeek` could produce a week-year just outside `[0000, 9999]` (proleptic `0000-01-01` is a Saturday belonging to ISO week-year -1), wrapping the `UInt16` `YYYYWW` result to 65535; clamp the adjusted year. - The cctz escape paths fed the raw `Time` into `system_clock::from_time_t` before bounding (which can overflow for extreme inputs), and clamped to the representable window in UTC space, shifting valid local boundary values. Bound to a chrono-safe window before constructing the time point and clamp the calendar result afterwards. Related: #107907 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The widened `parseDateTime64` / `parseDateTime64InJodaSyntax` accept years down to 0000, but the days-from-epoch computation looked the year base up in a table that only covered years near the epoch, so a year such as 1899 read past the end of `cumulativeYearDaysBefore1970` and produced a wrong date (e.g. `1970-12-31`). Compute the days from the civil date directly (Howard Hinnant's `days_from_civil`), valid for the whole supported range, and drop the now-unused cumulative-year tables. Update the function documentation to the new range. Related: #107907 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
With `Int64` ticks, precision 8 reaches about `4892-10-07`, not `2861-12-31`. Related: #107907 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Regenerate references and adjust expectations now that `DateTime64` spans `[0000, 9999]`: values that used to clamp at 1900 / 2299 now compute through (or saturate at the `DateTime` / `DateTime64` bounds), `parseDateTime64` accepts years outside the old `[1900, 2299]` range, `Date32` interval arithmetic extends beyond `[1900, 2299]` (parsing and cast still clamp), schema inference now types pre-1900 date strings as `DateTime64`, and scale-9 `toDateTime64` of a value whose ticks overflow `Int64` now reports `DECIMAL_OVERFLOW`. Related: #107907 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
`'2263-01-01 00:00:00'::DateTime64` parsed in the session time zone, so the (intentionally overflowing) result of `toStartOfNanosecond` shifted by the time-zone offset under CI's randomized `session_timezone`. Pin an explicit `UTC` time zone so the regression test for bug #71775 stays deterministic. Related: #107907 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
`ToDate32TransformFromSecondsOrDays` clamped the upper bound with `std::min(time_t(Int64(from)), time_t(MAX_DATE32_TIMESTAMP))`. For a floating-point `from` larger than `Int64` (e.g. `inf`, `3.4e38`, `1.79e308`), `Int64(from)` is undefined behavior: x86 yields `INT64_MIN` (a negative value that then maps far below Date32's range), while AArch64 saturates to `INT64_MAX`. After the lookup table was extended to `[0000, 9999]`, the x86 result surfaced as `0000-01-01` instead of the expected `2299-12-31`, which made `03212_variant_dynamic_cast_or_default` fail only on the x86 fast-test build. Cap the value in the floating-point domain before converting to `time_t`, so a huge float deterministically maps to `2299-12-31` on every architecture. Related: #107907 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The `add{Days,Months,Years}OutOfRange` and `make{Date,DateTime,DayNum}OutOfRange`
cctz escape paths added for the extended DateTime64 range did not bound their
result. A huge interval delta (e.g. `addYears(..., INT64_MAX)`) moved the target
year far outside `[0000, 9999]`, which (a) overflowed `Int64` while computing the
month/year index and (b) overflowed converting cctz's result back to a
`std::chrono::system_clock` time point (`seconds * 10^6`). The function-property
fuzzer reported the latter as a `signed integer overflow` in `<chrono>` under
UBSan.
Cap both the delta and the resulting calendar value to the representable
`[0000, 9999]` range, so out-of-range arithmetic saturates deterministically
instead of invoking undefined behavior.
Related: #107907
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
`addWeeks`/`addDays(toDateTime(...), INT64_MAX)` now saturate the intermediate to the representable calendar (9999-12-31) before the result is converted back to `DateTime`, instead of relying on an overflowing intermediate. The final `Int64 -> UInt32` cast is modular and deterministic across architectures, so the saturated value differs from the previous overflow artifact. Related: #107907 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
`CAST(toDate32('2299-12-31'), 'DateTime64(9)')` threw `DECIMAL_OVERFLOW`:
`ToDateTime64Transform::execute(Int32, ...)` converted the `Date32` day
number straight to `seconds * 10^scale`, and at scale 9 the whole-seconds
value `10413705600` exceeds the `Int64` tick range (only
`[1677-09-21, 2262-04-11]` is representable). The numeric
`ToDateTime64Transform*` transforms already clamp to
`minWholeSecondsForDateTime64` / `maxWholeSecondsForDateTime64` before
multiplying; the `Date32` source overload was missed.
Clamp the whole seconds to the same scale-dependent bounds so the
conversion saturates to the boundary instead of failing, matching the
numeric paths. `Date` (up to 2149) and `DateTime` (up to 2106) day
numbers stay within the bounds at every scale, so only the `Date32`
overload needs the clamp.
Adds `Date32` -> `DateTime64(8/9)` coverage to
`04402_datetime64_extended_range_review_fixes`.
This addresses one of the remaining AI-review blockers on the PR
(#107907).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
Pushed
The 5 remaining open threads are the deliberate design/scope calls already documented inline: year- Verification: the affected translation unit ( |
Resolve conflict in `src/Common/DateLUTImpl.h` `toStartOfMinuteInterval`: master hardened the divisor against overflow (`__builtin_mul_overflow`, saturating to `Int64::max`) while this branch added the out-of-LUT-range escape path for pre-1900 offsets. Combine both, mirroring the already-merged `toStartOfHourInterval`: compute the overflow-safe `divisor` first, then run the `may_be_out_of_lut_range` escape path (now using that safe divisor), then the fast path. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
Merged |
|
The one red on the merge head `184457edd9f` is `04061_spilling_hash_join_overflow_limits` (`Stateless tests (arm_binary, parallel)`) — it is not caused by this PR or by the `master` merge.
Already being fixed (test-only, pins `query_plan_join_swap_table = 'false'`) in #109334. No other failures on the merge head, so the `toStartOfMinuteInterval` conflict resolution is holding up. |
…ended-datetime64-range Pick up master including the fix for flaky test `04061_spilling_hash_join_overflow_limits` (#109334), the sole CI red on this PR.
|
Preview deployment for your docs. Learn more about Mintlify Previews.
|
…teTime64 range The test `04065_toStartOfInterval_overflow` was added on `master` (commit `f030d4d240a`, 2026-07-03) after this branch's merge-base, so its reference was written against the old `DateTime64` range that saturates extreme values at `2299-12-31`. This branch extends the representable range, so the same out-of-range microsecond-scale inputs now saturate at `9999-12-31 23:59:59` instead. The lines that changed all inject the maximum `Int64` raw value into `DateTime64(6)` (~292471 years, out of range for any calendar) and were rendered as `2299-12-31 23:00:54.xxx`; with the extended range they now render as `9999-12-31 23:59:59.xxx`. The `DateTime64(9)` lines (value ~292 years -> `2262-04-11`, in range) are unchanged, confirming this is the intended saturation behavior rather than a regression. This matches the convention already applied to the pre-existing overflow test `04270_toStartOfSubsecond_ubsan_int64_min`, whose reference already expects `9999-12-31 23:59:59`. Merge fallout from bringing `master` into the branch; no product code change.
…ended-datetime64-range
| if constexpr (date_time_overflow_behavior == FormatSettings::DateTimeOverflowBehavior::Throw) | ||
| { | ||
| if (from < MIN_DATETIME64_TIMESTAMP || from > MAX_DATETIME64_TIMESTAMP) [[unlikely]] | ||
| if (from < min_whole || from > max_whole) [[unlikely]] |
There was a problem hiding this comment.
The new scale-aware float guard is using the integer whole-second cutoff, which trims away valid fractional values on the last representable second.
At scale 9, toDateTime64(9223372036.5, 9, 'UTC') should still succeed: 9223372036.5 * 10^9 = 9223372036500000000, which is below Int64::max(). But this branch now treats any from > 9223372036 as out of range, so throw mode raises VALUE_IS_OUT_OF_RANGE_OF_DATA_TYPE and the non-throwing modes clamp the value down to 9223372036.000000000.
The float path needs full tick-range bounds in floating point (Int64::min/max / scale_multiplier, then intersected with the calendar-wide 0000..9999 window), not minWholeSecondsForDateTime64 / maxWholeSecondsForDateTime64, which are only correct for the integer transforms.
…ended-datetime64-range
LLVM Coverage Report
Changed lines: Changed C/C++ lines covered: 626/687 (91.12%) · Uncovered code |
Master extended the representable DateTime64 range to [0000, 9999] (ClickHouse#107907 and follow-ups). Out-of-LUT day arithmetic no longer clamps to the LUT boundaries; `addDays` now takes a cctz escape path that computes the value and saturates only at the representable calendar bounds. The reference of `04511_add_days_fixed_offset_fast_path` was written against the old clamping behavior, so the boundary section failed after merging master. Regenerate the reference and drop the comments that describe the removed clamping. The fast path itself is unaffected: `dayShiftStaysWithinLUT` only admits values strictly inside the LUT, where every day of a fixed-offset time zone is 86400 seconds long, and the new `isOutOfLUTRange` window is two days narrower than the LUT, which makes the guard more conservative rather than less. Verified by rebuilding with the fast-path call sites reverted to the calendar path: the output of the test is identical with and without them. Also correct two dates in the gtest comments - 10413792000 is 2300-01-01 00:00:00, so the last second of the LUT is 2299-12-31 23:59:59 and day index 146096 is 2299-12-31, not 2300-01-01. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ored valid_until encoding Master merged #107907, which extended `DateTime64` from [1900, 2299] to [0000-01-01, 9999-12-31]. This broke the `VALID FOR` deadline computation, which round-tripped the saturated `DateTime64` value through a datetime string: the new lower-bound string `0000-...` was silently misparsed by best-effort parsing as the current year, so `VALID FOR INTERVAL -1000000 YEAR` stored a deadline of "now" instead of a deep-past instant. Integration tests `test_valid_for_interval_overflow` and `test_valid_for_interval_negative_overflow` failed: https://s3.amazonaws.com/clickhouse-test-reports/json.html?PR=110171&sha=f13cf99d1000bd15318517bef457a09855cae128&name_0=PR&name_1=Integration%20tests%20%28amd_asan_ubsan%2C%20flaky%29 - The deadline is now extracted numerically (`toUnixTimestamp64Second`) instead of via a datetime string, and every pre-epoch result is clamped to the smallest expired instant, 1 (`1970-01-01 00:00:01`): all deadlines in the past are equivalent (the credential is expired), and 0 means "no expiration". A huge positive interval now saturates at year 9999 (the new upper bound). - The stored (`ATTACH USER`) encoding of `valid_until`, previously a raw `Int64` string, did not round-trip the full `time_t` range (review finding): `readDateTimeText` rejects 1-4 digit timestamps as ambiguous, so `VALID UNTIL '1970-01-01 00:00:01 UTC'` (stored as `'1'`) made the user unloadable after a restart, and older versions reject negative timestamps outright. Post-epoch deadlines are now zero-padded to 10 digits, which every supported reader parses exactly. Pre-1970 deadlines (reachable only via an explicit pre-1970 `VALID UNTIL`, since the interval path clamps) are stored in the `YYYY-MM-DD hh:mm:ss UTC` datetime form, clamped to the 1900 floor: current versions resolve the exact instant through the `UTC` suffix, and older versions parse the same datetime form they have always written themselves. - New unit test `gtest_valid_until_attach_encoding` pins the encoding contract (exact round-trip over the full supported range, and the literal stored forms), and new integration tests cover restart round-trips of `VALID UNTIL '1970-01-01 00:00:01 UTC'` and a pre-1970 deadline. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The stored access entity encoding (AuthenticationData::toAST, attach mode) writes a pre-epoch VALID UNTIL deadline as a date-time string that older servers - whose DateLUT has no year earlier than 1900 - can still parse, and clamps anything earlier to that floor. CREATE/ALTER USER ... VALID UNTIL still accepted and stored earlier deadlines exactly on the live object, so a user created with e.g. VALID UNTIL '0001-01-01 00:00:00 UTC' would come back from SHOW CREATE USER as 1900-01-01 00:00:00 after a restart or replication round-trip - a silent, lossy rewrite of the stored definition, and only reachable now that #107907 extended DateTime64 down to year 0000. Reject such deadlines at query time instead: getValidUntilFromAST throws BAD_ARGUMENTS when the parsed live deadline is earlier than the new MIN_VALID_UNTIL_TIME bound, so the inconsistency can no longer occur. The existing AuthenticationData::toAST clamp is kept as a defensive guard for AuthenticationData objects built without going through query parsing. Addresses the clickhouse-gh AI-Review "Request changes" verdict on #110171.
In #4534 we added logic to proactively clamp dates to CH Date32 limits at the time. However, the scope of that change didn't include CDC. CDC normalization was therefore constrained by ClickHouse's `parseDateTime64BestEffortOrNull` limits. These limits changed with with ClickHouse/ClickHouse#107907 leaving initial loads and cdc supported date ranges inconsistent with each other. This PR aligns CDC to use the same limits as initial load. From now this is a constant to `[1900-01-01, 2299-12-31]` but might want to follow up with a piece of functionality probing destination CH valid ranges to make the most of the target CH version capabilities.
Related: #65380
DateTime64was limited to[1900-01-01, 2299-12-31]— the exact span covered by theDateLUTImpllookup table — and out-of-range values were silently clamped to the boundary.This extends the supported range to
[0000-01-01, 9999-12-31].DateLUTImplkeeps the loaded cctz time zone and, for time points and day numbers outside[DATE_LUT_MIN_YEAR, DATE_LUT_MAX_YEAR], falls back to cctz instead of clamping. The escape paths are guarded byunlikelyand live inDateLUTImpl.cpp; the in-range fast paths forDate/DateTime/Date32are left unchanged — anif constexprguard emits the check only for theTimeandExtendedDayNumarguments that can actually fall out of range. Week / ISO computations reuse the in-range logic via exact 400-year (Gregorian) periodicity. Decomposition is clamped to the representable[0000, 9999]window so the 4-digit year used when formatting can never overflow.MIN_DATETIME64_TIMESTAMP/MAX_DATETIME64_TIMESTAMP, the text parser's "scaled integer" disambiguation threshold, andparseDateTime's year bounds are lifted accordingly; a dedicatedMAX_DATE32_TIMESTAMPkeepsDate32pinned to[1900, 2299]. With precision 8 or 9 the range stays narrower because ticks are stored in anInt64(nanoseconds reach only2262-04-11). ThemakeDateTime64/YYYYMMDDhhmmssToDateTime64numeric constructors still clamp to[1900, 2299]and can be extended in a follow-up.This addresses the
DateTime64part of the linked issue; extendingDate32is left as follow-up.Performance (release
-O3, 200M rows):Date/DateTimedecomposition is unchanged; the per-row out-of-range check adds ~2-14% toDateTime64decomposition and toformatDateTime(worst casetoYear(DateTime64)+14%,formatDateTime+6-8%). This can be brought back to parity with a per-column in-range pre-check in the function executor if the cost is not acceptable.Changelog category (leave one):
Changelog entry (a user-readable short description of the changes that goes into CHANGELOG.md):
Extended the supported range of
DateTime64from[1900-01-01, 2299-12-31]to[0000-01-01, 9999-12-31]. Values outside the former range are now computed correctly (via cctz) instead of being clamped to the boundary. With precision 8 or 9 the range remains narrower because the ticks are stored asInt64(with nanosecond precision the maximum is still2262-04-11). Backward compatibility: if you used out-of-range date-times before, but relied on the very specific saturation rules inside the old range, keep in mind that now the results are changing to be more correct.Version info
26.7.1.880(included in26.7and later)