From c0e656b4600b31d9e34e8eaf2f1b06d7333676f9 Mon Sep 17 00:00:00 2001 From: Georgy Moiseev Date: Tue, 10 Oct 2023 16:21:13 +0300 Subject: [PATCH] crud: support schema Support `crud.schema` request [1] and response parsing. 1. https://github.com/tarantool/crud/pull/380 --- CHANGELOG.md | 1 + Makefile | 2 +- crud/schema.go | 360 +++++++++++++++++++++++++++++++++++++++++ crud/tarantool_test.go | 110 +++++++++++++ 4 files changed, 472 insertions(+), 1 deletion(-) create mode 100644 crud/schema.go diff --git a/CHANGELOG.md b/CHANGELOG.md index 2dc22ecd2..5274d35ff 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,7 @@ Versioning](http://semver.org/spec/v2.0.0.html) except to the first release. - Support `operation_data` in `crud.Error` (#330) - Support `fetch_latest_metadata` option for crud requests with metadata (#335) - Support `noreturn` option for data change crud requests (#335) +- Support `crud.schema` request (#336) ### Changed diff --git a/Makefile b/Makefile index c435764a8..e3b5303eb 100644 --- a/Makefile +++ b/Makefile @@ -27,7 +27,7 @@ clean: .PHONY: deps deps: clean ( cd ./queue/testdata; $(TTCTL) rocks install queue 1.3.0 ) - ( cd ./crud/testdata; $(TTCTL) rocks install crud 1.3.0 ) + ( cd ./crud/testdata; $(TTCTL) rocks install crud) # Will be replaced with 1.4.0 after release .PHONY: datetime-timezones datetime-timezones: diff --git a/crud/schema.go b/crud/schema.go new file mode 100644 index 000000000..919b97f24 --- /dev/null +++ b/crud/schema.go @@ -0,0 +1,360 @@ +package crud + +import ( + "context" + "fmt" + + "github.com/vmihailenco/msgpack/v5" + "github.com/vmihailenco/msgpack/v5/msgpcode" + + "github.com/tarantool/go-tarantool/v2" +) + +func msgpackIsMap(code byte) bool { + return code == msgpcode.Map16 || code == msgpcode.Map32 || msgpcode.IsFixedMap(code) +} + +// SchemaRequest helps you to create request object to call `crud.schema` +// for execution by a Connection. +type SchemaRequest struct { + baseRequest + space OptString +} + +// MakeSchemaRequest returns a new empty StatsRequest. +func MakeSchemaRequest() SchemaRequest { + req := SchemaRequest{} + req.impl = newCall("crud.schema") + return req +} + +// Space sets the space name for the StatsRequest request. +// Note: default value is nil. +func (req SchemaRequest) Space(space string) SchemaRequest { + req.space = MakeOptString(space) + return req +} + +// Body fills an encoder with the call request body. +func (req SchemaRequest) Body(res tarantool.SchemaResolver, enc *msgpack.Encoder) error { + if value, ok := req.space.Get(); ok { + req.impl = req.impl.Args([]interface{}{value}) + } else { + req.impl = req.impl.Args([]interface{}{}) + } + + return req.impl.Body(res, enc) +} + +// Context sets a passed context to CRUD request. +func (req SchemaRequest) Context(ctx context.Context) SchemaRequest { + req.impl = req.impl.Context(ctx) + + return req +} + +// Schema contains CRUD cluster schema definition. +type Schema map[string]SpaceSchema + +// DecodeMsgpack provides custom msgpack decoder. +func (schema *Schema) DecodeMsgpack(d *msgpack.Decoder) error { + var l int + + code, err := d.PeekCode() + if err != nil { + return err + } + + if msgpackIsArray(code) { + // Process empty schema case. + l, err = d.DecodeArrayLen() + if err != nil { + return err + } + if l != 0 { + return fmt.Errorf("expected map or empty array, got non-empty array") + } + } else if msgpackIsMap(code) { + l, err := d.DecodeMapLen() + if err != nil { + return err + } + *schema = make(map[string]SpaceSchema, l) + + for i := 0; i < l; i++ { + key, err := d.DecodeString() + if err != nil { + return err + } + + var spaceSchema SpaceSchema + if err := d.Decode(&spaceSchema); err != nil { + return err + } + + (*schema)[key] = spaceSchema + } + } else { + return fmt.Errorf("unexpected code=%d decoding map or empty array", code) + } + + return nil +} + +// SpaceSchema contains a single CRUD space schema definition. +type SpaceSchema struct { + Format []FieldFormat + Indexes map[uint32]Index +} + +// DecodeMsgpack provides custom msgpack decoder. +func (spaceSchema *SpaceSchema) DecodeMsgpack(d *msgpack.Decoder) error { + l, err := d.DecodeMapLen() + if err != nil { + return err + } + + for i := 0; i < l; i++ { + key, err := d.DecodeString() + if err != nil { + return err + } + + switch key { + case "format": + la, err := d.DecodeArrayLen() + if err != nil { + return err + } + + spaceSchema.Format = make([]FieldFormat, la) + for j := 0; j < la; j++ { + if err := d.Decode(&spaceSchema.Format[j]); err != nil { + return err + } + } + case "indexes": + lm, err := d.DecodeMapLen() + if err != nil { + return err + } + + spaceSchema.Indexes = make(map[uint32]Index, lm) + for j := 0; j < lm; j++ { + indexId, err := d.DecodeUint32() + if err != nil { + return err + } + + var indexObj Index + if err := d.Decode(&indexObj); err != nil { + return err + } + + spaceSchema.Indexes[indexId] = indexObj + } + default: + if err := d.Skip(); err != nil { + return err + } + } + } + + return nil +} + +// Index contains a CRUD space index definition. +type Index struct { + Id uint32 + Name string + Type string + Unique bool + Parts []IndexPart +} + +// DecodeMsgpack provides custom msgpack decoder. +func (index *Index) DecodeMsgpack(d *msgpack.Decoder) error { + l, err := d.DecodeMapLen() + if err != nil { + return err + } + + for i := 0; i < l; i++ { + key, err := d.DecodeString() + if err != nil { + return err + } + + switch key { + case "id": + if index.Id, err = d.DecodeUint32(); err != nil { + return err + } + case "name": + if index.Name, err = d.DecodeString(); err != nil { + return err + } + case "type": + if index.Type, err = d.DecodeString(); err != nil { + return err + } + case "unique": + if index.Unique, err = d.DecodeBool(); err != nil { + return err + } + case "parts": + la, err := d.DecodeArrayLen() + if err != nil { + return err + } + + index.Parts = make([]IndexPart, la) + for j := 0; j < la; j++ { + if err := d.Decode(&index.Parts[j]); err != nil { + return err + } + } + default: + if err := d.Skip(); err != nil { + return err + } + } + } + + return nil +} + +// IndexField contains a CRUD space index part definition. +type IndexPart struct { + Fieldno uint32 + Type string + ExcludeNull bool + IsNullable bool +} + +// DecodeMsgpack provides custom msgpack decoder. +func (indexPart *IndexPart) DecodeMsgpack(d *msgpack.Decoder) error { + l, err := d.DecodeMapLen() + if err != nil { + return err + } + for i := 0; i < l; i++ { + key, err := d.DecodeString() + if err != nil { + return err + } + + switch key { + case "fieldno": + if indexPart.Fieldno, err = d.DecodeUint32(); err != nil { + return err + } + case "type": + if indexPart.Type, err = d.DecodeString(); err != nil { + return err + } + case "exclude_null": + if indexPart.ExcludeNull, err = d.DecodeBool(); err != nil { + return err + } + case "is_nullable": + if indexPart.IsNullable, err = d.DecodeBool(); err != nil { + return err + } + default: + if err := d.Skip(); err != nil { + return err + } + } + } + + return nil +} + +// SchemaResult contains a schema request result for all spaces. +type SchemaResult struct { + Value Schema +} + +// DecodeMsgpack provides custom msgpack decoder. +func (schemaResult *SchemaResult) DecodeMsgpack(d *msgpack.Decoder) error { + arrLen, err := d.DecodeArrayLen() + if err != nil { + return err + } + + if arrLen == 0 { + return fmt.Errorf("unexpected empty response array") + } + + // DecodeMapLen inside Schema decode processes `nil` as zero length map, + // so in `return nil, err` case we don't miss error info. + // https://github.com/vmihailenco/msgpack/blob/3f7bd806fea698e7a9fe80979aa3512dea0a7368/decode_map.go#L79-L81 + if err = d.Decode(&schemaResult.Value); err != nil { + return err + } + + if arrLen > 1 { + var crudErr *Error = nil + + if err := d.Decode(&crudErr); err != nil { + return err + } + + if crudErr != nil { + return crudErr + } + } + + for i := 2; i < arrLen; i++ { + if err := d.Skip(); err != nil { + return err + } + } + + return nil +} + +// SchemaResult contains a schema request result for a single space. +type SpaceSchemaResult struct { + Value SpaceSchema +} + +// DecodeMsgpack provides custom msgpack decoder. +func (spaceSchemaResult *SpaceSchemaResult) DecodeMsgpack(d *msgpack.Decoder) error { + arrLen, err := d.DecodeArrayLen() + if err != nil { + return err + } + + if arrLen == 0 { + return fmt.Errorf("unexpected empty response array") + } + + // DecodeMapLen inside SpaceSchema decode processes `nil` as zero length map, + // so in `return nil, err` case we don't miss error info. + // https://github.com/vmihailenco/msgpack/blob/3f7bd806fea698e7a9fe80979aa3512dea0a7368/decode_map.go#L79-L81 + if err = d.Decode(&spaceSchemaResult.Value); err != nil { + return err + } + + if arrLen > 1 { + var crudErr *Error = nil + + if err := d.Decode(&crudErr); err != nil { + return err + } + + if crudErr != nil { + return crudErr + } + } + + for i := 2; i < arrLen; i++ { + if err := d.Skip(); err != nil { + return err + } + } + + return nil +} diff --git a/crud/tarantool_test.go b/crud/tarantool_test.go index b361579ae..37fda065d 100644 --- a/crud/tarantool_test.go +++ b/crud/tarantool_test.go @@ -1207,6 +1207,116 @@ func TestNoreturnOptionTyped(t *testing.T) { } } +func getTestSchema(t *testing.T) crud.Schema { + schema := crud.Schema{ + "test": crud.SpaceSchema{ + Format: []crud.FieldFormat{ + crud.FieldFormat{ + Name: "id", + Type: "unsigned", + IsNullable: false, + }, + { + Name: "bucket_id", + Type: "unsigned", + IsNullable: true, + }, + { + Name: "name", + Type: "string", + IsNullable: false, + }, + }, + Indexes: map[uint32]crud.Index{ + 0: { + Id: 0, + Name: "primary_index", + Type: "TREE", + Unique: true, + Parts: []crud.IndexPart{ + { + Fieldno: 1, + Type: "unsigned", + ExcludeNull: false, + IsNullable: false, + }, + }, + }, + }, + }, + } + + // https://github.com/tarantool/tarantool/issues/4091 + uniqueIssue, err := test_helpers.IsTarantoolVersionLess(2, 2, 1) + require.Equal(t, err, nil, "expected version check to succeed") + + if uniqueIssue { + for sk, sv := range schema { + for ik, iv := range sv.Indexes { + iv.Unique = false + sv.Indexes[ik] = iv + } + schema[sk] = sv + } + } + + // https://github.com/tarantool/tarantool/commit/17c9c034933d726925910ce5bf8b20e8e388f6e3 + excludeNullUnsupported, err := test_helpers.IsTarantoolVersionLess(2, 8, 1) + require.Equal(t, err, nil, "expected version check to succeed") + + if excludeNullUnsupported { + for sk, sv := range schema { + for ik, iv := range sv.Indexes { + for pk, pv := range iv.Parts { + // Struct default value. + pv.ExcludeNull = false + iv.Parts[pk] = pv + } + sv.Indexes[ik] = iv + } + schema[sk] = sv + } + } + + return schema +} + +func TestSchemaTyped(t *testing.T) { + conn := connect(t) + defer conn.Close() + + req := crud.MakeSchemaRequest() + var result crud.SchemaResult + + err := conn.Do(req).GetTyped(&result) + require.Equal(t, err, nil, "Expected CRUD request to succeed") + require.Equal(t, result.Value, getTestSchema(t), "map with \"test\" schema expected") +} + +func TestSpaceSchemaTyped(t *testing.T) { + conn := connect(t) + defer conn.Close() + + req := crud.MakeSchemaRequest().Space("test") + var result crud.SpaceSchemaResult + + err := conn.Do(req).GetTyped(&result) + require.Equal(t, err, nil, "Expected CRUD request to succeed") + require.Equal(t, result.Value, getTestSchema(t)["test"], "map with \"test\" schema expected") +} + +func TestSpaceSchemaTypedError(t *testing.T) { + conn := connect(t) + defer conn.Close() + + req := crud.MakeSchemaRequest().Space("not_exist") + var result crud.SpaceSchemaResult + + err := conn.Do(req).GetTyped(&result) + require.NotEqual(t, err, nil, "Expected CRUD request to fail") + require.Regexp(t, "Space \"not_exist\" doesn't exist", err.Error()) +} + // runTestMain is a body of TestMain function // (see https://pkg.go.dev/testing#hdr-Main). // Using defer + os.Exit is not works so TestMain body