Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

crud: support schema #336

Merged
merged 2 commits into from
Oct 18, 2023
Merged
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Next Next commit
crud: support schema
Support `crud.schema` request [1] and response parsing.

1. tarantool/crud#380
DifferentialOrange committed Oct 18, 2023
commit 7a70686bb2c83cb91379845f6912d49d15dcce15
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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

2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
@@ -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:
29 changes: 29 additions & 0 deletions crud/example_test.go
Original file line number Diff line number Diff line change
@@ -299,3 +299,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
@@ -1207,6 +1207,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