diff --git a/.github/workflows/reusable_testing.yml b/.github/workflows/reusable_testing.yml index 94c671543..beb4c84c8 100644 --- a/.github/workflows/reusable_testing.yml +++ b/.github/workflows/reusable_testing.yml @@ -36,7 +36,7 @@ jobs: - name: Setup golang for connector and tests uses: actions/setup-go@v5 with: - go-version: '1.20' + go-version: '1.24' - name: Setup tt run: | diff --git a/.github/workflows/testing.yml b/.github/workflows/testing.yml index 2fce2f5ec..12e41dae6 100644 --- a/.github/workflows/testing.yml +++ b/.github/workflows/testing.yml @@ -16,7 +16,7 @@ jobs: strategy: fail-fast: false matrix: - golang: ['1.20', 'stable'] + golang: ['1.24', 'stable'] tarantool: ['1.10'] coveralls: [false] fuzzing: [false] @@ -36,19 +36,19 @@ jobs: strategy: fail-fast: false matrix: - golang: ['1.20', 'stable'] + golang: ['1.24', 'stable'] tarantool: ['2.11', '3.4', 'master'] coveralls: [false] fuzzing: [false] include: - - golang: '1.20' + - golang: '1.24' tarantool: 'master' coveralls: true fuzzing: false - - golang: '1.20' + - golang: '1.24' tarantool: 'master' - fuzzing: true coveralls: false + fuzzing: true uses: ./.github/workflows/reusable-run.yml with: os: ubuntu-24.04 @@ -72,7 +72,7 @@ jobs: fail-fast: false matrix: golang: - - '1.20' + - '1.24' - 'stable' runs-on: - macos-14 diff --git a/CHANGELOG.md b/CHANGELOG.md index 737db9029..ab3c73982 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,8 +10,12 @@ Versioning](http://semver.org/spec/v2.0.0.html) except to the first release. ### Added +* New types for MessagePack extensions compatible with go-option (#459). + ### Changed +* Required Go version is `1.24` now (#456). + ### Fixed ## [v2.4.1] - 2025-10-16 diff --git a/MIGRATION.md b/MIGRATION.md index fdd4893dc..54f4e3034 100644 --- a/MIGRATION.md +++ b/MIGRATION.md @@ -1,8 +1,18 @@ # Migration guide +## Migration from v2.x.x to v3.x.x + +* [Major changes](#major-changes-v3) + +TODO + +### Major changes + +* Required Go version is `1.24` now. + ## Migration from v1.x.x to v2.x.x -* [Major changes](#major-changes) +* [Major changes](#major-changes-v2) * [Main package](#main-package) * [Go version](#go-version) * [msgpack/v5](#msgpackv5) @@ -25,7 +35,7 @@ * [crud package](#crud-package) * [test_helpers package](#test_helpers-package) -### Major changes +### Major changes * The `go_tarantool_call_17` build tag is no longer needed, since by default the `CallRequest` is `Call17Request`. @@ -50,9 +60,9 @@ import ( "fmt" "github.com/tarantool/go-tarantool" - _ "github.com/tarantool/go-tarantool/v2/datetime" - _ "github.com/tarantool/go-tarantool/v2/decimal" - _ "github.com/tarantool/go-tarantool/v2/uuid" + _ "github.com/tarantool/go-tarantool/v3/datetime" + _ "github.com/tarantool/go-tarantool/v3/decimal" + _ "github.com/tarantool/go-tarantool/v3/uuid" ) func main() { @@ -82,10 +92,10 @@ import ( "fmt" "time" - "github.com/tarantool/go-tarantool/v2" - _ "github.com/tarantool/go-tarantool/v2/datetime" - _ "github.com/tarantool/go-tarantool/v2/decimal" - _ "github.com/tarantool/go-tarantool/v2/uuid" + "github.com/tarantool/go-tarantool/v3" + _ "github.com/tarantool/go-tarantool/v3/datetime" + _ "github.com/tarantool/go-tarantool/v3/decimal" + _ "github.com/tarantool/go-tarantool/v3/uuid" ) func main() { diff --git a/README.md b/README.md index f5c533390..6950724ca 100644 --- a/README.md +++ b/README.md @@ -36,10 +36,10 @@ faster than other packages according to public benchmarks. We assume that you have Tarantool version 1.10+ and a modern Linux or BSD operating system. -You need a current version of `go`, version 1.20 or later (use `go version` to +You need a current version of `go`, version 1.24 or later (use `go version` to check the version number). Do not use `gccgo-go`. -**Note:** If your `go` version is older than 1.20 or if `go` is not installed, +**Note:** If your `go` version is older than 1.24 or if `go` is not installed, download and run the latest tarball from [golang.org][golang-dl]. The package `go-tarantool` is located in [tarantool/go-tarantool][go-tarantool] @@ -98,10 +98,10 @@ import ( "fmt" "time" - "github.com/tarantool/go-tarantool/v2" - _ "github.com/tarantool/go-tarantool/v2/datetime" - _ "github.com/tarantool/go-tarantool/v2/decimal" - _ "github.com/tarantool/go-tarantool/v2/uuid" + "github.com/tarantool/go-tarantool/v3" + _ "github.com/tarantool/go-tarantool/v3/datetime" + _ "github.com/tarantool/go-tarantool/v3/decimal" + _ "github.com/tarantool/go-tarantool/v3/uuid" ) func main() { @@ -184,10 +184,10 @@ import ( "fmt" "time" - "github.com/tarantool/go-tarantool/v2" - _ "github.com/tarantool/go-tarantool/v2/datetime" - _ "github.com/tarantool/go-tarantool/v2/decimal" - _ "github.com/tarantool/go-tarantool/v2/uuid" + "github.com/tarantool/go-tarantool/v3" + _ "github.com/tarantool/go-tarantool/v3/datetime" + _ "github.com/tarantool/go-tarantool/v3/decimal" + _ "github.com/tarantool/go-tarantool/v3/uuid" "github.com/tarantool/go-tlsdialer" ) diff --git a/arrow/arrow.go b/arrow/arrow.go index aaeaccca9..a7767c459 100644 --- a/arrow/arrow.go +++ b/arrow/arrow.go @@ -7,6 +7,8 @@ import ( "github.com/vmihailenco/msgpack/v5" ) +//go:generate go tool gentypes -ext-code 8 Arrow + // Arrow MessagePack extension type. const arrowExtId = 8 @@ -26,6 +28,17 @@ func (a Arrow) Raw() []byte { return a.data } +// MarshalMsgpack implements a custom msgpack marshaler for extension type. +func (a Arrow) MarshalMsgpack() ([]byte, error) { + return a.data, nil +} + +// UnmarshalMsgpack implements a custom msgpack unmarshaler for extension type. +func (a *Arrow) UnmarshalMsgpack(data []byte) error { + a.data = data + return nil +} + func arrowDecoder(d *msgpack.Decoder, v reflect.Value, extLen int) error { arrow := Arrow{ data: make([]byte, extLen), diff --git a/arrow/arrow_gen.go b/arrow/arrow_gen.go new file mode 100644 index 000000000..c86c72778 --- /dev/null +++ b/arrow/arrow_gen.go @@ -0,0 +1,241 @@ +// Code generated by github.com/tarantool/go-option; DO NOT EDIT. + +package arrow + +import ( + "fmt" + + "github.com/vmihailenco/msgpack/v5" + "github.com/vmihailenco/msgpack/v5/msgpcode" + + "github.com/tarantool/go-option" +) + +// OptionalArrow represents an optional value of type Arrow. +// It can either hold a valid Arrow (IsSome == true) or be empty (IsZero == true). +type OptionalArrow struct { + value Arrow + exists bool +} + +// SomeOptionalArrow creates an optional OptionalArrow with the given Arrow value. +// The returned OptionalArrow will have IsSome() == true and IsZero() == false. +func SomeOptionalArrow(value Arrow) OptionalArrow { + return OptionalArrow{ + value: value, + exists: true, + } +} + +// NoneOptionalArrow creates an empty optional OptionalArrow value. +// The returned OptionalArrow will have IsSome() == false and IsZero() == true. +// +// Example: +// +// o := NoneOptionalArrow() +// if o.IsZero() { +// fmt.Println("value is absent") +// } +func NoneOptionalArrow() OptionalArrow { + return OptionalArrow{} +} + +func (o OptionalArrow) newEncodeError(err error) error { + if err == nil { + return nil + } + return &option.EncodeError{ + Type: "OptionalArrow", + Parent: err, + } +} + +func (o OptionalArrow) newDecodeError(err error) error { + if err == nil { + return nil + } + + return &option.DecodeError{ + Type: "OptionalArrow", + Parent: err, + } +} + +// IsSome returns true if the OptionalArrow contains a value. +// This indicates the value is explicitly set (not None). +func (o OptionalArrow) IsSome() bool { + return o.exists +} + +// IsZero returns true if the OptionalArrow does not contain a value. +// Equivalent to !IsSome(). Useful for consistency with types where +// zero value (e.g. 0, false, zero struct) is valid and needs to be distinguished. +func (o OptionalArrow) IsZero() bool { + return !o.exists +} + +// IsNil is an alias for IsZero. +// +// This method is provided for compatibility with the msgpack Encoder interface. +func (o OptionalArrow) IsNil() bool { + return o.IsZero() +} + +// Get returns the stored value and a boolean flag indicating its presence. +// If the value is present, returns (value, true). +// If the value is absent, returns (zero value of Arrow, false). +// +// Recommended usage: +// +// if value, ok := o.Get(); ok { +// // use value +// } +func (o OptionalArrow) Get() (Arrow, bool) { + return o.value, o.exists +} + +// MustGet returns the stored value if it is present. +// Panics if the value is absent (i.e., IsZero() == true). +// +// Use with caution — only when you are certain the value exists. +// +// Panics with: "optional value is not set" if no value is set. +func (o OptionalArrow) MustGet() Arrow { + if !o.exists { + panic("optional value is not set") + } + + return o.value +} + +// Unwrap returns the stored value regardless of presence. +// If no value is set, returns the zero value for Arrow. +// +// Warning: Does not check presence. Use IsSome() before calling if you need +// to distinguish between absent value and explicit zero value. +func (o OptionalArrow) Unwrap() Arrow { + return o.value +} + +// UnwrapOr returns the stored value if present. +// Otherwise, returns the provided default value. +// +// Example: +// +// o := NoneOptionalArrow() +// v := o.UnwrapOr(someDefaultOptionalArrow) +func (o OptionalArrow) UnwrapOr(defaultValue Arrow) Arrow { + if o.exists { + return o.value + } + + return defaultValue +} + +// UnwrapOrElse returns the stored value if present. +// Otherwise, calls the provided function and returns its result. +// Useful when the default value requires computation or side effects. +// +// Example: +// +// o := NoneOptionalArrow() +// v := o.UnwrapOrElse(func() Arrow { return computeDefault() }) +func (o OptionalArrow) UnwrapOrElse(defaultValue func() Arrow) Arrow { + if o.exists { + return o.value + } + + return defaultValue() +} + +func (o OptionalArrow) encodeValue(encoder *msgpack.Encoder) error { + value, err := o.value.MarshalMsgpack() + if err != nil { + return err + } + + err = encoder.EncodeExtHeader(8, len(value)) + if err != nil { + return err + } + + _, err = encoder.Writer().Write(value) + if err != nil { + return err + } + + return nil +} + +// EncodeMsgpack encodes the OptionalArrow value using MessagePack format. +// - If the value is present, it is encoded as Arrow. +// - If the value is absent (None), it is encoded as nil. +// +// Returns an error if encoding fails. +func (o OptionalArrow) EncodeMsgpack(encoder *msgpack.Encoder) error { + if o.exists { + return o.newEncodeError(o.encodeValue(encoder)) + } + + return o.newEncodeError(encoder.EncodeNil()) +} + +func (o *OptionalArrow) decodeValue(decoder *msgpack.Decoder) error { + tp, length, err := decoder.DecodeExtHeader() + switch { + case err != nil: + return o.newDecodeError(err) + case tp != 8: + return o.newDecodeError(fmt.Errorf("invalid extension code: %d", tp)) + } + + a := make([]byte, length) + if err := decoder.ReadFull(a); err != nil { + return o.newDecodeError(err) + } + + if err := o.value.UnmarshalMsgpack(a); err != nil { + return o.newDecodeError(err) + } + + o.exists = true + return nil +} + +func (o *OptionalArrow) checkCode(code byte) bool { + return msgpcode.IsExt(code) +} + +// DecodeMsgpack decodes a OptionalArrow value from MessagePack format. +// Supports two input types: +// - nil: interpreted as no value (NoneOptionalArrow) +// - Arrow: interpreted as a present value (SomeOptionalArrow) +// +// Returns an error if the input type is unsupported or decoding fails. +// +// After successful decoding: +// - on nil: exists = false, value = default zero value +// - on Arrow: exists = true, value = decoded value +func (o *OptionalArrow) DecodeMsgpack(decoder *msgpack.Decoder) error { + code, err := decoder.PeekCode() + if err != nil { + return o.newDecodeError(err) + } + + switch { + case code == msgpcode.Nil: + o.exists = false + + return o.newDecodeError(decoder.Skip()) + case o.checkCode(code): + err := o.decodeValue(decoder) + if err != nil { + return o.newDecodeError(err) + } + o.exists = true + + return err + default: + return o.newDecodeError(fmt.Errorf("unexpected code: %d", code)) + } +} diff --git a/arrow/arrow_gen_test.go b/arrow/arrow_gen_test.go new file mode 100644 index 000000000..d990499f4 --- /dev/null +++ b/arrow/arrow_gen_test.go @@ -0,0 +1,124 @@ +package arrow + +import ( + "bytes" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/vmihailenco/msgpack/v5" +) + +func TestSomeOptionalArrow(t *testing.T) { + val, err := MakeArrow([]byte{1, 2, 3}) + assert.NoError(t, err) + opt := SomeOptionalArrow(val) + + assert.True(t, opt.IsSome()) + assert.False(t, opt.IsZero()) + + v, ok := opt.Get() + assert.True(t, ok) + assert.Equal(t, val, v) +} + +func TestNoneOptionalArrow(t *testing.T) { + opt := NoneOptionalArrow() + + assert.False(t, opt.IsSome()) + assert.True(t, opt.IsZero()) + + _, ok := opt.Get() + assert.False(t, ok) +} + +func TestOptionalArrow_MustGet(t *testing.T) { + val, err := MakeArrow([]byte{1, 2, 3}) + assert.NoError(t, err) + optSome := SomeOptionalArrow(val) + optNone := NoneOptionalArrow() + + assert.Equal(t, val, optSome.MustGet()) + assert.Panics(t, func() { optNone.MustGet() }) +} + +func TestOptionalArrow_Unwrap(t *testing.T) { + val, err := MakeArrow([]byte{1, 2, 3}) + assert.NoError(t, err) + optSome := SomeOptionalArrow(val) + optNone := NoneOptionalArrow() + + assert.Equal(t, val, optSome.Unwrap()) + assert.Equal(t, Arrow{}, optNone.Unwrap()) +} + +func TestOptionalArrow_UnwrapOr(t *testing.T) { + val, err := MakeArrow([]byte{1, 2, 3}) + assert.NoError(t, err) + def, err := MakeArrow([]byte{4, 5, 6}) + assert.NoError(t, err) + optSome := SomeOptionalArrow(val) + optNone := NoneOptionalArrow() + + assert.Equal(t, val, optSome.UnwrapOr(def)) + assert.Equal(t, def, optNone.UnwrapOr(def)) +} + +func TestOptionalArrow_UnwrapOrElse(t *testing.T) { + val, err := MakeArrow([]byte{1, 2, 3}) + assert.NoError(t, err) + def, err := MakeArrow([]byte{4, 5, 6}) + assert.NoError(t, err) + optSome := SomeOptionalArrow(val) + optNone := NoneOptionalArrow() + + assert.Equal(t, val, optSome.UnwrapOrElse(func() Arrow { return def })) + assert.Equal(t, def, optNone.UnwrapOrElse(func() Arrow { return def })) +} + +func TestOptionalArrow_EncodeDecodeMsgpack_Some(t *testing.T) { + val, err := MakeArrow([]byte{1, 2, 3}) + assert.NoError(t, err) + some := SomeOptionalArrow(val) + + var buf bytes.Buffer + enc := msgpack.NewEncoder(&buf) + dec := msgpack.NewDecoder(&buf) + + err = enc.Encode(some) + assert.NoError(t, err) + + var decodedSome OptionalArrow + err = dec.Decode(&decodedSome) + assert.NoError(t, err) + assert.True(t, decodedSome.IsSome()) + assert.Equal(t, val, decodedSome.Unwrap()) +} + +func TestOptionalArrow_EncodeDecodeMsgpack_None(t *testing.T) { + none := NoneOptionalArrow() + + var buf bytes.Buffer + enc := msgpack.NewEncoder(&buf) + dec := msgpack.NewDecoder(&buf) + + err := enc.Encode(none) + assert.NoError(t, err) + + var decodedNone OptionalArrow + err = dec.Decode(&decodedNone) + assert.NoError(t, err) + assert.True(t, decodedNone.IsZero()) +} + +func TestOptionalArrow_EncodeDecodeMsgpack_InvalidType(t *testing.T) { + var buf bytes.Buffer + enc := msgpack.NewEncoder(&buf) + dec := msgpack.NewDecoder(&buf) + + err := enc.Encode(123) + assert.NoError(t, err) + + var decodedInvalid OptionalArrow + err = dec.Decode(&decodedInvalid) + assert.Error(t, err) +} diff --git a/arrow/arrow_test.go b/arrow/arrow_test.go index 5e8b440c4..c3f40090c 100644 --- a/arrow/arrow_test.go +++ b/arrow/arrow_test.go @@ -6,8 +6,9 @@ import ( "testing" "github.com/stretchr/testify/require" - "github.com/tarantool/go-tarantool/v2/arrow" "github.com/vmihailenco/msgpack/v5" + + "github.com/tarantool/go-tarantool/v3/arrow" ) var longArrow, _ = hex.DecodeString("ffffffff70000000040000009effffff0400010004000000" + diff --git a/arrow/example_test.go b/arrow/example_test.go index e85d195cd..3510a777d 100644 --- a/arrow/example_test.go +++ b/arrow/example_test.go @@ -15,8 +15,8 @@ import ( "log" "time" - "github.com/tarantool/go-tarantool/v2" - "github.com/tarantool/go-tarantool/v2/arrow" + "github.com/tarantool/go-tarantool/v3" + "github.com/tarantool/go-tarantool/v3/arrow" ) var arrowBinData, _ = hex.DecodeString("ffffffff70000000040000009effffff0400010004000000" + diff --git a/arrow/request.go b/arrow/request.go index 332720d3e..82b55f399 100644 --- a/arrow/request.go +++ b/arrow/request.go @@ -5,8 +5,9 @@ import ( "io" "github.com/tarantool/go-iproto" - "github.com/tarantool/go-tarantool/v2" "github.com/vmihailenco/msgpack/v5" + + "github.com/tarantool/go-tarantool/v3" ) // InsertRequest helps you to create an insert request object for execution diff --git a/arrow/request_test.go b/arrow/request_test.go index fba6b5563..251b2b31d 100644 --- a/arrow/request_test.go +++ b/arrow/request_test.go @@ -8,9 +8,10 @@ import ( "github.com/stretchr/testify/require" "github.com/tarantool/go-iproto" - "github.com/tarantool/go-tarantool/v2" - "github.com/tarantool/go-tarantool/v2/arrow" "github.com/vmihailenco/msgpack/v5" + + "github.com/tarantool/go-tarantool/v3" + "github.com/tarantool/go-tarantool/v3/arrow" ) const validSpace uint32 = 1 // Any valid value != default. diff --git a/arrow/tarantool_test.go b/arrow/tarantool_test.go index 728b38f8b..cc2ad552e 100644 --- a/arrow/tarantool_test.go +++ b/arrow/tarantool_test.go @@ -11,9 +11,9 @@ import ( "github.com/stretchr/testify/require" "github.com/tarantool/go-iproto" - "github.com/tarantool/go-tarantool/v2" - "github.com/tarantool/go-tarantool/v2/arrow" - "github.com/tarantool/go-tarantool/v2/test_helpers" + "github.com/tarantool/go-tarantool/v3" + "github.com/tarantool/go-tarantool/v3/arrow" + "github.com/tarantool/go-tarantool/v3/test_helpers" ) var isArrowSupported = false diff --git a/auth_test.go b/auth_test.go index 712226d63..a9a0b34e9 100644 --- a/auth_test.go +++ b/auth_test.go @@ -5,7 +5,7 @@ import ( "testing" "github.com/stretchr/testify/assert" - . "github.com/tarantool/go-tarantool/v2" + . "github.com/tarantool/go-tarantool/v3" ) func TestAuth_String(t *testing.T) { diff --git a/box/box.go b/box/box.go index c9b0e71dd..a4cc1780e 100644 --- a/box/box.go +++ b/box/box.go @@ -1,7 +1,7 @@ package box import ( - "github.com/tarantool/go-tarantool/v2" + "github.com/tarantool/go-tarantool/v3" ) // Box is a helper that wraps box.* requests. diff --git a/box/box_test.go b/box/box_test.go index 0180473ed..59ceaf6e3 100644 --- a/box/box_test.go +++ b/box/box_test.go @@ -7,8 +7,9 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "github.com/tarantool/go-tarantool/v2/box" - "github.com/tarantool/go-tarantool/v2/test_helpers" + + "github.com/tarantool/go-tarantool/v3/box" + "github.com/tarantool/go-tarantool/v3/test_helpers" ) func TestNew(t *testing.T) { diff --git a/box/example_test.go b/box/example_test.go index eac3f5e1a..fa65189a8 100644 --- a/box/example_test.go +++ b/box/example_test.go @@ -15,8 +15,8 @@ import ( "log" "time" - "github.com/tarantool/go-tarantool/v2" - "github.com/tarantool/go-tarantool/v2/box" + "github.com/tarantool/go-tarantool/v3" + "github.com/tarantool/go-tarantool/v3/box" ) func ExampleBox_Info() { diff --git a/box/info.go b/box/info.go index aa682822c..edd4894cd 100644 --- a/box/info.go +++ b/box/info.go @@ -3,8 +3,9 @@ package box import ( "fmt" - "github.com/tarantool/go-tarantool/v2" "github.com/vmihailenco/msgpack/v5" + + "github.com/tarantool/go-tarantool/v3" ) var _ tarantool.Request = (*InfoRequest)(nil) diff --git a/box/info_test.go b/box/info_test.go index 2cf00f69b..d4e8e9539 100644 --- a/box/info_test.go +++ b/box/info_test.go @@ -6,7 +6,7 @@ import ( "github.com/stretchr/testify/require" "github.com/vmihailenco/msgpack/v5" - "github.com/tarantool/go-tarantool/v2/box" + "github.com/tarantool/go-tarantool/v3/box" ) func TestInfo(t *testing.T) { diff --git a/box/schema.go b/box/schema.go index 25017e5a4..036c76c6d 100644 --- a/box/schema.go +++ b/box/schema.go @@ -1,6 +1,8 @@ package box -import "github.com/tarantool/go-tarantool/v2" +import ( + "github.com/tarantool/go-tarantool/v3" +) // Schema represents the schema-related operations in Tarantool. // It holds a connection to interact with the Tarantool instance. diff --git a/box/schema_user.go b/box/schema_user.go index 80874da92..6e9f558a5 100644 --- a/box/schema_user.go +++ b/box/schema_user.go @@ -5,8 +5,9 @@ import ( "fmt" "strings" - "github.com/tarantool/go-tarantool/v2" "github.com/vmihailenco/msgpack/v5" + + "github.com/tarantool/go-tarantool/v3" ) // SchemaUser provides methods to interact with schema-related user operations in Tarantool. diff --git a/box/schema_user_test.go b/box/schema_user_test.go index b9f3e18de..985e9fe86 100644 --- a/box/schema_user_test.go +++ b/box/schema_user_test.go @@ -7,11 +7,11 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "github.com/tarantool/go-tarantool/v2" "github.com/vmihailenco/msgpack/v5" "github.com/vmihailenco/msgpack/v5/msgpcode" - "github.com/tarantool/go-tarantool/v2/box" + "github.com/tarantool/go-tarantool/v3" + "github.com/tarantool/go-tarantool/v3/box" ) func TestUserExistsResponse_DecodeMsgpack(t *testing.T) { diff --git a/box/session.go b/box/session.go index b63113581..5a01b32a3 100644 --- a/box/session.go +++ b/box/session.go @@ -3,7 +3,7 @@ package box import ( "context" - "github.com/tarantool/go-tarantool/v2" + "github.com/tarantool/go-tarantool/v3" ) // Session struct represents a connection session to Tarantool. diff --git a/box/session_test.go b/box/session_test.go index 07ada0436..39b80d1d4 100644 --- a/box/session_test.go +++ b/box/session_test.go @@ -5,8 +5,8 @@ import ( "github.com/stretchr/testify/require" - "github.com/tarantool/go-tarantool/v2/box" - th "github.com/tarantool/go-tarantool/v2/test_helpers" + "github.com/tarantool/go-tarantool/v3/box" + th "github.com/tarantool/go-tarantool/v3/test_helpers" ) func TestBox_Session(t *testing.T) { diff --git a/box/tarantool_test.go b/box/tarantool_test.go index 0eb9e94b9..4244a98e9 100644 --- a/box/tarantool_test.go +++ b/box/tarantool_test.go @@ -11,9 +11,10 @@ import ( "github.com/google/uuid" "github.com/stretchr/testify/require" "github.com/tarantool/go-iproto" - "github.com/tarantool/go-tarantool/v2" - "github.com/tarantool/go-tarantool/v2/box" - "github.com/tarantool/go-tarantool/v2/test_helpers" + + "github.com/tarantool/go-tarantool/v3" + "github.com/tarantool/go-tarantool/v3/box" + "github.com/tarantool/go-tarantool/v3/test_helpers" ) var server = "127.0.0.1:3013" diff --git a/box_error.go b/boxerror.go similarity index 99% rename from box_error.go rename to boxerror.go index 59bfb4a05..2fb18268f 100644 --- a/box_error.go +++ b/boxerror.go @@ -9,6 +9,8 @@ import ( const errorExtID = 3 +//go:generate go tool gentypes -ext-code 3 BoxError + const ( keyErrorStack = 0x00 keyErrorType = 0x00 diff --git a/boxerror_gen.go b/boxerror_gen.go new file mode 100644 index 000000000..07a1be695 --- /dev/null +++ b/boxerror_gen.go @@ -0,0 +1,241 @@ +// Code generated by github.com/tarantool/go-option; DO NOT EDIT. + +package tarantool + +import ( + "fmt" + + "github.com/vmihailenco/msgpack/v5" + "github.com/vmihailenco/msgpack/v5/msgpcode" + + "github.com/tarantool/go-option" +) + +// OptionalBoxError represents an optional value of type BoxError. +// It can either hold a valid BoxError (IsSome == true) or be empty (IsZero == true). +type OptionalBoxError struct { + value BoxError + exists bool +} + +// SomeOptionalBoxError creates an optional OptionalBoxError with the given BoxError value. +// The returned OptionalBoxError will have IsSome() == true and IsZero() == false. +func SomeOptionalBoxError(value BoxError) OptionalBoxError { + return OptionalBoxError{ + value: value, + exists: true, + } +} + +// NoneOptionalBoxError creates an empty optional OptionalBoxError value. +// The returned OptionalBoxError will have IsSome() == false and IsZero() == true. +// +// Example: +// +// o := NoneOptionalBoxError() +// if o.IsZero() { +// fmt.Println("value is absent") +// } +func NoneOptionalBoxError() OptionalBoxError { + return OptionalBoxError{} +} + +func (o OptionalBoxError) newEncodeError(err error) error { + if err == nil { + return nil + } + return &option.EncodeError{ + Type: "OptionalBoxError", + Parent: err, + } +} + +func (o OptionalBoxError) newDecodeError(err error) error { + if err == nil { + return nil + } + + return &option.DecodeError{ + Type: "OptionalBoxError", + Parent: err, + } +} + +// IsSome returns true if the OptionalBoxError contains a value. +// This indicates the value is explicitly set (not None). +func (o OptionalBoxError) IsSome() bool { + return o.exists +} + +// IsZero returns true if the OptionalBoxError does not contain a value. +// Equivalent to !IsSome(). Useful for consistency with types where +// zero value (e.g. 0, false, zero struct) is valid and needs to be distinguished. +func (o OptionalBoxError) IsZero() bool { + return !o.exists +} + +// IsNil is an alias for IsZero. +// +// This method is provided for compatibility with the msgpack Encoder interface. +func (o OptionalBoxError) IsNil() bool { + return o.IsZero() +} + +// Get returns the stored value and a boolean flag indicating its presence. +// If the value is present, returns (value, true). +// If the value is absent, returns (zero value of BoxError, false). +// +// Recommended usage: +// +// if value, ok := o.Get(); ok { +// // use value +// } +func (o OptionalBoxError) Get() (BoxError, bool) { + return o.value, o.exists +} + +// MustGet returns the stored value if it is present. +// Panics if the value is absent (i.e., IsZero() == true). +// +// Use with caution — only when you are certain the value exists. +// +// Panics with: "optional value is not set" if no value is set. +func (o OptionalBoxError) MustGet() BoxError { + if !o.exists { + panic("optional value is not set") + } + + return o.value +} + +// Unwrap returns the stored value regardless of presence. +// If no value is set, returns the zero value for BoxError. +// +// Warning: Does not check presence. Use IsSome() before calling if you need +// to distinguish between absent value and explicit zero value. +func (o OptionalBoxError) Unwrap() BoxError { + return o.value +} + +// UnwrapOr returns the stored value if present. +// Otherwise, returns the provided default value. +// +// Example: +// +// o := NoneOptionalBoxError() +// v := o.UnwrapOr(someDefaultOptionalBoxError) +func (o OptionalBoxError) UnwrapOr(defaultValue BoxError) BoxError { + if o.exists { + return o.value + } + + return defaultValue +} + +// UnwrapOrElse returns the stored value if present. +// Otherwise, calls the provided function and returns its result. +// Useful when the default value requires computation or side effects. +// +// Example: +// +// o := NoneOptionalBoxError() +// v := o.UnwrapOrElse(func() BoxError { return computeDefault() }) +func (o OptionalBoxError) UnwrapOrElse(defaultValue func() BoxError) BoxError { + if o.exists { + return o.value + } + + return defaultValue() +} + +func (o OptionalBoxError) encodeValue(encoder *msgpack.Encoder) error { + value, err := o.value.MarshalMsgpack() + if err != nil { + return err + } + + err = encoder.EncodeExtHeader(3, len(value)) + if err != nil { + return err + } + + _, err = encoder.Writer().Write(value) + if err != nil { + return err + } + + return nil +} + +// EncodeMsgpack encodes the OptionalBoxError value using MessagePack format. +// - If the value is present, it is encoded as BoxError. +// - If the value is absent (None), it is encoded as nil. +// +// Returns an error if encoding fails. +func (o OptionalBoxError) EncodeMsgpack(encoder *msgpack.Encoder) error { + if o.exists { + return o.newEncodeError(o.encodeValue(encoder)) + } + + return o.newEncodeError(encoder.EncodeNil()) +} + +func (o *OptionalBoxError) decodeValue(decoder *msgpack.Decoder) error { + tp, length, err := decoder.DecodeExtHeader() + switch { + case err != nil: + return o.newDecodeError(err) + case tp != 3: + return o.newDecodeError(fmt.Errorf("invalid extension code: %d", tp)) + } + + a := make([]byte, length) + if err := decoder.ReadFull(a); err != nil { + return o.newDecodeError(err) + } + + if err := o.value.UnmarshalMsgpack(a); err != nil { + return o.newDecodeError(err) + } + + o.exists = true + return nil +} + +func (o *OptionalBoxError) checkCode(code byte) bool { + return msgpcode.IsExt(code) +} + +// DecodeMsgpack decodes a OptionalBoxError value from MessagePack format. +// Supports two input types: +// - nil: interpreted as no value (NoneOptionalBoxError) +// - BoxError: interpreted as a present value (SomeOptionalBoxError) +// +// Returns an error if the input type is unsupported or decoding fails. +// +// After successful decoding: +// - on nil: exists = false, value = default zero value +// - on BoxError: exists = true, value = decoded value +func (o *OptionalBoxError) DecodeMsgpack(decoder *msgpack.Decoder) error { + code, err := decoder.PeekCode() + if err != nil { + return o.newDecodeError(err) + } + + switch { + case code == msgpcode.Nil: + o.exists = false + + return o.newDecodeError(decoder.Skip()) + case o.checkCode(code): + err := o.decodeValue(decoder) + if err != nil { + return o.newDecodeError(err) + } + o.exists = true + + return err + default: + return o.newDecodeError(fmt.Errorf("unexpected code: %d", code)) + } +} diff --git a/boxerror_gen_test.go b/boxerror_gen_test.go new file mode 100644 index 000000000..4d7fada3a --- /dev/null +++ b/boxerror_gen_test.go @@ -0,0 +1,116 @@ +package tarantool + +import ( + "bytes" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/vmihailenco/msgpack/v5" +) + +func TestSomeOptionalBoxError(t *testing.T) { + val := BoxError{Code: 1, Msg: "error"} + opt := SomeOptionalBoxError(val) + + assert.True(t, opt.IsSome()) + assert.False(t, opt.IsZero()) + + v, ok := opt.Get() + assert.True(t, ok) + assert.Equal(t, val, v) +} + +func TestNoneOptionalBoxError(t *testing.T) { + opt := NoneOptionalBoxError() + + assert.False(t, opt.IsSome()) + assert.True(t, opt.IsZero()) + + _, ok := opt.Get() + assert.False(t, ok) +} + +func TestOptionalBoxError_MustGet(t *testing.T) { + val := BoxError{Code: 1, Msg: "error"} + optSome := SomeOptionalBoxError(val) + optNone := NoneOptionalBoxError() + + assert.Equal(t, val, optSome.MustGet()) + assert.Panics(t, func() { optNone.MustGet() }) +} + +func TestOptionalBoxError_Unwrap(t *testing.T) { + val := BoxError{Code: 1, Msg: "error"} + optSome := SomeOptionalBoxError(val) + optNone := NoneOptionalBoxError() + + assert.Equal(t, val, optSome.Unwrap()) + assert.Equal(t, BoxError{}, optNone.Unwrap()) +} + +func TestOptionalBoxError_UnwrapOr(t *testing.T) { + val := BoxError{Code: 1, Msg: "error"} + def := BoxError{Code: 2, Msg: "default"} + optSome := SomeOptionalBoxError(val) + optNone := NoneOptionalBoxError() + + assert.Equal(t, val, optSome.UnwrapOr(def)) + assert.Equal(t, def, optNone.UnwrapOr(def)) +} + +func TestOptionalBoxError_UnwrapOrElse(t *testing.T) { + val := BoxError{Code: 1, Msg: "error"} + def := BoxError{Code: 2, Msg: "default"} + optSome := SomeOptionalBoxError(val) + optNone := NoneOptionalBoxError() + + assert.Equal(t, val, optSome.UnwrapOrElse(func() BoxError { return def })) + assert.Equal(t, def, optNone.UnwrapOrElse(func() BoxError { return def })) +} + +func TestOptionalBoxError_EncodeDecodeMsgpack_Some(t *testing.T) { + val := BoxError{Code: 1, Msg: "error"} + some := SomeOptionalBoxError(val) + + var buf bytes.Buffer + enc := msgpack.NewEncoder(&buf) + dec := msgpack.NewDecoder(&buf) + + err := enc.Encode(some) + assert.NoError(t, err) + + var decodedSome OptionalBoxError + err = dec.Decode(&decodedSome) + assert.NoError(t, err) + assert.True(t, decodedSome.IsSome()) + assert.Equal(t, val, decodedSome.Unwrap()) +} + +func TestOptionalBoxError_EncodeDecodeMsgpack_None(t *testing.T) { + none := NoneOptionalBoxError() + + var buf bytes.Buffer + enc := msgpack.NewEncoder(&buf) + dec := msgpack.NewDecoder(&buf) + + err := enc.Encode(none) + assert.NoError(t, err) + + var decodedNone OptionalBoxError + err = dec.Decode(&decodedNone) + assert.NoError(t, err) + assert.True(t, decodedNone.IsZero()) +} + +func TestOptionalBoxError_EncodeDecodeMsgpack_InvalidType(t *testing.T) { + var buf bytes.Buffer + enc := msgpack.NewEncoder(&buf) + dec := msgpack.NewDecoder(&buf) + + err := enc.Encode(123) + assert.NoError(t, err) + + var decodedInvalid OptionalBoxError + err = dec.Decode(&decodedInvalid) + assert.Error(t, err) +} diff --git a/box_error_test.go b/boxerror_test.go similarity index 99% rename from box_error_test.go rename to boxerror_test.go index 3d7f7345d..acb051d31 100644 --- a/box_error_test.go +++ b/boxerror_test.go @@ -8,8 +8,8 @@ import ( "github.com/stretchr/testify/require" "github.com/vmihailenco/msgpack/v5" - . "github.com/tarantool/go-tarantool/v2" - "github.com/tarantool/go-tarantool/v2/test_helpers" + . "github.com/tarantool/go-tarantool/v3" + "github.com/tarantool/go-tarantool/v3/test_helpers" ) var samples = map[string]BoxError{ diff --git a/client_tools_test.go b/client_tools_test.go index fdd109152..fc911acf9 100644 --- a/client_tools_test.go +++ b/client_tools_test.go @@ -6,7 +6,7 @@ import ( "github.com/vmihailenco/msgpack/v5" - "github.com/tarantool/go-tarantool/v2" + "github.com/tarantool/go-tarantool/v3" ) func TestOperations_EncodeMsgpack(t *testing.T) { diff --git a/connection.go b/connection.go index 5f976fbf8..81172f79c 100644 --- a/connection.go +++ b/connection.go @@ -984,7 +984,8 @@ func (conn *Connection) newFuture(req Request) (fut *Future) { if ctx != nil { select { case <-ctx.Done(): - fut.SetError(fmt.Errorf("context is done (request ID %d)", fut.requestId)) + fut.SetError(fmt.Errorf("context is done (request ID %d) (%w)", + fut.requestId, ErrCancelledCtx)) shard.rmut.Unlock() return default: diff --git a/crud/common.go b/crud/common.go index 061818487..df3c4a795 100644 --- a/crud/common.go +++ b/crud/common.go @@ -59,7 +59,7 @@ import ( "github.com/tarantool/go-iproto" - "github.com/tarantool/go-tarantool/v2" + "github.com/tarantool/go-tarantool/v3" ) type baseRequest struct { diff --git a/crud/count.go b/crud/count.go index 9deb1e44c..b90198658 100644 --- a/crud/count.go +++ b/crud/count.go @@ -5,7 +5,7 @@ import ( "github.com/vmihailenco/msgpack/v5" - "github.com/tarantool/go-tarantool/v2" + "github.com/tarantool/go-tarantool/v3" ) // CountResult describes result for `crud.count` method. @@ -73,7 +73,7 @@ type CountRequest struct { } type countArgs struct { - _msgpack struct{} `msgpack:",asArray"` //nolint: structcheck,unused + _msgpack struct{} `msgpack:",asArray"` // nolint: structcheck,unused Space string Conditions []Condition Opts CountOpts diff --git a/crud/delete.go b/crud/delete.go index f37da7ac5..075b25b3c 100644 --- a/crud/delete.go +++ b/crud/delete.go @@ -5,7 +5,7 @@ import ( "github.com/vmihailenco/msgpack/v5" - "github.com/tarantool/go-tarantool/v2" + "github.com/tarantool/go-tarantool/v3" ) // DeleteOpts describes options for `crud.delete` method. @@ -20,7 +20,7 @@ type DeleteRequest struct { } type deleteArgs struct { - _msgpack struct{} `msgpack:",asArray"` //nolint: structcheck,unused + _msgpack struct{} `msgpack:",asArray"` // nolint: structcheck,unused Space string Key Tuple Opts DeleteOpts diff --git a/crud/error_test.go b/crud/error_test.go index a3db035bf..71fda30d4 100644 --- a/crud/error_test.go +++ b/crud/error_test.go @@ -4,7 +4,8 @@ import ( "testing" "github.com/stretchr/testify/require" - "github.com/tarantool/go-tarantool/v2/crud" + + "github.com/tarantool/go-tarantool/v3/crud" ) func TestErrorMany(t *testing.T) { diff --git a/crud/example_test.go b/crud/example_test.go index 7f9e34e0c..1b97308ae 100644 --- a/crud/example_test.go +++ b/crud/example_test.go @@ -6,8 +6,8 @@ import ( "reflect" "time" - "github.com/tarantool/go-tarantool/v2" - "github.com/tarantool/go-tarantool/v2/crud" + "github.com/tarantool/go-tarantool/v3" + "github.com/tarantool/go-tarantool/v3/crud" ) const ( @@ -65,7 +65,7 @@ func ExampleResult_rowsCustomType() { Tuple([]interface{}{uint(2010), nil, "bla"}) type Tuple struct { - _msgpack struct{} `msgpack:",asArray"` //nolint: structcheck,unused + _msgpack struct{} `msgpack:",asArray"` // nolint: structcheck,unused Id uint64 BucketId uint64 Name string @@ -92,7 +92,7 @@ func ExampleTuples_customType() { // The type will be encoded/decoded as an array. type Tuple struct { - _msgpack struct{} `msgpack:",asArray"` //nolint: structcheck,unused + _msgpack struct{} `msgpack:",asArray"` // nolint: structcheck,unused Id uint64 BucketId *uint64 Name string diff --git a/crud/get.go b/crud/get.go index 6ab91440e..5a31473ef 100644 --- a/crud/get.go +++ b/crud/get.go @@ -5,7 +5,7 @@ import ( "github.com/vmihailenco/msgpack/v5" - "github.com/tarantool/go-tarantool/v2" + "github.com/tarantool/go-tarantool/v3" ) // GetOpts describes options for `crud.get` method. @@ -66,7 +66,7 @@ type GetRequest struct { } type getArgs struct { - _msgpack struct{} `msgpack:",asArray"` //nolint: structcheck,unused + _msgpack struct{} `msgpack:",asArray"` // nolint: structcheck,unused Space string Key Tuple Opts GetOpts diff --git a/crud/insert.go b/crud/insert.go index c696d201f..4e56c6d91 100644 --- a/crud/insert.go +++ b/crud/insert.go @@ -5,7 +5,7 @@ import ( "github.com/vmihailenco/msgpack/v5" - "github.com/tarantool/go-tarantool/v2" + "github.com/tarantool/go-tarantool/v3" ) // InsertOpts describes options for `crud.insert` method. @@ -20,7 +20,7 @@ type InsertRequest struct { } type insertArgs struct { - _msgpack struct{} `msgpack:",asArray"` //nolint: structcheck,unused + _msgpack struct{} `msgpack:",asArray"` // nolint: structcheck,unused Space string Tuple Tuple Opts InsertOpts @@ -78,7 +78,7 @@ type InsertObjectRequest struct { } type insertObjectArgs struct { - _msgpack struct{} `msgpack:",asArray"` //nolint: structcheck,unused + _msgpack struct{} `msgpack:",asArray"` // nolint: structcheck,unused Space string Object Object Opts InsertObjectOpts diff --git a/crud/insert_many.go b/crud/insert_many.go index 866c1ceb5..17748564d 100644 --- a/crud/insert_many.go +++ b/crud/insert_many.go @@ -5,7 +5,7 @@ import ( "github.com/vmihailenco/msgpack/v5" - "github.com/tarantool/go-tarantool/v2" + "github.com/tarantool/go-tarantool/v3" ) // InsertManyOpts describes options for `crud.insert_many` method. @@ -20,7 +20,7 @@ type InsertManyRequest struct { } type insertManyArgs struct { - _msgpack struct{} `msgpack:",asArray"` //nolint: structcheck,unused + _msgpack struct{} `msgpack:",asArray"` // nolint: structcheck,unused Space string Tuples Tuples Opts InsertManyOpts @@ -78,7 +78,7 @@ type InsertObjectManyRequest struct { } type insertObjectManyArgs struct { - _msgpack struct{} `msgpack:",asArray"` //nolint: structcheck,unused + _msgpack struct{} `msgpack:",asArray"` // nolint: structcheck,unused Space string Objects Objects Opts InsertObjectManyOpts diff --git a/crud/len.go b/crud/len.go index 5fef700d7..a1da72f72 100644 --- a/crud/len.go +++ b/crud/len.go @@ -5,7 +5,7 @@ import ( "github.com/vmihailenco/msgpack/v5" - "github.com/tarantool/go-tarantool/v2" + "github.com/tarantool/go-tarantool/v3" ) // LenResult describes result for `crud.len` method. @@ -22,7 +22,7 @@ type LenRequest struct { } type lenArgs struct { - _msgpack struct{} `msgpack:",asArray"` //nolint: structcheck,unused + _msgpack struct{} `msgpack:",asArray"` // nolint: structcheck,unused Space string Opts LenOpts } diff --git a/crud/max.go b/crud/max.go index 727a17ac5..961e7724b 100644 --- a/crud/max.go +++ b/crud/max.go @@ -5,7 +5,7 @@ import ( "github.com/vmihailenco/msgpack/v5" - "github.com/tarantool/go-tarantool/v2" + "github.com/tarantool/go-tarantool/v3" ) // MaxOpts describes options for `crud.max` method. @@ -20,7 +20,7 @@ type MaxRequest struct { } type maxArgs struct { - _msgpack struct{} `msgpack:",asArray"` //nolint: structcheck,unused + _msgpack struct{} `msgpack:",asArray"` // nolint: structcheck,unused Space string Index interface{} Opts MaxOpts diff --git a/crud/min.go b/crud/min.go index ab3bcfe07..2bbf9b816 100644 --- a/crud/min.go +++ b/crud/min.go @@ -5,7 +5,7 @@ import ( "github.com/vmihailenco/msgpack/v5" - "github.com/tarantool/go-tarantool/v2" + "github.com/tarantool/go-tarantool/v3" ) // MinOpts describes options for `crud.min` method. @@ -20,7 +20,7 @@ type MinRequest struct { } type minArgs struct { - _msgpack struct{} `msgpack:",asArray"` //nolint: structcheck,unused + _msgpack struct{} `msgpack:",asArray"` // nolint: structcheck,unused Space string Index interface{} Opts MinOpts diff --git a/crud/operations_test.go b/crud/operations_test.go index 0ff3e818a..a7f61a8a7 100644 --- a/crud/operations_test.go +++ b/crud/operations_test.go @@ -6,7 +6,7 @@ import ( "github.com/vmihailenco/msgpack/v5" - "github.com/tarantool/go-tarantool/v2/crud" + "github.com/tarantool/go-tarantool/v3/crud" ) func TestOperation_EncodeMsgpack(t *testing.T) { diff --git a/crud/replace.go b/crud/replace.go index 8231c9aa5..b47bba9ab 100644 --- a/crud/replace.go +++ b/crud/replace.go @@ -5,7 +5,7 @@ import ( "github.com/vmihailenco/msgpack/v5" - "github.com/tarantool/go-tarantool/v2" + "github.com/tarantool/go-tarantool/v3" ) // ReplaceOpts describes options for `crud.replace` method. @@ -20,7 +20,7 @@ type ReplaceRequest struct { } type replaceArgs struct { - _msgpack struct{} `msgpack:",asArray"` //nolint: structcheck,unused + _msgpack struct{} `msgpack:",asArray"` // nolint: structcheck,unused Space string Tuple Tuple Opts ReplaceOpts @@ -78,7 +78,7 @@ type ReplaceObjectRequest struct { } type replaceObjectArgs struct { - _msgpack struct{} `msgpack:",asArray"` //nolint: structcheck,unused + _msgpack struct{} `msgpack:",asArray"` // nolint: structcheck,unused Space string Object Object Opts ReplaceObjectOpts diff --git a/crud/replace_many.go b/crud/replace_many.go index 5a5143ef8..024b863b7 100644 --- a/crud/replace_many.go +++ b/crud/replace_many.go @@ -5,7 +5,7 @@ import ( "github.com/vmihailenco/msgpack/v5" - "github.com/tarantool/go-tarantool/v2" + "github.com/tarantool/go-tarantool/v3" ) // ReplaceManyOpts describes options for `crud.replace_many` method. @@ -20,7 +20,7 @@ type ReplaceManyRequest struct { } type replaceManyArgs struct { - _msgpack struct{} `msgpack:",asArray"` //nolint: structcheck,unused + _msgpack struct{} `msgpack:",asArray"` // nolint: structcheck,unused Space string Tuples Tuples Opts ReplaceManyOpts @@ -78,7 +78,7 @@ type ReplaceObjectManyRequest struct { } type replaceObjectManyArgs struct { - _msgpack struct{} `msgpack:",asArray"` //nolint: structcheck,unused + _msgpack struct{} `msgpack:",asArray"` // nolint: structcheck,unused Space string Objects Objects Opts ReplaceObjectManyOpts diff --git a/crud/request_test.go b/crud/request_test.go index 7c889cf40..ba2bae859 100644 --- a/crud/request_test.go +++ b/crud/request_test.go @@ -9,8 +9,8 @@ import ( "github.com/tarantool/go-iproto" "github.com/vmihailenco/msgpack/v5" - "github.com/tarantool/go-tarantool/v2" - "github.com/tarantool/go-tarantool/v2/crud" + "github.com/tarantool/go-tarantool/v3" + "github.com/tarantool/go-tarantool/v3/crud" ) const validSpace = "test" // Any valid value != default. diff --git a/crud/result_test.go b/crud/result_test.go index c67649f96..578eebed1 100644 --- a/crud/result_test.go +++ b/crud/result_test.go @@ -6,8 +6,9 @@ import ( "testing" "github.com/stretchr/testify/require" - "github.com/tarantool/go-tarantool/v2/crud" "github.com/vmihailenco/msgpack/v5" + + "github.com/tarantool/go-tarantool/v3/crud" ) func TestResult_DecodeMsgpack(t *testing.T) { diff --git a/crud/schema.go b/crud/schema.go index 6f3b94a97..4c2d661ec 100644 --- a/crud/schema.go +++ b/crud/schema.go @@ -7,7 +7,7 @@ import ( "github.com/vmihailenco/msgpack/v5" "github.com/vmihailenco/msgpack/v5/msgpcode" - "github.com/tarantool/go-tarantool/v2" + "github.com/tarantool/go-tarantool/v3" ) func msgpackIsMap(code byte) bool { diff --git a/crud/select.go b/crud/select.go index 24dbd0cb0..b52eb7003 100644 --- a/crud/select.go +++ b/crud/select.go @@ -5,7 +5,7 @@ import ( "github.com/vmihailenco/msgpack/v5" - "github.com/tarantool/go-tarantool/v2" + "github.com/tarantool/go-tarantool/v3" ) // SelectOpts describes options for `crud.select` method. @@ -90,7 +90,7 @@ type SelectRequest struct { } type selectArgs struct { - _msgpack struct{} `msgpack:",asArray"` //nolint: structcheck,unused + _msgpack struct{} `msgpack:",asArray"` // nolint: structcheck,unused Space string Conditions []Condition Opts SelectOpts diff --git a/crud/stats.go b/crud/stats.go index 47737f33a..c4f6988a0 100644 --- a/crud/stats.go +++ b/crud/stats.go @@ -5,7 +5,7 @@ import ( "github.com/vmihailenco/msgpack/v5" - "github.com/tarantool/go-tarantool/v2" + "github.com/tarantool/go-tarantool/v3" ) // StatsRequest helps you to create request object to call `crud.stats` diff --git a/crud/storage_info.go b/crud/storage_info.go index e2d67aadb..b39bf37a5 100644 --- a/crud/storage_info.go +++ b/crud/storage_info.go @@ -5,7 +5,7 @@ import ( "github.com/vmihailenco/msgpack/v5" - "github.com/tarantool/go-tarantool/v2" + "github.com/tarantool/go-tarantool/v3" ) // StatusTable describes information for instance. @@ -98,7 +98,7 @@ type StorageInfoRequest struct { } type storageInfoArgs struct { - _msgpack struct{} `msgpack:",asArray"` //nolint: structcheck,unused + _msgpack struct{} `msgpack:",asArray"` // nolint: structcheck,unused Opts StorageInfoOpts } diff --git a/crud/tarantool_test.go b/crud/tarantool_test.go index 8ee28cf09..0e1c1791a 100644 --- a/crud/tarantool_test.go +++ b/crud/tarantool_test.go @@ -10,9 +10,9 @@ import ( "github.com/stretchr/testify/require" "github.com/tarantool/go-iproto" - "github.com/tarantool/go-tarantool/v2" - "github.com/tarantool/go-tarantool/v2/crud" - "github.com/tarantool/go-tarantool/v2/test_helpers" + "github.com/tarantool/go-tarantool/v3" + "github.com/tarantool/go-tarantool/v3/crud" + "github.com/tarantool/go-tarantool/v3/test_helpers" ) var server = "127.0.0.1:3013" @@ -182,7 +182,7 @@ func connect(t testing.TB) *tarantool.Connection { } ret := struct { - _msgpack struct{} `msgpack:",asArray"` //nolint: structcheck,unused + _msgpack struct{} `msgpack:",asArray"` // nolint: structcheck,unused Result bool }{} err = conn.Do(tarantool.NewCall17Request("is_ready")).GetTyped(&ret) diff --git a/crud/truncate.go b/crud/truncate.go index 9f80063d1..8313785d9 100644 --- a/crud/truncate.go +++ b/crud/truncate.go @@ -5,7 +5,7 @@ import ( "github.com/vmihailenco/msgpack/v5" - "github.com/tarantool/go-tarantool/v2" + "github.com/tarantool/go-tarantool/v3" ) // TruncateResult describes result for `crud.truncate` method. @@ -22,7 +22,7 @@ type TruncateRequest struct { } type truncateArgs struct { - _msgpack struct{} `msgpack:",asArray"` //nolint: structcheck,unused + _msgpack struct{} `msgpack:",asArray"` // nolint: structcheck,unused Space string Opts TruncateOpts } diff --git a/crud/update.go b/crud/update.go index 41ebd2c09..4bdeb01ce 100644 --- a/crud/update.go +++ b/crud/update.go @@ -5,7 +5,7 @@ import ( "github.com/vmihailenco/msgpack/v5" - "github.com/tarantool/go-tarantool/v2" + "github.com/tarantool/go-tarantool/v3" ) // UpdateOpts describes options for `crud.update` method. @@ -21,7 +21,7 @@ type UpdateRequest struct { } type updateArgs struct { - _msgpack struct{} `msgpack:",asArray"` //nolint: structcheck,unused + _msgpack struct{} `msgpack:",asArray"` // nolint: structcheck,unused Space string Key Tuple Operations []Operation diff --git a/crud/upsert.go b/crud/upsert.go index e44523d45..d55d1da1b 100644 --- a/crud/upsert.go +++ b/crud/upsert.go @@ -5,7 +5,7 @@ import ( "github.com/vmihailenco/msgpack/v5" - "github.com/tarantool/go-tarantool/v2" + "github.com/tarantool/go-tarantool/v3" ) // UpsertOpts describes options for `crud.upsert` method. @@ -21,7 +21,7 @@ type UpsertRequest struct { } type upsertArgs struct { - _msgpack struct{} `msgpack:",asArray"` //nolint: structcheck,unused + _msgpack struct{} `msgpack:",asArray"` // nolint: structcheck,unused Space string Tuple Tuple Operations []Operation @@ -90,7 +90,7 @@ type UpsertObjectRequest struct { } type upsertObjectArgs struct { - _msgpack struct{} `msgpack:",asArray"` //nolint: structcheck,unused + _msgpack struct{} `msgpack:",asArray"` // nolint: structcheck,unused Space string Object Object Operations []Operation diff --git a/crud/upsert_many.go b/crud/upsert_many.go index b0ccedf09..dad7dd158 100644 --- a/crud/upsert_many.go +++ b/crud/upsert_many.go @@ -5,7 +5,7 @@ import ( "github.com/vmihailenco/msgpack/v5" - "github.com/tarantool/go-tarantool/v2" + "github.com/tarantool/go-tarantool/v3" ) // UpsertManyOpts describes options for `crud.upsert_many` method. @@ -13,7 +13,7 @@ type UpsertManyOpts = OperationManyOpts // TupleOperationsData contains tuple with operations to be applied to tuple. type TupleOperationsData struct { - _msgpack struct{} `msgpack:",asArray"` //nolint: structcheck,unused + _msgpack struct{} `msgpack:",asArray"` // nolint: structcheck,unused Tuple Tuple Operations []Operation } @@ -27,7 +27,7 @@ type UpsertManyRequest struct { } type upsertManyArgs struct { - _msgpack struct{} `msgpack:",asArray"` //nolint: structcheck,unused + _msgpack struct{} `msgpack:",asArray"` // nolint: structcheck,unused Space string TuplesOperationsData []TupleOperationsData Opts UpsertManyOpts @@ -79,7 +79,7 @@ type UpsertObjectManyOpts = OperationManyOpts // ObjectOperationsData contains object with operations to be applied to object. type ObjectOperationsData struct { - _msgpack struct{} `msgpack:",asArray"` //nolint: structcheck,unused + _msgpack struct{} `msgpack:",asArray"` // nolint: structcheck,unused Object Object Operations []Operation } @@ -93,7 +93,7 @@ type UpsertObjectManyRequest struct { } type upsertObjectManyArgs struct { - _msgpack struct{} `msgpack:",asArray"` //nolint: structcheck,unused + _msgpack struct{} `msgpack:",asArray"` // nolint: structcheck,unused Space string ObjectsOperationsData []ObjectOperationsData Opts UpsertObjectManyOpts diff --git a/datetime/datetime.go b/datetime/datetime.go index f5a2a8278..23901305b 100644 --- a/datetime/datetime.go +++ b/datetime/datetime.go @@ -76,6 +76,7 @@ const ( const maxSize = secondsSize + nsecSize + tzIndexSize + tzOffsetSize +//go:generate go tool gentypes -ext-code 4 Datetime type Datetime struct { time time.Time } @@ -183,6 +184,89 @@ func addMonth(ival Interval, delta int64, adjust Adjust) Interval { return ival } +// MarshalMsgpack implements a custom msgpack marshaler. +func (d Datetime) MarshalMsgpack() ([]byte, error) { + tm := d.ToTime() + + var dt datetime + dt.seconds = tm.Unix() + dt.nsec = int32(tm.Nanosecond()) + + zone := tm.Location().String() + _, offset := tm.Zone() + if zone != NoTimezone { + // The zone value already checked in MakeDatetime() or + // UnmarshalMsgpack() calls. + dt.tzIndex = int16(timezoneToIndex[zone]) + } + dt.tzOffset = int16(offset / 60) + + var bytesSize = secondsSize + if dt.nsec != 0 || dt.tzOffset != 0 || dt.tzIndex != 0 { + bytesSize += nsecSize + tzIndexSize + tzOffsetSize + } + + buf := make([]byte, bytesSize) + binary.LittleEndian.PutUint64(buf, uint64(dt.seconds)) + if bytesSize == maxSize { + binary.LittleEndian.PutUint32(buf[secondsSize:], uint32(dt.nsec)) + binary.LittleEndian.PutUint16(buf[secondsSize+nsecSize:], uint16(dt.tzOffset)) + binary.LittleEndian.PutUint16(buf[secondsSize+nsecSize+tzOffsetSize:], uint16(dt.tzIndex)) + } + + return buf, nil +} + +// UnmarshalMsgpack implements a custom msgpack unmarshaler. +func (d *Datetime) UnmarshalMsgpack(data []byte) error { + var dt datetime + + sec := binary.LittleEndian.Uint64(data) + dt.seconds = int64(sec) + dt.nsec = 0 + if len(data) == maxSize { + dt.nsec = int32(binary.LittleEndian.Uint32(data[secondsSize:])) + dt.tzOffset = int16(binary.LittleEndian.Uint16(data[secondsSize+nsecSize:])) + dt.tzIndex = int16(binary.LittleEndian.Uint16(data[secondsSize+nsecSize+tzOffsetSize:])) + } + + tt := time.Unix(dt.seconds, int64(dt.nsec)) + + loc := noTimezoneLoc + if dt.tzIndex != 0 || dt.tzOffset != 0 { + zone := NoTimezone + offset := int(dt.tzOffset) * 60 + + if dt.tzIndex != 0 { + if _, ok := indexToTimezone[int(dt.tzIndex)]; !ok { + return fmt.Errorf("unknown timezone index %d", dt.tzIndex) + } + zone = indexToTimezone[int(dt.tzIndex)] + } + if zone != NoTimezone { + if loadLoc, err := time.LoadLocation(zone); err == nil { + loc = loadLoc + } else { + // Unable to load location. + loc = time.FixedZone(zone, offset) + } + } else { + // Only offset. + loc = time.FixedZone(zone, offset) + } + } + tt = tt.In(loc) + + newDatetime, err := MakeDatetime(tt) + if err != nil { + return err + } + + *d = newDatetime + + return nil +} + func (d Datetime) add(ival Interval, positive bool) (Datetime, error) { newVal := intervalFromDatetime(d) @@ -244,35 +328,8 @@ func (d *Datetime) ToTime() time.Time { func datetimeEncoder(e *msgpack.Encoder, v reflect.Value) ([]byte, error) { dtime := v.Interface().(Datetime) - tm := dtime.ToTime() - - var dt datetime - dt.seconds = tm.Unix() - dt.nsec = int32(tm.Nanosecond()) - - zone := tm.Location().String() - _, offset := tm.Zone() - if zone != NoTimezone { - // The zone value already checked in MakeDatetime() or - // UnmarshalMsgpack() calls. - dt.tzIndex = int16(timezoneToIndex[zone]) - } - dt.tzOffset = int16(offset / 60) - var bytesSize = secondsSize - if dt.nsec != 0 || dt.tzOffset != 0 || dt.tzIndex != 0 { - bytesSize += nsecSize + tzIndexSize + tzOffsetSize - } - - buf := make([]byte, bytesSize) - binary.LittleEndian.PutUint64(buf, uint64(dt.seconds)) - if bytesSize == maxSize { - binary.LittleEndian.PutUint32(buf[secondsSize:], uint32(dt.nsec)) - binary.LittleEndian.PutUint16(buf[secondsSize+nsecSize:], uint16(dt.tzOffset)) - binary.LittleEndian.PutUint16(buf[secondsSize+nsecSize+tzOffsetSize:], uint16(dt.tzIndex)) - } - - return buf, nil + return dtime.MarshalMsgpack() } func datetimeDecoder(d *msgpack.Decoder, v reflect.Value, extLen int) error { @@ -282,54 +339,15 @@ func datetimeDecoder(d *msgpack.Decoder, v reflect.Value, extLen int) error { } b := make([]byte, extLen) - n, err := d.Buffered().Read(b) - if err != nil { + switch n, err := d.Buffered().Read(b); { + case err != nil: return err - } - if n < extLen { + case 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 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:])) - } - - tt := time.Unix(dt.seconds, int64(dt.nsec)) - - loc := noTimezoneLoc - if dt.tzIndex != 0 || dt.tzOffset != 0 { - zone := NoTimezone - offset := int(dt.tzOffset) * 60 - - if dt.tzIndex != 0 { - if _, ok := indexToTimezone[int(dt.tzIndex)]; !ok { - return fmt.Errorf("unknown timezone index %d", dt.tzIndex) - } - zone = indexToTimezone[int(dt.tzIndex)] - } - if zone != NoTimezone { - if loadLoc, err := time.LoadLocation(zone); err == nil { - loc = loadLoc - } else { - // Unable to load location. - loc = time.FixedZone(zone, offset) - } - } else { - // Only offset. - loc = time.FixedZone(zone, offset) - } - } - tt = tt.In(loc) - ptr := v.Addr().Interface().(*Datetime) - *ptr, err = MakeDatetime(tt) - return err + return ptr.UnmarshalMsgpack(b) } func init() { diff --git a/datetime/datetime_gen.go b/datetime/datetime_gen.go new file mode 100644 index 000000000..753d9c371 --- /dev/null +++ b/datetime/datetime_gen.go @@ -0,0 +1,241 @@ +// Code generated by github.com/tarantool/go-option; DO NOT EDIT. + +package datetime + +import ( + "fmt" + + "github.com/vmihailenco/msgpack/v5" + "github.com/vmihailenco/msgpack/v5/msgpcode" + + "github.com/tarantool/go-option" +) + +// OptionalDatetime represents an optional value of type Datetime. +// It can either hold a valid Datetime (IsSome == true) or be empty (IsZero == true). +type OptionalDatetime struct { + value Datetime + exists bool +} + +// SomeOptionalDatetime creates an optional OptionalDatetime with the given Datetime value. +// The returned OptionalDatetime will have IsSome() == true and IsZero() == false. +func SomeOptionalDatetime(value Datetime) OptionalDatetime { + return OptionalDatetime{ + value: value, + exists: true, + } +} + +// NoneOptionalDatetime creates an empty optional OptionalDatetime value. +// The returned OptionalDatetime will have IsSome() == false and IsZero() == true. +// +// Example: +// +// o := NoneOptionalDatetime() +// if o.IsZero() { +// fmt.Println("value is absent") +// } +func NoneOptionalDatetime() OptionalDatetime { + return OptionalDatetime{} +} + +func (o OptionalDatetime) newEncodeError(err error) error { + if err == nil { + return nil + } + return &option.EncodeError{ + Type: "OptionalDatetime", + Parent: err, + } +} + +func (o OptionalDatetime) newDecodeError(err error) error { + if err == nil { + return nil + } + + return &option.DecodeError{ + Type: "OptionalDatetime", + Parent: err, + } +} + +// IsSome returns true if the OptionalDatetime contains a value. +// This indicates the value is explicitly set (not None). +func (o OptionalDatetime) IsSome() bool { + return o.exists +} + +// IsZero returns true if the OptionalDatetime does not contain a value. +// Equivalent to !IsSome(). Useful for consistency with types where +// zero value (e.g. 0, false, zero struct) is valid and needs to be distinguished. +func (o OptionalDatetime) IsZero() bool { + return !o.exists +} + +// IsNil is an alias for IsZero. +// +// This method is provided for compatibility with the msgpack Encoder interface. +func (o OptionalDatetime) IsNil() bool { + return o.IsZero() +} + +// Get returns the stored value and a boolean flag indicating its presence. +// If the value is present, returns (value, true). +// If the value is absent, returns (zero value of Datetime, false). +// +// Recommended usage: +// +// if value, ok := o.Get(); ok { +// // use value +// } +func (o OptionalDatetime) Get() (Datetime, bool) { + return o.value, o.exists +} + +// MustGet returns the stored value if it is present. +// Panics if the value is absent (i.e., IsZero() == true). +// +// Use with caution — only when you are certain the value exists. +// +// Panics with: "optional value is not set" if no value is set. +func (o OptionalDatetime) MustGet() Datetime { + if !o.exists { + panic("optional value is not set") + } + + return o.value +} + +// Unwrap returns the stored value regardless of presence. +// If no value is set, returns the zero value for Datetime. +// +// Warning: Does not check presence. Use IsSome() before calling if you need +// to distinguish between absent value and explicit zero value. +func (o OptionalDatetime) Unwrap() Datetime { + return o.value +} + +// UnwrapOr returns the stored value if present. +// Otherwise, returns the provided default value. +// +// Example: +// +// o := NoneOptionalDatetime() +// v := o.UnwrapOr(someDefaultOptionalDatetime) +func (o OptionalDatetime) UnwrapOr(defaultValue Datetime) Datetime { + if o.exists { + return o.value + } + + return defaultValue +} + +// UnwrapOrElse returns the stored value if present. +// Otherwise, calls the provided function and returns its result. +// Useful when the default value requires computation or side effects. +// +// Example: +// +// o := NoneOptionalDatetime() +// v := o.UnwrapOrElse(func() Datetime { return computeDefault() }) +func (o OptionalDatetime) UnwrapOrElse(defaultValue func() Datetime) Datetime { + if o.exists { + return o.value + } + + return defaultValue() +} + +func (o OptionalDatetime) encodeValue(encoder *msgpack.Encoder) error { + value, err := o.value.MarshalMsgpack() + if err != nil { + return err + } + + err = encoder.EncodeExtHeader(4, len(value)) + if err != nil { + return err + } + + _, err = encoder.Writer().Write(value) + if err != nil { + return err + } + + return nil +} + +// EncodeMsgpack encodes the OptionalDatetime value using MessagePack format. +// - If the value is present, it is encoded as Datetime. +// - If the value is absent (None), it is encoded as nil. +// +// Returns an error if encoding fails. +func (o OptionalDatetime) EncodeMsgpack(encoder *msgpack.Encoder) error { + if o.exists { + return o.newEncodeError(o.encodeValue(encoder)) + } + + return o.newEncodeError(encoder.EncodeNil()) +} + +func (o *OptionalDatetime) decodeValue(decoder *msgpack.Decoder) error { + tp, length, err := decoder.DecodeExtHeader() + switch { + case err != nil: + return o.newDecodeError(err) + case tp != 4: + return o.newDecodeError(fmt.Errorf("invalid extension code: %d", tp)) + } + + a := make([]byte, length) + if err := decoder.ReadFull(a); err != nil { + return o.newDecodeError(err) + } + + if err := o.value.UnmarshalMsgpack(a); err != nil { + return o.newDecodeError(err) + } + + o.exists = true + return nil +} + +func (o *OptionalDatetime) checkCode(code byte) bool { + return msgpcode.IsExt(code) +} + +// DecodeMsgpack decodes a OptionalDatetime value from MessagePack format. +// Supports two input types: +// - nil: interpreted as no value (NoneOptionalDatetime) +// - Datetime: interpreted as a present value (SomeOptionalDatetime) +// +// Returns an error if the input type is unsupported or decoding fails. +// +// After successful decoding: +// - on nil: exists = false, value = default zero value +// - on Datetime: exists = true, value = decoded value +func (o *OptionalDatetime) DecodeMsgpack(decoder *msgpack.Decoder) error { + code, err := decoder.PeekCode() + if err != nil { + return o.newDecodeError(err) + } + + switch { + case code == msgpcode.Nil: + o.exists = false + + return o.newDecodeError(decoder.Skip()) + case o.checkCode(code): + err := o.decodeValue(decoder) + if err != nil { + return o.newDecodeError(err) + } + o.exists = true + + return err + default: + return o.newDecodeError(fmt.Errorf("unexpected code: %d", code)) + } +} diff --git a/datetime/datetime_gen_test.go b/datetime/datetime_gen_test.go new file mode 100644 index 000000000..6b45b6fb1 --- /dev/null +++ b/datetime/datetime_gen_test.go @@ -0,0 +1,125 @@ +package datetime + +import ( + "bytes" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/vmihailenco/msgpack/v5" +) + +func TestSomeOptionalDatetime(t *testing.T) { + val, err := MakeDatetime(time.Now().In(time.UTC)) + assert.NoError(t, err) + opt := SomeOptionalDatetime(val) + + assert.True(t, opt.IsSome()) + assert.False(t, opt.IsZero()) + + v, ok := opt.Get() + assert.True(t, ok) + assert.Equal(t, val, v) +} + +func TestNoneOptionalDatetime(t *testing.T) { + opt := NoneOptionalDatetime() + + assert.False(t, opt.IsSome()) + assert.True(t, opt.IsZero()) + + _, ok := opt.Get() + assert.False(t, ok) +} + +func TestOptionalDatetime_MustGet(t *testing.T) { + val, err := MakeDatetime(time.Now().In(time.UTC)) + assert.NoError(t, err) + optSome := SomeOptionalDatetime(val) + optNone := NoneOptionalDatetime() + + assert.Equal(t, val, optSome.MustGet()) + assert.Panics(t, func() { optNone.MustGet() }) +} + +func TestOptionalDatetime_Unwrap(t *testing.T) { + val, err := MakeDatetime(time.Now().In(time.UTC)) + assert.NoError(t, err) + optSome := SomeOptionalDatetime(val) + optNone := NoneOptionalDatetime() + + assert.Equal(t, val, optSome.Unwrap()) + assert.Equal(t, Datetime{}, optNone.Unwrap()) +} + +func TestOptionalDatetime_UnwrapOr(t *testing.T) { + val, err := MakeDatetime(time.Now().In(time.UTC)) + assert.NoError(t, err) + def, err := MakeDatetime(time.Now().Add(1 * time.Hour).In(time.UTC)) + assert.NoError(t, err) + optSome := SomeOptionalDatetime(val) + optNone := NoneOptionalDatetime() + + assert.Equal(t, val, optSome.UnwrapOr(def)) + assert.Equal(t, def, optNone.UnwrapOr(def)) +} + +func TestOptionalDatetime_UnwrapOrElse(t *testing.T) { + val, err := MakeDatetime(time.Now().In(time.UTC)) + assert.NoError(t, err) + def, err := MakeDatetime(time.Now().Add(1 * time.Hour).In(time.UTC)) + assert.NoError(t, err) + optSome := SomeOptionalDatetime(val) + optNone := NoneOptionalDatetime() + + assert.Equal(t, val, optSome.UnwrapOrElse(func() Datetime { return def })) + assert.Equal(t, def, optNone.UnwrapOrElse(func() Datetime { return def })) +} + +func TestOptionalDatetime_EncodeDecodeMsgpack_Some(t *testing.T) { + val, err := MakeDatetime(time.Now().In(time.UTC)) + assert.NoError(t, err) + some := SomeOptionalDatetime(val) + + var buf bytes.Buffer + enc := msgpack.NewEncoder(&buf) + dec := msgpack.NewDecoder(&buf) + + err = enc.Encode(some) + assert.NoError(t, err) + + var decodedSome OptionalDatetime + err = dec.Decode(&decodedSome) + assert.NoError(t, err) + assert.True(t, decodedSome.IsSome()) + assert.Equal(t, val, decodedSome.Unwrap()) +} + +func TestOptionalDatetime_EncodeDecodeMsgpack_None(t *testing.T) { + none := NoneOptionalDatetime() + + var buf bytes.Buffer + enc := msgpack.NewEncoder(&buf) + dec := msgpack.NewDecoder(&buf) + + err := enc.Encode(none) + assert.NoError(t, err) + + var decodedNone OptionalDatetime + err = dec.Decode(&decodedNone) + assert.NoError(t, err) + assert.True(t, decodedNone.IsZero()) +} + +func TestOptionalDatetime_EncodeDecodeMsgpack_InvalidType(t *testing.T) { + var buf bytes.Buffer + enc := msgpack.NewEncoder(&buf) + dec := msgpack.NewDecoder(&buf) + + err := enc.Encode(123) + assert.NoError(t, err) + + var decodedInvalid OptionalDatetime + err = dec.Decode(&decodedInvalid) + assert.Error(t, err) +} diff --git a/datetime/datetime_test.go b/datetime/datetime_test.go index 8be50e0e7..d01153892 100644 --- a/datetime/datetime_test.go +++ b/datetime/datetime_test.go @@ -11,9 +11,9 @@ import ( "github.com/vmihailenco/msgpack/v5" - . "github.com/tarantool/go-tarantool/v2" - . "github.com/tarantool/go-tarantool/v2/datetime" - "github.com/tarantool/go-tarantool/v2/test_helpers" + . "github.com/tarantool/go-tarantool/v3" + . "github.com/tarantool/go-tarantool/v3/datetime" + "github.com/tarantool/go-tarantool/v3/test_helpers" ) var noTimezoneLoc = time.FixedZone(NoTimezone, 0) diff --git a/datetime/example_test.go b/datetime/example_test.go index ac5f40500..df5d55563 100644 --- a/datetime/example_test.go +++ b/datetime/example_test.go @@ -13,8 +13,8 @@ import ( "fmt" "time" - "github.com/tarantool/go-tarantool/v2" - . "github.com/tarantool/go-tarantool/v2/datetime" + "github.com/tarantool/go-tarantool/v3" + . "github.com/tarantool/go-tarantool/v3/datetime" ) // Example demonstrates how to use tuples with datetime. To enable support of diff --git a/datetime/interval.go b/datetime/interval.go index bcd052383..e6d39e4d8 100644 --- a/datetime/interval.go +++ b/datetime/interval.go @@ -2,7 +2,6 @@ package datetime import ( "bytes" - "fmt" "reflect" "github.com/vmihailenco/msgpack/v5" @@ -23,6 +22,8 @@ const ( ) // Interval type is GoLang implementation of Tarantool intervals. +// +//go:generate go tool gentypes -ext-code 6 Interval type Interval struct { Year int64 Month int64 @@ -35,6 +36,21 @@ type Interval struct { Adjust Adjust } +func (ival Interval) countNonZeroFields() int { + count := 0 + + for _, field := range []int64{ + ival.Year, ival.Month, ival.Week, ival.Day, ival.Hour, + ival.Min, ival.Sec, ival.Nsec, adjustToDt[ival.Adjust], + } { + if field != 0 { + count++ + } + } + + return count +} + // We use int64 for every field to avoid changes in the future, see: // https://github.com/tarantool/tarantool/blob/943ce3caf8401510ced4f074bca7006c3d73f9b3/src/lib/core/datetime.h#L106 @@ -66,115 +82,134 @@ func (ival Interval) Sub(sub Interval) Interval { return ival } -func encodeIntervalValue(e *msgpack.Encoder, typ uint64, value int64) (err error) { - if value == 0 { - return - } - err = e.EncodeUint(typ) - if err == nil { - if value > 0 { - err = e.EncodeUint(uint64(value)) - } else if value < 0 { - err = e.EncodeInt(value) - } +// MarshalMsgpack implements a custom msgpack marshaler. +func (ival Interval) MarshalMsgpack() ([]byte, error) { + var buf bytes.Buffer + enc := msgpack.NewEncoder(&buf) + + if err := ival.MarshalMsgpackTo(enc); err != nil { + return nil, err } - return -} -func encodeInterval(e *msgpack.Encoder, v reflect.Value) (err error) { - val := v.Interface().(Interval) + return buf.Bytes(), nil +} - var fieldNum uint64 - for _, val := range []int64{val.Year, val.Month, val.Week, val.Day, - val.Hour, val.Min, val.Sec, val.Nsec, - adjustToDt[val.Adjust]} { - if val != 0 { - fieldNum++ - } - } - if err = e.EncodeUint(fieldNum); err != nil { - return +// MarshalMsgpackTo implements a custom msgpack marshaler. +func (ival Interval) MarshalMsgpackTo(e *msgpack.Encoder) error { + var fieldNum = uint64(ival.countNonZeroFields()) + if err := e.EncodeUint(fieldNum); err != nil { + return err } - if err = encodeIntervalValue(e, fieldYear, val.Year); err != nil { - return + if err := encodeIntervalValue(e, fieldYear, ival.Year); err != nil { + return err } - if err = encodeIntervalValue(e, fieldMonth, val.Month); err != nil { - return + if err := encodeIntervalValue(e, fieldMonth, ival.Month); err != nil { + return err } - if err = encodeIntervalValue(e, fieldWeek, val.Week); err != nil { - return + if err := encodeIntervalValue(e, fieldWeek, ival.Week); err != nil { + return err } - if err = encodeIntervalValue(e, fieldDay, val.Day); err != nil { - return + if err := encodeIntervalValue(e, fieldDay, ival.Day); err != nil { + return err } - if err = encodeIntervalValue(e, fieldHour, val.Hour); err != nil { - return + if err := encodeIntervalValue(e, fieldHour, ival.Hour); err != nil { + return err } - if err = encodeIntervalValue(e, fieldMin, val.Min); err != nil { - return + if err := encodeIntervalValue(e, fieldMin, ival.Min); err != nil { + return err } - if err = encodeIntervalValue(e, fieldSec, val.Sec); err != nil { - return + if err := encodeIntervalValue(e, fieldSec, ival.Sec); err != nil { + return err } - if err = encodeIntervalValue(e, fieldNSec, val.Nsec); err != nil { - return + if err := encodeIntervalValue(e, fieldNSec, ival.Nsec); err != nil { + return err } - if err = encodeIntervalValue(e, fieldAdjust, adjustToDt[val.Adjust]); err != nil { - return + if err := encodeIntervalValue(e, fieldAdjust, adjustToDt[ival.Adjust]); err != nil { + return err } + return nil } -func decodeInterval(d *msgpack.Decoder, v reflect.Value) (err error) { - var fieldNum uint - if fieldNum, err = d.DecodeUint(); err != nil { - return +// UnmarshalMsgpackFrom implements a custom msgpack unmarshaler. +func (ival *Interval) UnmarshalMsgpackFrom(d *msgpack.Decoder) error { + fieldNum, err := d.DecodeUint() + if err != nil { + return err } - var val Interval + ival.Adjust = dtToAdjust[int64(NoneAdjust)] - hasAdjust := false for i := 0; i < int(fieldNum); i++ { var fieldType uint if fieldType, err = d.DecodeUint(); err != nil { - return + return err } + var fieldVal int64 if fieldVal, err = d.DecodeInt64(); err != nil { - return + return err } + switch fieldType { case fieldYear: - val.Year = fieldVal + ival.Year = fieldVal case fieldMonth: - val.Month = fieldVal + ival.Month = fieldVal case fieldWeek: - val.Week = fieldVal + ival.Week = fieldVal case fieldDay: - val.Day = fieldVal + ival.Day = fieldVal case fieldHour: - val.Hour = fieldVal + ival.Hour = fieldVal case fieldMin: - val.Min = fieldVal + ival.Min = fieldVal case fieldSec: - val.Sec = fieldVal + ival.Sec = fieldVal case fieldNSec: - val.Nsec = fieldVal + ival.Nsec = fieldVal case fieldAdjust: - hasAdjust = true - if adjust, ok := dtToAdjust[fieldVal]; ok { - val.Adjust = adjust - } else { - return fmt.Errorf("unsupported Adjust: %d", fieldVal) - } - default: - return fmt.Errorf("unsupported interval field type: %d", fieldType) + ival.Adjust = dtToAdjust[fieldVal] } } - if !hasAdjust { - val.Adjust = dtToAdjust[0] + return nil +} + +// UnmarshalMsgpack implements a custom msgpack unmarshaler. +func (ival *Interval) UnmarshalMsgpack(data []byte) error { + dec := msgpack.NewDecoder(bytes.NewReader(data)) + return ival.UnmarshalMsgpackFrom(dec) +} + +func encodeIntervalValue(e *msgpack.Encoder, typ uint64, value int64) error { + if value == 0 { + return nil + } + + err := e.EncodeUint(typ) + if err != nil { + return err + } + + switch { + case value > 0: + return e.EncodeUint(uint64(value)) + default: + return e.EncodeInt(value) + } +} + +func encodeInterval(e *msgpack.Encoder, v reflect.Value) (err error) { + val := v.Interface().(Interval) + return val.MarshalMsgpackTo(e) +} + +func decodeInterval(d *msgpack.Decoder, v reflect.Value) (err error) { + val := Interval{} + if err = val.UnmarshalMsgpackFrom(d); err != nil { + return } v.Set(reflect.ValueOf(val)) diff --git a/datetime/interval_gen.go b/datetime/interval_gen.go new file mode 100644 index 000000000..2cccaaca0 --- /dev/null +++ b/datetime/interval_gen.go @@ -0,0 +1,241 @@ +// Code generated by github.com/tarantool/go-option; DO NOT EDIT. + +package datetime + +import ( + "fmt" + + "github.com/vmihailenco/msgpack/v5" + "github.com/vmihailenco/msgpack/v5/msgpcode" + + "github.com/tarantool/go-option" +) + +// OptionalInterval represents an optional value of type Interval. +// It can either hold a valid Interval (IsSome == true) or be empty (IsZero == true). +type OptionalInterval struct { + value Interval + exists bool +} + +// SomeOptionalInterval creates an optional OptionalInterval with the given Interval value. +// The returned OptionalInterval will have IsSome() == true and IsZero() == false. +func SomeOptionalInterval(value Interval) OptionalInterval { + return OptionalInterval{ + value: value, + exists: true, + } +} + +// NoneOptionalInterval creates an empty optional OptionalInterval value. +// The returned OptionalInterval will have IsSome() == false and IsZero() == true. +// +// Example: +// +// o := NoneOptionalInterval() +// if o.IsZero() { +// fmt.Println("value is absent") +// } +func NoneOptionalInterval() OptionalInterval { + return OptionalInterval{} +} + +func (o OptionalInterval) newEncodeError(err error) error { + if err == nil { + return nil + } + return &option.EncodeError{ + Type: "OptionalInterval", + Parent: err, + } +} + +func (o OptionalInterval) newDecodeError(err error) error { + if err == nil { + return nil + } + + return &option.DecodeError{ + Type: "OptionalInterval", + Parent: err, + } +} + +// IsSome returns true if the OptionalInterval contains a value. +// This indicates the value is explicitly set (not None). +func (o OptionalInterval) IsSome() bool { + return o.exists +} + +// IsZero returns true if the OptionalInterval does not contain a value. +// Equivalent to !IsSome(). Useful for consistency with types where +// zero value (e.g. 0, false, zero struct) is valid and needs to be distinguished. +func (o OptionalInterval) IsZero() bool { + return !o.exists +} + +// IsNil is an alias for IsZero. +// +// This method is provided for compatibility with the msgpack Encoder interface. +func (o OptionalInterval) IsNil() bool { + return o.IsZero() +} + +// Get returns the stored value and a boolean flag indicating its presence. +// If the value is present, returns (value, true). +// If the value is absent, returns (zero value of Interval, false). +// +// Recommended usage: +// +// if value, ok := o.Get(); ok { +// // use value +// } +func (o OptionalInterval) Get() (Interval, bool) { + return o.value, o.exists +} + +// MustGet returns the stored value if it is present. +// Panics if the value is absent (i.e., IsZero() == true). +// +// Use with caution — only when you are certain the value exists. +// +// Panics with: "optional value is not set" if no value is set. +func (o OptionalInterval) MustGet() Interval { + if !o.exists { + panic("optional value is not set") + } + + return o.value +} + +// Unwrap returns the stored value regardless of presence. +// If no value is set, returns the zero value for Interval. +// +// Warning: Does not check presence. Use IsSome() before calling if you need +// to distinguish between absent value and explicit zero value. +func (o OptionalInterval) Unwrap() Interval { + return o.value +} + +// UnwrapOr returns the stored value if present. +// Otherwise, returns the provided default value. +// +// Example: +// +// o := NoneOptionalInterval() +// v := o.UnwrapOr(someDefaultOptionalInterval) +func (o OptionalInterval) UnwrapOr(defaultValue Interval) Interval { + if o.exists { + return o.value + } + + return defaultValue +} + +// UnwrapOrElse returns the stored value if present. +// Otherwise, calls the provided function and returns its result. +// Useful when the default value requires computation or side effects. +// +// Example: +// +// o := NoneOptionalInterval() +// v := o.UnwrapOrElse(func() Interval { return computeDefault() }) +func (o OptionalInterval) UnwrapOrElse(defaultValue func() Interval) Interval { + if o.exists { + return o.value + } + + return defaultValue() +} + +func (o OptionalInterval) encodeValue(encoder *msgpack.Encoder) error { + value, err := o.value.MarshalMsgpack() + if err != nil { + return err + } + + err = encoder.EncodeExtHeader(6, len(value)) + if err != nil { + return err + } + + _, err = encoder.Writer().Write(value) + if err != nil { + return err + } + + return nil +} + +// EncodeMsgpack encodes the OptionalInterval value using MessagePack format. +// - If the value is present, it is encoded as Interval. +// - If the value is absent (None), it is encoded as nil. +// +// Returns an error if encoding fails. +func (o OptionalInterval) EncodeMsgpack(encoder *msgpack.Encoder) error { + if o.exists { + return o.newEncodeError(o.encodeValue(encoder)) + } + + return o.newEncodeError(encoder.EncodeNil()) +} + +func (o *OptionalInterval) decodeValue(decoder *msgpack.Decoder) error { + tp, length, err := decoder.DecodeExtHeader() + switch { + case err != nil: + return o.newDecodeError(err) + case tp != 6: + return o.newDecodeError(fmt.Errorf("invalid extension code: %d", tp)) + } + + a := make([]byte, length) + if err := decoder.ReadFull(a); err != nil { + return o.newDecodeError(err) + } + + if err := o.value.UnmarshalMsgpack(a); err != nil { + return o.newDecodeError(err) + } + + o.exists = true + return nil +} + +func (o *OptionalInterval) checkCode(code byte) bool { + return msgpcode.IsExt(code) +} + +// DecodeMsgpack decodes a OptionalInterval value from MessagePack format. +// Supports two input types: +// - nil: interpreted as no value (NoneOptionalInterval) +// - Interval: interpreted as a present value (SomeOptionalInterval) +// +// Returns an error if the input type is unsupported or decoding fails. +// +// After successful decoding: +// - on nil: exists = false, value = default zero value +// - on Interval: exists = true, value = decoded value +func (o *OptionalInterval) DecodeMsgpack(decoder *msgpack.Decoder) error { + code, err := decoder.PeekCode() + if err != nil { + return o.newDecodeError(err) + } + + switch { + case code == msgpcode.Nil: + o.exists = false + + return o.newDecodeError(decoder.Skip()) + case o.checkCode(code): + err := o.decodeValue(decoder) + if err != nil { + return o.newDecodeError(err) + } + o.exists = true + + return err + default: + return o.newDecodeError(fmt.Errorf("unexpected code: %d", code)) + } +} diff --git a/datetime/interval_gen_test.go b/datetime/interval_gen_test.go new file mode 100644 index 000000000..162db3336 --- /dev/null +++ b/datetime/interval_gen_test.go @@ -0,0 +1,116 @@ +package datetime + +import ( + "bytes" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/vmihailenco/msgpack/v5" +) + +func TestSomeOptionalInterval(t *testing.T) { + val := Interval{Year: 1} + opt := SomeOptionalInterval(val) + + assert.True(t, opt.IsSome()) + assert.False(t, opt.IsZero()) + + v, ok := opt.Get() + assert.True(t, ok) + assert.Equal(t, val, v) +} + +func TestNoneOptionalInterval(t *testing.T) { + opt := NoneOptionalInterval() + + assert.False(t, opt.IsSome()) + assert.True(t, opt.IsZero()) + + _, ok := opt.Get() + assert.False(t, ok) +} + +func TestOptionalInterval_MustGet(t *testing.T) { + val := Interval{Year: 1} + optSome := SomeOptionalInterval(val) + optNone := NoneOptionalInterval() + + assert.Equal(t, val, optSome.MustGet()) + assert.Panics(t, func() { optNone.MustGet() }) +} + +func TestOptionalInterval_Unwrap(t *testing.T) { + val := Interval{Year: 1} + optSome := SomeOptionalInterval(val) + optNone := NoneOptionalInterval() + + assert.Equal(t, val, optSome.Unwrap()) + assert.Equal(t, Interval{}, optNone.Unwrap()) +} + +func TestOptionalInterval_UnwrapOr(t *testing.T) { + val := Interval{Year: 1} + def := Interval{Year: 2} + optSome := SomeOptionalInterval(val) + optNone := NoneOptionalInterval() + + assert.Equal(t, val, optSome.UnwrapOr(def)) + assert.Equal(t, def, optNone.UnwrapOr(def)) +} + +func TestOptionalInterval_UnwrapOrElse(t *testing.T) { + val := Interval{Year: 1} + def := Interval{Year: 2} + optSome := SomeOptionalInterval(val) + optNone := NoneOptionalInterval() + + assert.Equal(t, val, optSome.UnwrapOrElse(func() Interval { return def })) + assert.Equal(t, def, optNone.UnwrapOrElse(func() Interval { return def })) +} + +func TestOptionalInterval_EncodeDecodeMsgpack_Some(t *testing.T) { + val := Interval{Year: 1} + some := SomeOptionalInterval(val) + + var buf bytes.Buffer + enc := msgpack.NewEncoder(&buf) + dec := msgpack.NewDecoder(&buf) + + err := enc.Encode(some) + assert.NoError(t, err) + + var decodedSome OptionalInterval + err = dec.Decode(&decodedSome) + assert.NoError(t, err) + assert.True(t, decodedSome.IsSome()) + assert.Equal(t, val, decodedSome.Unwrap()) +} + +func TestOptionalInterval_EncodeDecodeMsgpack_None(t *testing.T) { + none := NoneOptionalInterval() + + var buf bytes.Buffer + enc := msgpack.NewEncoder(&buf) + dec := msgpack.NewDecoder(&buf) + + err := enc.Encode(none) + assert.NoError(t, err) + + var decodedNone OptionalInterval + err = dec.Decode(&decodedNone) + assert.NoError(t, err) + assert.True(t, decodedNone.IsZero()) +} + +func TestOptionalInterval_EncodeDecodeMsgpack_InvalidType(t *testing.T) { + var buf bytes.Buffer + enc := msgpack.NewEncoder(&buf) + dec := msgpack.NewDecoder(&buf) + + err := enc.Encode(123) + assert.NoError(t, err) + + var decodedInvalid OptionalInterval + err = dec.Decode(&decodedInvalid) + assert.Error(t, err) +} diff --git a/datetime/interval_test.go b/datetime/interval_test.go index 95142fe47..2f4bb8a66 100644 --- a/datetime/interval_test.go +++ b/datetime/interval_test.go @@ -5,9 +5,9 @@ import ( "reflect" "testing" - "github.com/tarantool/go-tarantool/v2" - . "github.com/tarantool/go-tarantool/v2/datetime" - "github.com/tarantool/go-tarantool/v2/test_helpers" + "github.com/tarantool/go-tarantool/v3" + . "github.com/tarantool/go-tarantool/v3/datetime" + "github.com/tarantool/go-tarantool/v3/test_helpers" ) func TestIntervalAdd(t *testing.T) { diff --git a/decimal/decimal.go b/decimal/decimal.go index 3a1abb76e..3c2681238 100644 --- a/decimal/decimal.go +++ b/decimal/decimal.go @@ -44,13 +44,14 @@ const ( ) var ( - one decimal.Decimal = decimal.NewFromInt(1) + one = decimal.NewFromInt(1) // -10^decimalPrecision - 1 - minSupportedDecimal decimal.Decimal = maxSupportedDecimal.Neg().Sub(one) + minSupportedDecimal = maxSupportedDecimal.Neg().Sub(one) // 10^decimalPrecision - 1 - maxSupportedDecimal decimal.Decimal = decimal.New(1, decimalPrecision).Sub(one) + maxSupportedDecimal = decimal.New(1, decimalPrecision).Sub(one) ) +//go:generate go tool gentypes -ext-code 1 Decimal type Decimal struct { decimal.Decimal } @@ -71,37 +72,20 @@ func MakeDecimalFromString(src string) (Decimal, error) { return result, nil } -func decimalEncoder(e *msgpack.Encoder, v reflect.Value) ([]byte, error) { - dec := v.Interface().(Decimal) - if dec.GreaterThan(maxSupportedDecimal) { - return nil, - fmt.Errorf( - "msgpack: decimal number is bigger than maximum supported number (10^%d - 1)", - decimalPrecision) - } - if dec.LessThan(minSupportedDecimal) { - return nil, - fmt.Errorf( - "msgpack: decimal number is lesser than minimum supported number (-10^%d - 1)", - decimalPrecision) - } - - strBuf := dec.String() - bcdBuf, err := encodeStringToBCD(strBuf) - if err != nil { - return nil, fmt.Errorf("msgpack: can't encode string (%s) to a BCD buffer: %w", strBuf, err) - } - return bcdBuf, nil -} +var ( + ErrDecimalOverflow = fmt.Errorf("msgpack: decimal number is bigger than"+ + " maximum supported number (10^%d - 1)", decimalPrecision) + ErrDecimalUnderflow = fmt.Errorf("msgpack: decimal number is lesser than"+ + " minimum supported number (-10^%d - 1)", decimalPrecision) +) -func decimalDecoder(d *msgpack.Decoder, v reflect.Value, extLen int) error { - 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 decimal bytes", n) +// MarshalMsgpack implements a custom msgpack marshaler. +func (d Decimal) MarshalMsgpack() ([]byte, error) { + switch { + case d.GreaterThan(maxSupportedDecimal): + return nil, ErrDecimalOverflow + case d.LessThan(minSupportedDecimal): + return nil, ErrDecimalUnderflow } // Decimal values can be encoded to fixext MessagePack, where buffer @@ -112,9 +96,19 @@ func decimalDecoder(d *msgpack.Decoder, v reflect.Value, extLen int) error { // +--------+-------------------+------------+===============+ // | MP_EXT | length (optional) | MP_DECIMAL | PackedDecimal | // +--------+-------------------+------------+===============+ - digits, exp, err := decodeStringFromBCD(b) + strBuf := d.String() + bcdBuf, err := encodeStringToBCD(strBuf) if err != nil { - return fmt.Errorf("msgpack: can't decode string from BCD buffer (%x): %w", b, err) + return nil, fmt.Errorf("msgpack: can't encode string (%s) to a BCD buffer: %w", strBuf, err) + } + return bcdBuf, nil +} + +// UnmarshalMsgpack implements a custom msgpack unmarshaler. +func (d *Decimal) UnmarshalMsgpack(data []byte) error { + digits, exp, err := decodeStringFromBCD(data) + if err != nil { + return fmt.Errorf("msgpack: can't decode string from BCD buffer (%x): %w", data, err) } dec, err := decimal.NewFromString(digits) @@ -125,11 +119,31 @@ func decimalDecoder(d *msgpack.Decoder, v reflect.Value, extLen int) error { if exp != 0 { dec = dec.Shift(int32(exp)) } - ptr := v.Addr().Interface().(*Decimal) - *ptr = MakeDecimal(dec) + + *d = MakeDecimal(dec) return nil } +func decimalEncoder(e *msgpack.Encoder, v reflect.Value) ([]byte, error) { + dec := v.Interface().(Decimal) + + return dec.MarshalMsgpack() +} + +func decimalDecoder(d *msgpack.Decoder, v reflect.Value, extLen int) error { + b := make([]byte, extLen) + + switch n, err := d.Buffered().Read(b); { + case err != nil: + return err + case n < extLen: + return fmt.Errorf("msgpack: unexpected end of stream after %d decimal bytes", n) + } + + ptr := v.Addr().Interface().(*Decimal) + return ptr.UnmarshalMsgpack(b) +} + func init() { msgpack.RegisterExtDecoder(decimalExtID, Decimal{}, decimalDecoder) msgpack.RegisterExtEncoder(decimalExtID, Decimal{}, decimalEncoder) diff --git a/decimal/decimal_gen.go b/decimal/decimal_gen.go new file mode 100644 index 000000000..0f9b18e3a --- /dev/null +++ b/decimal/decimal_gen.go @@ -0,0 +1,241 @@ +// Code generated by github.com/tarantool/go-option; DO NOT EDIT. + +package decimal + +import ( + "fmt" + + "github.com/vmihailenco/msgpack/v5" + "github.com/vmihailenco/msgpack/v5/msgpcode" + + "github.com/tarantool/go-option" +) + +// OptionalDecimal represents an optional value of type Decimal. +// It can either hold a valid Decimal (IsSome == true) or be empty (IsZero == true). +type OptionalDecimal struct { + value Decimal + exists bool +} + +// SomeOptionalDecimal creates an optional OptionalDecimal with the given Decimal value. +// The returned OptionalDecimal will have IsSome() == true and IsZero() == false. +func SomeOptionalDecimal(value Decimal) OptionalDecimal { + return OptionalDecimal{ + value: value, + exists: true, + } +} + +// NoneOptionalDecimal creates an empty optional OptionalDecimal value. +// The returned OptionalDecimal will have IsSome() == false and IsZero() == true. +// +// Example: +// +// o := NoneOptionalDecimal() +// if o.IsZero() { +// fmt.Println("value is absent") +// } +func NoneOptionalDecimal() OptionalDecimal { + return OptionalDecimal{} +} + +func (o OptionalDecimal) newEncodeError(err error) error { + if err == nil { + return nil + } + return &option.EncodeError{ + Type: "OptionalDecimal", + Parent: err, + } +} + +func (o OptionalDecimal) newDecodeError(err error) error { + if err == nil { + return nil + } + + return &option.DecodeError{ + Type: "OptionalDecimal", + Parent: err, + } +} + +// IsSome returns true if the OptionalDecimal contains a value. +// This indicates the value is explicitly set (not None). +func (o OptionalDecimal) IsSome() bool { + return o.exists +} + +// IsZero returns true if the OptionalDecimal does not contain a value. +// Equivalent to !IsSome(). Useful for consistency with types where +// zero value (e.g. 0, false, zero struct) is valid and needs to be distinguished. +func (o OptionalDecimal) IsZero() bool { + return !o.exists +} + +// IsNil is an alias for IsZero. +// +// This method is provided for compatibility with the msgpack Encoder interface. +func (o OptionalDecimal) IsNil() bool { + return o.IsZero() +} + +// Get returns the stored value and a boolean flag indicating its presence. +// If the value is present, returns (value, true). +// If the value is absent, returns (zero value of Decimal, false). +// +// Recommended usage: +// +// if value, ok := o.Get(); ok { +// // use value +// } +func (o OptionalDecimal) Get() (Decimal, bool) { + return o.value, o.exists +} + +// MustGet returns the stored value if it is present. +// Panics if the value is absent (i.e., IsZero() == true). +// +// Use with caution — only when you are certain the value exists. +// +// Panics with: "optional value is not set" if no value is set. +func (o OptionalDecimal) MustGet() Decimal { + if !o.exists { + panic("optional value is not set") + } + + return o.value +} + +// Unwrap returns the stored value regardless of presence. +// If no value is set, returns the zero value for Decimal. +// +// Warning: Does not check presence. Use IsSome() before calling if you need +// to distinguish between absent value and explicit zero value. +func (o OptionalDecimal) Unwrap() Decimal { + return o.value +} + +// UnwrapOr returns the stored value if present. +// Otherwise, returns the provided default value. +// +// Example: +// +// o := NoneOptionalDecimal() +// v := o.UnwrapOr(someDefaultOptionalDecimal) +func (o OptionalDecimal) UnwrapOr(defaultValue Decimal) Decimal { + if o.exists { + return o.value + } + + return defaultValue +} + +// UnwrapOrElse returns the stored value if present. +// Otherwise, calls the provided function and returns its result. +// Useful when the default value requires computation or side effects. +// +// Example: +// +// o := NoneOptionalDecimal() +// v := o.UnwrapOrElse(func() Decimal { return computeDefault() }) +func (o OptionalDecimal) UnwrapOrElse(defaultValue func() Decimal) Decimal { + if o.exists { + return o.value + } + + return defaultValue() +} + +func (o OptionalDecimal) encodeValue(encoder *msgpack.Encoder) error { + value, err := o.value.MarshalMsgpack() + if err != nil { + return err + } + + err = encoder.EncodeExtHeader(1, len(value)) + if err != nil { + return err + } + + _, err = encoder.Writer().Write(value) + if err != nil { + return err + } + + return nil +} + +// EncodeMsgpack encodes the OptionalDecimal value using MessagePack format. +// - If the value is present, it is encoded as Decimal. +// - If the value is absent (None), it is encoded as nil. +// +// Returns an error if encoding fails. +func (o OptionalDecimal) EncodeMsgpack(encoder *msgpack.Encoder) error { + if o.exists { + return o.newEncodeError(o.encodeValue(encoder)) + } + + return o.newEncodeError(encoder.EncodeNil()) +} + +func (o *OptionalDecimal) decodeValue(decoder *msgpack.Decoder) error { + tp, length, err := decoder.DecodeExtHeader() + switch { + case err != nil: + return o.newDecodeError(err) + case tp != 1: + return o.newDecodeError(fmt.Errorf("invalid extension code: %d", tp)) + } + + a := make([]byte, length) + if err := decoder.ReadFull(a); err != nil { + return o.newDecodeError(err) + } + + if err := o.value.UnmarshalMsgpack(a); err != nil { + return o.newDecodeError(err) + } + + o.exists = true + return nil +} + +func (o *OptionalDecimal) checkCode(code byte) bool { + return msgpcode.IsExt(code) +} + +// DecodeMsgpack decodes a OptionalDecimal value from MessagePack format. +// Supports two input types: +// - nil: interpreted as no value (NoneOptionalDecimal) +// - Decimal: interpreted as a present value (SomeOptionalDecimal) +// +// Returns an error if the input type is unsupported or decoding fails. +// +// After successful decoding: +// - on nil: exists = false, value = default zero value +// - on Decimal: exists = true, value = decoded value +func (o *OptionalDecimal) DecodeMsgpack(decoder *msgpack.Decoder) error { + code, err := decoder.PeekCode() + if err != nil { + return o.newDecodeError(err) + } + + switch { + case code == msgpcode.Nil: + o.exists = false + + return o.newDecodeError(decoder.Skip()) + case o.checkCode(code): + err := o.decodeValue(decoder) + if err != nil { + return o.newDecodeError(err) + } + o.exists = true + + return err + default: + return o.newDecodeError(fmt.Errorf("unexpected code: %d", code)) + } +} diff --git a/decimal/decimal_gen_test.go b/decimal/decimal_gen_test.go new file mode 100644 index 000000000..50f22bf23 --- /dev/null +++ b/decimal/decimal_gen_test.go @@ -0,0 +1,117 @@ +package decimal + +import ( + "bytes" + "testing" + + "github.com/shopspring/decimal" + "github.com/stretchr/testify/assert" + "github.com/vmihailenco/msgpack/v5" +) + +func TestSomeOptionalDecimal(t *testing.T) { + val := MakeDecimal(decimal.NewFromFloat(1.23)) + opt := SomeOptionalDecimal(val) + + assert.True(t, opt.IsSome()) + assert.False(t, opt.IsZero()) + + v, ok := opt.Get() + assert.True(t, ok) + assert.Equal(t, val, v) +} + +func TestNoneOptionalDecimal(t *testing.T) { + opt := NoneOptionalDecimal() + + assert.False(t, opt.IsSome()) + assert.True(t, opt.IsZero()) + + _, ok := opt.Get() + assert.False(t, ok) +} + +func TestOptionalDecimal_MustGet(t *testing.T) { + val := MakeDecimal(decimal.NewFromFloat(1.23)) + optSome := SomeOptionalDecimal(val) + optNone := NoneOptionalDecimal() + + assert.Equal(t, val, optSome.MustGet()) + assert.Panics(t, func() { optNone.MustGet() }) +} + +func TestOptionalDecimal_Unwrap(t *testing.T) { + val := MakeDecimal(decimal.NewFromFloat(1.23)) + optSome := SomeOptionalDecimal(val) + optNone := NoneOptionalDecimal() + + assert.Equal(t, val, optSome.Unwrap()) + assert.Equal(t, Decimal{}, optNone.Unwrap()) +} + +func TestOptionalDecimal_UnwrapOr(t *testing.T) { + val := MakeDecimal(decimal.NewFromFloat(1.23)) + def := MakeDecimal(decimal.NewFromFloat(4.56)) + optSome := SomeOptionalDecimal(val) + optNone := NoneOptionalDecimal() + + assert.Equal(t, val, optSome.UnwrapOr(def)) + assert.Equal(t, def, optNone.UnwrapOr(def)) +} + +func TestOptionalDecimal_UnwrapOrElse(t *testing.T) { + val := MakeDecimal(decimal.NewFromFloat(1.23)) + def := MakeDecimal(decimal.NewFromFloat(4.56)) + optSome := SomeOptionalDecimal(val) + optNone := NoneOptionalDecimal() + + assert.Equal(t, val, optSome.UnwrapOrElse(func() Decimal { return def })) + assert.Equal(t, def, optNone.UnwrapOrElse(func() Decimal { return def })) +} + +func TestOptionalDecimal_EncodeDecodeMsgpack_Some(t *testing.T) { + val := MakeDecimal(decimal.NewFromFloat(1.23)) + some := SomeOptionalDecimal(val) + + var buf bytes.Buffer + enc := msgpack.NewEncoder(&buf) + dec := msgpack.NewDecoder(&buf) + + err := enc.Encode(some) + assert.NoError(t, err) + + var decodedSome OptionalDecimal + err = dec.Decode(&decodedSome) + assert.NoError(t, err) + assert.True(t, decodedSome.IsSome()) + assert.Equal(t, val, decodedSome.Unwrap()) +} + +func TestOptionalDecimal_EncodeDecodeMsgpack_None(t *testing.T) { + none := NoneOptionalDecimal() + + var buf bytes.Buffer + enc := msgpack.NewEncoder(&buf) + dec := msgpack.NewDecoder(&buf) + + err := enc.Encode(none) + assert.NoError(t, err) + + var decodedNone OptionalDecimal + err = dec.Decode(&decodedNone) + assert.NoError(t, err) + assert.True(t, decodedNone.IsZero()) +} + +func TestOptionalDecimal_EncodeDecodeMsgpack_InvalidType(t *testing.T) { + var buf bytes.Buffer + enc := msgpack.NewEncoder(&buf) + dec := msgpack.NewDecoder(&buf) + + err := enc.Encode(123) + assert.NoError(t, err) + + var decodedInvalid OptionalDecimal + err = dec.Decode(&decodedInvalid) + assert.Error(t, err) +} diff --git a/decimal/decimal_test.go b/decimal/decimal_test.go index 573daa8f6..f75494204 100644 --- a/decimal/decimal_test.go +++ b/decimal/decimal_test.go @@ -12,9 +12,9 @@ import ( "github.com/shopspring/decimal" "github.com/vmihailenco/msgpack/v5" - . "github.com/tarantool/go-tarantool/v2" - . "github.com/tarantool/go-tarantool/v2/decimal" - "github.com/tarantool/go-tarantool/v2/test_helpers" + . "github.com/tarantool/go-tarantool/v3" + . "github.com/tarantool/go-tarantool/v3/decimal" + "github.com/tarantool/go-tarantool/v3/test_helpers" ) var isDecimalSupported = false diff --git a/decimal/example_test.go b/decimal/example_test.go index 5597590dc..7903b077e 100644 --- a/decimal/example_test.go +++ b/decimal/example_test.go @@ -13,8 +13,8 @@ import ( "log" "time" - "github.com/tarantool/go-tarantool/v2" - . "github.com/tarantool/go-tarantool/v2/decimal" + "github.com/tarantool/go-tarantool/v3" + . "github.com/tarantool/go-tarantool/v3/decimal" ) // To enable support of decimal in msgpack with diff --git a/decimal/fuzzing_test.go b/decimal/fuzzing_test.go index 5ec2a2b8f..b6c49dcd9 100644 --- a/decimal/fuzzing_test.go +++ b/decimal/fuzzing_test.go @@ -7,7 +7,8 @@ import ( "testing" "github.com/shopspring/decimal" - . "github.com/tarantool/go-tarantool/v2/decimal" + + . "github.com/tarantool/go-tarantool/v3/decimal" ) func strToDecimal(t *testing.T, buf string, exp int) decimal.Decimal { diff --git a/dial_test.go b/dial_test.go index 87b9af5d8..2d0dee8ce 100644 --- a/dial_test.go +++ b/dial_test.go @@ -16,8 +16,8 @@ import ( "github.com/stretchr/testify/require" "github.com/tarantool/go-iproto" - "github.com/tarantool/go-tarantool/v2" - "github.com/tarantool/go-tarantool/v2/test_helpers" + "github.com/tarantool/go-tarantool/v3" + "github.com/tarantool/go-tarantool/v3/test_helpers" ) type mockErrorDialer struct { diff --git a/errors.go b/errors.go index 60f71a2b1..32483a9f6 100644 --- a/errors.go +++ b/errors.go @@ -25,13 +25,13 @@ func (tnterr Error) Error() string { // ClientError is connection error produced by this client, // i.e. connection failures or timeouts. type ClientError struct { - Code uint32 + Code CodeError Msg string } // Error converts a ClientError to a string. func (clierr ClientError) Error() string { - return fmt.Sprintf("%s (0x%x)", clierr.Msg, clierr.Code) + return fmt.Sprintf("%s (%#x)", clierr.Msg, uint32(clierr.Code)) } // Temporary returns true if next attempt to perform request may succeeded. @@ -52,13 +52,23 @@ func (clierr ClientError) Temporary() bool { } } +// CodeError is an error providing code of failure. +// Allows to differ them and returning error using errors.Is. +type CodeError uint32 + +// Error converts CodeError to a string. +func (err CodeError) Error() string { + return fmt.Sprintf("%#x", uint32(err)) +} + // Tarantool client error codes. const ( - ErrConnectionNotReady = 0x4000 + iota - ErrConnectionClosed = 0x4000 + iota - ErrProtocolError = 0x4000 + iota - ErrTimeouted = 0x4000 + iota - ErrRateLimited = 0x4000 + iota - ErrConnectionShutdown = 0x4000 + iota - ErrIoError = 0x4000 + iota + ErrConnectionNotReady CodeError = 0x4000 + iota + ErrConnectionClosed + ErrProtocolError + ErrTimeouted + ErrRateLimited + ErrConnectionShutdown + ErrIoError + ErrCancelledCtx ) diff --git a/example_custom_unpacking_test.go b/example_custom_unpacking_test.go index a2706f3f6..d8c790a25 100644 --- a/example_custom_unpacking_test.go +++ b/example_custom_unpacking_test.go @@ -8,7 +8,7 @@ import ( "github.com/vmihailenco/msgpack/v5" - "github.com/tarantool/go-tarantool/v2" + "github.com/tarantool/go-tarantool/v3" ) type Tuple2 struct { @@ -19,7 +19,7 @@ type Tuple2 struct { // Same effect in a "magic" way, but slower. type Tuple3 struct { - _msgpack struct{} `msgpack:",asArray"` //nolint: structcheck,unused + _msgpack struct{} `msgpack:",asArray"` // nolint: structcheck,unused Cid uint Orig string diff --git a/example_test.go b/example_test.go index 463f3289c..4186c06b8 100644 --- a/example_test.go +++ b/example_test.go @@ -9,14 +9,14 @@ import ( "github.com/tarantool/go-iproto" - "github.com/tarantool/go-tarantool/v2" - "github.com/tarantool/go-tarantool/v2/test_helpers" + "github.com/tarantool/go-tarantool/v3" + "github.com/tarantool/go-tarantool/v3/test_helpers" ) type Tuple struct { // Instruct msgpack to pack this struct as array, so no custom packer // is needed. - _msgpack struct{} `msgpack:",asArray"` //nolint: structcheck,unused + _msgpack struct{} `msgpack:",asArray"` // nolint: structcheck,unused Id uint Msg string Name string @@ -167,7 +167,7 @@ func ExamplePingRequest_Context() { fmt.Println("Ping Error", regexp.MustCompile("[0-9]+").ReplaceAllString(err.Error(), "N")) // Output: // Ping Resp data [] - // Ping Error context is done (request ID N) + // Ping Error context is done (request ID N) (NxN) } func ExampleSelectRequest() { diff --git a/future_test.go b/future_test.go index fbb30fe62..fe13bc103 100644 --- a/future_test.go +++ b/future_test.go @@ -11,8 +11,8 @@ import ( "github.com/stretchr/testify/assert" "github.com/tarantool/go-iproto" - . "github.com/tarantool/go-tarantool/v2" - "github.com/tarantool/go-tarantool/v2/test_helpers" + . "github.com/tarantool/go-tarantool/v3" + "github.com/tarantool/go-tarantool/v3/test_helpers" "github.com/vmihailenco/msgpack/v5" ) diff --git a/go.mod b/go.mod index 5b44eb1c5..7582412da 100644 --- a/go.mod +++ b/go.mod @@ -1,18 +1,29 @@ -module github.com/tarantool/go-tarantool/v2 +module github.com/tarantool/go-tarantool/v3 -go 1.20 +go 1.24 require ( - github.com/google/uuid v1.3.0 + github.com/google/uuid v1.6.0 github.com/shopspring/decimal v1.3.1 - github.com/stretchr/testify v1.9.0 + github.com/stretchr/testify v1.11.1 github.com/tarantool/go-iproto v1.1.0 + github.com/tarantool/go-option v1.0.0 github.com/vmihailenco/msgpack/v5 v5.4.1 ) require ( github.com/davecgh/go-spew v1.1.1 // indirect + github.com/kr/text v0.2.0 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/rogpeppe/go-internal v1.14.1 // indirect github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect + golang.org/x/mod v0.27.0 // indirect + golang.org/x/sync v0.16.0 // indirect + golang.org/x/tools v0.36.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) + +tool ( + github.com/tarantool/go-option/cmd/gentypes + golang.org/x/tools/cmd/stringer +) diff --git a/go.sum b/go.sum index 099647b8c..91e1c4f25 100644 --- a/go.sum +++ b/go.sum @@ -1,20 +1,38 @@ +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= -github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= +github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= github.com/shopspring/decimal v1.3.1 h1:2Usl1nmF/WZucqkFZhnfFYxxxu8LG21F6nPQBE5gKV8= github.com/shopspring/decimal v1.3.1/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= -github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= -github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/tarantool/go-iproto v1.1.0 h1:HULVOIHsiehI+FnHfM7wMDntuzUddO09DKqu2WnFQ5A= github.com/tarantool/go-iproto v1.1.0/go.mod h1:LNCtdyZxojUed8SbOiYHoc3v9NvaZTB7p96hUySMlIo= +github.com/tarantool/go-option v1.0.0 h1:+Etw0i3TjsXvADTo5rfZNCfsXe3BfHOs+iVfIrl0Nlo= +github.com/tarantool/go-option v1.0.0/go.mod h1:lXzzeZtL+rPUtLOCDP6ny3FemFBjruG9aHKzNN2bS08= github.com/vmihailenco/msgpack/v5 v5.4.1 h1:cQriyiUvjTwOHg8QZaPihLWeRAAVoCpE00IUPn0Bjt8= github.com/vmihailenco/msgpack/v5 v5.4.1/go.mod h1:GaZTsDaehaPpQVyxrf5mtQlH+pc21PIudVV/E3rRQok= github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g= github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +golang.org/x/mod v0.27.0 h1:kb+q2PyFnEADO2IEF935ehFUXlWiNjJWtRNgBLSfbxQ= +golang.org/x/mod v0.27.0/go.mod h1:rWI627Fq0DEoudcK+MBkNkCe0EetEaDSwJJkCcjpazc= +golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw= +golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= +golang.org/x/tools v0.36.0 h1:kWS0uv/zsvHEle1LbV5LE8QujrxB3wfQyxHfhOk0Qkg= +golang.org/x/tools v0.36.0/go.mod h1:WBDiHKJK8YgLHlcQPYQzNCkUxUypCaa5ZegCVutKm+s= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/golden_test.go b/golden_test.go index 271a98ce3..c2ee52f89 100644 --- a/golden_test.go +++ b/golden_test.go @@ -17,7 +17,7 @@ import ( "github.com/tarantool/go-iproto" "github.com/vmihailenco/msgpack/v5" - "github.com/tarantool/go-tarantool/v2" + "github.com/tarantool/go-tarantool/v3" ) // golden_test.go contains tests that will check that the msgpack diff --git a/pool/connection_pool.go b/pool/connection_pool.go index e62cb2b3e..572fe6cde 100644 --- a/pool/connection_pool.go +++ b/pool/connection_pool.go @@ -20,7 +20,7 @@ import ( "github.com/tarantool/go-iproto" - "github.com/tarantool/go-tarantool/v2" + "github.com/tarantool/go-tarantool/v3" ) var ( diff --git a/pool/connection_pool_test.go b/pool/connection_pool_test.go index 8120c613d..e0fcc8c47 100644 --- a/pool/connection_pool_test.go +++ b/pool/connection_pool_test.go @@ -15,12 +15,12 @@ import ( "time" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" "github.com/vmihailenco/msgpack/v5" - "github.com/stretchr/testify/require" - "github.com/tarantool/go-tarantool/v2" - "github.com/tarantool/go-tarantool/v2/pool" - "github.com/tarantool/go-tarantool/v2/test_helpers" + "github.com/tarantool/go-tarantool/v3" + "github.com/tarantool/go-tarantool/v3/pool" + "github.com/tarantool/go-tarantool/v3/test_helpers" ) var user = "test" @@ -2572,7 +2572,7 @@ func TestSelect(t *testing.T) { err = test_helpers.InsertOnInstances(ctx, makeDialers(allServers), connOpts, spaceNo, anyTpl) require.Nil(t, err) - //default: ANY + // default: ANY data, err := connPool.Select(spaceNo, indexNo, 0, 1, tarantool.IterEq, anyKey) require.Nilf(t, err, "failed to Select") require.NotNilf(t, data, "response is nil after Select") diff --git a/pool/connector.go b/pool/connector.go index 23cc7275d..391b83b75 100644 --- a/pool/connector.go +++ b/pool/connector.go @@ -5,7 +5,7 @@ import ( "fmt" "time" - "github.com/tarantool/go-tarantool/v2" + "github.com/tarantool/go-tarantool/v3" ) // ConnectorAdapter allows to use Pooler as Connector. diff --git a/pool/connector_test.go b/pool/connector_test.go index 87bebbd53..9b8106c12 100644 --- a/pool/connector_test.go +++ b/pool/connector_test.go @@ -7,8 +7,9 @@ import ( "time" "github.com/stretchr/testify/require" - "github.com/tarantool/go-tarantool/v2" - . "github.com/tarantool/go-tarantool/v2/pool" + + "github.com/tarantool/go-tarantool/v3" + . "github.com/tarantool/go-tarantool/v3/pool" ) var testMode Mode = RW diff --git a/pool/const.go b/pool/const.go index b1748ae52..d15490928 100644 --- a/pool/const.go +++ b/pool/const.go @@ -1,4 +1,4 @@ -//go:generate stringer -type Role -linecomment +//go:generate go tool stringer -type Role -linecomment package pool /* diff --git a/pool/example_test.go b/pool/example_test.go index dce8bb1af..6cf339baf 100644 --- a/pool/example_test.go +++ b/pool/example_test.go @@ -6,15 +6,15 @@ import ( "github.com/tarantool/go-iproto" - "github.com/tarantool/go-tarantool/v2" - "github.com/tarantool/go-tarantool/v2/pool" - "github.com/tarantool/go-tarantool/v2/test_helpers" + "github.com/tarantool/go-tarantool/v3" + "github.com/tarantool/go-tarantool/v3/pool" + "github.com/tarantool/go-tarantool/v3/test_helpers" ) type Tuple struct { // Instruct msgpack to pack this struct as array, so no custom packer // is needed. - _msgpack struct{} `msgpack:",asArray"` //nolint: structcheck,unused + _msgpack struct{} `msgpack:",asArray"` // nolint: structcheck,unused Key string Value string } diff --git a/pool/pooler.go b/pool/pooler.go index d4c0f1b5e..fd3df3401 100644 --- a/pool/pooler.go +++ b/pool/pooler.go @@ -4,7 +4,7 @@ import ( "context" "time" - "github.com/tarantool/go-tarantool/v2" + "github.com/tarantool/go-tarantool/v3" ) // TopologyEditor is the interface that must be implemented by a connection pool. diff --git a/pool/round_robin.go b/pool/round_robin.go index 82cf26f39..f3ccb014c 100644 --- a/pool/round_robin.go +++ b/pool/round_robin.go @@ -4,7 +4,7 @@ import ( "sync" "sync/atomic" - "github.com/tarantool/go-tarantool/v2" + "github.com/tarantool/go-tarantool/v3" ) type roundRobinStrategy struct { diff --git a/pool/round_robin_test.go b/pool/round_robin_test.go index 6f028f2de..dcc219fd4 100644 --- a/pool/round_robin_test.go +++ b/pool/round_robin_test.go @@ -3,7 +3,7 @@ package pool import ( "testing" - "github.com/tarantool/go-tarantool/v2" + "github.com/tarantool/go-tarantool/v3" ) const ( diff --git a/pool/watcher.go b/pool/watcher.go index f7c08213e..aee3103fd 100644 --- a/pool/watcher.go +++ b/pool/watcher.go @@ -3,7 +3,7 @@ package pool import ( "sync" - "github.com/tarantool/go-tarantool/v2" + "github.com/tarantool/go-tarantool/v3" ) // watcherContainer is a very simple implementation of a thread-safe container diff --git a/protocol_test.go b/protocol_test.go index 81ed2d3b5..c79a8afd3 100644 --- a/protocol_test.go +++ b/protocol_test.go @@ -6,7 +6,7 @@ import ( "github.com/stretchr/testify/require" "github.com/tarantool/go-iproto" - . "github.com/tarantool/go-tarantool/v2" + . "github.com/tarantool/go-tarantool/v3" ) func TestProtocolInfoClonePreservesFeatures(t *testing.T) { diff --git a/queue/example_connection_pool_test.go b/queue/example_connection_pool_test.go index a126e13a1..8b5aab7cb 100644 --- a/queue/example_connection_pool_test.go +++ b/queue/example_connection_pool_test.go @@ -8,10 +8,11 @@ import ( "time" "github.com/google/uuid" - "github.com/tarantool/go-tarantool/v2" - "github.com/tarantool/go-tarantool/v2/pool" - "github.com/tarantool/go-tarantool/v2/queue" - "github.com/tarantool/go-tarantool/v2/test_helpers" + + "github.com/tarantool/go-tarantool/v3" + "github.com/tarantool/go-tarantool/v3/pool" + "github.com/tarantool/go-tarantool/v3/queue" + "github.com/tarantool/go-tarantool/v3/test_helpers" ) // QueueConnectionHandler handles new connections in a ConnectionPool. diff --git a/queue/example_msgpack_test.go b/queue/example_msgpack_test.go index cdd1a4e1c..53e54dc72 100644 --- a/queue/example_msgpack_test.go +++ b/queue/example_msgpack_test.go @@ -16,8 +16,8 @@ import ( "github.com/vmihailenco/msgpack/v5" - "github.com/tarantool/go-tarantool/v2" - "github.com/tarantool/go-tarantool/v2/queue" + "github.com/tarantool/go-tarantool/v3" + "github.com/tarantool/go-tarantool/v3/queue" ) type dummyData struct { diff --git a/queue/example_test.go b/queue/example_test.go index 1b411603a..99efa769b 100644 --- a/queue/example_test.go +++ b/queue/example_test.go @@ -13,8 +13,8 @@ import ( "fmt" "time" - "github.com/tarantool/go-tarantool/v2" - "github.com/tarantool/go-tarantool/v2/queue" + "github.com/tarantool/go-tarantool/v3" + "github.com/tarantool/go-tarantool/v3/queue" ) // Example demonstrates an operations like Put and Take with queue. diff --git a/queue/queue.go b/queue/queue.go index 99b1a722f..c8f968dff 100644 --- a/queue/queue.go +++ b/queue/queue.go @@ -15,7 +15,7 @@ import ( "github.com/google/uuid" "github.com/vmihailenco/msgpack/v5" - "github.com/tarantool/go-tarantool/v2" + "github.com/tarantool/go-tarantool/v3" ) // Queue is a handle to Tarantool queue's tube. diff --git a/queue/queue_test.go b/queue/queue_test.go index 840c18b4f..81f768e18 100644 --- a/queue/queue_test.go +++ b/queue/queue_test.go @@ -10,9 +10,9 @@ import ( "github.com/vmihailenco/msgpack/v5" - . "github.com/tarantool/go-tarantool/v2" - "github.com/tarantool/go-tarantool/v2/queue" - "github.com/tarantool/go-tarantool/v2/test_helpers" + . "github.com/tarantool/go-tarantool/v3" + "github.com/tarantool/go-tarantool/v3/queue" + "github.com/tarantool/go-tarantool/v3/test_helpers" ) const ( @@ -31,8 +31,8 @@ var dialer = NetDialer{ var opts = Opts{ Timeout: 5 * time.Second, - //Concurrency: 32, - //RateLimit: 4*1024, + // Concurrency: 32, + // RateLimit: 4*1024, } func createQueue(t *testing.T, conn *Connection, name string, cfg queue.Cfg) queue.Queue { @@ -54,7 +54,7 @@ func dropQueue(t *testing.T, q queue.Queue) { } } -/////////QUEUE///////// +// ///////QUEUE///////// func TestFifoQueue(t *testing.T) { conn := test_helpers.ConnectWithValidation(t, dialer, opts) diff --git a/request_test.go b/request_test.go index 43e22d311..fb4290299 100644 --- a/request_test.go +++ b/request_test.go @@ -12,7 +12,7 @@ import ( "github.com/tarantool/go-iproto" "github.com/vmihailenco/msgpack/v5" - . "github.com/tarantool/go-tarantool/v2" + . "github.com/tarantool/go-tarantool/v3" ) const invalidSpaceMsg = "invalid space" diff --git a/response_test.go b/response_test.go index 1edbf018e..e58b4d47c 100644 --- a/response_test.go +++ b/response_test.go @@ -7,8 +7,9 @@ import ( "github.com/stretchr/testify/require" "github.com/tarantool/go-iproto" - "github.com/tarantool/go-tarantool/v2" "github.com/vmihailenco/msgpack/v5" + + "github.com/tarantool/go-tarantool/v3" ) func encodeResponseData(t *testing.T, data interface{}) io.Reader { diff --git a/schema_test.go b/schema_test.go index cbc863917..e30d52690 100644 --- a/schema_test.go +++ b/schema_test.go @@ -9,8 +9,8 @@ import ( "github.com/stretchr/testify/require" "github.com/vmihailenco/msgpack/v5" - "github.com/tarantool/go-tarantool/v2" - "github.com/tarantool/go-tarantool/v2/test_helpers" + "github.com/tarantool/go-tarantool/v3" + "github.com/tarantool/go-tarantool/v3/test_helpers" ) func TestGetSchema_ok(t *testing.T) { diff --git a/settings/example_test.go b/settings/example_test.go index e51cadef0..fdad495f3 100644 --- a/settings/example_test.go +++ b/settings/example_test.go @@ -5,9 +5,9 @@ import ( "fmt" "time" - "github.com/tarantool/go-tarantool/v2" - "github.com/tarantool/go-tarantool/v2/settings" - "github.com/tarantool/go-tarantool/v2/test_helpers" + "github.com/tarantool/go-tarantool/v3" + "github.com/tarantool/go-tarantool/v3/settings" + "github.com/tarantool/go-tarantool/v3/test_helpers" ) var exampleDialer = tarantool.NetDialer{ diff --git a/settings/request.go b/settings/request.go index 1c106dc8d..10c6cac25 100644 --- a/settings/request.go +++ b/settings/request.go @@ -65,7 +65,7 @@ import ( "github.com/tarantool/go-iproto" "github.com/vmihailenco/msgpack/v5" - "github.com/tarantool/go-tarantool/v2" + "github.com/tarantool/go-tarantool/v3" ) // SetRequest helps to set session settings. diff --git a/settings/request_test.go b/settings/request_test.go index b4c537a29..bb6c9e5c5 100644 --- a/settings/request_test.go +++ b/settings/request_test.go @@ -9,8 +9,8 @@ import ( "github.com/tarantool/go-iproto" "github.com/vmihailenco/msgpack/v5" - "github.com/tarantool/go-tarantool/v2" - . "github.com/tarantool/go-tarantool/v2/settings" + "github.com/tarantool/go-tarantool/v3" + . "github.com/tarantool/go-tarantool/v3/settings" ) type ValidSchemeResolver struct { diff --git a/settings/tarantool_test.go b/settings/tarantool_test.go index 56cee33ce..891959397 100644 --- a/settings/tarantool_test.go +++ b/settings/tarantool_test.go @@ -7,9 +7,10 @@ import ( "time" "github.com/stretchr/testify/require" - "github.com/tarantool/go-tarantool/v2" - . "github.com/tarantool/go-tarantool/v2/settings" - "github.com/tarantool/go-tarantool/v2/test_helpers" + + "github.com/tarantool/go-tarantool/v3" + . "github.com/tarantool/go-tarantool/v3/settings" + "github.com/tarantool/go-tarantool/v3/test_helpers" ) // There is no way to skip tests in testing.M, diff --git a/shutdown_test.go b/shutdown_test.go index 4df34aef5..434600824 100644 --- a/shutdown_test.go +++ b/shutdown_test.go @@ -13,8 +13,9 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - . "github.com/tarantool/go-tarantool/v2" - "github.com/tarantool/go-tarantool/v2/test_helpers" + + . "github.com/tarantool/go-tarantool/v3" + "github.com/tarantool/go-tarantool/v3/test_helpers" ) var shtdnServer = "127.0.0.1:3014" diff --git a/tarantool_test.go b/tarantool_test.go index 4902e96f4..483a90d52 100644 --- a/tarantool_test.go +++ b/tarantool_test.go @@ -3,6 +3,7 @@ package tarantool_test import ( "context" "encoding/binary" + "errors" "fmt" "io" "log" @@ -23,8 +24,8 @@ import ( "github.com/tarantool/go-iproto" "github.com/vmihailenco/msgpack/v5" - . "github.com/tarantool/go-tarantool/v2" - "github.com/tarantool/go-tarantool/v2/test_helpers" + . "github.com/tarantool/go-tarantool/v3" + "github.com/tarantool/go-tarantool/v3/test_helpers" ) var startOpts test_helpers.StartOpts = test_helpers.StartOpts{ @@ -48,7 +49,8 @@ type Member struct { Val uint } -var contextDoneErrRegexp = regexp.MustCompile(`^context is done \(request ID [0-9]+\)$`) +var contextDoneErrRegexp = regexp.MustCompile( + fmt.Sprintf(`^context is done \(request ID [0-9]+\) \(%s\)$`, ErrCancelledCtx.Error())) func (m *Member) EncodeMsgpack(e *msgpack.Encoder) error { if err := e.EncodeArrayLen(2); err != nil { @@ -89,8 +91,8 @@ var indexNo = uint32(0) var indexName = "primary" var opts = Opts{ Timeout: 5 * time.Second, - //Concurrency: 32, - //RateLimit: 4*1024, + // Concurrency: 32, + // RateLimit: 4*1024, } const N = 500 @@ -888,7 +890,7 @@ func TestFutureMultipleGetTypedWithError(t *testing.T) { } } -/////////////////// +// ///////////////// func TestClient(t *testing.T) { var err error @@ -2716,7 +2718,7 @@ func TestCallRequest(t *testing.T) { func TestClientRequestObjectsWithNilContext(t *testing.T) { conn := test_helpers.ConnectWithValidation(t, dialer, opts) defer conn.Close() - req := NewPingRequest().Context(nil) //nolint + req := NewPingRequest().Context(nil) // nolint data, err := conn.Do(req).Get() if err != nil { t.Fatalf("Failed to Ping: %s", err) @@ -2737,6 +2739,8 @@ func TestClientRequestObjectsWithPassedCanceledContext(t *testing.T) { if !contextDoneErrRegexp.Match([]byte(err.Error())) { t.Fatalf("Failed to catch an error from done context") } + // checking that we could use errors.Is to get known about error. + require.True(t, errors.Is(err, ErrCancelledCtx)) if resp != nil { t.Fatalf("Response is not nil after the occurred error") } @@ -3269,7 +3273,7 @@ func TestClientIdRequestObjectWithNilContext(t *testing.T) { req := NewIdRequest(ProtocolInfo{ Version: ProtocolVersion(1), Features: []iproto.Feature{iproto.IPROTO_FEATURE_STREAMS}, - }).Context(nil) //nolint + }).Context(nil) // nolint data, err := conn.Do(req).Get() require.Nilf(t, err, "No errors on Id request execution") require.NotNilf(t, data, "Response data not empty") @@ -3293,11 +3297,12 @@ func TestClientIdRequestObjectWithPassedCanceledContext(t *testing.T) { req := NewIdRequest(ProtocolInfo{ Version: ProtocolVersion(1), Features: []iproto.Feature{iproto.IPROTO_FEATURE_STREAMS}, - }).Context(ctx) //nolint + }).Context(ctx) // nolint cancel() resp, err := conn.Do(req).Get() require.Nilf(t, resp, "Response is empty") require.NotNilf(t, err, "Error is not empty") + require.True(t, errors.Is(err, ErrCancelledCtx)) require.Regexp(t, contextDoneErrRegexp, err.Error()) } diff --git a/test_helpers/doer.go b/test_helpers/doer.go index c33ff0e69..b61692c43 100644 --- a/test_helpers/doer.go +++ b/test_helpers/doer.go @@ -4,7 +4,7 @@ import ( "bytes" "testing" - "github.com/tarantool/go-tarantool/v2" + "github.com/tarantool/go-tarantool/v3" ) type doerResponse struct { diff --git a/test_helpers/example_test.go b/test_helpers/example_test.go index 6272d737d..3b1ed5d64 100644 --- a/test_helpers/example_test.go +++ b/test_helpers/example_test.go @@ -5,8 +5,9 @@ import ( "testing" "github.com/stretchr/testify/assert" - "github.com/tarantool/go-tarantool/v2" - "github.com/tarantool/go-tarantool/v2/test_helpers" + + "github.com/tarantool/go-tarantool/v3" + "github.com/tarantool/go-tarantool/v3/test_helpers" ) func TestExampleMockDoer(t *testing.T) { diff --git a/test_helpers/main.go b/test_helpers/main.go index 4ebe4c622..35c57f810 100644 --- a/test_helpers/main.go +++ b/test_helpers/main.go @@ -24,7 +24,7 @@ import ( "strconv" "time" - "github.com/tarantool/go-tarantool/v2" + "github.com/tarantool/go-tarantool/v3" ) type StartOpts struct { diff --git a/test_helpers/pool_helper.go b/test_helpers/pool_helper.go index 599e56594..7831b9f6e 100644 --- a/test_helpers/pool_helper.go +++ b/test_helpers/pool_helper.go @@ -8,8 +8,8 @@ import ( "sync" "time" - "github.com/tarantool/go-tarantool/v2" - "github.com/tarantool/go-tarantool/v2/pool" + "github.com/tarantool/go-tarantool/v3" + "github.com/tarantool/go-tarantool/v3/pool" ) type ListenOnInstanceArgs struct { diff --git a/test_helpers/request.go b/test_helpers/request.go index 3756a2b54..003a97ab3 100644 --- a/test_helpers/request.go +++ b/test_helpers/request.go @@ -7,7 +7,7 @@ import ( "github.com/tarantool/go-iproto" "github.com/vmihailenco/msgpack/v5" - "github.com/tarantool/go-tarantool/v2" + "github.com/tarantool/go-tarantool/v3" ) // MockRequest is an empty mock request used for testing purposes. diff --git a/test_helpers/response.go b/test_helpers/response.go index f8757f563..630ac7726 100644 --- a/test_helpers/response.go +++ b/test_helpers/response.go @@ -7,7 +7,7 @@ import ( "github.com/vmihailenco/msgpack/v5" - "github.com/tarantool/go-tarantool/v2" + "github.com/tarantool/go-tarantool/v3" ) // MockResponse is a mock response used for testing purposes. diff --git a/test_helpers/tcs/prepare.go b/test_helpers/tcs/prepare.go index c7b87d0f6..92a13afb1 100644 --- a/test_helpers/tcs/prepare.go +++ b/test_helpers/tcs/prepare.go @@ -8,8 +8,8 @@ import ( "text/template" "time" - "github.com/tarantool/go-tarantool/v2" - "github.com/tarantool/go-tarantool/v2/test_helpers" + "github.com/tarantool/go-tarantool/v3" + "github.com/tarantool/go-tarantool/v3/test_helpers" ) const ( diff --git a/test_helpers/tcs/tcs.go b/test_helpers/tcs/tcs.go index 1ba26a19e..a54ba5fda 100644 --- a/test_helpers/tcs/tcs.go +++ b/test_helpers/tcs/tcs.go @@ -7,8 +7,8 @@ import ( "net" "testing" - "github.com/tarantool/go-tarantool/v2" - "github.com/tarantool/go-tarantool/v2/test_helpers" + "github.com/tarantool/go-tarantool/v3" + "github.com/tarantool/go-tarantool/v3/test_helpers" ) // ErrNotSupported identifies result of `Start()` why storage was not started. diff --git a/test_helpers/utils.go b/test_helpers/utils.go index 579f507c9..d844a822a 100644 --- a/test_helpers/utils.go +++ b/test_helpers/utils.go @@ -6,7 +6,8 @@ import ( "time" "github.com/stretchr/testify/require" - "github.com/tarantool/go-tarantool/v2" + + "github.com/tarantool/go-tarantool/v3" ) // ConnectWithValidation tries to connect to a Tarantool instance. diff --git a/testdata/sidecar/main.go b/testdata/sidecar/main.go index 971b8694c..a2c571b87 100644 --- a/testdata/sidecar/main.go +++ b/testdata/sidecar/main.go @@ -5,7 +5,7 @@ import ( "os" "strconv" - "github.com/tarantool/go-tarantool/v2" + "github.com/tarantool/go-tarantool/v3" ) func main() { diff --git a/uuid/example_test.go b/uuid/example_test.go index c79dc35be..8673d390c 100644 --- a/uuid/example_test.go +++ b/uuid/example_test.go @@ -15,8 +15,9 @@ import ( "time" "github.com/google/uuid" - "github.com/tarantool/go-tarantool/v2" - _ "github.com/tarantool/go-tarantool/v2/uuid" + + "github.com/tarantool/go-tarantool/v3" + _ "github.com/tarantool/go-tarantool/v3/uuid" ) var exampleOpts = tarantool.Opts{ diff --git a/uuid/uuid.go b/uuid/uuid.go index cc2be736f..ca7b0ad05 100644 --- a/uuid/uuid.go +++ b/uuid/uuid.go @@ -1,4 +1,4 @@ -// Package with support of Tarantool's UUID data type. +// Package uuid with support of Tarantool's UUID data type. // // UUID data type supported in Tarantool since 2.4.1. // @@ -27,6 +27,17 @@ import ( // UUID external type. const uuid_extID = 2 +//go:generate go tool gentypes -ext-code 2 -marshal-func marshalUUID -unmarshal-func unmarshalUUID -imports "github.com/google/uuid" uuid.UUID + +func marshalUUID(id uuid.UUID) ([]byte, error) { + return id.MarshalBinary() +} + +func unmarshalUUID(uuid *uuid.UUID, data []byte) error { + return uuid.UnmarshalBinary(data) +} + +// encodeUUID encodes a uuid.UUID value into the msgpack format. func encodeUUID(e *msgpack.Encoder, v reflect.Value) error { id := v.Interface().(uuid.UUID) @@ -43,6 +54,7 @@ func encodeUUID(e *msgpack.Encoder, v reflect.Value) error { return nil } +// decodeUUID decodes a uuid.UUID value from the msgpack format. func decodeUUID(d *msgpack.Decoder, v reflect.Value) error { var bytesCount = 16 bytes := make([]byte, bytesCount) diff --git a/uuid/uuid_gen.go b/uuid/uuid_gen.go new file mode 100644 index 000000000..f1b1992ce --- /dev/null +++ b/uuid/uuid_gen.go @@ -0,0 +1,243 @@ +// Code generated by github.com/tarantool/go-option; DO NOT EDIT. + +package uuid + +import ( + "github.com/google/uuid" + + "fmt" + + "github.com/vmihailenco/msgpack/v5" + "github.com/vmihailenco/msgpack/v5/msgpcode" + + "github.com/tarantool/go-option" +) + +// OptionalUUID represents an optional value of type uuid.UUID. +// It can either hold a valid uuid.UUID (IsSome == true) or be empty (IsZero == true). +type OptionalUUID struct { + value uuid.UUID + exists bool +} + +// SomeOptionalUUID creates an optional OptionalUUID with the given uuid.UUID value. +// The returned OptionalUUID will have IsSome() == true and IsZero() == false. +func SomeOptionalUUID(value uuid.UUID) OptionalUUID { + return OptionalUUID{ + value: value, + exists: true, + } +} + +// NoneOptionalUUID creates an empty optional OptionalUUID value. +// The returned OptionalUUID will have IsSome() == false and IsZero() == true. +// +// Example: +// +// o := NoneOptionalUUID() +// if o.IsZero() { +// fmt.Println("value is absent") +// } +func NoneOptionalUUID() OptionalUUID { + return OptionalUUID{} +} + +func (o OptionalUUID) newEncodeError(err error) error { + if err == nil { + return nil + } + return &option.EncodeError{ + Type: "OptionalUUID", + Parent: err, + } +} + +func (o OptionalUUID) newDecodeError(err error) error { + if err == nil { + return nil + } + + return &option.DecodeError{ + Type: "OptionalUUID", + Parent: err, + } +} + +// IsSome returns true if the OptionalUUID contains a value. +// This indicates the value is explicitly set (not None). +func (o OptionalUUID) IsSome() bool { + return o.exists +} + +// IsZero returns true if the OptionalUUID does not contain a value. +// Equivalent to !IsSome(). Useful for consistency with types where +// zero value (e.g. 0, false, zero struct) is valid and needs to be distinguished. +func (o OptionalUUID) IsZero() bool { + return !o.exists +} + +// IsNil is an alias for IsZero. +// +// This method is provided for compatibility with the msgpack Encoder interface. +func (o OptionalUUID) IsNil() bool { + return o.IsZero() +} + +// Get returns the stored value and a boolean flag indicating its presence. +// If the value is present, returns (value, true). +// If the value is absent, returns (zero value of uuid.UUID, false). +// +// Recommended usage: +// +// if value, ok := o.Get(); ok { +// // use value +// } +func (o OptionalUUID) Get() (uuid.UUID, bool) { + return o.value, o.exists +} + +// MustGet returns the stored value if it is present. +// Panics if the value is absent (i.e., IsZero() == true). +// +// Use with caution — only when you are certain the value exists. +// +// Panics with: "optional value is not set" if no value is set. +func (o OptionalUUID) MustGet() uuid.UUID { + if !o.exists { + panic("optional value is not set") + } + + return o.value +} + +// Unwrap returns the stored value regardless of presence. +// If no value is set, returns the zero value for uuid.UUID. +// +// Warning: Does not check presence. Use IsSome() before calling if you need +// to distinguish between absent value and explicit zero value. +func (o OptionalUUID) Unwrap() uuid.UUID { + return o.value +} + +// UnwrapOr returns the stored value if present. +// Otherwise, returns the provided default value. +// +// Example: +// +// o := NoneOptionalUUID() +// v := o.UnwrapOr(someDefaultOptionalUUID) +func (o OptionalUUID) UnwrapOr(defaultValue uuid.UUID) uuid.UUID { + if o.exists { + return o.value + } + + return defaultValue +} + +// UnwrapOrElse returns the stored value if present. +// Otherwise, calls the provided function and returns its result. +// Useful when the default value requires computation or side effects. +// +// Example: +// +// o := NoneOptionalUUID() +// v := o.UnwrapOrElse(func() uuid.UUID { return computeDefault() }) +func (o OptionalUUID) UnwrapOrElse(defaultValue func() uuid.UUID) uuid.UUID { + if o.exists { + return o.value + } + + return defaultValue() +} + +func (o OptionalUUID) encodeValue(encoder *msgpack.Encoder) error { + value, err := marshalUUID(o.value) + if err != nil { + return err + } + + err = encoder.EncodeExtHeader(2, len(value)) + if err != nil { + return err + } + + _, err = encoder.Writer().Write(value) + if err != nil { + return err + } + + return nil +} + +// EncodeMsgpack encodes the OptionalUUID value using MessagePack format. +// - If the value is present, it is encoded as uuid.UUID. +// - If the value is absent (None), it is encoded as nil. +// +// Returns an error if encoding fails. +func (o OptionalUUID) EncodeMsgpack(encoder *msgpack.Encoder) error { + if o.exists { + return o.newEncodeError(o.encodeValue(encoder)) + } + + return o.newEncodeError(encoder.EncodeNil()) +} + +func (o *OptionalUUID) decodeValue(decoder *msgpack.Decoder) error { + tp, length, err := decoder.DecodeExtHeader() + switch { + case err != nil: + return o.newDecodeError(err) + case tp != 2: + return o.newDecodeError(fmt.Errorf("invalid extension code: %d", tp)) + } + + a := make([]byte, length) + if err := decoder.ReadFull(a); err != nil { + return o.newDecodeError(err) + } + + if err := unmarshalUUID(&o.value, a); err != nil { + return o.newDecodeError(err) + } + + o.exists = true + return nil +} + +func (o *OptionalUUID) checkCode(code byte) bool { + return msgpcode.IsExt(code) +} + +// DecodeMsgpack decodes a OptionalUUID value from MessagePack format. +// Supports two input types: +// - nil: interpreted as no value (NoneOptionalUUID) +// - uuid.UUID: interpreted as a present value (SomeOptionalUUID) +// +// Returns an error if the input type is unsupported or decoding fails. +// +// After successful decoding: +// - on nil: exists = false, value = default zero value +// - on uuid.UUID: exists = true, value = decoded value +func (o *OptionalUUID) DecodeMsgpack(decoder *msgpack.Decoder) error { + code, err := decoder.PeekCode() + if err != nil { + return o.newDecodeError(err) + } + + switch { + case code == msgpcode.Nil: + o.exists = false + + return o.newDecodeError(decoder.Skip()) + case o.checkCode(code): + err := o.decodeValue(decoder) + if err != nil { + return o.newDecodeError(err) + } + o.exists = true + + return err + default: + return o.newDecodeError(fmt.Errorf("unexpected code: %d", code)) + } +} diff --git a/uuid/uuid_gen_test.go b/uuid/uuid_gen_test.go new file mode 100644 index 000000000..616bb2314 --- /dev/null +++ b/uuid/uuid_gen_test.go @@ -0,0 +1,117 @@ +package uuid + +import ( + "bytes" + "testing" + + "github.com/google/uuid" + "github.com/stretchr/testify/assert" + "github.com/vmihailenco/msgpack/v5" +) + +func TestSomeOptionalUUID(t *testing.T) { + val := uuid.New() + opt := SomeOptionalUUID(val) + + assert.True(t, opt.IsSome()) + assert.False(t, opt.IsZero()) + + v, ok := opt.Get() + assert.True(t, ok) + assert.Equal(t, val, v) +} + +func TestNoneOptionalUUID(t *testing.T) { + opt := NoneOptionalUUID() + + assert.False(t, opt.IsSome()) + assert.True(t, opt.IsZero()) + + _, ok := opt.Get() + assert.False(t, ok) +} + +func TestOptionalUUID_MustGet(t *testing.T) { + val := uuid.New() + optSome := SomeOptionalUUID(val) + optNone := NoneOptionalUUID() + + assert.Equal(t, val, optSome.MustGet()) + assert.Panics(t, func() { optNone.MustGet() }) +} + +func TestOptionalUUID_Unwrap(t *testing.T) { + val := uuid.New() + optSome := SomeOptionalUUID(val) + optNone := NoneOptionalUUID() + + assert.Equal(t, val, optSome.Unwrap()) + assert.Equal(t, uuid.Nil, optNone.Unwrap()) +} + +func TestOptionalUUID_UnwrapOr(t *testing.T) { + val := uuid.New() + def := uuid.New() + optSome := SomeOptionalUUID(val) + optNone := NoneOptionalUUID() + + assert.Equal(t, val, optSome.UnwrapOr(def)) + assert.Equal(t, def, optNone.UnwrapOr(def)) +} + +func TestOptionalUUID_UnwrapOrElse(t *testing.T) { + val := uuid.New() + def := uuid.New() + optSome := SomeOptionalUUID(val) + optNone := NoneOptionalUUID() + + assert.Equal(t, val, optSome.UnwrapOrElse(func() uuid.UUID { return def })) + assert.Equal(t, def, optNone.UnwrapOrElse(func() uuid.UUID { return def })) +} + +func TestOptionalUUID_EncodeDecodeMsgpack_Some(t *testing.T) { + val := uuid.New() + some := SomeOptionalUUID(val) + + var buf bytes.Buffer + enc := msgpack.NewEncoder(&buf) + dec := msgpack.NewDecoder(&buf) + + err := enc.Encode(some) + assert.NoError(t, err) + + var decodedSome OptionalUUID + err = dec.Decode(&decodedSome) + assert.NoError(t, err) + assert.True(t, decodedSome.IsSome()) + assert.Equal(t, val, decodedSome.Unwrap()) +} + +func TestOptionalUUID_EncodeDecodeMsgpack_None(t *testing.T) { + none := NoneOptionalUUID() + + var buf bytes.Buffer + enc := msgpack.NewEncoder(&buf) + dec := msgpack.NewDecoder(&buf) + + err := enc.Encode(none) + assert.NoError(t, err) + + var decodedNone OptionalUUID + err = dec.Decode(&decodedNone) + assert.NoError(t, err) + assert.True(t, decodedNone.IsZero()) +} + +func TestOptionalUUID_EncodeDecodeMsgpack_InvalidType(t *testing.T) { + var buf bytes.Buffer + enc := msgpack.NewEncoder(&buf) + dec := msgpack.NewDecoder(&buf) + + err := enc.Encode(123) + assert.NoError(t, err) + + var decodedInvalid OptionalUUID + err = dec.Decode(&decodedInvalid) + assert.Error(t, err) +} diff --git a/uuid/uuid_test.go b/uuid/uuid_test.go index 0ce317979..22ffd7eb5 100644 --- a/uuid/uuid_test.go +++ b/uuid/uuid_test.go @@ -10,9 +10,9 @@ import ( "github.com/google/uuid" "github.com/vmihailenco/msgpack/v5" - . "github.com/tarantool/go-tarantool/v2" - "github.com/tarantool/go-tarantool/v2/test_helpers" - _ "github.com/tarantool/go-tarantool/v2/uuid" + . "github.com/tarantool/go-tarantool/v3" + "github.com/tarantool/go-tarantool/v3/test_helpers" + _ "github.com/tarantool/go-tarantool/v3/uuid" ) // There is no way to skip tests in testing.M,