Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 13 additions & 3 deletions proto/datetime64.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,11 +52,21 @@ func ToDateTime64(t time.Time, p Precision) DateTime64 {
if t.IsZero() {
return 0
}
return DateTime64(t.UnixNano() / p.Scale())
// Compute the tick count at the column's own scale rather than via t.UnixNano(): the int64
// nanosecond count overflows just past 2262-04-11, so far-future values (e.g. ClickHouse's
// 9999-12-31 ceiling for DateTime64(3)) would otherwise wrap to a garbage tick on the way in.
// secScale is ticks-per-second (10^precision); the result is the same int64 tick count ClickHouse
// stores, so each precision reaches exactly ClickHouse's DateTime64(precision) range. Nanosecond
// precision still tops out near 2262-04-11, where an int64 count of nanoseconds runs out — there
// this reduces to the old UnixNano computation.
secScale := int64(1e9) / p.Scale()
return DateTime64(t.Unix()*secScale + int64(t.Nanosecond())/p.Scale())
}

// Time returns DateTime64 as time.Time.
func (d DateTime64) Time(p Precision) time.Time {
nsec := int64(d) * p.Scale()
return time.Unix(nsec/1e9, nsec%1e9)
// Split the tick count into whole seconds and sub-second ticks before scaling up to
// nanoseconds, so a far-future value never forms an int64-overflowing nanosecond intermediate.
secScale := int64(1e9) / p.Scale()
return time.Unix(int64(d)/secScale, (int64(d)%secScale)*p.Scale())
}
31 changes: 31 additions & 0 deletions proto/datetime64_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,3 +49,34 @@ func TestDateTime64_Time(t *testing.T) {
assert.Equal(t, time.Nanosecond, PrecisionNano.Duration(), "ns")
})
}

// TestDateTime64_FarFuture guards against the t.UnixNano() overflow: int64 nanoseconds wrap just
// past 2262-04-11, which corrupted any later timestamp on both the write and read paths. For
// precisions coarser than a nanosecond the int64 tick count spans the full DateTime64 range, so
// values such as ClickHouse's 9999-12-31 ceiling must survive a round-trip.
func TestDateTime64_FarFuture(t *testing.T) {
values := []time.Time{
time.Date(2262, 1, 1, 0, 0, 0, 0, time.UTC), // last value the old UnixNano path got right
time.Date(2263, 1, 1, 0, 0, 0, 0, time.UTC), // first value it corrupted
time.Date(2999, 12, 31, 23, 59, 59, 0, time.UTC),
time.Date(9999, 12, 31, 23, 59, 59, 0, time.UTC), // near ClickHouse's DateTime64 ceiling
}
// Precisions 8 and 9 are excluded: their int64 tick count tops out near years ~4900 and ~2262
// respectively (the same limits ClickHouse's DateTime64(8)/(9) carry), so they cannot represent
// these values by construction.
for _, p := range []Precision{PrecisionSecond, 1, PrecisionMilli, PrecisionMicro, 7} {
t.Run(p.Duration().String(), func(t *testing.T) {
for _, v := range values {
got := ToDateTime64(v, p).Time(p)
assert.Truef(t, got.Equal(v), "precision %d: %s round-tripped to %s", p, v, got)
}
})
}

// Sub-second precision survives too: 9999-12-31 23:59:59.999 in DateTime64(3).
t.Run("millisecond_fraction", func(t *testing.T) {
v := time.Date(9999, 12, 31, 23, 59, 59, 999_000_000, time.UTC)
got := ToDateTime64(v, PrecisionMilli).Time(PrecisionMilli)
assert.True(t, got.Equal(v), got.String())
})
}
Loading