Skip to content

Support DateTime64 values before 1900 and after 2299#107907

Merged
alexey-milovidov merged 38 commits into
masterfrom
feature/datelut-extended-datetime64-range
Jul 13, 2026
Merged

Support DateTime64 values before 1900 and after 2299#107907
alexey-milovidov merged 38 commits into
masterfrom
feature/datelut-extended-datetime64-range

Conversation

@alexey-milovidov

@alexey-milovidov alexey-milovidov commented Jun 18, 2026

Copy link
Copy Markdown
Member

Related: #65380

DateTime64 was limited to [1900-01-01, 2299-12-31] — the exact span covered by the DateLUTImpl lookup table — and out-of-range values were silently clamped to the boundary.

This extends the supported range to [0000-01-01, 9999-12-31]. DateLUTImpl 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 for Date / DateTime / Date32 are left unchanged — an if constexpr guard emits the check only for the Time and ExtendedDayNum arguments 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, and parseDateTime's year bounds are lifted accordingly; a dedicated MAX_DATE32_TIMESTAMP keeps Date32 pinned to [1900, 2299]. With precision 8 or 9 the range stays narrower because 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.

This addresses the DateTime64 part of the linked issue; extending Date32 is left as follow-up.

Performance (release -O3, 200M rows): Date / DateTime decomposition is unchanged; the per-row out-of-range check adds ~2-14% to DateTime64 decomposition and to formatDateTime (worst case toYear(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):

  • Backward Incompatible Change

Changelog entry (a user-readable short description of the changes that goes into CHANGELOG.md):

Extended the supported range of DateTime64 from [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 as Int64 (with nanosecond precision the maximum is still 2262-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

  • Merged into: 26.7.1.880 (included in 26.7 and later)

alexey-milovidov and others added 2 commits June 18, 2026 03:33
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>
@clickhouse-gh

clickhouse-gh Bot commented Jun 18, 2026

Copy link
Copy Markdown
Contributor

Workflow [PR], commit [d3d54c6]

Summary:


AI Review

Summary

This PR fixes most of the earlier DateTime64 range-extension regressions, but the current head still leaves four user-visible paths on the old or incorrect boundary logic. Fixed-format text input still mishandles year 0000, scale 8/9 text parsing still ignores the narrower Int64 tick range, makeDateTime64 / YYYYMMDDhhmmssToDateTime64 still clamp to [1900, 2299], and the float numeric path trims valid fractional values on the last representable second. I would keep the review in Request changes until those are closed.

Findings

❌ Blockers

  • [src/IO/ReadHelpers.h:932] [dismissed by author -- https://github.com/Support DateTime64 values before 1900 and after 2299 #107907#discussion_r3440447029] The fixed-format DateTime64 reader still special-cases year == 0 to datetime = 0, so CAST('0000-01-01 00:00:00' AS DateTime64(0, 'UTC')) goes to the Unix epoch instead of the documented lower bound. The author's reply focused on the best-effort parser, but this optimistic path is still taken for ordinary YYYY-MM-DD[ hh:mm:ss] input. Suggested fix: keep the zero-date fallback for invalid 0000-00-00, but let valid DateTime64 year-0000 values reach makeDateTime / tryToMakeDateTime.
  • [src/IO/ReadHelpers.h:1238] [dismissed by author -- https://github.com/Support DateTime64 values before 1900 and after 2299 #107907#discussion_r3496559713] The remaining scale 8/9 parser bug is not limited to ambiguous bare integers. Unambiguous text like 2500-01-01 00:00:00 parses to whole = 16725225600, and the outer DateTime64 builder then multiplies it by 10^9, which exceeds Int64 and throws DECIMAL_OVERFLOW. The widened date parser now reaches years beyond 2299, but this path never applies the scale-dependent tick bounds before DecimalUtils::decimalFromComponents. Suggested fix: after the date string is converted to whole seconds, clamp or reject against the same scale-dependent bounds used by the numeric conversion paths.
  • [src/Functions/FunctionsConversion.h:604] ToDateTime64TransformFloat uses minWholeSecondsForDateTime64 / maxWholeSecondsForDateTime64, which are only correct for integer inputs. At scale 9, values like 9223372036.5 seconds are still representable (9223372036500000000 < Int64::max()), but the current guard rejects or clamps them because it truncates the boundary to 9223372036. Suggested fix: compare and clamp floats against the full tick-space limits (Int64::min/max / scale_multiplier) instead of the whole-second limits.

⚠️ Majors

  • [src/Functions/makeDate.cpp:272] FunctionDateTimeBase::dateTime still routes every year below 1900 or above 2299 to minDateTime / maxDateTime, so both makeDateTime64 and YYYYMMDDhhmmssToDateTime64 keep returning the old clamp range even though the rest of the PR advertises DateTime64 support through year 9999. Suggested fix: keep the narrow clamp only for DateTime, and give the DateTime64 callers the same extended, scale-aware bounds already used elsewhere in the PR.
Final Verdict

❌ Request changes. The core DateLUT work is in place, but the remaining parser and construction boundary gaps mean the advertised DateTime64 range is still not correct end-to-end.

@clickhouse-gh clickhouse-gh Bot added the pr-feature Pull request with new product feature label Jun 18, 2026
Comment thread src/Common/DateLUTImpl.cpp
Comment thread src/Common/DateLUTImpl.cpp Outdated
Comment thread src/Functions/DateTimeTransforms.h
Comment thread src/DataTypes/registerDataTypeDateTime.cpp Outdated
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>
Comment thread src/Common/DateLUTImpl.h Outdated
Comment thread src/Common/DateLUTImpl.h Outdated
Comment thread src/Common/DateLUTImpl.h Outdated
Comment thread src/Functions/parseDateTime.cpp
alexey-milovidov and others added 4 commits June 19, 2026 01:20
… 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>
Comment thread src/Functions/DateTimeTransforms.h
`'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>
Comment thread src/DataTypes/registerDataTypeDateTime.cpp
Comment thread src/Functions/DateTimeTransforms.h
Comment thread src/Common/DateLUTImpl.h
`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>
Comment thread src/Functions/parseDateTime.cpp
Comment thread src/IO/ReadHelpers.h
Comment thread src/Common/DateLUTImpl.cpp
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>
Comment thread src/Common/DateLUTImpl.h Outdated
`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>
Comment thread src/Common/DateLUTImpl.h Outdated
Comment thread src/Functions/FunctionsConversion.h
`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>
@alexey-milovidov

Copy link
Copy Markdown
Member Author

Pushed 87fe8a7471f addressing the one remaining AI-review blocker that is a clear bug rather than a deliberate scope call: the Date32 -> DateTime64 conversion.

ToDateTime64Transform::execute(Int32, ...) converted the Date32 day number straight to seconds * 10^scale, so CAST(toDate32('2299-12-31'), 'DateTime64(9)') (10413705600 * 10^9) overflowed the Int64 ticks and raised DECIMAL_OVERFLOW, while the numeric ToDateTime64Transform* transforms already saturate per-scale. It now clamps the whole seconds to minWholeSecondsForDateTime64 / maxWholeSecondsForDateTime64 before multiplying, so scale 9 saturates to the 2262-04-11 boundary and scale 8 keeps the true 2299-12-31. Only the Date32 overload needs it — Date (up to 2149) and DateTime (up to 2106) day numbers stay within the bounds at every scale. New Date32 -> DateTime64(8/9) coverage in 04402_datetime64_extended_range_review_fixes; thread resolved.

The 5 remaining open threads are the deliberate design/scope calls already documented inline: year-0000 fixed-format parsing (!dt64_mode gate tried in b3a3e9326c7 and reverted), Date32 out-of-LUT decomposition, the symmetric negative scaled-integer threshold, the scale-dependent text-parser disambiguation, and makeDateTime64 / YYYYMMDDhhmmssToDateTime64 (called out as follow-up in the description).

Verification: the affected translation unit (FunctionsConversion_impl19.cpp, which instantiates FunctionConvert<DataTypeDateTime64, ...>) compiles clean under -Werror against this branch; relying on CI for the runtime test run, as with the earlier commits. The sole current red — 04237_streaming_merge_tree_cursors / "Some queries hung" on amd_tsan — is a transient timeout unrelated to this change (the in-harness rerun diagnosis passed 11/11).

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>
@alexey-milovidov

Copy link
Copy Markdown
Member Author

Merged master (184457edd9f) to resolve a fresh conflict in src/Common/DateLUTImpl.h. Master's toStartOfHourInterval/toStartOfMinuteInterval overflow-hardening (from #108848: __builtin_mul_overflow on the divisor, addSaturating) collided with this branch's out-of-LUT-range escape path in toStartOfMinuteInterval. Resolved by combining both — compute the overflow-safe divisor first, then run the may_be_out_of_lut_range escape path (now using the safe divisor), then the fast path — mirroring the already-merged toStartOfHourInterval. Verified with -fsyntax-only on DateLUTImpl.cpp. The 5 open design threads (year-0000 parser, Date32 range, scaled-int parser, makeDate* constructors) are untouched — those remain your design calls. The only prior CI red was the unrelated msgpack ASan Stress flake (already delegated to @groeneai).

@alexey-milovidov

Copy link
Copy Markdown
Member Author

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.

  • The test file (`04061_spilling_hash_join_overflow_limits.sql`) comes entirely from `master`; it does not touch any of the `DateTime64` / `DateLUT` / conversion code this PR changes.
  • It is failing fleet-wide: on `master` itself and ~9 unrelated PRs on 2026-07-03.
  • Root cause: CI randomizes `query_plan_join_swap_table` over `{auto, false}`. When the planner swaps the tiny 4-row left table to the build side, the accumulating hash table never exceeds `max_rows_in_join`, so `SET_SIZE_LIMIT_EXCEEDED` (191) is not thrown and the query returns a count instead — which is why it reproduces 100% of the time once that setting is chosen.

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.
@mintlify

mintlify Bot commented Jul 8, 2026

Copy link
Copy Markdown
Contributor

Preview deployment for your docs. Learn more about Mintlify Previews.

Project Status Preview Updated (UTC)
ClickHouse-docs 🟢 Ready View Preview Jul 8, 2026, 7:10 PM

…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.
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]]

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@clickhouse-gh

clickhouse-gh Bot commented Jul 13, 2026

Copy link
Copy Markdown
Contributor

LLVM Coverage Report

Metric Baseline Current Δ
Lines 85.80% 85.80% +0.00%
Functions 92.70% 92.70% +0.00%
Branches 78.00% 78.00% +0.00%

Changed lines: Changed C/C++ lines covered: 626/687 (91.12%) · Uncovered code

Full report · Diff report

@alexey-milovidov alexey-milovidov left a comment

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Amazing!

@alexey-milovidov alexey-milovidov self-assigned this Jul 13, 2026
@alexey-milovidov
alexey-milovidov added this pull request to the merge queue Jul 13, 2026
Merged via the queue into master with commit a63a425 Jul 13, 2026
176 checks passed
@alexey-milovidov
alexey-milovidov deleted the feature/datelut-extended-datetime64-range branch July 13, 2026 23:34
@robot-ch-test-poll4 robot-ch-test-poll4 added the pr-synced-to-cloud The PR is synced to the cloud repo label Jul 14, 2026
raimannma added a commit to raimannma/ClickHouse that referenced this pull request Jul 14, 2026
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>
alexey-milovidov added a commit that referenced this pull request Jul 14, 2026
…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>
alexey-milovidov added a commit that referenced this pull request Jul 16, 2026
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.
pfcoperez added a commit to PeerDB-io/peerdb that referenced this pull request Jul 21, 2026
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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

pr-feature Pull request with new product feature pr-synced-to-cloud The PR is synced to the cloud repo

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants