Skip to content

Commit 9f736f4

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 9f736f4

File tree

6 files changed

+322
-3
lines changed

6 files changed

+322
-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

+220
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,220 @@
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 - if true, prevents an error if the user already exists.
73+
IfNotExists bool `msgpack:"if_not_exists"`
74+
// Password for the new user.
75+
Password string `msgpack:"password"`
76+
}
77+
78+
// UserCreateRequest represents a request to create a new user in Tarantool.
79+
type UserCreateRequest struct {
80+
*tarantool.CallRequest // Underlying Tarantool call request.
81+
}
82+
83+
// NewUserCreateRequest creates a new request to create a user with specified options.
84+
func NewUserCreateRequest(username string, options UserCreateOptions) UserCreateRequest {
85+
callReq := tarantool.NewCallRequest("box.schema.user.create").
86+
Args([]interface{}{username, options})
87+
88+
return UserCreateRequest{
89+
callReq,
90+
}
91+
}
92+
93+
// UserCreateResponse represents the response to a user creation request.
94+
type UserCreateResponse struct {
95+
}
96+
97+
// DecodeMsgpack decodes the response for a user creation request.
98+
// In this case, the response does not contain any data.
99+
func (uer *UserCreateResponse) DecodeMsgpack(_ *msgpack.Decoder) error {
100+
return nil
101+
}
102+
103+
// Create creates a new user in Tarantool with the given username and options.
104+
func (u *SchemaUser) Create(ctx context.Context, username string, options UserCreateOptions) error {
105+
// Create a request and send it to Tarantool.
106+
req := NewUserCreateRequest(username, options).Context(ctx)
107+
resp := &UserCreateResponse{}
108+
109+
// Execute the request and handle the response.
110+
fut := u.conn.Do(req)
111+
112+
err := fut.GetTyped(resp)
113+
if err != nil {
114+
return err
115+
}
116+
117+
return nil
118+
}
119+
120+
// UserDropOptions represents options for dropping a user in Tarantool.
121+
type UserDropOptions struct {
122+
IfExists bool `msgpack:"if_exists"` // If true, prevents an error if the user does not exist.
123+
}
124+
125+
// UserDropRequest represents a request to drop a user from Tarantool.
126+
type UserDropRequest struct {
127+
*tarantool.CallRequest // Underlying Tarantool call request.
128+
}
129+
130+
// NewUserDropRequest creates a new request to drop a user with specified options.
131+
func NewUserDropRequest(username string, options UserDropOptions) UserDropRequest {
132+
callReq := tarantool.NewCallRequest("box.schema.user.drop").
133+
Args([]interface{}{username, options})
134+
135+
return UserDropRequest{
136+
callReq,
137+
}
138+
}
139+
140+
// UserDropResponse represents the response to a user drop request.
141+
type UserDropResponse struct{}
142+
143+
// Drop drops the specified user from Tarantool, with optional conditions.
144+
func (u *SchemaUser) Drop(ctx context.Context, username string, options UserDropOptions) error {
145+
// Create a request and send it to Tarantool.
146+
req := NewUserDropRequest(username, options).Context(ctx)
147+
resp := &UserCreateResponse{}
148+
149+
// Execute the request and handle the response.
150+
fut := u.conn.Do(req)
151+
152+
err := fut.GetTyped(resp)
153+
if err != nil {
154+
return err
155+
}
156+
157+
return nil
158+
}
159+
160+
// UserPasswordRequest represents a request to retrieve a user's password from Tarantool.
161+
type UserPasswordRequest struct {
162+
*tarantool.CallRequest // Underlying Tarantool call request.
163+
}
164+
165+
// NewUserPasswordRequest creates a new request to fetch the user's password.
166+
// It takes the username and constructs the request to Tarantool.
167+
func NewUserPasswordRequest(username string) UserPasswordRequest {
168+
// Create a request to get the user's password.
169+
callReq := tarantool.NewCallRequest("box.schema.user.password").Args([]interface{}{username})
170+
171+
return UserPasswordRequest{
172+
callReq,
173+
}
174+
}
175+
176+
// UserPasswordResponse represents the response to the user password request.
177+
// It contains the password hash.
178+
type UserPasswordResponse struct {
179+
Hash string // The password hash of the user.
180+
}
181+
182+
// DecodeMsgpack decodes the response from Tarantool in Msgpack format.
183+
// It expects the response to be an array of length 1, containing the password hash string.
184+
func (upr *UserPasswordResponse) DecodeMsgpack(d *msgpack.Decoder) error {
185+
// Decode the array length.
186+
arrayLen, err := d.DecodeArrayLen()
187+
if err != nil {
188+
return err
189+
}
190+
191+
// Ensure the array contains exactly 1 element (the password hash).
192+
if arrayLen != 1 {
193+
return fmt.Errorf("protocol violation; expected 1 array entry, got %d", arrayLen)
194+
}
195+
196+
// Decode the string containing the password hash.
197+
upr.Hash, err = d.DecodeString()
198+
199+
return err
200+
}
201+
202+
// Password sends a request to retrieve the user's password from Tarantool.
203+
// It returns the password hash as a string or an error if the request fails.
204+
func (u *SchemaUser) Password(ctx context.Context, username string) (string, error) {
205+
// Create the request and send it to Tarantool.
206+
req := NewUserPasswordRequest(username).Context(ctx)
207+
resp := &UserPasswordResponse{}
208+
209+
// Execute the request and handle the response.
210+
fut := u.conn.Do(req)
211+
212+
// Get the decoded response.
213+
err := fut.GetTyped(resp)
214+
if err != nil {
215+
return "", err
216+
}
217+
218+
// Return the password hash.
219+
return resp.Hash, nil
220+
}

box/tarantool_test.go

+81
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,85 @@ 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{
99+
Password: password,
100+
IfNotExists: true,
101+
})
102+
103+
require.NoError(t, err)
104+
105+
// Require password hash
106+
hash, err := b.Schema().User().Password(ctx, username)
107+
require.NoError(t, err)
108+
require.NotEmpty(t, hash)
109+
110+
// Check that password is valid and we can connect to tarantool with such credentials
111+
var newUserDialer = tarantool.NetDialer{
112+
Address: server,
113+
User: username,
114+
Password: password,
115+
}
116+
117+
// We can connect with our new credentials
118+
newUserConn, err := tarantool.Connect(ctx, newUserDialer, tarantool.Opts{})
119+
require.NoError(t, err)
120+
require.NotNil(t, newUserConn)
121+
require.NoError(t, newUserConn.Close())
122+
123+
// Try to drop user
124+
err = b.Schema().User().Drop(ctx, username, box.UserDropOptions{})
125+
require.NoError(t, err)
126+
127+
// Require error cause user already deleted
128+
err = b.Schema().User().Drop(ctx, username, box.UserDropOptions{})
129+
require.Error(t, err)
130+
131+
// Require that error code is ER_NO_SUCH_USER
132+
errors.As(err, &boxErr)
133+
require.Equal(t, iproto.ER_NO_SUCH_USER, boxErr.Code)
134+
135+
// No error with option IfExists: true
136+
err = b.Schema().User().Drop(ctx, username, box.UserDropOptions{IfExists: true})
137+
require.NoError(t, err)
138+
139+
// Check that user not exists after drop
140+
exists, err = b.Schema().User().Exists(ctx, username)
141+
require.False(t, exists)
142+
require.NoError(t, err)
143+
}
144+
64145
func runTestMain(m *testing.M) int {
65146
instance, err := test_helpers.StartTarantool(test_helpers.StartOpts{
66147
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)