Skip to content

Commit 2e271da

Browse files
author
maksim.konovalov
committed
Added logic for working with Tarantool schema via Box
- Implemented the `box.Schema()` method that returns a `Schema` object for schema-related operations
1 parent 7eae014 commit 2e271da

File tree

6 files changed

+301
-3
lines changed

6 files changed

+301
-3
lines changed

CHANGELOG.md

+2
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ Versioning](http://semver.org/spec/v2.0.0.html) except to the first release.
1010

1111
### Added
1212

13+
- Implemented box.schema.user operations requests and sugar interface.
14+
1315
### Changed
1416

1517
### Fixed

box/box_test.go

-1
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@ import (
1010
func TestNew(t *testing.T) {
1111
// Create a box instance with a nil connection. This should lead to a panic later.
1212
b := box.New(nil)
13-
1413
// Ensure the box instance is not nil (which it shouldn't be), but this is not meaningful
1514
// since we will panic when we call the Info method with the nil connection.
1615
require.NotNil(t, b)

box/schema.go

+17
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
package box
2+
3+
import "github.com/tarantool/go-tarantool/v2"
4+
5+
// Schema represents the schema-related operations in Tarantool.
6+
// It holds a connection to interact with the Tarantool instance.
7+
type Schema struct {
8+
conn tarantool.Doer // Connection interface for interacting with Tarantool.
9+
}
10+
11+
// Schema returns a new Schema instance, providing access to schema-related operations.
12+
// It uses the connection from the Box instance to communicate with Tarantool.
13+
func (b *Box) Schema() *Schema {
14+
return &Schema{
15+
conn: b.conn, // Pass the Box connection to the Schema.
16+
}
17+
}

box/schema_user.go

+203
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,203 @@
1+
package box
2+
3+
import (
4+
"context"
5+
"fmt"
6+
7+
"github.com/tarantool/go-tarantool/v2"
8+
"github.com/vmihailenco/msgpack/v5"
9+
)
10+
11+
// SchemaUser provides methods to interact with schema-related user operations in Tarantool.
12+
type SchemaUser struct {
13+
conn tarantool.Doer // Connection interface for interacting with Tarantool.
14+
}
15+
16+
// User returns a new SchemaUser instance, allowing schema-related user operations.
17+
func (s *Schema) User() *SchemaUser {
18+
return &SchemaUser{conn: s.conn}
19+
}
20+
21+
// UserExistsRequest represents a request to check if a user exists in Tarantool.
22+
type UserExistsRequest struct {
23+
*tarantool.CallRequest // Underlying Tarantool call request.
24+
}
25+
26+
// UserExistsResponse represents the response to a user existence check.
27+
type UserExistsResponse struct {
28+
Exists bool // True if the user exists, false otherwise.
29+
}
30+
31+
// DecodeMsgpack decodes the response from a Msgpack-encoded byte slice.
32+
func (uer *UserExistsResponse) DecodeMsgpack(d *msgpack.Decoder) error {
33+
arrayLen, err := d.DecodeArrayLen()
34+
if err != nil {
35+
return err
36+
}
37+
38+
// Ensure that the response array contains exactly 1 element (the "Exists" field).
39+
if arrayLen != 1 {
40+
return fmt.Errorf("protocol violation; expected 1 array entry, got %d", arrayLen)
41+
}
42+
43+
// Decode the boolean value indicating whether the user exists.
44+
uer.Exists, err = d.DecodeBool()
45+
46+
return err
47+
}
48+
49+
// NewUserExistsRequest creates a new request to check if a user exists.
50+
func NewUserExistsRequest(username string) UserExistsRequest {
51+
callReq := tarantool.NewCallRequest("box.schema.user.exists").Args([]interface{}{username})
52+
53+
return UserExistsRequest{
54+
callReq,
55+
}
56+
}
57+
58+
// Exists checks if the specified user exists in Tarantool.
59+
func (u *SchemaUser) Exists(ctx context.Context, username string) (bool, error) {
60+
// Create a request and send it to Tarantool.
61+
req := NewUserExistsRequest(username).Context(ctx)
62+
resp := &UserExistsResponse{}
63+
64+
// Execute the request and parse the response.
65+
err := u.conn.Do(req).GetTyped(resp)
66+
67+
return resp.Exists, err
68+
}
69+
70+
// UserCreateOptions represents options for creating a user in Tarantool.
71+
type UserCreateOptions struct {
72+
IfNotExists bool `msgpack:"if_not_exists"` // If true, prevents an error if the user already exists.
73+
Password string `msgpack:"password"` // The password for the new user.
74+
}
75+
76+
// UserCreateRequest represents a request to create a new user in Tarantool.
77+
type UserCreateRequest struct {
78+
*tarantool.CallRequest // Underlying Tarantool call request.
79+
}
80+
81+
// NewUserCreateRequest creates a new request to create a user with specified options.
82+
func NewUserCreateRequest(username string, options UserCreateOptions) UserCreateRequest {
83+
callReq := tarantool.NewCallRequest("box.schema.user.create").Args([]interface{}{username, options})
84+
85+
return UserCreateRequest{
86+
callReq,
87+
}
88+
}
89+
90+
// UserCreateResponse represents the response to a user creation request.
91+
type UserCreateResponse struct {
92+
}
93+
94+
// DecodeMsgpack decodes the response for a user creation request.
95+
// In this case, the response does not contain any data.
96+
func (uer *UserCreateResponse) DecodeMsgpack(_ *msgpack.Decoder) error {
97+
return nil
98+
}
99+
100+
// Create creates a new user in Tarantool with the given username and options.
101+
func (u *SchemaUser) Create(ctx context.Context, username string, options UserCreateOptions) error {
102+
// Create a request and send it to Tarantool.
103+
req := NewUserCreateRequest(username, options).Context(ctx)
104+
resp := &UserCreateResponse{}
105+
106+
// Execute the request and handle the response.
107+
fut := u.conn.Do(req)
108+
109+
err := fut.GetTyped(resp)
110+
if err != nil {
111+
return err
112+
}
113+
114+
return nil
115+
}
116+
117+
// UserDropOptions represents options for dropping a user in Tarantool.
118+
type UserDropOptions struct {
119+
IfExists bool `msgpack:"if_exists"` // If true, prevents an error if the user does not exist.
120+
}
121+
122+
// UserDropRequest represents a request to drop a user from Tarantool.
123+
type UserDropRequest struct {
124+
*tarantool.CallRequest // Underlying Tarantool call request.
125+
}
126+
127+
// NewUserDropRequest creates a new request to drop a user with specified options.
128+
func NewUserDropRequest(username string, options UserDropOptions) UserDropRequest {
129+
callReq := tarantool.NewCallRequest("box.schema.user.drop").Args([]interface{}{username, options})
130+
131+
return UserDropRequest{
132+
callReq,
133+
}
134+
}
135+
136+
// UserDropResponse represents the response to a user drop request.
137+
type UserDropResponse struct{}
138+
139+
// Drop drops the specified user from Tarantool, with optional conditions.
140+
func (u *SchemaUser) Drop(ctx context.Context, username string, options UserDropOptions) error {
141+
// Create a request and send it to Tarantool.
142+
req := NewUserDropRequest(username, options).Context(ctx)
143+
resp := &UserCreateResponse{}
144+
145+
// Execute the request and handle the response.
146+
fut := u.conn.Do(req)
147+
148+
err := fut.GetTyped(resp)
149+
if err != nil {
150+
return err
151+
}
152+
153+
return nil
154+
}
155+
156+
type UserPasswordRequest struct {
157+
*tarantool.CallRequest // Underlying Tarantool call request.
158+
}
159+
160+
func NewUserPasswordRequest(username string) UserPasswordRequest {
161+
callReq := tarantool.NewCallRequest("box.schema.user.password").Args([]interface{}{username})
162+
163+
return UserPasswordRequest{
164+
callReq,
165+
}
166+
}
167+
168+
type UserPasswordResponse struct {
169+
Hash string
170+
}
171+
172+
func (upr *UserPasswordResponse) DecodeMsgpack(d *msgpack.Decoder) error {
173+
arrayLen, err := d.DecodeArrayLen()
174+
if err != nil {
175+
return err
176+
}
177+
178+
// Ensure that the response array contains exactly 1 element (the "Exists" field).
179+
if arrayLen != 1 {
180+
return fmt.Errorf("protocol violation; expected 1 array entry, got %d", arrayLen)
181+
}
182+
183+
// Decode the boolean value indicating whether the user exists.
184+
upr.Hash, err = d.DecodeString()
185+
186+
return err
187+
}
188+
189+
func (u *SchemaUser) Password(ctx context.Context, username string) (string, error) {
190+
// Create a request and send it to Tarantool.
191+
req := NewUserPasswordRequest(username).Context(ctx)
192+
resp := &UserPasswordResponse{}
193+
194+
// Execute the request and handle the response.
195+
fut := u.conn.Do(req)
196+
197+
err := fut.GetTyped(resp)
198+
if err != nil {
199+
return "", err
200+
}
201+
202+
return resp.Hash, nil
203+
}

box/tarantool_test.go

+77
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,15 @@ package box_test
22

33
import (
44
"context"
5+
"errors"
56
"log"
67
"os"
78
"testing"
89
"time"
910

1011
"github.com/google/uuid"
1112
"github.com/stretchr/testify/require"
13+
"github.com/tarantool/go-iproto"
1214
"github.com/tarantool/go-tarantool/v2"
1315
"github.com/tarantool/go-tarantool/v2/box"
1416
"github.com/tarantool/go-tarantool/v2/test_helpers"
@@ -61,6 +63,81 @@ func TestBox_Info(t *testing.T) {
6163
validateInfo(t, resp.Info)
6264
}
6365

66+
func TestBox_Sugar_Schema(t *testing.T) {
67+
const (
68+
username = "opensource"
69+
password = "enterprise"
70+
)
71+
72+
ctx := context.TODO()
73+
74+
conn, err := tarantool.Connect(ctx, dialer, tarantool.Opts{})
75+
require.NoError(t, err)
76+
77+
b := box.New(conn)
78+
79+
// Create new user
80+
err = b.Schema().User().Create(ctx, username, box.UserCreateOptions{Password: password})
81+
require.NoError(t, err)
82+
83+
// Get error that user already exists
84+
err = b.Schema().User().Create(ctx, username, box.UserCreateOptions{Password: password})
85+
require.Error(t, err)
86+
87+
// Require that error code is ER_USER_EXISTS
88+
var boxErr tarantool.Error
89+
errors.As(err, &boxErr)
90+
require.Equal(t, iproto.ER_USER_EXISTS, boxErr.Code)
91+
92+
// Check that already exists by exists call procedure
93+
exists, err := b.Schema().User().Exists(ctx, username)
94+
require.True(t, exists)
95+
require.NoError(t, err)
96+
97+
// There is no error if IfNotExists option is true
98+
err = b.Schema().User().Create(ctx, username, box.UserCreateOptions{Password: password, IfNotExists: true})
99+
require.NoError(t, err)
100+
101+
// Require password hash
102+
hash, err := b.Schema().User().Password(ctx, username)
103+
require.NoError(t, err)
104+
require.NotEmpty(t, hash)
105+
106+
// Check that password is valid and we can connect to tarantool with such credentials
107+
var newUserDialer = tarantool.NetDialer{
108+
Address: server,
109+
User: username,
110+
Password: password,
111+
}
112+
113+
// We can connect with our new credentials
114+
newUserConn, err := tarantool.Connect(ctx, newUserDialer, tarantool.Opts{})
115+
require.NoError(t, err)
116+
require.NotNil(t, newUserConn)
117+
require.NoError(t, newUserConn.Close())
118+
119+
// Try to drop user
120+
err = b.Schema().User().Drop(ctx, username, box.UserDropOptions{})
121+
require.NoError(t, err)
122+
123+
// Require error cause user already deleted
124+
err = b.Schema().User().Drop(ctx, username, box.UserDropOptions{})
125+
require.Error(t, err)
126+
127+
// Require that error code is ER_NO_SUCH_USER
128+
errors.As(err, &boxErr)
129+
require.Equal(t, iproto.ER_NO_SUCH_USER, boxErr.Code)
130+
131+
// No error with option IfExists: true
132+
err = b.Schema().User().Drop(ctx, username, box.UserDropOptions{IfExists: true})
133+
require.NoError(t, err)
134+
135+
// Check that user not exists after drop
136+
exists, err = b.Schema().User().Exists(ctx, username)
137+
require.False(t, exists)
138+
require.NoError(t, err)
139+
}
140+
64141
func runTestMain(m *testing.M) int {
65142
instance, err := test_helpers.StartTarantool(test_helpers.StartOpts{
66143
Dialer: dialer,

box/testdata/config.lua

+2-2
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,9 @@ box.cfg{
55
}
66

77
box.schema.user.create('test', { password = 'test' , if_not_exists = true })
8-
box.schema.user.grant('test', 'execute', 'universe', nil, { if_not_exists = true })
8+
box.schema.user.grant('test', 'super', nil)
99

1010
-- Set listen only when every other thing is configured.
1111
box.cfg{
1212
listen = os.getenv("TEST_TNT_LISTEN"),
13-
}
13+
}

0 commit comments

Comments
 (0)