Skip to content

Commit

Permalink
crud: support schema
Browse files Browse the repository at this point in the history
Support `crud.schema` request [1] and response parsing.

1. tarantool/crud#380
  • Loading branch information
DifferentialOrange authored and oleg-jukovec committed Oct 18, 2023
1 parent 8075914 commit 8e1a911
Show file tree
Hide file tree
Showing 5 changed files with 369 additions and 1 deletion.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -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 1.4.0 )

.PHONY: datetime-timezones
datetime-timezones:
Expand Down
29 changes: 29 additions & 0 deletions crud/example_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -302,3 +302,32 @@ func ExampleSelectRequest_pagination() {
// [{id unsigned false} {bucket_id unsigned true} {name string false}]
// [[3006 32 bla] [3007 33 bla]]
}

func ExampleSchema() {
conn := exampleConnect()

req := crud.MakeSchemaRequest()
var result crud.SchemaResult

if err := conn.Do(req).GetTyped(&result); err != nil {
fmt.Printf("Failed to execute request: %s", err)
return
}

// Schema may differ between different Tarantool versions.
// https://github.com/tarantool/tarantool/issues/4091
// https://github.com/tarantool/tarantool/commit/17c9c034933d726925910ce5bf8b20e8e388f6e3
for spaceName, spaceSchema := range result.Value {
fmt.Printf("Space format for '%s' is as follows:\n", spaceName)

for _, field := range spaceSchema.Format {
fmt.Printf(" - field '%s' with type '%s'\n", field.Name, field.Type)
}
}

// Output:
// Space format for 'test' is as follows:
// - field 'id' with type 'unsigned'
// - field 'bucket_id' with type 'unsigned'
// - field 'name' with type 'string'
}
214 changes: 214 additions & 0 deletions crud/schema.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,214 @@
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")
}
*schema = make(map[string]SpaceSchema, l)
} 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 `msgpack:"format"`
Indexes map[uint32]Index `msgpack:"indexes"`
}

// Index contains a CRUD space index definition.
type Index struct {
Id uint32 `msgpack:"id"`
Name string `msgpack:"name"`
Type string `msgpack:"type"`
Unique bool `msgpack:"unique"`
Parts []IndexPart `msgpack:"parts"`
}

// IndexField contains a CRUD space index part definition.
type IndexPart struct {
Fieldno uint32 `msgpack:"fieldno"`
Type string `msgpack:"type"`
ExcludeNull bool `msgpack:"exclude_null"`
IsNullable bool `msgpack:"is_nullable"`
}

// SchemaResult contains a schema request result for all spaces.
type SchemaResult struct {
Value Schema
}

// DecodeMsgpack provides custom msgpack decoder.
func (result *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(&result.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 (result *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(&result.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
}
124 changes: 124 additions & 0 deletions crud/tarantool_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1209,6 +1209,130 @@ 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())
}

func TestUnitEmptySchema(t *testing.T) {
// We need to create another cluster with no spaces
// to test `{}` schema, so let's at least add a unit test.
conn := connect(t)
defer conn.Close()

req := tarantool.NewEvalRequest("return {}")
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, crud.Schema{}, "empty schema expected")
}

// 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
Expand Down

0 comments on commit 8e1a911

Please sign in to comment.