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 &timestampCodec{
    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  }