Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit b82d157

Browse files
author
maksim.konovalov
committedJan 28, 2025·
box: added logic for working with Tarantool schema
Implemented the `box.Schema()` method that returns a `Schema` object for schema-related operations
1 parent d8e2284 commit b82d157

File tree

13 files changed

+1436
-50
lines changed

13 files changed

+1436
-50
lines changed
 

‎CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ Versioning](http://semver.org/spec/v2.0.0.html) except to the first release.
1111
### Added
1212

1313
- Extend box with replication information (#427).
14+
- Implemented all box.schema.user operations requests and sugar interface (#426).
15+
- Implemented box.session.su request and sugar interface only for current session granting (#426).
1416

1517
### Changed
1618

‎box/box.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,12 @@ func New(conn tarantool.Doer) *Box {
1717
}
1818
}
1919

20+
// Schema returns a new Schema instance, providing access to schema-related operations.
21+
// It uses the connection from the Box instance to communicate with Tarantool.
22+
func (b *Box) Schema() *Schema {
23+
return NewSchema(b.conn)
24+
}
25+
2026
// Info retrieves the current information of the Tarantool instance.
2127
// It calls the "box.info" function and parses the result into the Info structure.
2228
func (b *Box) Info() (Info, error) {

‎box/example_test.go

Lines changed: 127 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ import (
1818
"github.com/tarantool/go-tarantool/v2/box"
1919
)
2020

21-
func Example() {
21+
func ExampleBox_Info() {
2222
dialer := tarantool.NetDialer{
2323
Address: "127.0.0.1:3013",
2424
User: "test",
@@ -58,3 +58,129 @@ func Example() {
5858
fmt.Printf("Box info uuids are equal")
5959
fmt.Printf("Current box info: %+v\n", resp.Info)
6060
}
61+
62+
func ExampleSchemaUser_Exists() {
63+
dialer := tarantool.NetDialer{
64+
Address: "127.0.0.1:3013",
65+
User: "test",
66+
Password: "test",
67+
}
68+
ctx, cancel := context.WithTimeout(context.Background(), 500*time.Millisecond)
69+
client, err := tarantool.Connect(ctx, dialer, tarantool.Opts{})
70+
cancel()
71+
if err != nil {
72+
log.Fatalf("Failed to connect: %s", err)
73+
}
74+
75+
// You can use UserExistsRequest type and call it directly.
76+
fut := client.Do(box.NewUserExistsRequest("user"))
77+
78+
resp := &box.UserExistsResponse{}
79+
80+
err = fut.GetTyped(resp)
81+
if err != nil {
82+
log.Fatalf("Failed get box schema user exists with error: %s", err)
83+
}
84+
85+
// Or use simple User implementation.
86+
b := box.New(client)
87+
exists, err := b.Schema().User().Exists(ctx, "user")
88+
if err != nil {
89+
log.Fatalf("Failed get box schema user exists with error: %s", err)
90+
}
91+
92+
if exists != resp.Exists {
93+
log.Fatalf("Box schema users exists are not equal")
94+
}
95+
96+
fmt.Printf("Box schema users exists are equal")
97+
fmt.Printf("Current exists state: %+v\n", exists)
98+
}
99+
100+
func ExampleSchemaUser_Create() {
101+
// Connect to Tarantool.
102+
dialer := tarantool.NetDialer{
103+
Address: "127.0.0.1:3013",
104+
User: "test",
105+
Password: "test",
106+
}
107+
ctx, cancel := context.WithTimeout(context.Background(), 500*time.Millisecond)
108+
client, err := tarantool.Connect(ctx, dialer, tarantool.Opts{})
109+
cancel()
110+
if err != nil {
111+
log.Fatalf("Failed to connect: %s", err)
112+
}
113+
114+
// Create SchemaUser.
115+
schemaUser := box.NewSchemaUser(client)
116+
117+
// Create a new user.
118+
username := "new_user"
119+
options := box.UserCreateOptions{
120+
IfNotExists: true,
121+
Password: "secure_password",
122+
}
123+
err = schemaUser.Create(ctx, username, options)
124+
if err != nil {
125+
log.Fatalf("Failed to create user: %s", err)
126+
}
127+
128+
fmt.Printf("User '%s' created successfully\n", username)
129+
}
130+
131+
func ExampleSchemaUser_Drop() {
132+
// Connect to Tarantool.
133+
dialer := tarantool.NetDialer{
134+
Address: "127.0.0.1:3013",
135+
User: "test",
136+
Password: "test",
137+
}
138+
ctx, cancel := context.WithTimeout(context.Background(), 500*time.Millisecond)
139+
client, err := tarantool.Connect(ctx, dialer, tarantool.Opts{})
140+
cancel()
141+
if err != nil {
142+
log.Fatalf("Failed to connect: %s", err)
143+
}
144+
145+
// Create SchemaUser.
146+
schemaUser := box.NewSchemaUser(client)
147+
148+
// Drop an existing user.
149+
username := "new_user"
150+
options := box.UserDropOptions{
151+
IfExists: true,
152+
}
153+
err = schemaUser.Drop(ctx, username, options)
154+
if err != nil {
155+
log.Fatalf("Failed to drop user: %s", err)
156+
}
157+
158+
fmt.Printf("User '%s' dropped successfully\n", username)
159+
}
160+
161+
func ExampleSchemaUser_Password() {
162+
// Connect to Tarantool.
163+
dialer := tarantool.NetDialer{
164+
Address: "127.0.0.1:3013",
165+
User: "test",
166+
Password: "test",
167+
}
168+
ctx, cancel := context.WithTimeout(context.Background(), 500*time.Millisecond)
169+
client, err := tarantool.Connect(ctx, dialer, tarantool.Opts{})
170+
cancel()
171+
if err != nil {
172+
log.Fatalf("Failed to connect: %s", err)
173+
}
174+
175+
// Create SchemaUser.
176+
schemaUser := box.NewSchemaUser(client)
177+
178+
// Get the password hash.
179+
password := "my-password"
180+
passwordHash, err := schemaUser.Password(ctx, password)
181+
if err != nil {
182+
log.Fatalf("Failed to get password hash: %s", err)
183+
}
184+
185+
fmt.Printf("Password '%s' hash: %s\n", password, passwordHash)
186+
}

‎box/info.go

Lines changed: 6 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -112,18 +112,14 @@ func (ir *InfoResponse) DecodeMsgpack(d *msgpack.Decoder) error {
112112
// InfoRequest represents a request to retrieve information about the Tarantool instance.
113113
// It implements the tarantool.Request interface.
114114
type InfoRequest struct {
115-
baseRequest
116-
}
117-
118-
// Body method is used to serialize the request's body.
119-
// It is part of the tarantool.Request interface implementation.
120-
func (i InfoRequest) Body(res tarantool.SchemaResolver, enc *msgpack.Encoder) error {
121-
return i.impl.Body(res, enc)
115+
*tarantool.CallRequest // Underlying Tarantool call request.
122116
}
123117

124118
// NewInfoRequest returns a new empty info request.
125119
func NewInfoRequest() InfoRequest {
126-
req := InfoRequest{}
127-
req.impl = newCall("box.info")
128-
return req
120+
callReq := tarantool.NewCallRequest("box.info")
121+
122+
return InfoRequest{
123+
callReq,
124+
}
129125
}

‎box/request.go

Lines changed: 0 additions & 38 deletions
This file was deleted.

‎box/schema.go

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
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+
// NewSchema creates a new Schema instance with the provided Tarantool connection.
12+
// It initializes a Schema object that can be used for schema-related operations
13+
// such as managing users, tables, and other schema elements in the Tarantool instance.
14+
func NewSchema(conn tarantool.Doer) *Schema {
15+
return &Schema{conn: conn} // Pass the connection to the Schema.
16+
}
17+
18+
// User returns a new SchemaUser instance, allowing schema-related user operations.
19+
func (s *Schema) User() *SchemaUser {
20+
return NewSchemaUser(s.conn)
21+
}

‎box/schema_test.go

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
package box
2+
3+
import (
4+
"context"
5+
"testing"
6+
7+
"github.com/stretchr/testify/require"
8+
"github.com/tarantool/go-tarantool/v2"
9+
"github.com/tarantool/go-tarantool/v2/test_helpers"
10+
)
11+
12+
func TestNewSchema(t *testing.T) {
13+
ctx := context.Background()
14+
15+
// Create a schema instance with a nil connection. This should lead to a panic later.
16+
b := NewSchema(nil)
17+
18+
// Ensure the schema is not nil (which it shouldn't be), but this is not meaningful
19+
// since we will panic when we call the schema methods with the nil connection.
20+
t.Run("internal sugar sub-objects not panics", func(t *testing.T) {
21+
require.NotNil(t, b)
22+
require.NotNil(t, b.User())
23+
})
24+
25+
t.Run("check that connections are equal", func(t *testing.T) {
26+
var tCases []tarantool.Doer
27+
for i := 0; i < 10; i++ {
28+
29+
doer := test_helpers.NewMockDoer(t)
30+
tCases = append(tCases, &doer)
31+
}
32+
33+
for _, tCase := range tCases {
34+
sch := NewSchema(tCase)
35+
require.Equal(t, tCase, sch.conn)
36+
require.Equal(t, tCase, sch.User().conn)
37+
}
38+
})
39+
40+
t.Run("nil conn panics", func(t *testing.T) {
41+
require.Panics(t, func() {
42+
_, _ = b.User().Info(ctx, "panic-on")
43+
})
44+
})
45+
}

‎box/schema_user.go

Lines changed: 542 additions & 0 deletions
Large diffs are not rendered by default.

‎box/schema_user_test.go

Lines changed: 191 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,191 @@
1+
package box
2+
3+
import (
4+
"bytes"
5+
"context"
6+
"fmt"
7+
"testing"
8+
9+
"github.com/stretchr/testify/require"
10+
"github.com/tarantool/go-tarantool/v2"
11+
"github.com/vmihailenco/msgpack/v5"
12+
"github.com/vmihailenco/msgpack/v5/msgpcode"
13+
)
14+
15+
func TestNewSchemaUser(t *testing.T) {
16+
// Create a schema user instance with a nil connection. This should lead to a panic later.
17+
su := NewSchemaUser(nil)
18+
19+
// Ensure the schema user is not nil (which it shouldn't be), but this is not meaningful
20+
// since we will panic when we call some method with the nil connection.
21+
require.NotNil(t, su)
22+
23+
// We expect a panic because we are passing a nil connection (nil Doer) to the By function.
24+
// The library does not control this zone, and the nil connection would cause a runtime error
25+
// when we attempt to call methods (like Info) on it.
26+
// This test ensures that such an invalid state is correctly handled by causing a panic,
27+
// as it's outside the library's responsibility.
28+
require.Panics(t, func() {
29+
// Calling Exists on a schema user with a nil connection will result in a panic,
30+
// since the underlying connection (Doer) cannot perform the requested action (it's nil).
31+
_, _ = su.Exists(context.TODO(), "test")
32+
})
33+
}
34+
35+
func TestUserExistsResponse_DecodeMsgpack(t *testing.T) {
36+
tCases := map[bool]func() *bytes.Buffer{
37+
true: func() *bytes.Buffer {
38+
buf := bytes.NewBuffer(nil)
39+
buf.WriteByte(msgpcode.FixedArrayLow | byte(1))
40+
buf.WriteByte(msgpcode.True)
41+
42+
return buf
43+
},
44+
false: func() *bytes.Buffer {
45+
buf := bytes.NewBuffer(nil)
46+
buf.WriteByte(msgpcode.FixedArrayLow | byte(1))
47+
buf.WriteByte(msgpcode.False)
48+
49+
return buf
50+
},
51+
}
52+
53+
for tCaseBool, tCaseBuf := range tCases {
54+
tCaseBool := tCaseBool
55+
tCaseBuf := tCaseBuf()
56+
57+
t.Run(fmt.Sprintf("case: %t", tCaseBool), func(t *testing.T) {
58+
t.Parallel()
59+
60+
resp := UserExistsResponse{}
61+
62+
require.NoError(t, resp.DecodeMsgpack(msgpack.NewDecoder(tCaseBuf)))
63+
require.Equal(t, tCaseBool, resp.Exists)
64+
})
65+
}
66+
67+
}
68+
69+
func TestUserPasswordResponse_DecodeMsgpack(t *testing.T) {
70+
tCases := []string{
71+
"test",
72+
"$tr0ng_pass",
73+
}
74+
75+
for _, tCase := range tCases {
76+
tCase := tCase
77+
78+
t.Run(tCase, func(t *testing.T) {
79+
t.Parallel()
80+
buf := bytes.NewBuffer(nil)
81+
buf.WriteByte(msgpcode.FixedArrayLow | byte(1))
82+
83+
bts, err := msgpack.Marshal(tCase)
84+
require.NoError(t, err)
85+
buf.Write(bts)
86+
87+
resp := UserPasswordResponse{}
88+
89+
err = resp.DecodeMsgpack(msgpack.NewDecoder(buf))
90+
require.NoError(t, err)
91+
require.Equal(t, tCase, resp.Hash)
92+
})
93+
}
94+
95+
}
96+
97+
func FuzzUserPasswordResponse_DecodeMsgpack(f *testing.F) {
98+
f.Fuzz(func(t *testing.T, orig string) {
99+
buf := bytes.NewBuffer(nil)
100+
buf.WriteByte(msgpcode.FixedArrayLow | byte(1))
101+
102+
bts, err := msgpack.Marshal(orig)
103+
require.NoError(t, err)
104+
buf.Write(bts)
105+
106+
resp := UserPasswordResponse{}
107+
108+
err = resp.DecodeMsgpack(msgpack.NewDecoder(buf))
109+
require.NoError(t, err)
110+
require.Equal(t, orig, resp.Hash)
111+
})
112+
}
113+
114+
func TestNewUserExistsRequest(t *testing.T) {
115+
t.Parallel()
116+
117+
req := UserExistsRequest{}
118+
119+
require.NotPanics(t, func() {
120+
req = NewUserExistsRequest("test")
121+
})
122+
123+
require.Implements(t, (*tarantool.Request)(nil), req)
124+
}
125+
126+
func TestNewUserCreateRequest(t *testing.T) {
127+
t.Parallel()
128+
129+
req := UserCreateRequest{}
130+
131+
require.NotPanics(t, func() {
132+
req = NewUserCreateRequest("test", UserCreateOptions{})
133+
})
134+
135+
require.Implements(t, (*tarantool.Request)(nil), req)
136+
}
137+
138+
func TestNewUserDropRequest(t *testing.T) {
139+
t.Parallel()
140+
141+
req := UserDropRequest{}
142+
143+
require.NotPanics(t, func() {
144+
req = NewUserDropRequest("test", UserDropOptions{})
145+
})
146+
147+
require.Implements(t, (*tarantool.Request)(nil), req)
148+
}
149+
150+
func TestNewUserPasswordRequest(t *testing.T) {
151+
t.Parallel()
152+
153+
req := UserPasswordRequest{}
154+
155+
require.NotPanics(t, func() {
156+
req = NewUserPasswordRequest("test")
157+
})
158+
159+
require.Implements(t, (*tarantool.Request)(nil), req)
160+
}
161+
162+
func TestNewUserPasswdRequest(t *testing.T) {
163+
t.Parallel()
164+
165+
var err error
166+
req := UserPasswdRequest{}
167+
168+
require.NotPanics(t, func() {
169+
req, err = NewUserPasswdRequest("test")
170+
require.NoError(t, err)
171+
})
172+
173+
_, err = NewUserPasswdRequest()
174+
require.Errorf(t, err, "invalid arguments count")
175+
176+
require.Implements(t, (*tarantool.Request)(nil), req)
177+
}
178+
179+
func TestNewUserInfoRequest(t *testing.T) {
180+
t.Parallel()
181+
182+
var err error
183+
req := UserInfoRequest{}
184+
185+
require.NotPanics(t, func() {
186+
req = NewUserInfoRequest("test")
187+
require.NoError(t, err)
188+
})
189+
190+
require.Implements(t, (*tarantool.Request)(nil), req)
191+
}

‎box/session.go

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
package box
2+
3+
import (
4+
"context"
5+
"fmt"
6+
7+
"github.com/tarantool/go-tarantool/v2"
8+
)
9+
10+
// Session struct represents a connection session to Tarantool.
11+
type Session struct {
12+
conn tarantool.Doer // Connection interface for interacting with Tarantool.
13+
}
14+
15+
// NewSession creates a new Session instance, taking a Tarantool connection as an argument.
16+
func NewSession(conn tarantool.Doer) *Session {
17+
return &Session{conn: conn} // Pass the connection to the Session structure.
18+
}
19+
20+
// Session method returns a new Session object associated with the Box instance.
21+
func (b *Box) Session() *Session {
22+
return NewSession(b.conn)
23+
}
24+
25+
// SessionSuRequest struct wraps a Tarantool call request specifically for session switching.
26+
type SessionSuRequest struct {
27+
*tarantool.CallRequest // Underlying Tarantool call request.
28+
}
29+
30+
// NewSessionSuRequest creates a new SessionSuRequest for switching session to a specified username.
31+
// It returns an error if any execute functions are provided, as they are not supported now.
32+
func NewSessionSuRequest(username string, execute ...any) (SessionSuRequest, error) {
33+
args := []interface{}{username} // Create args slice with the username.
34+
35+
// Check if any execute functions were provided and return an error if so.
36+
if len(execute) > 0 {
37+
return SessionSuRequest{},
38+
fmt.Errorf("user functions call inside su command is unsupported now," +
39+
" because Tarantool needs functions signature instead of name")
40+
}
41+
42+
// Create a new call request for the box.session.su method with the given args.
43+
callReq := tarantool.NewCallRequest("box.session.su").Args(args)
44+
45+
return SessionSuRequest{
46+
callReq, // Return the new SessionSuRequest containing the call request.
47+
}, nil
48+
}
49+
50+
// Su method is used to switch the session to the specified username.
51+
// It sends the request to Tarantool and returns a future response or an error.
52+
func (s *Session) Su(ctx context.Context, username string,
53+
execute ...any) (*tarantool.Future, error) {
54+
// Create a request and send it to Tarantool.
55+
req, err := NewSessionSuRequest(username, execute...)
56+
if err != nil {
57+
return nil, err // Return any errors encountered while creating the request.
58+
}
59+
60+
req.Context(ctx) // Attach the context to the request for cancellation and timeout.
61+
62+
// Execute the request and return the future response, or an error.
63+
return s.conn.Do(req), nil
64+
}

‎box/session_test.go

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
package box
2+
3+
import (
4+
"testing"
5+
6+
"github.com/stretchr/testify/require"
7+
)
8+
9+
func TestBox_Session(t *testing.T) {
10+
b := New(nil)
11+
require.NotNil(t, b.Session())
12+
}
13+
14+
func TestNewSession(t *testing.T) {
15+
require.NotPanics(t, func() {
16+
NewSession(nil)
17+
})
18+
}
19+
20+
func TestNewSessionSuRequest(t *testing.T) {
21+
_, err := NewSessionSuRequest("admin", 1, 2, 3)
22+
require.Error(t, err, "error should be occurred, because of tarantool signature requires")
23+
24+
_, err = NewSessionSuRequest("admin")
25+
require.NoError(t, err)
26+
}

‎box/tarantool_test.go

Lines changed: 403 additions & 0 deletions
Large diffs are not rendered by default.

‎box/testdata/config.lua

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,10 @@ box.cfg{
44
work_dir = os.getenv("TEST_TNT_WORK_DIR"),
55
}
66

7+
box.schema.space.create('space1')
8+
79
box.schema.user.create('test', { password = 'test' , if_not_exists = true })
8-
box.schema.user.grant('test', 'execute', 'universe', nil, { if_not_exists = true })
10+
box.schema.user.grant('test', 'super', nil, nil, { if_not_exists = true })
911

1012
-- Set listen only when every other thing is configured.
1113
box.cfg{

0 commit comments

Comments
 (0)
Please sign in to comment.