github.com/datastax/go-cassandra-native-protocol@v0.0.0-20220706104457-5e8aad05cf90/datacodec/timestamp.go (about) 1 // Copyright 2021 DataStax 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 package datacodec 16 17 import ( 18 "time" 19 20 "github.com/datastax/go-cassandra-native-protocol/datatype" 21 "github.com/datastax/go-cassandra-native-protocol/primitive" 22 ) 23 24 const TimestampLayoutDefault = "2006-01-02T15:04:05.999999999-07:00" 25 26 const millisecond = int64(time.Millisecond) 27 28 var ( 29 // TimestampMin is the minimum representable CQL timestamp: -292275055-05-16 16:47:04.192 UTC. 30 TimestampMin = time.Unix(-9223372036854776, 192_000_000).UTC() 31 32 // TimestampMax is the maximum representable CQL timestamp: +292278994-08-17 07:12:55.807 UTC. 33 TimestampMax = time.Unix(9223372036854775, 807_000_000).UTC() 34 ) 35 36 // ConvertTimeToEpochMillis is a function that converts from a time.Time into milliseconds since the Epoch. An error is 37 // returned if the given time value cannot be converted to milliseconds since the Epoch; convertible values range from 38 // TimestampMin to TimestampMax inclusive. 39 func ConvertTimeToEpochMillis(t time.Time) (int64, error) { 40 // Implementation note: we avoid t.UnixNano() because it has a limited range of [1678,2262]; 41 // values outside this range overflow. 42 seconds := t.Unix() 43 nanos := int64(t.Nanosecond()) 44 var millis int64 45 var overflow bool 46 // This is taken from Java's Instant.toEpochMilli() 47 if seconds < 0 && nanos > 0 { 48 if millis, overflow = multiplyExact(seconds+1, 1000); !overflow { 49 millis, overflow = addExact(millis, nanos/millisecond-1000) 50 } 51 } else { 52 if millis, overflow = multiplyExact(seconds, 1000); !overflow { 53 millis, overflow = addExact(millis, nanos/millisecond) 54 } 55 } 56 if overflow { 57 return 0, errValueOutOfRange(t) 58 } 59 return millis, nil 60 } 61 62 // ConvertEpochMillisToTime is a function that converts from milliseconds since the Epoch into a time.Time. The returned 63 // time will be in UTC. 64 func ConvertEpochMillisToTime(millis int64) time.Time { 65 // This is taken from Java's Instant.ofEpochMilli() 66 seconds := floorDiv(millis, 1000) 67 nanos := floorMod(millis, 1000) * millisecond 68 return time.Unix(seconds, nanos).UTC() 69 } 70 71 // Timestamp is the default codec for the CQL timestamp type. 72 // Its preferred Go type is time.Time, but it can encode from and decode to string and to most numeric types as well. 73 // Note that not all time.Time values can be converted to CQL timestamp: the valid range is from 74 // TimestampMin to TimestampMax inclusive. 75 // When encoding from and decoding to numeric types, the numeric value is supposed to be the number of milliseconds 76 // since the Epoch. 77 // The layout is Cassandra's preferred layout for CQL timestamp literals and is ISO-8601-compatible. 78 var Timestamp = NewTimestamp(TimestampLayoutDefault, time.UTC) 79 80 // NewTimestamp creates a new codec for CQL timestamp values, with the given layout and location. The Layout is 81 // used only when encoding from or decoding to string; it is ignored otherwise. The location is only useful if the 82 // layout does not include any time zone, in which case the time zone is assumed to be in the given location. 83 func NewTimestamp(layout string, location *time.Location) Codec { 84 return ×tampCodec{ 85 layout: layout, 86 location: location, 87 innerCodec: &bigintCodec{dataType: datatype.Timestamp}, 88 } 89 } 90 91 type timestampCodec struct { 92 layout string 93 location *time.Location 94 innerCodec *bigintCodec 95 } 96 97 func (c *timestampCodec) DataType() datatype.DataType { 98 return datatype.Timestamp 99 } 100 101 func (c *timestampCodec) Encode(source interface{}, version primitive.ProtocolVersion) (dest []byte, err error) { 102 var val int64 103 var wasNil bool 104 if val, wasNil, err = convertToInt64Timestamp(source, c.layout, c.location); err == nil && !wasNil { 105 dest = writeInt64(val) 106 } 107 if err != nil { 108 err = errCannotEncode(source, c.DataType(), version, err) 109 } 110 return 111 } 112 113 func (c *timestampCodec) Decode(source []byte, dest interface{}, version primitive.ProtocolVersion) (wasNull bool, err error) { 114 var val int64 115 if val, wasNull, err = readInt64(source); err == nil { 116 err = convertFromInt64Timestamp(val, wasNull, dest, c.layout, c.location) 117 } 118 if err != nil { 119 err = errCannotDecode(dest, c.DataType(), version, err) 120 } 121 return 122 } 123 124 func convertToInt64Timestamp(source interface{}, layout string, location *time.Location) (val int64, wasNil bool, err error) { 125 switch s := source.(type) { 126 case time.Time: 127 val, err = ConvertTimeToEpochMillis(s) 128 case *time.Time: 129 if wasNil = s == nil; !wasNil { 130 val, err = ConvertTimeToEpochMillis(*s) 131 } 132 case string: 133 val, err = stringToEpochMillis(s, layout, location) 134 case *string: 135 if wasNil = s == nil; !wasNil { 136 val, err = stringToEpochMillis(*s, layout, location) 137 } 138 case nil: 139 wasNil = true 140 default: 141 return convertToInt64(source) 142 } 143 if err != nil { 144 err = errSourceConversionFailed(source, val, err) 145 } 146 return 147 } 148 149 func convertFromInt64Timestamp(val int64, wasNull bool, dest interface{}, layout string, location *time.Location) (err error) { 150 switch d := dest.(type) { 151 case *interface{}: 152 if d == nil { 153 err = ErrNilDestination 154 } else if wasNull { 155 *d = nil 156 } else { 157 *d = ConvertEpochMillisToTime(val).In(location) 158 } 159 case *time.Time: 160 if d == nil { 161 err = ErrNilDestination 162 } else if wasNull { 163 *d = time.Time{} 164 } else { 165 *d = ConvertEpochMillisToTime(val).In(location) 166 } 167 case *string: 168 if d == nil { 169 err = ErrNilDestination 170 } else if wasNull { 171 *d = "" 172 } else { 173 *d = ConvertEpochMillisToTime(val).In(location).Format(layout) 174 } 175 default: 176 return convertFromInt64(val, wasNull, dest) 177 } 178 if err != nil { 179 err = errDestinationConversionFailed(val, dest, err) 180 } 181 return 182 }