This is an automated email from the ASF dual-hosted git repository.
laskoviymishka pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/iceberg-go.git
The following commit(s) were added to refs/heads/main by this push:
new 9cc9211c9 fix(types): floor negative nanosecond timestamps (#1586)
9cc9211c9 is described below
commit 9cc9211c9a8497572e9e4b4b7e1ec5ff0144011e
Author: Minh Vu <[email protected]>
AuthorDate: Thu Jul 30 09:55:54 2026 +0200
fix(types): floor negative nanosecond timestamps (#1586)
## What changed
Use signed floor division when converting nanosecond timestamps to
microseconds. Add boundary coverage around zero and verify casts to both
timestamp variants.
## Why
Go integer division truncates toward zero. For pre-epoch timestamps with
sub-microsecond precision, that could move the converted value forward
by one microsecond and even cross the Unix epoch.
## Testing
- `go test .`
Signed-off-by: Minh Vu <[email protected]>
---
literals_test.go | 34 ++++++++++++++++++++++++++++++++--
types.go | 2 +-
2 files changed, 33 insertions(+), 3 deletions(-)
diff --git a/literals_test.go b/literals_test.go
index 42875df10..e0030092f 100644
--- a/literals_test.go
+++ b/literals_test.go
@@ -201,8 +201,8 @@ func TestTimestampNanoLiteralConversions(t *testing.T) {
{
name: "negative timestamp",
nanoLit:
iceberg.TimestampNsLiteral(-1234567890123456789),
- expectedMicro: iceberg.Timestamp(-1234567890123456),
- expectedRoundTrip:
iceberg.TimestampNano(-1234567890123456000),
+ expectedMicro: iceberg.Timestamp(-1234567890123457),
+ expectedRoundTrip:
iceberg.TimestampNano(-1234567890123457000),
},
{
name: "maximum precision truncation",
@@ -235,6 +235,36 @@ func TestTimestampNanoLiteralConversions(t *testing.T) {
}
}
+func TestTimestampNanoToMicrosFloorsNegativeValues(t *testing.T) {
+ tests := []struct {
+ nanos iceberg.TimestampNano
+ micros iceberg.Timestamp
+ }{
+ {nanos: -1001, micros: -2},
+ {nanos: -1000, micros: -1},
+ {nanos: -999, micros: -1},
+ {nanos: -1, micros: -1},
+ {nanos: 0, micros: 0},
+ {nanos: 1, micros: 0},
+ {nanos: 999, micros: 0},
+ {nanos: 1000, micros: 1},
+ {nanos: 1001, micros: 1},
+ }
+
+ for _, tt := range tests {
+ assert.Equal(t, tt.micros, tt.nanos.ToMicros())
+
+ for _, target := range []iceberg.Type{
+ iceberg.PrimitiveTypes.Timestamp,
+ iceberg.PrimitiveTypes.TimestampTz,
+ } {
+ converted, err :=
iceberg.TimestampNsLiteral(tt.nanos).To(target)
+ require.NoError(t, err)
+ assert.Equal(t, iceberg.TimestampLiteral(tt.micros),
converted)
+ }
+ }
+}
+
func TestTimestampLiteralToNanosRejectsOverflow(t *testing.T) {
targets := []iceberg.Type{
iceberg.PrimitiveTypes.TimestampNs,
diff --git a/types.go b/types.go
index 3dad8a2a3..f19bd1003 100644
--- a/types.go
+++ b/types.go
@@ -799,7 +799,7 @@ func (t TimestampNano) ToTime() time.Time {
}
func (t TimestampNano) ToMicros() Timestamp {
- return Timestamp(int64(t) / 1000)
+ return Timestamp(internal.FloorDiv(int64(t), 1000))
}
func (t TimestampNano) ToDate() Date {