From 22de6487f4096a9309d77766bcb2ea25f70650a3 Mon Sep 17 00:00:00 2001 From: Oleg Jukovec Date: Thu, 22 Jun 2023 14:20:00 +0300 Subject: [PATCH] api: the Datetime type is immutable The patch forces the use of objects of type Datetime instead of pointers. Part of #238 --- CHANGELOG.md | 1 + README.md | 7 +++++ datetime/datetime.go | 66 ++++++++++++++++++++++----------------- datetime/datetime_test.go | 66 ++++++++++++++++++--------------------- datetime/example_test.go | 22 ++++++------- 5 files changed, 87 insertions(+), 75 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5ece0ade0..e1fb7a242 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,7 @@ Versioning](http://semver.org/spec/v2.0.0.html) except to the first release. - Use msgpack/v5 instead of msgpack.v2 (#236) - Call/NewCallRequest = Call17/NewCall17Request (#235) - Use objects of the Decimal type instead of pointers (#238) +- Use objects of the Datetime type instead of pointers (#238) ### Deprecated diff --git a/README.md b/README.md index df71d2ad9..aa4c6deac 100644 --- a/README.md +++ b/README.md @@ -27,6 +27,7 @@ faster than other packages according to public benchmarks. * [API reference](#api-reference) * [Walking\-through example](#walking-through-example) * [Migration to v2](#migration-to-v2) + * [datetime package](#datetime-package) * [decimal package](#decimal-package) * [multi package](#multi-package) * [pool package](#pool-package) @@ -149,6 +150,12 @@ by `Connect()`. The article describes migration from go-tarantool to go-tarantool/v2. +#### datetime package + +Now you need to use objects of the Datetime type instead of pointers to it. A +new constructor `MakeDatetime` returns an object. `NewDatetime` has been +removed. + #### decimal package Now you need to use objects of the Decimal type instead of pointers to it. A diff --git a/datetime/datetime.go b/datetime/datetime.go index 09e86ce46..1f68ff6b4 100644 --- a/datetime/datetime.go +++ b/datetime/datetime.go @@ -12,6 +12,7 @@ package datetime import ( "encoding/binary" "fmt" + "reflect" "time" "github.com/vmihailenco/msgpack/v5" @@ -35,7 +36,7 @@ import ( // Datetime external type. Supported since Tarantool 2.10. See more details in // issue https://github.com/tarantool/tarantool/issues/5946. -const datetime_extId = 4 +const datetimeExtID = 4 // datetime structure keeps a number of seconds and nanoseconds since Unix Epoch. // Time is normalized by UTC, so time-zone offset is informative only. @@ -93,7 +94,7 @@ const ( offsetMax = 14 * 60 * 60 ) -// NewDatetime returns a pointer to a new datetime.Datetime that contains a +// MakeDatetime returns a datetime.Datetime object that contains a // specified time.Time. It may return an error if the Time value is out of // supported range: [-5879610-06-22T00:00Z .. 5879611-07-11T00:00Z] or // an invalid timezone or offset value is out of supported range: @@ -101,33 +102,33 @@ const ( // // NOTE: Tarantool's datetime.tz value is picked from t.Location().String(). // "Local" location is unsupported, see ExampleNewDatetime_localUnsupported. -func NewDatetime(t time.Time) (*Datetime, error) { +func MakeDatetime(t time.Time) (Datetime, error) { + dt := Datetime{} seconds := t.Unix() if seconds < minSeconds || seconds > maxSeconds { - return nil, fmt.Errorf("time %s is out of supported range", t) + return dt, fmt.Errorf("time %s is out of supported range", t) } zone := t.Location().String() _, offset := t.Zone() if zone != NoTimezone { if _, ok := timezoneToIndex[zone]; !ok { - return nil, fmt.Errorf("unknown timezone %s with offset %d", + return dt, fmt.Errorf("unknown timezone %s with offset %d", zone, offset) } } if offset < offsetMin || offset > offsetMax { - return nil, fmt.Errorf("offset must be between %d and %d hours", + return dt, fmt.Errorf("offset must be between %d and %d hours", offsetMin, offsetMax) } - dt := new(Datetime) dt.time = t return dt, nil } -func intervalFromDatetime(dtime *Datetime) (ival Interval) { +func intervalFromDatetime(dtime Datetime) (ival Interval) { ival.Year = int64(dtime.time.Year()) ival.Month = int64(dtime.time.Month()) ival.Day = int64(dtime.time.Day()) @@ -158,7 +159,7 @@ func daysInMonth(year int64, month int64) int64 { // C implementation: // https://github.com/tarantool/c-dt/blob/cec6acebb54d9e73ea0b99c63898732abd7683a6/dt_arithmetic.c#L74-L98 -func addMonth(ival *Interval, delta int64, adjust Adjust) { +func addMonth(ival Interval, delta int64, adjust Adjust) Interval { oldYear := ival.Year oldMonth := ival.Month @@ -172,16 +173,17 @@ func addMonth(ival *Interval, delta int64, adjust Adjust) { } } if adjust == ExcessAdjust || ival.Day < 28 { - return + return ival } dim := daysInMonth(ival.Year, ival.Month) if ival.Day > dim || (adjust == LastAdjust && ival.Day == daysInMonth(oldYear, oldMonth)) { ival.Day = dim } + return ival } -func (dtime *Datetime) add(ival Interval, positive bool) (*Datetime, error) { +func (dtime Datetime) add(ival Interval, positive bool) (Datetime, error) { newVal := intervalFromDatetime(dtime) var direction int64 @@ -191,7 +193,7 @@ func (dtime *Datetime) add(ival Interval, positive bool) (*Datetime, error) { direction = -1 } - addMonth(&newVal, direction*ival.Year*12+direction*ival.Month, ival.Adjust) + newVal = addMonth(newVal, direction*ival.Year*12+direction*ival.Month, ival.Adjust) newVal.Day += direction * 7 * ival.Week newVal.Day += direction * ival.Day newVal.Hour += direction * ival.Hour @@ -203,23 +205,23 @@ func (dtime *Datetime) add(ival Interval, positive bool) (*Datetime, error) { int(newVal.Day), int(newVal.Hour), int(newVal.Min), int(newVal.Sec), int(newVal.Nsec), dtime.time.Location()) - return NewDatetime(tm) + return MakeDatetime(tm) } // Add creates a new Datetime as addition of the Datetime and Interval. It may // return an error if a new Datetime is out of supported range. -func (dtime *Datetime) Add(ival Interval) (*Datetime, error) { +func (dtime Datetime) Add(ival Interval) (Datetime, error) { return dtime.add(ival, true) } // Sub creates a new Datetime as subtraction of the Datetime and Interval. It // may return an error if a new Datetime is out of supported range. -func (dtime *Datetime) Sub(ival Interval) (*Datetime, error) { +func (dtime Datetime) Sub(ival Interval) (Datetime, error) { return dtime.add(ival, false) } // Interval returns an Interval value to a next Datetime value. -func (dtime *Datetime) Interval(next *Datetime) Interval { +func (dtime Datetime) Interval(next Datetime) Interval { curIval := intervalFromDatetime(dtime) nextIval := intervalFromDatetime(next) _, curOffset := dtime.time.Zone() @@ -236,11 +238,12 @@ func (dtime *Datetime) Interval(next *Datetime) Interval { // If a Datetime created via unmarshaling Tarantool's datetime then we try to // create a location with time.LoadLocation() first. In case of failure, we use // a location created with time.FixedZone(). -func (dtime *Datetime) ToTime() time.Time { +func (dtime Datetime) ToTime() time.Time { return dtime.time } -func (dtime *Datetime) MarshalMsgpack() ([]byte, error) { +func datetimeEncoder(e *msgpack.Encoder, v reflect.Value) ([]byte, error) { + dtime := v.Interface().(Datetime) tm := dtime.ToTime() var dt datetime @@ -272,17 +275,25 @@ func (dtime *Datetime) MarshalMsgpack() ([]byte, error) { return buf, nil } -func (tm *Datetime) UnmarshalMsgpack(b []byte) error { - l := len(b) - if l != maxSize && l != secondsSize { - return fmt.Errorf("invalid data length: got %d, wanted %d or %d", len(b), secondsSize, maxSize) +func datetimeDecoder(d *msgpack.Decoder, v reflect.Value, extLen int) error { + if extLen != maxSize && extLen != secondsSize { + return fmt.Errorf("invalid data length: got %d, wanted %d or %d", extLen, secondsSize, maxSize) + } + + b := make([]byte, extLen) + n, err := d.Buffered().Read(b) + if err != nil { + return err + } + if n < extLen { + return fmt.Errorf("msgpack: unexpected end of stream after %d datetime bytes", n) } var dt datetime sec := binary.LittleEndian.Uint64(b) dt.seconds = int64(sec) dt.nsec = 0 - if l == maxSize { + if extLen == maxSize { dt.nsec = int32(binary.LittleEndian.Uint32(b[secondsSize:])) dt.tzOffset = int16(binary.LittleEndian.Uint16(b[secondsSize+nsecSize:])) dt.tzIndex = int16(binary.LittleEndian.Uint16(b[secondsSize+nsecSize+tzOffsetSize:])) @@ -315,13 +326,12 @@ func (tm *Datetime) UnmarshalMsgpack(b []byte) error { } tt = tt.In(loc) - dtp, err := NewDatetime(tt) - if dtp != nil { - *tm = *dtp - } + ptr := v.Addr().Interface().(*Datetime) + *ptr, err = MakeDatetime(tt) return err } func init() { - msgpack.RegisterExt(datetime_extId, (*Datetime)(nil)) + msgpack.RegisterExtDecoder(datetimeExtID, Datetime{}, datetimeDecoder) + msgpack.RegisterExtEncoder(datetimeExtID, Datetime{}, datetimeEncoder) } diff --git a/datetime/datetime_test.go b/datetime/datetime_test.go index e2f707f6f..abb00e50c 100644 --- a/datetime/datetime_test.go +++ b/datetime/datetime_test.go @@ -58,7 +58,7 @@ func skipIfDatetimeUnsupported(t *testing.T) { func TestDatetimeAdd(t *testing.T) { tm := time.Unix(0, 0).UTC() - dt, err := NewDatetime(tm) + dt, err := MakeDatetime(tm) if err != nil { t.Fatalf("Unexpected error: %s", err.Error()) } @@ -229,7 +229,7 @@ func TestDatetimeAddAdjust(t *testing.T) { if err != nil { t.Fatalf("Unexpected error: %s", err.Error()) } - dt, err := NewDatetime(tm) + dt, err := MakeDatetime(tm) if err != nil { t.Fatalf("Unexpected error: %s", err.Error()) } @@ -243,9 +243,6 @@ func TestDatetimeAddAdjust(t *testing.T) { if err != nil { t.Fatalf("Unable to add: %s", err.Error()) } - if newdt == nil { - t.Fatalf("Unexpected nil value") - } res := newdt.ToTime().Format(time.RFC3339) if res != tc.want { t.Fatalf("Unexpected result %s, expected %s", res, tc.want) @@ -256,7 +253,7 @@ func TestDatetimeAddAdjust(t *testing.T) { func TestDatetimeAddSubSymmetric(t *testing.T) { tm := time.Unix(0, 0).UTC() - dt, err := NewDatetime(tm) + dt, err := MakeDatetime(tm) if err != nil { t.Fatalf("Unexpected error: %s", err.Error()) } @@ -304,7 +301,7 @@ func TestDatetimeAddSubSymmetric(t *testing.T) { // We have a separate test for accurate Datetime boundaries. func TestDatetimeAddOutOfRange(t *testing.T) { tm := time.Unix(0, 0).UTC() - dt, err := NewDatetime(tm) + dt, err := MakeDatetime(tm) if err != nil { t.Fatalf("Unexpected error: %s", err.Error()) } @@ -317,9 +314,6 @@ func TestDatetimeAddOutOfRange(t *testing.T) { if err.Error() != expected { t.Fatalf("Unexpected error: %s", err.Error()) } - if newdt != nil { - t.Fatalf("Unexpected result: %v", newdt) - } } func TestDatetimeInterval(t *testing.T) { @@ -335,11 +329,11 @@ func TestDatetimeInterval(t *testing.T) { t.Fatalf("Error in time.Parse(): %s", err) } - dtFirst, err := NewDatetime(tmFirst) + dtFirst, err := MakeDatetime(tmFirst) if err != nil { t.Fatalf("Unable to create Datetime from %s: %s", tmFirst, err) } - dtSecond, err := NewDatetime(tmSecond) + dtSecond, err := MakeDatetime(tmSecond) if err != nil { t.Fatalf("Unable to create Datetime from %s: %s", tmSecond, err) } @@ -389,15 +383,15 @@ func TestDatetimeTarantoolInterval(t *testing.T) { "2015-12-21T17:50:53Z", "1980-03-28T13:18:39.000099Z", } - datetimes := []*Datetime{} + datetimes := []Datetime{} for _, date := range dates { tm, err := time.Parse(time.RFC3339, date) if err != nil { t.Fatalf("Error in time.Parse(%s): %s", date, err) } - dt, err := NewDatetime(tm) + dt, err := MakeDatetime(tm) if err != nil { - t.Fatalf("Error in NewDatetime(%s): %s", tm, err) + t.Fatalf("Error in MakeDatetime(%s): %s", tm, err) } datetimes = append(datetimes, dt) } @@ -434,7 +428,7 @@ func assertDatetimeIsEqual(t *testing.T, tuples []interface{}, tm time.Time) { if len(tpl) != 2 { t.Fatalf("Unexpected return value body (tuple len = %d)", len(tpl)) } - if val, ok := tpl[dtIndex].(*Datetime); !ok || !val.ToTime().Equal(tm) { + if val, ok := tpl[dtIndex].(Datetime); !ok || !val.ToTime().Equal(tm) { t.Fatalf("Unexpected tuple %d field %v, expected %v", dtIndex, val, @@ -466,7 +460,7 @@ func TestInvalidTimezone(t *testing.T) { t.Fatalf("Time parse failed: %s", err) } tm = tm.In(invalidLoc) - dt, err := NewDatetime(tm) + dt, err := MakeDatetime(tm) if err == nil { t.Fatalf("Unexpected success: %v", dt) } @@ -502,7 +496,7 @@ func TestInvalidOffset(t *testing.T) { t.Fatalf("Time parse failed: %s", err) } tm = tm.In(loc) - dt, err := NewDatetime(tm) + dt, err := MakeDatetime(tm) if testcase.ok && err != nil { t.Fatalf("Unexpected error: %s", err.Error()) } @@ -537,7 +531,7 @@ func TestCustomTimezone(t *testing.T) { t.Fatalf("Time parse failed: %s", err) } tm = tm.In(customLoc) - dt, err := NewDatetime(tm) + dt, err := MakeDatetime(tm) if err != nil { t.Fatalf("Unable to create datetime: %s", err.Error()) } @@ -550,7 +544,7 @@ func TestCustomTimezone(t *testing.T) { assertDatetimeIsEqual(t, resp.Data, tm) tpl := resp.Data[0].([]interface{}) - if respDt, ok := tpl[0].(*Datetime); ok { + if respDt, ok := tpl[0].(Datetime); ok { zone := respDt.ToTime().Location().String() _, offset := respDt.ToTime().Zone() if zone != customZone { @@ -574,7 +568,7 @@ func TestCustomTimezone(t *testing.T) { func tupleInsertSelectDelete(t *testing.T, conn *Connection, tm time.Time) { t.Helper() - dt, err := NewDatetime(tm) + dt, err := MakeDatetime(tm) if err != nil { t.Fatalf("Unable to create Datetime from %s: %s", tm, err) } @@ -726,7 +720,7 @@ func TestDatetimeOutOfRange(t *testing.T) { for _, tm := range greaterBoundaryTimes { t.Run(tm.String(), func(t *testing.T) { - _, err := NewDatetime(tm) + _, err := MakeDatetime(tm) if err == nil { t.Errorf("Time %s should be unsupported!", tm) } @@ -745,7 +739,7 @@ func TestDatetimeReplace(t *testing.T) { t.Fatalf("Time parse failed: %s", err) } - dt, err := NewDatetime(tm) + dt, err := MakeDatetime(tm) if err != nil { t.Fatalf("Unable to create Datetime from %s: %s", tm, err) } @@ -852,10 +846,10 @@ func (ev *Event) DecodeMsgpack(d *msgpack.Decoder) error { return err } - if dt, ok := res.(*Datetime); !ok { + if dt, ok := res.(Datetime); !ok { return fmt.Errorf("Datetime doesn't match") } else { - ev.Datetime = *dt + ev.Datetime = dt } return nil } @@ -907,11 +901,11 @@ func TestCustomEncodeDecodeTuple1(t *testing.T) { tm1, _ := time.Parse(time.RFC3339, "2010-05-24T17:51:56.000000009Z") tm2, _ := time.Parse(time.RFC3339, "2022-05-24T17:51:56.000000009Z") - dt1, err := NewDatetime(tm1) + dt1, err := MakeDatetime(tm1) if err != nil { t.Fatalf("Unable to create Datetime from %s: %s", tm1, err) } - dt2, err := NewDatetime(tm2) + dt2, err := MakeDatetime(tm2) if err != nil { t.Fatalf("Unable to create Datetime from %s: %s", tm2, err) } @@ -921,8 +915,8 @@ func TestCustomEncodeDecodeTuple1(t *testing.T) { tuple := Tuple2{Cid: cid, Orig: orig, Events: []Event{ - {*dt1, "Minsk"}, - {*dt2, "Moscow"}, + {dt1, "Minsk"}, + {dt2, "Moscow"}, }, } rep := NewReplaceRequest(spaceTuple2).Tuple(&tuple) @@ -962,7 +956,7 @@ func TestCustomEncodeDecodeTuple1(t *testing.T) { } for i, tv := range []time.Time{tm1, tm2} { - dt, ok := events[i].([]interface{})[1].(*Datetime) + dt, ok := events[i].([]interface{})[1].(Datetime) if !ok || !dt.ToTime().Equal(tv) { t.Fatalf("%v != %v", dt.ToTime(), tv) } @@ -1020,7 +1014,7 @@ func TestCustomEncodeDecodeTuple5(t *testing.T) { defer conn.Close() tm := time.Unix(500, 1000).In(time.FixedZone(NoTimezone, 0)) - dt, err := NewDatetime(tm) + dt, err := MakeDatetime(tm) if err != nil { t.Fatalf("Unable to create Datetime from %s: %s", tm, err) } @@ -1043,7 +1037,7 @@ func TestCustomEncodeDecodeTuple5(t *testing.T) { if tpl, ok := resp.Data[0].([]interface{}); !ok { t.Errorf("Unexpected body of Select") } else { - if val, ok := tpl[0].(*Datetime); !ok || !val.ToTime().Equal(tm) { + if val, ok := tpl[0].(Datetime); !ok || !val.ToTime().Equal(tm) { t.Fatalf("Unexpected body of Select") } } @@ -1072,7 +1066,7 @@ func TestMPEncode(t *testing.T) { if err != nil { t.Fatalf("Time (%s) parse failed: %s", testcase.dt, err) } - dt, err := NewDatetime(tm) + dt, err := MakeDatetime(tm) if err != nil { t.Fatalf("Unable to create Datetime from %s: %s", tm, err) } @@ -1126,7 +1120,7 @@ func TestMPDecode(t *testing.T) { func TestUnmarshalMsgpackInvalidLength(t *testing.T) { var v Datetime - err := v.UnmarshalMsgpack([]byte{0x04}) + err := msgpack.Unmarshal([]byte{0xd4, 0x04, 0x04}, &v) if err == nil { t.Fatalf("Unexpected success %v", v) } @@ -1142,8 +1136,8 @@ func TestUnmarshalMsgpackInvalidZone(t *testing.T) { // {time.RFC3339 + " MST", // "2006-01-02T15:04:00+03:00 MSK", // "d804b016b9430000000000000000b400ee00"} - buf, _ := hex.DecodeString("b016b9430000000000000000b400ee01") - err := v.UnmarshalMsgpack(buf) + buf, _ := hex.DecodeString("d804b016b9430000000000000000b400ee01") + err := msgpack.Unmarshal(buf, &v) if err == nil { t.Fatalf("Unexpected success %v", v) } diff --git a/datetime/example_test.go b/datetime/example_test.go index 5fb1e9ba8..346551629 100644 --- a/datetime/example_test.go +++ b/datetime/example_test.go @@ -35,7 +35,7 @@ func Example() { fmt.Printf("Error in time.Parse() is %v", err) return } - dt, err := NewDatetime(tm) + dt, err := MakeDatetime(tm) if err != nil { fmt.Printf("Unable to create Datetime from %s: %s", tm, err) return @@ -91,13 +91,13 @@ func Example() { fmt.Printf("Data: %v\n", respDt.ToTime()) } -// ExampleNewDatetime_localUnsupported demonstrates that "Local" location is +// ExampleMakeDatetime_localUnsupported demonstrates that "Local" location is // unsupported. -func ExampleNewDatetime_localUnsupported() { +func ExampleMakeDatetime_localUnsupported() { tm := time.Now().Local() loc := tm.Location() fmt.Println("Location:", loc) - if _, err := NewDatetime(tm); err != nil { + if _, err := MakeDatetime(tm); err != nil { fmt.Printf("Could not create a Datetime with %s location.\n", loc) } else { fmt.Printf("A Datetime with %s location created.\n", loc) @@ -109,7 +109,7 @@ func ExampleNewDatetime_localUnsupported() { // Example demonstrates how to create a datetime for Tarantool without UTC // timezone in datetime. -func ExampleNewDatetime_noTimezone() { +func ExampleMakeDatetime_noTimezone() { var datetime = "2013-10-28T17:51:56.000000009Z" tm, err := time.Parse(time.RFC3339, datetime) if err != nil { @@ -119,7 +119,7 @@ func ExampleNewDatetime_noTimezone() { tm = tm.In(time.FixedZone(NoTimezone, 0)) - dt, err := NewDatetime(tm) + dt, err := MakeDatetime(tm) if err != nil { fmt.Printf("Unable to create Datetime from %s: %s", tm, err) return @@ -145,12 +145,12 @@ func ExampleDatetime_Interval() { return } - dtFirst, err := NewDatetime(tmFirst) + dtFirst, err := MakeDatetime(tmFirst) if err != nil { fmt.Printf("Unable to create Datetime from %s: %s", tmFirst, err) return } - dtSecond, err := NewDatetime(tmSecond) + dtSecond, err := MakeDatetime(tmSecond) if err != nil { fmt.Printf("Unable to create Datetime from %s: %s", tmSecond, err) return @@ -170,7 +170,7 @@ func ExampleDatetime_Add() { fmt.Printf("Error in time.Parse() is %s", err) return } - dt, err := NewDatetime(tm) + dt, err := MakeDatetime(tm) if err != nil { fmt.Printf("Unable to create Datetime from %s: %s", tm, err) return @@ -201,7 +201,7 @@ func ExampleDatetime_Add_dst() { return } tm := time.Date(2008, 1, 1, 1, 1, 1, 1, loc) - dt, err := NewDatetime(tm) + dt, err := MakeDatetime(tm) if err != nil { fmt.Printf("Unable to create Datetime: %s", err) return @@ -237,7 +237,7 @@ func ExampleDatetime_Sub() { fmt.Printf("Error in time.Parse() is %s", err) return } - dt, err := NewDatetime(tm) + dt, err := MakeDatetime(tm) if err != nil { fmt.Printf("Unable to create Datetime from %s: %s", tm, err) return