Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added logic for working with Tarantool schema via Box #426

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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)

Comment on lines 12 to -13
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please, revert the change if it is no necessary.

// 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.
}
Comment on lines +5 to +9
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's ok in the separate file.


// 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.
}
}
Comment on lines +11 to +17
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The method is related to the Box type, please, move it into box.go.

Comment on lines +11 to +17
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The method is a helper. It's ok. But please, add a NewSchema(conn tarantool.Doer) *Schema function as the type constructor to avoid are too close relationships between types.

220 changes: 220 additions & 0 deletions box/schema_user.go
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please, add unit-test for requests and responses types in the file.

We probably also need to think about naming and location of requests types in this package. As example, at now some requests are at request.go file. It could be confusing.

There are two ways I see:

  1. We could locate all requests/responses into the request.go file (I don't like the idea).
  2. We need move types from request.go into a proper files in the same manner as for the current ones.

Original file line number Diff line number Diff line change
@@ -0,0 +1,220 @@
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}
}
Comment on lines +16 to +19
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The same about a constructor as for the Schema type.


// 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 - if true, prevents an error if the user already exists.
IfNotExists bool `msgpack:"if_not_exists"`
// Password for the new user.
Password string `msgpack:"password"`
}

// 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})

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})

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
}

// UserPasswordRequest represents a request to retrieve a user's password from Tarantool.
type UserPasswordRequest struct {
*tarantool.CallRequest // Underlying Tarantool call request.
}

// NewUserPasswordRequest creates a new request to fetch the user's password.
// It takes the username and constructs the request to Tarantool.
func NewUserPasswordRequest(username string) UserPasswordRequest {
// Create a request to get the user's password.
callReq := tarantool.NewCallRequest("box.schema.user.password").Args([]interface{}{username})

return UserPasswordRequest{
callReq,
}
}

// UserPasswordResponse represents the response to the user password request.
// It contains the password hash.
type UserPasswordResponse struct {
Hash string // The password hash of the user.
}

// DecodeMsgpack decodes the response from Tarantool in Msgpack format.
// It expects the response to be an array of length 1, containing the password hash string.
func (upr *UserPasswordResponse) DecodeMsgpack(d *msgpack.Decoder) error {
// Decode the array length.
arrayLen, err := d.DecodeArrayLen()
if err != nil {
return err
}

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

// Decode the string containing the password hash.
upr.Hash, err = d.DecodeString()

return err
}

// Password sends a request to retrieve the user's password from Tarantool.
// It returns the password hash as a string or an error if the request fails.
func (u *SchemaUser) Password(ctx context.Context, username string) (string, error) {
// Create the 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)

// Get the decoded response.
err := fut.GetTyped(resp)
if err != nil {
return "", err
}

// Return the password hash.
return resp.Hash, nil
}
81 changes: 81 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,85 @@ 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,
})

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)
}
Comment on lines +66 to +143
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The test could be splitted into a several separate tests. Please, do it.


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"),
}
}
Loading