Read a Fixed/UTC offset the way the server does - #612
Merged
Conversation
ClickHouse 26.9 caps a fixed UTC offset at a whole number of quarters of an hour, no further from UTC than 14 hours, so the server no longer emits the Fixed/UTC+19:00:00 name an integration test needed. Move that null branch to the parse fixture that exists for names the server cannot send. Each field of the name carries into the next, which is how cctz reads it: 26.3 and 26.9 both resolve Fixed/UTC+05:60:00 to +06:00. Restricting the minute and second fields to 00-59 rejected such a name, so the read fell through to the UTC projection and shifted the wall clock. Accept two digits per field and let the existing 18 h range guard reject the rest. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
Contributor
There was a problem hiding this comment.
🟢 Approval recommended
The behavioral change is small, server-aligned, and covered by updated tests, with only minor changelog traceability feedback remaining.
Pull request overview
This PR fixes a silent wall-clock shift when reading DateTime/DateTime64 columns whose server-emitted Fixed/UTC±HH:MM:SS timezone name contains minute/second fields above 59 (e.g. Fixed/UTC+05:60:00), by parsing fixed-offset names the same way ClickHouse does (field carry), and updating tests to match real server behavior across supported versions.
Changes:
- Loosened the
Fixed/UTCparsing regex to accept any two-digitMM/SSand rely on carry + the existing ±18h range guard. - Updated/expanded timezone handling tests: removed the now-unreachable integration test for
Fixed/UTC+19:00:00, added integration coverage for minute-carry and ±14:00:00 extremes, and adjusted parse expectations. - Added a
changelog.d/fragment documenting the user-visible fix.
File summaries
| File | Description |
|---|---|
| ClickHouse.Driver/Types/AbstractDateTimeType.cs | Updates Fixed/UTC timezone parsing to match server carry semantics while keeping the existing ±18h representability guard. |
| ClickHouse.Driver.Tests/Types/TimezoneHandlingTests.cs | Refreshes integration + parse tests to reflect real server acceptance rules and adds coverage for minute-carry and ±14h offsets. |
| changelog.d/fixed-utc-offset-field-carry.fixes.md | Adds a changelog fragment describing the bug fix. |
Review details
- Files reviewed: 3/3 changed files
- Comments generated: 1
- Review effort level: Lite
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
alex-clickhouse
enabled auto-merge (squash)
September 7, 2026 07:24
slabko
approved these changes
Sep 7, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What changed
The nightly "Test against ClickHouse HEAD" job started failing on
ReadDateTime_WithOutOfRangeFixedUtcOffset_FallsBackToUtcWallClock:The cause is server-side, in ClickHouse
#118286 (merged 2026-09-06, one nightly
before the failure).
cctzsynthesizes a zone for anyFixed/UTC±HH:MM:SSname up to 24 hours,which is 172,801 distinct names, and each name that gets loaded costs ~4.6 MiB in a
DateLUTcache that never evicts. The new
DateLUTImpl::isSupportedTimeZoneNamebounds that by acceptingonly the offsets a time zone can really have: a whole number of quarters of an hour, no further
from UTC than 14 hours.
So the test's
Fixed/UTC+19:00:00is no longer a name any server will produce, and the branch itcovered is no longer reachable through the server.
Where the driver applies these limits
One place on
main:AbstractDateTimeType.ResolveTimezone. (DateTimeZones.ResolveinClickHouse.Driver.Tcphas the same job but is not onmainyet — see the note at the end.)I checked its two limits against a real 26.3 (support floor) and a real 26.9.1 server:
±18 hrange guardOffsetrange, which is the widest the driver can represent, and it already covers everything any supported server can emit.[0-5]\don the minute and second fieldsThe second one is a real bug. The comment claimed the restriction stops a malformed name from
"being misread as a different valid offset (e.g. 60 minutes as +1 h)" — but reading 60 minutes as
an extra hour is exactly what the server does, because
cctzcarries each field into the next:Fixed/UTC+05:60:00is accepted by every supported server, including 26.9. The driver's regexrejected the name,
ResolveTimezonereturned null, and the read fell through to the UTCprojection — a silent 6-hour wall-clock shift, the same class of bug as #370.
The fix is to parse the family the way the server does: two digits per field, carrying into the
next. The existing
±18 hguard still rejects whatever that produces out of range, so nothingnew is accepted —
Fixed/UTC+99:99:99carries to ~100 h and is still null.The
hours * 3600 + minutes * 60 + secondsarithmetic already carried correctly, so the regex wasthe only change needed.
Tests
Removed
ReadDateTime_WithOutOfRangeFixedUtcOffset_FallsBackToUtcWallClock. It needed the serverto emit a
Fixed/UTC+19:00:00column, which 26.9 refuses to do. Its coverage of theResolveTimezonenull branch moves toResolveTimezoneParseTests, the class that already existsfor branches the server cannot reach.
Two expectations flipped, because they pinned behavior that a real server contradicts.
ParseDateTime_FixedUtcMinutesOutOfRangeandParseDateTime_FixedUtcSecondsOutOfRangeasserted anull zone for
+05:60:00/+05:00:60; they are nowParseDateTime_FixedUtcMinutesCarryIntoHourandParseDateTime_FixedUtcSecondsCarryIntoMinute,asserting the carried offsets shown in the table above.
Added:
ReadDateTime_FixedUtcOffsetWithMinuteCarry— integration coverage of the fix.+05:60:00readsas +06:00 against a real server, and the name is accepted on 26.3 through 26.9.
ReadDateTime_MaxFixedUtcOffset/ReadDateTime_MinFixedUtcOffset—±14:00:00, now the widestoffset a column can carry.
ParseDateTime_FixedUtcHoursOutOfRange,ParseDateTime64_FixedUtcHoursOutOfRange,ParseDateTime_FixedUtcAllFieldsOutOfRange— the range guard.ParseDateTime_FixedUtcSingleDigitHour— the two-digit shape, which the server also requires.ResolveTimezonehas three branches (IANA hit, fixed-offset parse, null fallback) and all threeare exercised, so no separate coverage run.
Verified locally on
net9.0against two servers, both green:ClickHouse.Driver.Testssuite.Follow-up, not in this PR
ClickHouse.Driver.Tcp/Types/Codecs/DateTimeZones.cson the unmergedtcp/**stack has the sameparsing with the same
[0-5]\drestriction, so it needs the same one-line change when that stackis next restacked. Its
±14 hand whole-minute limits are the BCL'sTimeZoneInfo.CreateCustomTimeZoneconstraints and should stay: they happen to match 26.9's ruleexactly, so nothing there is broken today.
🤖 Generated with Claude Code