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 Jan 10, 2025
1 parent 7eae014 commit f8f0801
Show file tree
Hide file tree
Showing 10 changed files with 344 additions and 55 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,12 @@ 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

- Box Info method now requires context and implements CallRequest type instead custom baseRequest.

### Fixed

## [v2.2.1] - 2024-12-17
Expand Down
12 changes: 10 additions & 2 deletions box/box.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package box

import (
"context"

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

Expand All @@ -17,13 +19,19 @@ func New(conn tarantool.Doer) *Box {
}
}

// 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 NewSchema(b.conn)
}

// Info retrieves the current information of the Tarantool instance.
// It calls the "box.info" function and parses the result into the Info structure.
func (b *Box) Info() (Info, error) {
func (b *Box) Info(ctx context.Context) (Info, error) {
var infoResp InfoResponse

// Call "box.info" to get instance information from Tarantool.
fut := b.conn.Do(NewInfoRequest())
fut := b.conn.Do(NewInfoRequest().Context(ctx))

// Parse the result into the Info structure.
err := fut.GetTyped(&infoResp)
Expand Down
3 changes: 2 additions & 1 deletion box/box_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package box_test

import (
"context"
"testing"

"github.com/stretchr/testify/require"
Expand All @@ -24,6 +25,6 @@ func TestNew(t *testing.T) {

// Calling Info on a box with a nil connection will result in a panic, since the underlying
// connection (Doer) cannot perform the requested action (it's nil).
_, _ = b.Info()
_, _ = b.Info(context.TODO())
})
}
2 changes: 1 addition & 1 deletion box/example_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ func Example() {

b := box.New(client)

info, err := b.Info()
info, err := b.Info(ctx)
if err != nil {
log.Fatalf("Failed get box info: %s", err)
}
Expand Down
16 changes: 6 additions & 10 deletions box/info.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,18 +59,14 @@ func (ir *InfoResponse) DecodeMsgpack(d *msgpack.Decoder) error {
// InfoRequest represents a request to retrieve information about the Tarantool instance.
// It implements the tarantool.Request interface.
type InfoRequest struct {
baseRequest
}

// Body method is used to serialize the request's body.
// It is part of the tarantool.Request interface implementation.
func (i InfoRequest) Body(res tarantool.SchemaResolver, enc *msgpack.Encoder) error {
return i.impl.Body(res, enc)
*tarantool.CallRequest // Underlying Tarantool call request.
}

// NewInfoRequest returns a new empty info request.
func NewInfoRequest() InfoRequest {
req := InfoRequest{}
req.impl = newCall("box.info")
return req
callReq := tarantool.NewCallRequest("box.info")

return InfoRequest{
callReq,
}
}
38 changes: 0 additions & 38 deletions box/request.go

This file was deleted.

18 changes: 18 additions & 0 deletions box/schema.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
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.
}

func NewSchema(conn tarantool.Doer) *Schema {
return &Schema{conn: conn} // Pass the connection to the Schema.
}

// User returns a new SchemaUser instance, allowing schema-related user operations.
func (s *Schema) User() *SchemaUser {
return NewSchemaUser(s.conn)
}
219 changes: 219 additions & 0 deletions box/schema_user.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,219 @@
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.
}

func NewSchemaUser(conn tarantool.Doer) *SchemaUser {
return &SchemaUser{conn: 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 - 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
}
Loading

0 comments on commit f8f0801

Please sign in to comment.