Skip to content

Commit 8e1a911

Browse files
DifferentialOrangeoleg-jukovec
authored andcommitted
crud: support schema
Support `crud.schema` request [1] and response parsing. 1. tarantool/crud#380
1 parent 8075914 commit 8e1a911

File tree

5 files changed

+369
-1
lines changed

5 files changed

+369
-1
lines changed

CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ Versioning](http://semver.org/spec/v2.0.0.html) except to the first release.
1919
- Support `operation_data` in `crud.Error` (#330)
2020
- Support `fetch_latest_metadata` option for crud requests with metadata (#335)
2121
- Support `noreturn` option for data change crud requests (#335)
22+
- Support `crud.schema` request (#336)
2223

2324
### Changed
2425

Makefile

+1-1
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ clean:
2727
.PHONY: deps
2828
deps: clean
2929
( cd ./queue/testdata; $(TTCTL) rocks install queue 1.3.0 )
30-
( cd ./crud/testdata; $(TTCTL) rocks install crud 1.3.0 )
30+
( cd ./crud/testdata; $(TTCTL) rocks install crud 1.4.0 )
3131

3232
.PHONY: datetime-timezones
3333
datetime-timezones:

crud/example_test.go

+29
Original file line numberDiff line numberDiff line change
@@ -302,3 +302,32 @@ func ExampleSelectRequest_pagination() {
302302
// [{id unsigned false} {bucket_id unsigned true} {name string false}]
303303
// [[3006 32 bla] [3007 33 bla]]
304304
}
305+
306+
func ExampleSchema() {
307+
conn := exampleConnect()
308+
309+
req := crud.MakeSchemaRequest()
310+
var result crud.SchemaResult
311+
312+
if err := conn.Do(req).GetTyped(&result); err != nil {
313+
fmt.Printf("Failed to execute request: %s", err)
314+
return
315+
}
316+
317+
// Schema may differ between different Tarantool versions.
318+
// https://github.com/tarantool/tarantool/issues/4091
319+
// https://github.com/tarantool/tarantool/commit/17c9c034933d726925910ce5bf8b20e8e388f6e3
320+
for spaceName, spaceSchema := range result.Value {
321+
fmt.Printf("Space format for '%s' is as follows:\n", spaceName)
322+
323+
for _, field := range spaceSchema.Format {
324+
fmt.Printf(" - field '%s' with type '%s'\n", field.Name, field.Type)
325+
}
326+
}
327+
328+
// Output:
329+
// Space format for 'test' is as follows:
330+
// - field 'id' with type 'unsigned'
331+
// - field 'bucket_id' with type 'unsigned'
332+
// - field 'name' with type 'string'
333+
}

crud/schema.go

+214
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,214 @@
1+
package crud
2+
3+
import (
4+
"context"
5+
"fmt"
6+
7+
"github.com/vmihailenco/msgpack/v5"
8+
"github.com/vmihailenco/msgpack/v5/msgpcode"
9+
10+
"github.com/tarantool/go-tarantool/v2"
11+
)
12+
13+
func msgpackIsMap(code byte) bool {
14+
return code == msgpcode.Map16 || code == msgpcode.Map32 || msgpcode.IsFixedMap(code)
15+
}
16+
17+
// SchemaRequest helps you to create request object to call `crud.schema`
18+
// for execution by a Connection.
19+
type SchemaRequest struct {
20+
baseRequest
21+
space OptString
22+
}
23+
24+
// MakeSchemaRequest returns a new empty StatsRequest.
25+
func MakeSchemaRequest() SchemaRequest {
26+
req := SchemaRequest{}
27+
req.impl = newCall("crud.schema")
28+
return req
29+
}
30+
31+
// Space sets the space name for the StatsRequest request.
32+
// Note: default value is nil.
33+
func (req SchemaRequest) Space(space string) SchemaRequest {
34+
req.space = MakeOptString(space)
35+
return req
36+
}
37+
38+
// Body fills an encoder with the call request body.
39+
func (req SchemaRequest) Body(res tarantool.SchemaResolver, enc *msgpack.Encoder) error {
40+
if value, ok := req.space.Get(); ok {
41+
req.impl = req.impl.Args([]interface{}{value})
42+
} else {
43+
req.impl = req.impl.Args([]interface{}{})
44+
}
45+
46+
return req.impl.Body(res, enc)
47+
}
48+
49+
// Context sets a passed context to CRUD request.
50+
func (req SchemaRequest) Context(ctx context.Context) SchemaRequest {
51+
req.impl = req.impl.Context(ctx)
52+
53+
return req
54+
}
55+
56+
// Schema contains CRUD cluster schema definition.
57+
type Schema map[string]SpaceSchema
58+
59+
// DecodeMsgpack provides custom msgpack decoder.
60+
func (schema *Schema) DecodeMsgpack(d *msgpack.Decoder) error {
61+
var l int
62+
63+
code, err := d.PeekCode()
64+
if err != nil {
65+
return err
66+
}
67+
68+
if msgpackIsArray(code) {
69+
// Process empty schema case.
70+
l, err = d.DecodeArrayLen()
71+
if err != nil {
72+
return err
73+
}
74+
if l != 0 {
75+
return fmt.Errorf("expected map or empty array, got non-empty array")
76+
}
77+
*schema = make(map[string]SpaceSchema, l)
78+
} else if msgpackIsMap(code) {
79+
l, err := d.DecodeMapLen()
80+
if err != nil {
81+
return err
82+
}
83+
*schema = make(map[string]SpaceSchema, l)
84+
85+
for i := 0; i < l; i++ {
86+
key, err := d.DecodeString()
87+
if err != nil {
88+
return err
89+
}
90+
91+
var spaceSchema SpaceSchema
92+
if err := d.Decode(&spaceSchema); err != nil {
93+
return err
94+
}
95+
96+
(*schema)[key] = spaceSchema
97+
}
98+
} else {
99+
return fmt.Errorf("unexpected code=%d decoding map or empty array", code)
100+
}
101+
102+
return nil
103+
}
104+
105+
// SpaceSchema contains a single CRUD space schema definition.
106+
type SpaceSchema struct {
107+
Format []FieldFormat `msgpack:"format"`
108+
Indexes map[uint32]Index `msgpack:"indexes"`
109+
}
110+
111+
// Index contains a CRUD space index definition.
112+
type Index struct {
113+
Id uint32 `msgpack:"id"`
114+
Name string `msgpack:"name"`
115+
Type string `msgpack:"type"`
116+
Unique bool `msgpack:"unique"`
117+
Parts []IndexPart `msgpack:"parts"`
118+
}
119+
120+
// IndexField contains a CRUD space index part definition.
121+
type IndexPart struct {
122+
Fieldno uint32 `msgpack:"fieldno"`
123+
Type string `msgpack:"type"`
124+
ExcludeNull bool `msgpack:"exclude_null"`
125+
IsNullable bool `msgpack:"is_nullable"`
126+
}
127+
128+
// SchemaResult contains a schema request result for all spaces.
129+
type SchemaResult struct {
130+
Value Schema
131+
}
132+
133+
// DecodeMsgpack provides custom msgpack decoder.
134+
func (result *SchemaResult) DecodeMsgpack(d *msgpack.Decoder) error {
135+
arrLen, err := d.DecodeArrayLen()
136+
if err != nil {
137+
return err
138+
}
139+
140+
if arrLen == 0 {
141+
return fmt.Errorf("unexpected empty response array")
142+
}
143+
144+
// DecodeMapLen inside Schema decode processes `nil` as zero length map,
145+
// so in `return nil, err` case we don't miss error info.
146+
// https://github.com/vmihailenco/msgpack/blob/3f7bd806fea698e7a9fe80979aa3512dea0a7368/decode_map.go#L79-L81
147+
if err = d.Decode(&result.Value); err != nil {
148+
return err
149+
}
150+
151+
if arrLen > 1 {
152+
var crudErr *Error = nil
153+
154+
if err := d.Decode(&crudErr); err != nil {
155+
return err
156+
}
157+
158+
if crudErr != nil {
159+
return crudErr
160+
}
161+
}
162+
163+
for i := 2; i < arrLen; i++ {
164+
if err := d.Skip(); err != nil {
165+
return err
166+
}
167+
}
168+
169+
return nil
170+
}
171+
172+
// SchemaResult contains a schema request result for a single space.
173+
type SpaceSchemaResult struct {
174+
Value SpaceSchema
175+
}
176+
177+
// DecodeMsgpack provides custom msgpack decoder.
178+
func (result *SpaceSchemaResult) DecodeMsgpack(d *msgpack.Decoder) error {
179+
arrLen, err := d.DecodeArrayLen()
180+
if err != nil {
181+
return err
182+
}
183+
184+
if arrLen == 0 {
185+
return fmt.Errorf("unexpected empty response array")
186+
}
187+
188+
// DecodeMapLen inside SpaceSchema decode processes `nil` as zero length map,
189+
// so in `return nil, err` case we don't miss error info.
190+
// https://github.com/vmihailenco/msgpack/blob/3f7bd806fea698e7a9fe80979aa3512dea0a7368/decode_map.go#L79-L81
191+
if err = d.Decode(&result.Value); err != nil {
192+
return err
193+
}
194+
195+
if arrLen > 1 {
196+
var crudErr *Error = nil
197+
198+
if err := d.Decode(&crudErr); err != nil {
199+
return err
200+
}
201+
202+
if crudErr != nil {
203+
return crudErr
204+
}
205+
}
206+
207+
for i := 2; i < arrLen; i++ {
208+
if err := d.Skip(); err != nil {
209+
return err
210+
}
211+
}
212+
213+
return nil
214+
}

crud/tarantool_test.go

+124
Original file line numberDiff line numberDiff line change
@@ -1209,6 +1209,130 @@ func TestNoreturnOptionTyped(t *testing.T) {
12091209
}
12101210
}
12111211

1212+
func getTestSchema(t *testing.T) crud.Schema {
1213+
schema := crud.Schema{
1214+
"test": crud.SpaceSchema{
1215+
Format: []crud.FieldFormat{
1216+
crud.FieldFormat{
1217+
Name: "id",
1218+
Type: "unsigned",
1219+
IsNullable: false,
1220+
},
1221+
{
1222+
Name: "bucket_id",
1223+
Type: "unsigned",
1224+
IsNullable: true,
1225+
},
1226+
{
1227+
Name: "name",
1228+
Type: "string",
1229+
IsNullable: false,
1230+
},
1231+
},
1232+
Indexes: map[uint32]crud.Index{
1233+
0: {
1234+
Id: 0,
1235+
Name: "primary_index",
1236+
Type: "TREE",
1237+
Unique: true,
1238+
Parts: []crud.IndexPart{
1239+
{
1240+
Fieldno: 1,
1241+
Type: "unsigned",
1242+
ExcludeNull: false,
1243+
IsNullable: false,
1244+
},
1245+
},
1246+
},
1247+
},
1248+
},
1249+
}
1250+
1251+
// https://github.com/tarantool/tarantool/issues/4091
1252+
uniqueIssue, err := test_helpers.IsTarantoolVersionLess(2, 2, 1)
1253+
require.Equal(t, err, nil, "expected version check to succeed")
1254+
1255+
if uniqueIssue {
1256+
for sk, sv := range schema {
1257+
for ik, iv := range sv.Indexes {
1258+
iv.Unique = false
1259+
sv.Indexes[ik] = iv
1260+
}
1261+
schema[sk] = sv
1262+
}
1263+
}
1264+
1265+
// https://github.com/tarantool/tarantool/commit/17c9c034933d726925910ce5bf8b20e8e388f6e3
1266+
excludeNullUnsupported, err := test_helpers.IsTarantoolVersionLess(2, 8, 1)
1267+
require.Equal(t, err, nil, "expected version check to succeed")
1268+
1269+
if excludeNullUnsupported {
1270+
for sk, sv := range schema {
1271+
for ik, iv := range sv.Indexes {
1272+
for pk, pv := range iv.Parts {
1273+
// Struct default value.
1274+
pv.ExcludeNull = false
1275+
iv.Parts[pk] = pv
1276+
}
1277+
sv.Indexes[ik] = iv
1278+
}
1279+
schema[sk] = sv
1280+
}
1281+
}
1282+
1283+
return schema
1284+
}
1285+
1286+
func TestSchemaTyped(t *testing.T) {
1287+
conn := connect(t)
1288+
defer conn.Close()
1289+
1290+
req := crud.MakeSchemaRequest()
1291+
var result crud.SchemaResult
1292+
1293+
err := conn.Do(req).GetTyped(&result)
1294+
require.Equal(t, err, nil, "Expected CRUD request to succeed")
1295+
require.Equal(t, result.Value, getTestSchema(t), "map with \"test\" schema expected")
1296+
}
1297+
1298+
func TestSpaceSchemaTyped(t *testing.T) {
1299+
conn := connect(t)
1300+
defer conn.Close()
1301+
1302+
req := crud.MakeSchemaRequest().Space("test")
1303+
var result crud.SpaceSchemaResult
1304+
1305+
err := conn.Do(req).GetTyped(&result)
1306+
require.Equal(t, err, nil, "Expected CRUD request to succeed")
1307+
require.Equal(t, result.Value, getTestSchema(t)["test"], "map with \"test\" schema expected")
1308+
}
1309+
1310+
func TestSpaceSchemaTypedError(t *testing.T) {
1311+
conn := connect(t)
1312+
defer conn.Close()
1313+
1314+
req := crud.MakeSchemaRequest().Space("not_exist")
1315+
var result crud.SpaceSchemaResult
1316+
1317+
err := conn.Do(req).GetTyped(&result)
1318+
require.NotEqual(t, err, nil, "Expected CRUD request to fail")
1319+
require.Regexp(t, "Space \"not_exist\" doesn't exist", err.Error())
1320+
}
1321+
1322+
func TestUnitEmptySchema(t *testing.T) {
1323+
// We need to create another cluster with no spaces
1324+
// to test `{}` schema, so let's at least add a unit test.
1325+
conn := connect(t)
1326+
defer conn.Close()
1327+
1328+
req := tarantool.NewEvalRequest("return {}")
1329+
var result crud.SchemaResult
1330+
1331+
err := conn.Do(req).GetTyped(&result)
1332+
require.Equal(t, err, nil, "Expected CRUD request to succeed")
1333+
require.Equal(t, result.Value, crud.Schema{}, "empty schema expected")
1334+
}
1335+
12121336
// runTestMain is a body of TestMain function
12131337
// (see https://pkg.go.dev/testing#hdr-Main).
12141338
// Using defer + os.Exit is not works so TestMain body

0 commit comments

Comments
 (0)