Repository: gocql/gocql
Pull request #1692 (2023-04-27 08:51:19)

Support larger dates in TypeDate marshalling
We are using nanosecond precision when reading a `TypeDate` into a `time.Time` value, this limits the dates we can accurately read and write from cassandra to `[1677-09-22, 2262-04-11]` since outside of this range we end up with integer overflow. Dates outside of this range encode correctly and are persisted in Cassandra, but are then read as an incorrect value.

The new supported date range is `[-5877641-06-23, 5881580-07-11]` given that we are decoding the epoch day as a 32 bit int (`-2147483648` = minimum int = `-5877641-06-23`). I think it's possible to expand the range if ever needed by decoding a 64 bit int and using `time.Unix(seconds, 0)`, but I didn't think it was worth looking into that change.

Please let me know if I missed anything from the testing.


Patch:
diff --git a/AUTHORS b/AUTHORS
index 9527b03..4259173 100644
--- a/AUTHORS
+++ b/AUTHORS
@@ -138,3 +138,4 @@ Wojciech Przytuła <wojciech.przytula@scylladb.com>
 João Reis <joao.reis@datastax.com>
 Lauro Ramos Venancio <lauro.venancio@incognia.com>
 Dmitry Kropachev <dmitry.kropachev@gmail.com>
+Oliver Boyle <pleasedontspamme4321+gocql@gmail.com>
diff --git a/marshal.go b/marshal.go
index 0ae6806..b887b3f 100644
--- a/marshal.go
+++ b/marshal.go
@@ -1329,6 +1329,8 @@ func unmarshalTimestamp(info TypeInfo, data []byte, value interface{}) error {
 	return unmarshalErrorf("can not unmarshal %s into %T", info, value)
 }
 
+const millisecondsInADay int64 = 24 * 60 * 60 * 1000
+
 func marshalDate(info TypeInfo, value interface{}) ([]byte, error) {
 	var timestamp int64
 	switch v := value.(type) {
@@ -1338,21 +1340,21 @@ func marshalDate(info TypeInfo, value interface{}) ([]byte, error) {
 		return nil, nil
 	case int64:
 		timestamp = v
-		x := timestamp/86400000 + int64(1<<31)
+		x := timestamp/millisecondsInADay + int64(1<<31)
 		return encInt(int32(x)), nil
 	case time.Time:
 		if v.IsZero() {
 			return []byte{}, nil
 		}
 		timestamp = int64(v.UTC().Unix()*1e3) + int64(v.UTC().Nanosecond()/1e6)
-		x := timestamp/86400000 + int64(1<<31)
+		x := timestamp/millisecondsInADay + int64(1<<31)
 		return encInt(int32(x)), nil
 	case *time.Time:
 		if v.IsZero() {
 			return []byte{}, nil
 		}
 		timestamp = int64(v.UTC().Unix()*1e3) + int64(v.UTC().Nanosecond()/1e6)
-		x := timestamp/86400000 + int64(1<<31)
+		x := timestamp/millisecondsInADay + int64(1<<31)
 		return encInt(int32(x)), nil
 	case string:
 		if v == "" {
@@ -1363,7 +1365,7 @@ func marshalDate(info TypeInfo, value interface{}) ([]byte, error) {
 			return nil, marshalErrorf("can not marshal %T into %s, date layout must be '2006-01-02'", value, info)
 		}
 		timestamp = int64(t.UTC().Unix()*1e3) + int64(t.UTC().Nanosecond()/1e6)
-		x := timestamp/86400000 + int64(1<<31)
+		x := timestamp/millisecondsInADay + int64(1<<31)
 		return encInt(int32(x)), nil
 	}
 
@@ -1384,8 +1386,8 @@ func unmarshalDate(info TypeInfo, data []byte, value interface{}) error {
 		}
 		var origin uint32 = 1 << 31
 		var current uint32 = binary.BigEndian.Uint32(data)
-		timestamp := (int64(current) - int64(origin)) * 86400000
-		*v = time.Unix(0, timestamp*int64(time.Millisecond)).In(time.UTC)
+		timestamp := (int64(current) - int64(origin)) * millisecondsInADay
+		*v = time.UnixMilli(timestamp).In(time.UTC)
 		return nil
 	case *string:
 		if len(data) == 0 {
@@ -1394,8 +1396,8 @@ func unmarshalDate(info TypeInfo, data []byte, value interface{}) error {
 		}
 		var origin uint32 = 1 << 31
 		var current uint32 = binary.BigEndian.Uint32(data)
-		timestamp := (int64(current) - int64(origin)) * 86400000
-		*v = time.Unix(0, timestamp*int64(time.Millisecond)).In(time.UTC).Format("2006-01-02")
+		timestamp := (int64(current) - int64(origin)) * millisecondsInADay
+		*v = time.UnixMilli(timestamp).In(time.UTC).Format("2006-01-02")
 		return nil
 	}
 	return unmarshalErrorf("can not unmarshal %s into %T", info, value)


Test patch:
diff --git a/marshal_test.go b/marshal_test.go
index f007ab9..6b70a5d 100644
--- a/marshal_test.go
+++ b/marshal_test.go
@@ -2221,6 +2221,7 @@ func TestMarshalDate(t *testing.T) {
 	now := time.Now().UTC()
 	timestamp := now.UnixNano() / int64(time.Millisecond)
 	expectedData := encInt(int32(timestamp/86400000 + int64(1<<31)))
+
 	var marshalDateTests = []struct {
 		Info  TypeInfo
 		Data  []byte
@@ -2262,6 +2263,57 @@ func TestMarshalDate(t *testing.T) {
 	}
 }
 
+func TestLargeDate(t *testing.T) {
+	farFuture := time.Date(999999, time.December, 31, 0, 0, 0, 0, time.UTC)
+	expectedFutureData := encInt(int32(farFuture.UnixMilli()/86400000 + int64(1<<31)))
+
+	farPast := time.Date(-999999, time.January, 1, 0, 0, 0, 0, time.UTC)
+	expectedPastData := encInt(int32(farPast.UnixMilli()/86400000 + int64(1<<31)))
+
+	var marshalDateTests = []struct {
+		Data         []byte
+		Value        interface{}
+		ExpectedDate string
+	}{
+		{
+			expectedFutureData,
+			farFuture,
+			"999999-12-31",
+		},
+		{
+			expectedPastData,
+			farPast,
+			"-999999-01-01",
+		},
+	}
+
+	nativeType := NativeType{proto: 4, typ: TypeDate}
+
+	for i, test := range marshalDateTests {
+		t.Log(i, test)
+
+		data, err := Marshal(nativeType, test.Value)
+		if err != nil {
+			t.Errorf("largeDateTest[%d]: %v", i, err)
+			continue
+		}
+		if !bytes.Equal(data, test.Data) {
+			t.Errorf("largeDateTest[%d]: expected %x (%v), got %x (%v) for time %s", i,
+				test.Data, decInt(test.Data), data, decInt(data), test.Value)
+		}
+
+		var date time.Time
+		if err := Unmarshal(nativeType, data, &date); err != nil {
+			t.Fatal(err)
+		}
+
+		formattedDate := date.Format("2006-01-02")
+		if test.ExpectedDate != formattedDate {
+			t.Fatalf("largeDateTest: expected %v, got %v", test.ExpectedDate, formattedDate)
+		}
+	}
+}
+
 func BenchmarkUnmarshalVarchar(b *testing.B) {
 	b.ReportAllocs()
 	src := make([]byte, 1024)