From 799ad70640b051542f9ffe0b26baf7a6a09a34aa Mon Sep 17 00:00:00 2001 From: Minh Vu Date: Thu, 6 Aug 2026 19:16:41 +0200 Subject: [PATCH 1/2] fix(arrow/array/arreflect): reject unrepresentable timestamps --- arrow/array/arreflect/reflect_go_to_arrow.go | 6 +++- .../arreflect/reflect_go_to_arrow_test.go | 12 ++++++++ arrow/datatype_fixedwidth.go | 29 +++++++++++++++++-- 3 files changed, 43 insertions(+), 4 deletions(-) diff --git a/arrow/array/arreflect/reflect_go_to_arrow.go b/arrow/array/arreflect/reflect_go_to_arrow.go index e2acfabcd..424a5ab1b 100644 --- a/arrow/array/arreflect/reflect_go_to_arrow.go +++ b/arrow/array/arreflect/reflect_go_to_arrow.go @@ -308,7 +308,11 @@ func appendTemporalValue(b array.Builder, v reflect.Value) error { if err != nil { return err } - tb.Append(arrow.Timestamp(t.UnixNano() / int64(unit.Multiplier()))) + timestamp, err := arrow.TimestampFromTime(t, unit) + if err != nil { + return err + } + tb.Append(timestamp) case *array.Date32Builder: t, err := asTime(v) if err != nil { diff --git a/arrow/array/arreflect/reflect_go_to_arrow_test.go b/arrow/array/arreflect/reflect_go_to_arrow_test.go index 4992ae77d..dc62d6ecf 100644 --- a/arrow/array/arreflect/reflect_go_to_arrow_test.go +++ b/arrow/array/arreflect/reflect_go_to_arrow_test.go @@ -132,6 +132,18 @@ func TestBuildTemporalArray(t *testing.T) { }) } +func TestBuildTemporalArrayRejectsTimestampOverflow(t *testing.T) { + mem := checkedMem(t) + values := []time.Time{ + time.Date(1000, time.January, 1, 0, 0, 0, 0, time.UTC), + time.Date(3000, time.January, 1, 0, 0, 0, 0, time.UTC), + } + + arr, err := FromSlice(values, mem) + require.ErrorIs(t, err, arrow.ErrInvalid) + require.Nil(t, arr) +} + func TestBuildDecimalArray(t *testing.T) { mem := checkedMem(t) diff --git a/arrow/datatype_fixedwidth.go b/arrow/datatype_fixedwidth.go index c0595b516..663f34745 100644 --- a/arrow/datatype_fixedwidth.go +++ b/arrow/datatype_fixedwidth.go @@ -20,6 +20,7 @@ import ( "cmp" "errors" "fmt" + "math" "strconv" "sync" "time" @@ -203,16 +204,38 @@ func TimestampFromTime(val time.Time, unit TimeUnit) (Timestamp, error) { case Second: return Timestamp(val.Unix()), nil case Millisecond: - return Timestamp(val.Unix()*1e3 + int64(val.Nanosecond())/1e6), nil + return timestampFromTime(val, 1e3, 1e6, unit) case Microsecond: - return Timestamp(val.Unix()*1e6 + int64(val.Nanosecond())/1e3), nil + return timestampFromTime(val, 1e6, 1e3, unit) case Nanosecond: - return Timestamp(val.UnixNano()), nil + return timestampFromTime(val, 1e9, 1, unit) default: return 0, fmt.Errorf("%w: unexpected timestamp unit: %s", ErrInvalid, unit) } } +func timestampFromTime(val time.Time, multiplier, divisor int64, unit TimeUnit) (Timestamp, error) { + seconds := val.Unix() + fraction := int64(val.Nanosecond()) / divisor + maxSeconds := math.MaxInt64 / multiplier + if seconds > maxSeconds || (seconds == maxSeconds && fraction > math.MaxInt64%multiplier) { + return 0, fmt.Errorf("%w: timestamp value is out of range for %s", ErrInvalid, unit) + } + + minSeconds := math.MinInt64 / multiplier + minRemainder := math.MinInt64 % multiplier + minFraction := multiplier + minRemainder + if seconds < minSeconds { + if minSeconds == math.MinInt64 || seconds != minSeconds-1 || fraction < minFraction { + return 0, fmt.Errorf("%w: timestamp value is out of range for %s", ErrInvalid, unit) + } + return Timestamp(math.MinInt64 + fraction - minFraction), nil + } + + timestamp := seconds * multiplier + return Timestamp(timestamp + fraction), nil +} + // Time32FromString parses a string to return a Time32 value in the given unit, // unit needs to be only seconds or milliseconds and the string should be in the // form of HH:MM or HH:MM:SS[.zzz] where the fractions of a second are optional. From 172042be60fa6b945b5cf7e1325654613419da04 Mon Sep 17 00:00:00 2001 From: Minh Vu Date: Thu, 6 Aug 2026 21:32:16 +0200 Subject: [PATCH 2/2] fix(arrow/arreflect): reject unrepresentable timestamps --- .../arreflect/reflect_go_to_arrow_test.go | 2 +- arrow/datatype_fixedwidth_test.go | 78 +++++++++++++++++++ 2 files changed, 79 insertions(+), 1 deletion(-) diff --git a/arrow/array/arreflect/reflect_go_to_arrow_test.go b/arrow/array/arreflect/reflect_go_to_arrow_test.go index dc62d6ecf..eafdafe0f 100644 --- a/arrow/array/arreflect/reflect_go_to_arrow_test.go +++ b/arrow/array/arreflect/reflect_go_to_arrow_test.go @@ -135,7 +135,7 @@ func TestBuildTemporalArray(t *testing.T) { func TestBuildTemporalArrayRejectsTimestampOverflow(t *testing.T) { mem := checkedMem(t) values := []time.Time{ - time.Date(1000, time.January, 1, 0, 0, 0, 0, time.UTC), + time.Date(1970, time.January, 1, 0, 0, 0, 0, time.UTC), time.Date(3000, time.January, 1, 0, 0, 0, 0, time.UTC), } diff --git a/arrow/datatype_fixedwidth_test.go b/arrow/datatype_fixedwidth_test.go index bc899f34b..05afa3349 100644 --- a/arrow/datatype_fixedwidth_test.go +++ b/arrow/datatype_fixedwidth_test.go @@ -17,6 +17,7 @@ package arrow_test import ( + "math" "sync" "testing" "time" @@ -494,6 +495,83 @@ func TestDateFromTime(t *testing.T) { assert.EqualValues(t, wantD32, arrow.Date32FromTime(tm)) } +func timestampBoundaryTime(value arrow.Timestamp, unit arrow.TimeUnit) time.Time { + nanosPerUnit := int64(unit.Multiplier()) + unitsPerSecond := int64(time.Second) / nanosPerUnit + seconds := int64(value) / unitsPerSecond + remainder := int64(value) % unitsPerSecond + if remainder < 0 { + seconds-- + remainder += unitsPerSecond + } + return time.Unix(seconds, remainder*nanosPerUnit).UTC() +} + +func TestTimestampFromTimeBoundaries(t *testing.T) { + for _, unit := range arrow.TimeUnitValues { + t.Run(unit.String(), func(t *testing.T) { + maxTime := timestampBoundaryTime(arrow.Timestamp(math.MaxInt64), unit) + got, err := arrow.TimestampFromTime(maxTime, unit) + require.NoError(t, err) + assert.Equal(t, arrow.Timestamp(math.MaxInt64), got) + + minTime := timestampBoundaryTime(arrow.Timestamp(math.MinInt64), unit) + got, err = arrow.TimestampFromTime(minTime, unit) + require.NoError(t, err) + assert.Equal(t, arrow.Timestamp(math.MinInt64), got) + + if unit != arrow.Second { + _, err = arrow.TimestampFromTime(maxTime.Add(unit.Multiplier()), unit) + assert.ErrorIs(t, err, arrow.ErrInvalid) + + _, err = arrow.TimestampFromTime(minTime.Add(-unit.Multiplier()), unit) + assert.ErrorIs(t, err, arrow.ErrInvalid) + } + }) + } +} + +func TestTimestampFromTimeFractionalSeconds(t *testing.T) { + tests := []struct { + unit arrow.TimeUnit + want arrow.Timestamp + }{ + {unit: arrow.Second, want: -1}, + {unit: arrow.Millisecond, want: -500}, + {unit: arrow.Microsecond, want: -500_000}, + {unit: arrow.Nanosecond, want: -500_000_000}, + } + + value := time.Date(1969, time.December, 31, 23, 59, 59, 500_000_000, time.UTC) + for _, tc := range tests { + t.Run(tc.unit.String(), func(t *testing.T) { + got, err := arrow.TimestampFromTime(value, tc.unit) + require.NoError(t, err) + assert.Equal(t, tc.want, got) + }) + } + + positive := time.Date(1970, time.January, 1, 0, 0, 0, 500_000_000, time.UTC) + for _, tc := range []struct { + unit arrow.TimeUnit + want arrow.Timestamp + }{ + {unit: arrow.Second, want: 0}, + {unit: arrow.Millisecond, want: 500}, + {unit: arrow.Microsecond, want: 500_000}, + {unit: arrow.Nanosecond, want: 500_000_000}, + } { + t.Run("positive_"+tc.unit.String(), func(t *testing.T) { + got, err := arrow.TimestampFromTime(positive, tc.unit) + require.NoError(t, err) + assert.Equal(t, tc.want, got) + }) + } + + _, err := arrow.TimestampFromTime(value, arrow.TimeUnit(99)) + assert.ErrorIs(t, err, arrow.ErrInvalid) +} + func TestNarrowestDecimalType(t *testing.T) { tests := []struct { min, max int32