Skip to content

Commit

Permalink
Added logic for working with Tarantool schema via Box
Browse files Browse the repository at this point in the history
- Implemented the `box.Schema()` method that returns a `Schema` object for schema-related operations
  • Loading branch information
maksim.konovalov committed Dec 27, 2024
1 parent 7eae014 commit 2e271da
Show file tree
Hide file tree
Showing 6 changed files with 301 additions and 3 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ Versioning](http://semver.org/spec/v2.0.0.html) except to the first release.

### Added

- Implemented box.schema.user operations requests and sugar interface.

### Changed

### Fixed
Expand Down
1 change: 0 additions & 1 deletion box/box_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ import (
func TestNew(t *testing.T) {
// Create a box instance with a nil connection. This should lead to a panic later.
b := box.New(nil)

// Ensure the box instance is not nil (which it shouldn't be), but this is not meaningful
// since we will panic when we call the Info method with the nil connection.
require.NotNil(t, b)
Expand Down
17 changes: 17 additions & 0 deletions box/schema.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package box

import "github.com/tarantool/go-tarantool/v2"

// Schema represents the schema-related operations in Tarantool.
// It holds a connection to interact with the Tarantool instance.
type Schema struct {
conn tarantool.Doer // Connection interface for interacting with Tarantool.
}

// Schema returns a new Schema instance, providing access to schema-related operations.
// It uses the connection from the Box instance to communicate with Tarantool.
func (b *Box) Schema() *Schema {
return &Schema{
conn: b.conn, // Pass the Box connection to the Schema.
}
}
203 changes: 203 additions & 0 deletions box/schema_user.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,203 @@
package box

import (
"context"
"fmt"

"github.com/tarantool/go-tarantool/v2"
"github.com/vmihailenco/msgpack/v5"
)

// SchemaUser provides methods to interact with schema-related user operations in Tarantool.
type SchemaUser struct {
conn tarantool.Doer // Connection interface for interacting with Tarantool.
}

// User returns a new SchemaUser instance, allowing schema-related user operations.
func (s *Schema) User() *SchemaUser {
return &SchemaUser{conn: s.conn}
}

// UserExistsRequest represents a request to check if a user exists in Tarantool.
type UserExistsRequest struct {
*tarantool.CallRequest // Underlying Tarantool call request.
}

// UserExistsResponse represents the response to a user existence check.
type UserExistsResponse struct {
Exists bool // True if the user exists, false otherwise.
}

// DecodeMsgpack decodes the response from a Msgpack-encoded byte slice.
func (uer *UserExistsResponse) DecodeMsgpack(d *msgpack.Decoder) error {
arrayLen, err := d.DecodeArrayLen()
if err != nil {
return err
}

// Ensure that the response array contains exactly 1 element (the "Exists" field).
if arrayLen != 1 {
return fmt.Errorf("protocol violation; expected 1 array entry, got %d", arrayLen)
}

// Decode the boolean value indicating whether the user exists.
uer.Exists, err = d.DecodeBool()

return err
}

// NewUserExistsRequest creates a new request to check if a user exists.
func NewUserExistsRequest(username string) UserExistsRequest {
callReq := tarantool.NewCallRequest("box.schema.user.exists").Args([]interface{}{username})

return UserExistsRequest{
callReq,
}
}

// Exists checks if the specified user exists in Tarantool.
func (u *SchemaUser) Exists(ctx context.Context, username string) (bool, error) {
// Create a request and send it to Tarantool.
req := NewUserExistsRequest(username).Context(ctx)
resp := &UserExistsResponse{}

// Execute the request and parse the response.
err := u.conn.Do(req).GetTyped(resp)

return resp.Exists, err
}

// UserCreateOptions represents options for creating a user in Tarantool.
type UserCreateOptions struct {
IfNotExists bool `msgpack:"if_not_exists"` // If true, prevents an error if the user already exists.

Check failure on line 72 in box/schema_user.go

View workflow job for this annotation

GitHub Actions / golangci-lint

the line is 106 characters long, which exceeds the maximum of 100 characters. (lll)
Password string `msgpack:"password"` // The password for the new user.
}

// UserCreateRequest represents a request to create a new user in Tarantool.
type UserCreateRequest struct {
*tarantool.CallRequest // Underlying Tarantool call request.
}

// NewUserCreateRequest creates a new request to create a user with specified options.
func NewUserCreateRequest(username string, options UserCreateOptions) UserCreateRequest {
callReq := tarantool.NewCallRequest("box.schema.user.create").Args([]interface{}{username, options})

Check failure on line 83 in box/schema_user.go

View workflow job for this annotation

GitHub Actions / golangci-lint

the line is 104 characters long, which exceeds the maximum of 100 characters. (lll)

return UserCreateRequest{
callReq,
}
}

// UserCreateResponse represents the response to a user creation request.
type UserCreateResponse struct {
}

// DecodeMsgpack decodes the response for a user creation request.
// In this case, the response does not contain any data.
func (uer *UserCreateResponse) DecodeMsgpack(_ *msgpack.Decoder) error {
return nil
}

// Create creates a new user in Tarantool with the given username and options.
func (u *SchemaUser) Create(ctx context.Context, username string, options UserCreateOptions) error {
// Create a request and send it to Tarantool.
req := NewUserCreateRequest(username, options).Context(ctx)
resp := &UserCreateResponse{}

// Execute the request and handle the response.
fut := u.conn.Do(req)

err := fut.GetTyped(resp)
if err != nil {
return err
}

return nil
}

// UserDropOptions represents options for dropping a user in Tarantool.
type UserDropOptions struct {
IfExists bool `msgpack:"if_exists"` // If true, prevents an error if the user does not exist.
}

// UserDropRequest represents a request to drop a user from Tarantool.
type UserDropRequest struct {
*tarantool.CallRequest // Underlying Tarantool call request.
}

// NewUserDropRequest creates a new request to drop a user with specified options.
func NewUserDropRequest(username string, options UserDropOptions) UserDropRequest {
callReq := tarantool.NewCallRequest("box.schema.user.drop").Args([]interface{}{username, options})

Check failure on line 129 in box/schema_user.go

View workflow job for this annotation

GitHub Actions / golangci-lint

the line is 102 characters long, which exceeds the maximum of 100 characters. (lll)

return UserDropRequest{
callReq,
}
}

// UserDropResponse represents the response to a user drop request.
type UserDropResponse struct{}

// Drop drops the specified user from Tarantool, with optional conditions.
func (u *SchemaUser) Drop(ctx context.Context, username string, options UserDropOptions) error {
// Create a request and send it to Tarantool.
req := NewUserDropRequest(username, options).Context(ctx)
resp := &UserCreateResponse{}

// Execute the request and handle the response.
fut := u.conn.Do(req)

err := fut.GetTyped(resp)
if err != nil {
return err
}

return nil
}

type UserPasswordRequest struct {
*tarantool.CallRequest // Underlying Tarantool call request.
}

func NewUserPasswordRequest(username string) UserPasswordRequest {
callReq := tarantool.NewCallRequest("box.schema.user.password").Args([]interface{}{username})

return UserPasswordRequest{
callReq,
}
}

type UserPasswordResponse struct {
Hash string
}

func (upr *UserPasswordResponse) DecodeMsgpack(d *msgpack.Decoder) error {
arrayLen, err := d.DecodeArrayLen()
if err != nil {
return err
}

// Ensure that the response array contains exactly 1 element (the "Exists" field).
if arrayLen != 1 {
return fmt.Errorf("protocol violation; expected 1 array entry, got %d", arrayLen)
}

// Decode the boolean value indicating whether the user exists.
upr.Hash, err = d.DecodeString()

return err
}

func (u *SchemaUser) Password(ctx context.Context, username string) (string, error) {
// Create a request and send it to Tarantool.
req := NewUserPasswordRequest(username).Context(ctx)
resp := &UserPasswordResponse{}

// Execute the request and handle the response.
fut := u.conn.Do(req)

err := fut.GetTyped(resp)
if err != nil {
return "", err
}

return resp.Hash, nil
}
77 changes: 77 additions & 0 deletions box/tarantool_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,15 @@ package box_test

import (
"context"
"errors"
"log"
"os"
"testing"
"time"

"github.com/google/uuid"
"github.com/stretchr/testify/require"
"github.com/tarantool/go-iproto"
"github.com/tarantool/go-tarantool/v2"
"github.com/tarantool/go-tarantool/v2/box"
"github.com/tarantool/go-tarantool/v2/test_helpers"
Expand Down Expand Up @@ -61,6 +63,81 @@ func TestBox_Info(t *testing.T) {
validateInfo(t, resp.Info)
}

func TestBox_Sugar_Schema(t *testing.T) {
const (
username = "opensource"
password = "enterprise"
)

ctx := context.TODO()

conn, err := tarantool.Connect(ctx, dialer, tarantool.Opts{})
require.NoError(t, err)

b := box.New(conn)

// Create new user
err = b.Schema().User().Create(ctx, username, box.UserCreateOptions{Password: password})
require.NoError(t, err)

// Get error that user already exists
err = b.Schema().User().Create(ctx, username, box.UserCreateOptions{Password: password})
require.Error(t, err)

// Require that error code is ER_USER_EXISTS
var boxErr tarantool.Error
errors.As(err, &boxErr)
require.Equal(t, iproto.ER_USER_EXISTS, boxErr.Code)

// Check that already exists by exists call procedure
exists, err := b.Schema().User().Exists(ctx, username)
require.True(t, exists)
require.NoError(t, err)

// There is no error if IfNotExists option is true
err = b.Schema().User().Create(ctx, username, box.UserCreateOptions{Password: password, IfNotExists: true})

Check failure on line 98 in box/tarantool_test.go

View workflow job for this annotation

GitHub Actions / golangci-lint

the line is 111 characters long, which exceeds the maximum of 100 characters. (lll)
require.NoError(t, err)

// Require password hash
hash, err := b.Schema().User().Password(ctx, username)
require.NoError(t, err)
require.NotEmpty(t, hash)

// Check that password is valid and we can connect to tarantool with such credentials
var newUserDialer = tarantool.NetDialer{
Address: server,
User: username,
Password: password,
}

// We can connect with our new credentials
newUserConn, err := tarantool.Connect(ctx, newUserDialer, tarantool.Opts{})
require.NoError(t, err)
require.NotNil(t, newUserConn)
require.NoError(t, newUserConn.Close())

// Try to drop user
err = b.Schema().User().Drop(ctx, username, box.UserDropOptions{})
require.NoError(t, err)

// Require error cause user already deleted
err = b.Schema().User().Drop(ctx, username, box.UserDropOptions{})
require.Error(t, err)

// Require that error code is ER_NO_SUCH_USER
errors.As(err, &boxErr)
require.Equal(t, iproto.ER_NO_SUCH_USER, boxErr.Code)

// No error with option IfExists: true
err = b.Schema().User().Drop(ctx, username, box.UserDropOptions{IfExists: true})
require.NoError(t, err)

// Check that user not exists after drop
exists, err = b.Schema().User().Exists(ctx, username)
require.False(t, exists)
require.NoError(t, err)
}

func runTestMain(m *testing.M) int {
instance, err := test_helpers.StartTarantool(test_helpers.StartOpts{
Dialer: dialer,
Expand Down
4 changes: 2 additions & 2 deletions box/testdata/config.lua
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,9 @@ box.cfg{
}

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

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

0 comments on commit 2e271da

Please sign in to comment.