diff --git a/CHANGELOG.md b/CHANGELOG.md index 9c2c49d39..2693a58df 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ Versioning](http://semver.org/spec/v2.0.0.html) except to the first release. - Support errors extended information (#209) - Error type support in MessagePack (#209) - Event subscription support (#119) +- Session settings support (#115) ### Changed diff --git a/Makefile b/Makefile index 7b6f8ec5f..718139967 100644 --- a/Makefile +++ b/Makefile @@ -81,6 +81,12 @@ test-uuid: go clean -testcache go test -tags "$(TAGS)" ./uuid/ -v -p 1 +.PHONY: test-settings +test-settings: + @echo "Running tests in settings package" + go clean -testcache + go test -tags "$(TAGS)" ./settings/ -v -p 1 + .PHONY: test-main test-main: @echo "Running tests in main package" diff --git a/settings/config.lua b/settings/config.lua new file mode 100644 index 000000000..7f6af1db2 --- /dev/null +++ b/settings/config.lua @@ -0,0 +1,15 @@ +-- Do not set listen for now so connector won't be +-- able to send requests until everything is configured. +box.cfg{ + work_dir = os.getenv("TEST_TNT_WORK_DIR"), +} + +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', 'create,read,write,drop,alter', 'space', nil, { if_not_exists = true }) +box.schema.user.grant('test', 'create', 'sequence', nil, { if_not_exists = true }) + +-- Set listen only when every other thing is configured. +box.cfg{ + listen = os.getenv("TEST_TNT_LISTEN"), +} diff --git a/settings/const.go b/settings/const.go new file mode 100644 index 000000000..cc980cd7a --- /dev/null +++ b/settings/const.go @@ -0,0 +1,21 @@ +package settings + +const sessionSettingsSpace string = "_session_settings" + +// In Go and IPROTO_UPDATE count starts with 0. +const sessionSettingValueField int = 1 + +const ( + errorMarshalingEnabled string = "error_marshaling_enabled" + sqlDefaultEngine string = "sql_default_engine" + sqlDeferForeignKeys string = "sql_defer_foreign_keys" + sqlFullColumnNames string = "sql_full_column_names" + sqlFullMetadata string = "sql_full_metadata" + sqlParserDebug string = "sql_parser_debug" + sqlRecursiveTriggers string = "sql_recursive_triggers" + sqlReverseUnorderedSelects string = "sql_reverse_unordered_selects" + sqlSelectDebug string = "sql_select_debug" + sqlVDBEDebug string = "sql_vdbe_debug" +) + +const selectAllLimit uint32 = 1000 diff --git a/settings/example_test.go b/settings/example_test.go new file mode 100644 index 000000000..a2391328f --- /dev/null +++ b/settings/example_test.go @@ -0,0 +1,78 @@ +package settings_test + +import ( + "fmt" + + "github.com/tarantool/go-tarantool" + "github.com/tarantool/go-tarantool/settings" + "github.com/tarantool/go-tarantool/test_helpers" +) + +func example_connect(opts tarantool.Opts) *tarantool.Connection { + conn, err := tarantool.Connect(server, opts) + if err != nil { + panic("Connection is not established: " + err.Error()) + } + return conn +} + +func Example_sqlFullColumnNames() { + var resp *tarantool.Response + var err error + var isLess bool + + conn := example_connect(opts) + defer conn.Close() + + // Tarantool supports session settings since version 2.3.1 + isLess, err = test_helpers.IsTarantoolVersionLess(2, 3, 1) + if err != nil || isLess { + return + } + + // Create a space. + _, err = conn.Execute("CREATE TABLE example(id INT PRIMARY KEY, x INT);", []interface{}{}) + if err != nil { + fmt.Printf("error in create table: %v\n", err) + return + } + + // Insert some tuple into space. + _, err = conn.Execute("INSERT INTO example VALUES (1, 1);", []interface{}{}) + if err != nil { + fmt.Printf("error on insert: %v\n", err) + return + } + + // Enable showing full column names in SQL responses. + _, err = conn.Do(settings.NewSQLFullColumnNamesSetRequest(true)).Get() + if err != nil { + fmt.Printf("error on setting setup: %v\n", err) + return + } + + // Get some data with SQL query. + resp, err = conn.Execute("SELECT x FROM example WHERE id = 1;", []interface{}{}) + if err != nil { + fmt.Printf("error on select: %v\n", err) + return + } + // Show response metadata. + fmt.Printf("full column name: %v\n", resp.MetaData[0].FieldName) + + // Disable showing full column names in SQL responses. + _, err = conn.Do(settings.NewSQLFullColumnNamesSetRequest(false)).Get() + if err != nil { + fmt.Printf("error on setting setup: %v\n", err) + return + } + + // Get some data with SQL query. + resp, err = conn.Execute("SELECT x FROM example WHERE id = 1;", []interface{}{}) + if err != nil { + fmt.Printf("error on select: %v\n", err) + return + } + // Show response metadata. + fmt.Printf("short column name: %v\n", resp.MetaData[0].FieldName) +} diff --git a/settings/msgpack.go b/settings/msgpack.go new file mode 100644 index 000000000..295620aba --- /dev/null +++ b/settings/msgpack.go @@ -0,0 +1,10 @@ +//go:build !go_tarantool_msgpack_v5 +// +build !go_tarantool_msgpack_v5 + +package settings + +import ( + "gopkg.in/vmihailenco/msgpack.v2" +) + +type encoder = msgpack.Encoder diff --git a/settings/msgpack_helper_test.go b/settings/msgpack_helper_test.go new file mode 100644 index 000000000..d868d7ea9 --- /dev/null +++ b/settings/msgpack_helper_test.go @@ -0,0 +1,13 @@ +//go:build !go_tarantool_msgpack_v5 +// +build !go_tarantool_msgpack_v5 + +package settings_test + +import ( + "github.com/tarantool/go-tarantool" +) + +func toBoxError(i interface{}) (v tarantool.BoxError, ok bool) { + v, ok = i.(tarantool.BoxError) + return +} diff --git a/settings/msgpack_v5.go b/settings/msgpack_v5.go new file mode 100644 index 000000000..288418ec6 --- /dev/null +++ b/settings/msgpack_v5.go @@ -0,0 +1,10 @@ +//go:build go_tarantool_msgpack_v5 +// +build go_tarantool_msgpack_v5 + +package settings + +import ( + "github.com/vmihailenco/msgpack/v5" +) + +type encoder = msgpack.Encoder diff --git a/settings/msgpack_v5_helper_test.go b/settings/msgpack_v5_helper_test.go new file mode 100644 index 000000000..a3448492c --- /dev/null +++ b/settings/msgpack_v5_helper_test.go @@ -0,0 +1,16 @@ +//go:build go_tarantool_msgpack_v5 +// +build go_tarantool_msgpack_v5 + +package settings_test + +import ( + "github.com/tarantool/go-tarantool" +) + +func toBoxError(i interface{}) (v tarantool.BoxError, ok bool) { + var ptr *tarantool.BoxError + if ptr, ok = i.(*tarantool.BoxError); ok { + v = *ptr + } + return +} diff --git a/settings/request.go b/settings/request.go new file mode 100644 index 000000000..fa045db56 --- /dev/null +++ b/settings/request.go @@ -0,0 +1,278 @@ +// Package settings is a collection of requests to set a connection session setting +// or get current session configuration. +// +// +============================+=========================+=========+===========================+ +// | Setting | Meaning | Default | Supported in | +// | | | | Tarantool versions | +// +============================+=========================+=========+===========================+ +// | ErrorMarshalingEnabled | Defines whether error | false | Since 2.4.1 till 2.10.0, | +// | | objectshave a special | | replaced with IPROTO_ID | +// | | structure. | | feature flag. | +// +----------------------------+-------------------------+---------+---------------------------+ +// | SQLDefaultEngine | Defines default storage | "memtx" | Since 2.3.1. | +// | | engine for new SQL | | | +// | | tables. | | | +// +----------------------------+-------------------------+---------+---------------------------+ +// | SQLDeferForeignKeys | Defines whether | false | Since 2.3.1 till master | +// | | foreign-key checks can | | commit 14618c4 (possible | +// | | wait till commit. | | 2.10.5 or 2.11.0) | +// +----------------------------+-------------------------+---------+---------------------------+ +// | SQLFullColumnNames | Defines whether full | false | Since 2.3.1. | +// | | column names is | | | +// | | displayed in SQL result | | | +// | | set metadata. | | | +// +----------------------------+-------------------------+---------+---------------------------+ +// | SQLFullMetadata | Defines whether SQL | false | Since 2.3.1. | +// | | result set metadata | | | +// | | will have more than | | | +// | | just name and type. | | | +// +----------------------------+-------------------------+---------+---------------------------+ +// | SQLParserDebug | Defines whether to show | false | Since 2.3.1 (only if | +// | | parser steps for | | built with | +// | | following statements. | | -DCMAKE_BUILD_TYPE=Debug) | +// +----------------------------+-------------------------+---------+---------------------------+ +// | SQLRecursiveTriggers | Defines whether a | true | Since 2.3.1. | +// | | triggered statement can | | | +// | | activate a trigger. | | | +// +----------------------------+-------------------------+---------+---------------------------+ +// | SQLReverseUnorderedSelects | Defines defines whether | false | Since 2.3.1. | +// | | result rows are usually | | | +// | | in reverse order if | | | +// | | there is no ORDER BY | | | +// | | clause. | | | +// +----------------------------+-------------------------+---------+---------------------------+ +// | SQLSelectDebug | Defines whether to show | false | Since 2.3.1 (only if | +// | | to show execution steps | | built with | +// | | during SELECT. | | -DCMAKE_BUILD_TYPE=Debug) | +// +----------------------------+-------------------------+---------+---------------------------+ +// | SQLVDBEDebug | Defines whether VDBE | false | Since 2.3.1 (only if | +// | | debug mode is enabled. | | built with | +// | | | | -DCMAKE_BUILD_TYPE=Debug) | +// +----------------------------+-------------------------+---------+---------------------------+ +// +// Since: 1.10.0 +// +// See also: +// +// * Session settings https://www.tarantool.io/en/doc/latest/reference/reference_lua/box_space/_session_settings/ +package settings + +import ( + "context" + + "github.com/tarantool/go-tarantool" +) + +// Request helps to work with session settings. +type Request struct { + get *tarantool.SelectRequest + set *tarantool.UpdateRequest +} + +func (req *Request) assertBuiltWithNew() { + if (req.get == nil) && (req.set == nil) { + panic("Invalid settings.Request (empty), possibly build not with New*Request function") + } + + if (req.get != nil) && (req.set != nil) { + panic("Invalid settings.Request (both types), possibly build not with New*Request function") + } +} + +func newSetRequest(setting string, value interface{}) *Request { + return &Request{ + set: tarantool.NewUpdateRequest(sessionSettingsSpace). + Key(tarantool.StringKey{S: setting}). + Operations(tarantool.NewOperations().Assign(sessionSettingValueField, value)), + } +} + +func newGetRequest(setting string) *Request { + return &Request{ + get: tarantool.NewSelectRequest(sessionSettingsSpace). + Key(tarantool.StringKey{S: setting}). + Limit(1), + } +} + +// Context sets a passed context to the session settings request. +func (req *Request) Context(ctx context.Context) *Request { + req.assertBuiltWithNew() + + if req.set != nil { + req.set = req.set.Context(ctx) + } else { + req.get = req.get.Context(ctx) + } + + return req +} + +// Code returns IPROTO code for the session settings request. +func (req *Request) Code() int32 { + req.assertBuiltWithNew() + + if req.set != nil { + return req.set.Code() + } + return req.get.Code() +} + +// Body fills an encoder with the session settings request body. +func (req *Request) Body(res tarantool.SchemaResolver, enc *encoder) error { + req.assertBuiltWithNew() + + if req.set != nil { + return req.set.Body(res, enc) + } + return req.get.Body(res, enc) +} + +// Ctx returns a context of the session settings request. +func (req *Request) Ctx() context.Context { + req.assertBuiltWithNew() + + if req.set != nil { + return req.set.Ctx() + } + return req.get.Ctx() +} + +// Async returns is the session settings request expects a response. +func (req *Request) Async() bool { + req.assertBuiltWithNew() + + if req.set != nil { + return req.set.Async() + } + return req.get.Async() +} + +// NewErrorMarshalingEnabledSetRequest creates a request to +// update current session ErrorMarshalingEnabled setting. +func NewErrorMarshalingEnabledSetRequest(value bool) *Request { + return newSetRequest(errorMarshalingEnabled, value) +} + +// NewErrorMarshalingEnabledGetRequest creates a request to get +// current session ErrorMarshalingEnabled setting in tuple format. +func NewErrorMarshalingEnabledGetRequest() *Request { + return newGetRequest(errorMarshalingEnabled) +} + +// NewSQLDefaultEngineSetRequest creates a request to +// update current session SQLDefaultEngine setting. +func NewSQLDefaultEngineSetRequest(value string) *Request { + return newSetRequest(sqlDefaultEngine, value) +} + +// NewSQLDefaultEngineGetRequest creates a request to get +// current session SQLDefaultEngine setting in tuple format. +func NewSQLDefaultEngineGetRequest() *Request { + return newGetRequest(sqlDefaultEngine) +} + +// NewSQLDeferForeignKeysSetRequest creates a request to +// update current session SQLDeferForeignKeys setting. +func NewSQLDeferForeignKeysSetRequest(value bool) *Request { + return newSetRequest(sqlDeferForeignKeys, value) +} + +// NewSQLDeferForeignKeysGetRequest creates a request to get +// current session SQLDeferForeignKeys setting in tuple format. +func NewSQLDeferForeignKeysGetRequest() *Request { + return newGetRequest(sqlDeferForeignKeys) +} + +// NewSQLFullColumnNamesSetRequest creates a request to +// update current session SQLFullColumnNames setting. +func NewSQLFullColumnNamesSetRequest(value bool) *Request { + return newSetRequest(sqlFullColumnNames, value) +} + +// NewSQLFullColumnNamesGetRequest creates a request to get +// current session SQLFullColumnNames setting in tuple format. +func NewSQLFullColumnNamesGetRequest() *Request { + return newGetRequest(sqlFullColumnNames) +} + +// NewSQLFullMetadataSetRequest creates a request to +// update current session SQLFullMetadata setting. +func NewSQLFullMetadataSetRequest(value bool) *Request { + return newSetRequest(sqlFullMetadata, value) +} + +// NewSQLFullMetadataGetRequest creates a request to get +// current session SQLFullMetadata setting in tuple format. +func NewSQLFullMetadataGetRequest() *Request { + return newGetRequest(sqlFullMetadata) +} + +// NewSQLParserDebugSetRequest creates a request to +// update current session SQLParserDebug setting. +func NewSQLParserDebugSetRequest(value bool) *Request { + return newSetRequest(sqlParserDebug, value) +} + +// NewSQLParserDebugGetRequest creates a request to get +// current session SQLParserDebug setting in tuple format. +func NewSQLParserDebugGetRequest() *Request { + return newGetRequest(sqlParserDebug) +} + +// NewSQLRecursiveTriggersSetRequest creates a request to +// update current session SQLRecursiveTriggers setting. +func NewSQLRecursiveTriggersSetRequest(value bool) *Request { + return newSetRequest(sqlRecursiveTriggers, value) +} + +// NewSQLRecursiveTriggersGetRequest creates a request to get +// current session SQLRecursiveTriggers setting in tuple format. +func NewSQLRecursiveTriggersGetRequest() *Request { + return newGetRequest(sqlRecursiveTriggers) +} + +// NewSQLReverseUnorderedSelectsSetRequest creates a request to +// update current session SQLReverseUnorderedSelects setting. +func NewSQLReverseUnorderedSelectsSetRequest(value bool) *Request { + return newSetRequest(sqlReverseUnorderedSelects, value) +} + +// NewSQLReverseUnorderedSelectsGetRequest creates a request to get +// current session SQLReverseUnorderedSelects setting in tuple format. +func NewSQLReverseUnorderedSelectsGetRequest() *Request { + return newGetRequest(sqlReverseUnorderedSelects) +} + +// NewSQLSelectDebugSetRequest creates a request to +// update current session SQLSelectDebug setting. +func NewSQLSelectDebugSetRequest(value bool) *Request { + return newSetRequest(sqlSelectDebug, value) +} + +// NewSQLSelectDebugGetRequest creates a request to get +// current session SQLSelectDebug setting in tuple format. +func NewSQLSelectDebugGetRequest() *Request { + return newGetRequest(sqlSelectDebug) +} + +// NewSQLVDBEDebugSetRequest creates a request to +// update current session SQLVDBEDebug setting. +func NewSQLVDBEDebugSetRequest(value bool) *Request { + return newSetRequest(sqlVDBEDebug, value) +} + +// NewSQLVDBEDebugGetRequest creates a request to get +// current session SQLVDBEDebug setting in tuple format. +func NewSQLVDBEDebugGetRequest() *Request { + return newGetRequest(sqlVDBEDebug) +} + +// NewSessionSettingsGetRequest creates a request to get all +// current session settings in tuple format. +func NewSessionSettingsGetRequest() *Request { + return &Request{ + get: tarantool.NewSelectRequest(sessionSettingsSpace). + Limit(selectAllLimit), + } +} diff --git a/settings/request_test.go b/settings/request_test.go new file mode 100644 index 000000000..36f72de45 --- /dev/null +++ b/settings/request_test.go @@ -0,0 +1,690 @@ +package settings_test + +import ( + "log" + "os" + "testing" + "time" + + "github.com/stretchr/testify/require" + "github.com/tarantool/go-tarantool" + . "github.com/tarantool/go-tarantool/settings" + "github.com/tarantool/go-tarantool/test_helpers" +) + +// There is no way to skip tests in testing.M, +// so we use this variable to pass info +// to each testing.T that it should skip. +var isSettingsSupported = false + +var server = "127.0.0.1:3013" +var opts = tarantool.Opts{ + Timeout: 500 * time.Millisecond, + User: "test", + Pass: "test", +} + +func skipIfSettingsUnsupported(t *testing.T) { + t.Helper() + + if isSettingsSupported == false { + t.Skip("Skipping test for Tarantool without session settings support") + } +} + +func skipIfErrorMarshalingEnabledSettingUnsupported(t *testing.T) { + t.Helper() + + test_helpers.SkipIfFeatureUnsupported(t, "error_marshaling_enabled session setting", 2, 4, 1) + test_helpers.SkipIfFeatureDropped(t, "error_marshaling_enabled session setting", 2, 10, 0) +} + +func skipIfSQLDeferForeignKeysSettingUnsupported(t *testing.T) { + t.Helper() + + test_helpers.SkipIfFeatureUnsupported(t, "sql_defer_foreign_keys session setting", 2, 3, 1) + test_helpers.SkipIfFeatureDropped(t, "sql_defer_foreign_keys session setting", 2, 10, 5) +} + +func TestErrorMarshalingEnabledSetting(t *testing.T) { + skipIfErrorMarshalingEnabledSettingUnsupported(t) + + var resp *tarantool.Response + var err error + + conn := test_helpers.ConnectWithValidation(t, server, opts) + defer conn.Close() + + // Disable receiving box.error as MP_EXT 3. + resp, err = conn.Do(NewErrorMarshalingEnabledSetRequest(false)).Get() + require.Nil(t, err) + require.NotNil(t, resp) + require.Equal(t, []interface{}{[]interface{}{"error_marshaling_enabled", false}}, resp.Data) + + // Fetch current setting value. + resp, err = conn.Do(NewErrorMarshalingEnabledGetRequest()).Get() + require.Nil(t, err) + require.NotNil(t, resp) + require.Equal(t, []interface{}{[]interface{}{"error_marshaling_enabled", false}}, resp.Data) + + // Get a box.Error value. + resp, err = conn.Eval("return box.error.new(box.error.UNKNOWN)", []interface{}{}) + require.Nil(t, err) + require.NotNil(t, resp) + require.IsType(t, "string", resp.Data[0]) + + // Enable receiving box.error as MP_EXT 3. + resp, err = conn.Do(NewErrorMarshalingEnabledSetRequest(true)).Get() + require.Nil(t, err) + require.NotNil(t, resp) + require.Equal(t, []interface{}{[]interface{}{"error_marshaling_enabled", true}}, resp.Data) + + // Fetch current setting value. + resp, err = conn.Do(NewErrorMarshalingEnabledGetRequest()).Get() + require.Nil(t, err) + require.NotNil(t, resp) + require.Equal(t, []interface{}{[]interface{}{"error_marshaling_enabled", true}}, resp.Data) + + // Get a box.Error value. + resp, err = conn.Eval("return box.error.new(box.error.UNKNOWN)", []interface{}{}) + require.Nil(t, err) + require.NotNil(t, resp) + _, ok := toBoxError(resp.Data[0]) + require.True(t, ok) +} + +func TestSQLDefaultEngineSetting(t *testing.T) { + // https: //github.com/tarantool/tarantool/blob/680990a082374e4790539215f69d9e9ee39c3307/test/sql/engine.test.lua + skipIfSettingsUnsupported(t) + + var resp *tarantool.Response + var err error + + conn := test_helpers.ConnectWithValidation(t, server, opts) + defer conn.Close() + + // Set default SQL "CREATE TABLE" engine to "vinyl". + resp, err = conn.Do(NewSQLDefaultEngineSetRequest("vinyl")).Get() + require.Nil(t, err) + require.NotNil(t, resp) + require.EqualValues(t, []interface{}{[]interface{}{"sql_default_engine", "vinyl"}}, resp.Data) + + // Fetch current setting value. + resp, err = conn.Do(NewSQLDefaultEngineGetRequest()).Get() + require.Nil(t, err) + require.NotNil(t, resp) + require.Equal(t, []interface{}{[]interface{}{"sql_default_engine", "vinyl"}}, resp.Data) + + // Create a space with "CREATE TABLE". + resp, err = conn.Execute("CREATE TABLE t1_vinyl(a INT PRIMARY KEY, b INT, c INT);", []interface{}{}) + require.Nil(t, err) + require.NotNil(t, resp) + require.Equal(t, uint64(1), resp.SQLInfo.AffectedCount) + + // Check new space engine. + resp, err = conn.Eval("return box.space['T1_VINYL'].engine", []interface{}{}) + require.Nil(t, err) + require.NotNil(t, resp) + require.Equal(t, "vinyl", resp.Data[0]) + + // Set default SQL "CREATE TABLE" engine to "memtx". + resp, err = conn.Do(NewSQLDefaultEngineSetRequest("memtx")).Get() + require.Nil(t, err) + require.NotNil(t, resp) + require.Equal(t, []interface{}{[]interface{}{"sql_default_engine", "memtx"}}, resp.Data) + + // Fetch current setting value. + resp, err = conn.Do(NewSQLDefaultEngineGetRequest()).Get() + require.Nil(t, err) + require.NotNil(t, resp) + require.Equal(t, []interface{}{[]interface{}{"sql_default_engine", "memtx"}}, resp.Data) + + // Create a space with "CREATE TABLE". + resp, err = conn.Execute("CREATE TABLE t2_memtx(a INT PRIMARY KEY, b INT, c INT);", []interface{}{}) + require.Nil(t, err) + require.NotNil(t, resp) + require.Equal(t, uint64(1), resp.SQLInfo.AffectedCount) + + // Check new space engine. + resp, err = conn.Eval("return box.space['T2_MEMTX'].engine", []interface{}{}) + require.Nil(t, err) + require.NotNil(t, resp) + require.Equal(t, "memtx", resp.Data[0]) +} + +func TestSQLDeferForeignKeysSetting(t *testing.T) { + // https://github.com/tarantool/tarantool/blob/eafadc13425f14446d7aaa49dea67dfc1d5f45e9/test/sql/transitive-transactions.result + skipIfSQLDeferForeignKeysSettingUnsupported(t) + + var resp *tarantool.Response + var err error + + conn := test_helpers.ConnectWithValidation(t, server, opts) + defer conn.Close() + + // Create a parent space. + resp, err = conn.Execute("CREATE TABLE parent(id INT PRIMARY KEY, y INT UNIQUE);", []interface{}{}) + require.Nil(t, err) + require.NotNil(t, resp) + require.Equal(t, uint64(1), resp.SQLInfo.AffectedCount) + + // Create a space with reference to the parent space. + resp, err = conn.Execute("CREATE TABLE child(id INT PRIMARY KEY, x INT REFERENCES parent(y));", []interface{}{}) + require.Nil(t, err) + require.NotNil(t, resp) + require.Equal(t, uint64(1), resp.SQLInfo.AffectedCount) + + deferEval := ` + box.begin() + local _, err = box.execute('INSERT INTO child VALUES (2, 2);') + if err ~= nil then + box.rollback() + error(err) + end + box.execute('INSERT INTO parent VALUES (2, 2);') + box.commit() + return true + ` + + // Disable foreign key constraint checks before commit. + resp, err = conn.Do(NewSQLDeferForeignKeysSetRequest(false)).Get() + require.Nil(t, err) + require.NotNil(t, resp) + require.Equal(t, []interface{}{[]interface{}{"sql_defer_foreign_keys", false}}, resp.Data) + + // Fetch current setting value. + resp, err = conn.Do(NewSQLDeferForeignKeysGetRequest()).Get() + require.Nil(t, err) + require.NotNil(t, resp) + require.Equal(t, []interface{}{[]interface{}{"sql_defer_foreign_keys", false}}, resp.Data) + + // Evaluate a scenario when foreign key not exists + // on INSERT, but exists on commit. + _, err = conn.Eval(deferEval, []interface{}{}) + require.NotNil(t, err) + require.ErrorContains(t, err, "Failed to execute SQL statement: FOREIGN KEY constraint failed") + + resp, err = conn.Do(NewSQLDeferForeignKeysSetRequest(true)).Get() + require.Nil(t, err) + require.NotNil(t, resp) + require.Equal(t, []interface{}{[]interface{}{"sql_defer_foreign_keys", true}}, resp.Data) + + // Fetch current setting value. + resp, err = conn.Do(NewSQLDeferForeignKeysGetRequest()).Get() + require.Nil(t, err) + require.NotNil(t, resp) + require.Equal(t, []interface{}{[]interface{}{"sql_defer_foreign_keys", true}}, resp.Data) + + // Evaluate a scenario when foreign key not exists + // on INSERT, but exists on commit. + resp, err = conn.Eval(deferEval, []interface{}{}) + require.Nil(t, err) + require.NotNil(t, resp) + require.Equal(t, true, resp.Data[0]) +} + +func TestSQLFullColumnNamesSetting(t *testing.T) { + skipIfSettingsUnsupported(t) + + var resp *tarantool.Response + var err error + + conn := test_helpers.ConnectWithValidation(t, server, opts) + defer conn.Close() + + // Create a space. + resp, err = conn.Execute("CREATE TABLE fkname(id INT PRIMARY KEY, x INT);", []interface{}{}) + require.Nil(t, err) + require.NotNil(t, resp) + require.Equal(t, uint64(1), resp.SQLInfo.AffectedCount) + + // Fill it with some data. + resp, err = conn.Execute("INSERT INTO fkname VALUES (1, 1);", []interface{}{}) + require.Nil(t, err) + require.NotNil(t, resp) + require.Equal(t, uint64(1), resp.SQLInfo.AffectedCount) + + // Disable displaying full column names in metadata. + resp, err = conn.Do(NewSQLFullColumnNamesSetRequest(false)).Get() + require.Nil(t, err) + require.NotNil(t, resp) + require.Equal(t, []interface{}{[]interface{}{"sql_full_column_names", false}}, resp.Data) + + // Fetch current setting value. + resp, err = conn.Do(NewSQLFullColumnNamesGetRequest()).Get() + require.Nil(t, err) + require.NotNil(t, resp) + require.Equal(t, []interface{}{[]interface{}{"sql_full_column_names", false}}, resp.Data) + + // Get a data with short column names in metadata. + resp, err = conn.Execute("SELECT x FROM fkname WHERE id = 1;", []interface{}{}) + require.Nil(t, err) + require.NotNil(t, resp) + require.Equal(t, "X", resp.MetaData[0].FieldName) + + // Enable displaying full column names in metadata. + resp, err = conn.Do(NewSQLFullColumnNamesSetRequest(true)).Get() + require.Nil(t, err) + require.NotNil(t, resp) + require.Equal(t, []interface{}{[]interface{}{"sql_full_column_names", true}}, resp.Data) + + // Fetch current setting value. + resp, err = conn.Do(NewSQLFullColumnNamesGetRequest()).Get() + require.Nil(t, err) + require.NotNil(t, resp) + require.Equal(t, []interface{}{[]interface{}{"sql_full_column_names", true}}, resp.Data) + + // Get a data with full column names in metadata. + resp, err = conn.Execute("SELECT x FROM fkname WHERE id = 1;", []interface{}{}) + require.Nil(t, err) + require.NotNil(t, resp) + require.Equal(t, "FKNAME.X", resp.MetaData[0].FieldName) +} + +func TestSQLFullMetadataSetting(t *testing.T) { + skipIfSettingsUnsupported(t) + + var resp *tarantool.Response + var err error + + conn := test_helpers.ConnectWithValidation(t, server, opts) + defer conn.Close() + + // Create a space. + resp, err = conn.Execute("CREATE TABLE fmt(id INT PRIMARY KEY, x INT);", []interface{}{}) + require.Nil(t, err) + require.NotNil(t, resp) + require.Equal(t, uint64(1), resp.SQLInfo.AffectedCount) + + // Fill it with some data. + resp, err = conn.Execute("INSERT INTO fmt VALUES (1, 1);", []interface{}{}) + require.Nil(t, err) + require.NotNil(t, resp) + require.Equal(t, uint64(1), resp.SQLInfo.AffectedCount) + + // Disable displaying additional fields in metadata. + resp, err = conn.Do(NewSQLFullMetadataSetRequest(false)).Get() + require.Nil(t, err) + require.Equal(t, []interface{}{[]interface{}{"sql_full_metadata", false}}, resp.Data) + + // Fetch current setting value. + resp, err = conn.Do(NewSQLFullMetadataGetRequest()).Get() + require.Nil(t, err) + require.NotNil(t, resp) + require.Equal(t, []interface{}{[]interface{}{"sql_full_metadata", false}}, resp.Data) + + // Get a data without additional fields in metadata. + resp, err = conn.Execute("SELECT x FROM fmt WHERE id = 1;", []interface{}{}) + require.Nil(t, err) + require.NotNil(t, resp) + require.Equal(t, "", resp.MetaData[0].FieldSpan) + + // Enable displaying full column names in metadata. + resp, err = conn.Do(NewSQLFullMetadataSetRequest(true)).Get() + require.Nil(t, err) + require.NotNil(t, resp) + require.Equal(t, []interface{}{[]interface{}{"sql_full_metadata", true}}, resp.Data) + + // Fetch current setting value. + resp, err = conn.Do(NewSQLFullMetadataGetRequest()).Get() + require.Nil(t, err) + require.NotNil(t, resp) + require.Equal(t, []interface{}{[]interface{}{"sql_full_metadata", true}}, resp.Data) + + // Get a data with additional fields in metadata. + resp, err = conn.Execute("SELECT x FROM fmt WHERE id = 1;", []interface{}{}) + require.Nil(t, err) + require.NotNil(t, resp) + require.Equal(t, "x", resp.MetaData[0].FieldSpan) +} + +func TestSQLParserDebugSetting(t *testing.T) { + skipIfSettingsUnsupported(t) + + var resp *tarantool.Response + var err error + + conn := test_helpers.ConnectWithValidation(t, server, opts) + defer conn.Close() + + // Disable parser debug mode. + resp, err = conn.Do(NewSQLParserDebugSetRequest(false)).Get() + require.Nil(t, err) + require.NotNil(t, resp) + require.Equal(t, []interface{}{[]interface{}{"sql_parser_debug", false}}, resp.Data) + + // Fetch current setting value. + resp, err = conn.Do(NewSQLParserDebugGetRequest()).Get() + require.Nil(t, err) + require.NotNil(t, resp) + require.Equal(t, []interface{}{[]interface{}{"sql_parser_debug", false}}, resp.Data) + + // Enable parser debug mode. + resp, err = conn.Do(NewSQLParserDebugSetRequest(true)).Get() + require.Nil(t, err) + require.NotNil(t, resp) + require.Equal(t, []interface{}{[]interface{}{"sql_parser_debug", true}}, resp.Data) + + // Fetch current setting value. + resp, err = conn.Do(NewSQLParserDebugGetRequest()).Get() + require.Nil(t, err) + require.NotNil(t, resp) + require.Equal(t, []interface{}{[]interface{}{"sql_parser_debug", true}}, resp.Data) + + // To test real effect we need a Tarantool instance built with + // `-DCMAKE_BUILD_TYPE=Debug`. +} + +func TestSQLRecursiveTriggersSetting(t *testing.T) { + // https://github.com/tarantool/tarantool/blob/d11fb3061e15faf4e0eb5375fb8056b4e64348ae/test/sql-tap/triggerC.test.lua + skipIfSettingsUnsupported(t) + + var resp *tarantool.Response + var err error + + conn := test_helpers.ConnectWithValidation(t, server, opts) + defer conn.Close() + + // Create a space. + resp, err = conn.Execute("CREATE TABLE rec(id INTEGER PRIMARY KEY, a INT, b INT);", []interface{}{}) + require.Nil(t, err) + require.NotNil(t, resp) + require.Equal(t, uint64(1), resp.SQLInfo.AffectedCount) + + // Fill it with some data. + resp, err = conn.Execute("INSERT INTO rec VALUES(1, 1, 2);", []interface{}{}) + require.Nil(t, err) + require.NotNil(t, resp) + require.Equal(t, uint64(1), resp.SQLInfo.AffectedCount) + + // Create a recursive trigger (with infinite depth). + resp, err = conn.Execute(` + CREATE TRIGGER tr12 AFTER UPDATE ON rec FOR EACH ROW BEGIN + UPDATE rec SET a=new.a+1, b=new.b+1; + END;`, []interface{}{}) + require.Nil(t, err) + require.NotNil(t, resp) + require.Equal(t, uint64(1), resp.SQLInfo.AffectedCount) + + // Enable SQL recursive triggers. + resp, err = conn.Do(NewSQLRecursiveTriggersSetRequest(true)).Get() + require.Nil(t, err) + require.NotNil(t, resp) + require.Equal(t, []interface{}{[]interface{}{"sql_recursive_triggers", true}}, resp.Data) + + // Fetch current setting value. + resp, err = conn.Do(NewSQLRecursiveTriggersGetRequest()).Get() + require.Nil(t, err) + require.NotNil(t, resp) + require.Equal(t, []interface{}{[]interface{}{"sql_recursive_triggers", true}}, resp.Data) + + // Trigger the recursion. + _, err = conn.Execute("UPDATE rec SET a=a+1, b=b+1;", []interface{}{}) + require.NotNil(t, err) + require.ErrorContains(t, err, "Failed to execute SQL statement: too many levels of trigger recursion") + + // Disable SQL recursive triggers. + resp, err = conn.Do(NewSQLRecursiveTriggersSetRequest(false)).Get() + require.Nil(t, err) + require.NotNil(t, resp) + require.Equal(t, []interface{}{[]interface{}{"sql_recursive_triggers", false}}, resp.Data) + + // Fetch current setting value. + resp, err = conn.Do(NewSQLRecursiveTriggersGetRequest()).Get() + require.Nil(t, err) + require.NotNil(t, resp) + require.Equal(t, []interface{}{[]interface{}{"sql_recursive_triggers", false}}, resp.Data) + + // Trigger the recursion. + resp, err = conn.Execute("UPDATE rec SET a=a+1, b=b+1;", []interface{}{}) + require.Nil(t, err) + require.NotNil(t, resp) + require.Equal(t, uint64(1), resp.SQLInfo.AffectedCount) +} + +func TestSQLReverseUnorderedSelectsSetting(t *testing.T) { + skipIfSettingsUnsupported(t) + + var resp *tarantool.Response + var err error + + conn := test_helpers.ConnectWithValidation(t, server, opts) + defer conn.Close() + + // Create a space. + resp, err = conn.Execute("CREATE TABLE data(id STRING PRIMARY KEY);", []interface{}{}) + require.Nil(t, err) + require.NotNil(t, resp) + require.Equal(t, uint64(1), resp.SQLInfo.AffectedCount) + + // Fill it with some data. + resp, err = conn.Execute("INSERT INTO data VALUES('1');", []interface{}{}) + require.Nil(t, err) + require.NotNil(t, resp) + require.Equal(t, uint64(1), resp.SQLInfo.AffectedCount) + + resp, err = conn.Execute("INSERT INTO data VALUES('2');", []interface{}{}) + require.Nil(t, err) + require.NotNil(t, resp) + require.Equal(t, uint64(1), resp.SQLInfo.AffectedCount) + + // Disable reverse order in unordered selects. + resp, err = conn.Do(NewSQLReverseUnorderedSelectsSetRequest(false)).Get() + require.Nil(t, err) + require.NotNil(t, resp) + require.Equal(t, []interface{}{[]interface{}{"sql_reverse_unordered_selects", false}}, resp.Data) + + // Fetch current setting value. + resp, err = conn.Do(NewSQLReverseUnorderedSelectsGetRequest()).Get() + require.Nil(t, err) + require.NotNil(t, resp) + require.Equal(t, []interface{}{[]interface{}{"sql_reverse_unordered_selects", false}}, resp.Data) + + // Select multiple records. + resp, err = conn.Execute("SELECT * FROM data;", []interface{}{}) + require.Nil(t, err) + require.NotNil(t, resp) + require.EqualValues(t, []interface{}{"1"}, resp.Data[0]) + require.EqualValues(t, []interface{}{"2"}, resp.Data[1]) + + // Enable reverse order in unordered selects. + resp, err = conn.Do(NewSQLReverseUnorderedSelectsSetRequest(true)).Get() + require.Nil(t, err) + require.NotNil(t, resp) + require.Equal(t, []interface{}{[]interface{}{"sql_reverse_unordered_selects", true}}, resp.Data) + + // Fetch current setting value. + resp, err = conn.Do(NewSQLReverseUnorderedSelectsGetRequest()).Get() + require.Nil(t, err) + require.NotNil(t, resp) + require.Equal(t, []interface{}{[]interface{}{"sql_reverse_unordered_selects", true}}, resp.Data) + + // Select multiple records. + resp, err = conn.Execute("SELECT * FROM data;", []interface{}{}) + require.Nil(t, err) + require.NotNil(t, resp) + require.EqualValues(t, []interface{}{"2"}, resp.Data[0]) + require.EqualValues(t, []interface{}{"1"}, resp.Data[1]) +} + +func TestSQLSelectDebugSetting(t *testing.T) { + skipIfSettingsUnsupported(t) + + var resp *tarantool.Response + var err error + + conn := test_helpers.ConnectWithValidation(t, server, opts) + defer conn.Close() + + // Disable select debug mode. + resp, err = conn.Do(NewSQLSelectDebugSetRequest(false)).Get() + require.Nil(t, err) + require.NotNil(t, resp) + require.Equal(t, []interface{}{[]interface{}{"sql_select_debug", false}}, resp.Data) + + // Fetch current setting value. + resp, err = conn.Do(NewSQLSelectDebugGetRequest()).Get() + require.Nil(t, err) + require.NotNil(t, resp) + require.Equal(t, []interface{}{[]interface{}{"sql_select_debug", false}}, resp.Data) + + // Enable select debug mode. + resp, err = conn.Do(NewSQLSelectDebugSetRequest(true)).Get() + require.Nil(t, err) + require.NotNil(t, resp) + require.Equal(t, []interface{}{[]interface{}{"sql_select_debug", true}}, resp.Data) + + // Fetch current setting value. + resp, err = conn.Do(NewSQLSelectDebugGetRequest()).Get() + require.Nil(t, err) + require.NotNil(t, resp) + require.Equal(t, []interface{}{[]interface{}{"sql_select_debug", true}}, resp.Data) + + // To test real effect we need a Tarantool instance built with + // `-DCMAKE_BUILD_TYPE=Debug`. +} + +func TestSQLVDBEDebugSetting(t *testing.T) { + skipIfSettingsUnsupported(t) + + var resp *tarantool.Response + var err error + + conn := test_helpers.ConnectWithValidation(t, server, opts) + defer conn.Close() + + // Disable VDBE debug mode. + resp, err = conn.Do(NewSQLVDBEDebugSetRequest(false)).Get() + require.Nil(t, err) + require.NotNil(t, resp) + require.Equal(t, []interface{}{[]interface{}{"sql_vdbe_debug", false}}, resp.Data) + + // Fetch current setting value. + resp, err = conn.Do(NewSQLVDBEDebugGetRequest()).Get() + require.Nil(t, err) + require.NotNil(t, resp) + require.Equal(t, []interface{}{[]interface{}{"sql_vdbe_debug", false}}, resp.Data) + + // Enable VDBE debug mode. + resp, err = conn.Do(NewSQLVDBEDebugSetRequest(true)).Get() + require.Nil(t, err) + require.NotNil(t, resp) + require.Equal(t, []interface{}{[]interface{}{"sql_vdbe_debug", true}}, resp.Data) + + // Fetch current setting value. + resp, err = conn.Do(NewSQLVDBEDebugGetRequest()).Get() + require.Nil(t, err) + require.NotNil(t, resp) + require.Equal(t, []interface{}{[]interface{}{"sql_vdbe_debug", true}}, resp.Data) + + // To test real effect we need a Tarantool instance built with + // `-DCMAKE_BUILD_TYPE=Debug`. +} + +func TestSessionSettings(t *testing.T) { + skipIfSettingsUnsupported(t) + + var resp *tarantool.Response + var err error + + conn := test_helpers.ConnectWithValidation(t, server, opts) + defer conn.Close() + + // Set some settings values. + resp, err = conn.Do(NewSQLDefaultEngineSetRequest("memtx")).Get() + require.Nil(t, err) + require.NotNil(t, resp) + require.Equal(t, []interface{}{[]interface{}{"sql_default_engine", "memtx"}}, resp.Data) + + resp, err = conn.Do(NewSQLFullColumnNamesSetRequest(true)).Get() + require.Nil(t, err) + require.NotNil(t, resp) + require.Equal(t, []interface{}{[]interface{}{"sql_full_column_names", true}}, resp.Data) + + // Fetch current settings values. + resp, err = conn.Do(NewSessionSettingsGetRequest()).Get() + require.Nil(t, err) + require.NotNil(t, resp) + require.Subset(t, resp.Data, + []interface{}{ + []interface{}{"sql_default_engine", "memtx"}, + []interface{}{"sql_full_column_names", true}, + }) +} + +func TestRequestsAsync(t *testing.T) { + tests := []struct { + req tarantool.Request + async bool + }{ + {req: NewErrorMarshalingEnabledSetRequest(false), async: false}, + {req: NewErrorMarshalingEnabledGetRequest(), async: false}, + {req: NewSQLDefaultEngineSetRequest("memtx"), async: false}, + {req: NewSQLDefaultEngineGetRequest(), async: false}, + {req: NewSQLDeferForeignKeysSetRequest(false), async: false}, + {req: NewSQLDeferForeignKeysGetRequest(), async: false}, + {req: NewSQLFullColumnNamesSetRequest(false), async: false}, + {req: NewSQLFullColumnNamesGetRequest(), async: false}, + {req: NewSQLFullMetadataSetRequest(false), async: false}, + {req: NewSQLFullMetadataGetRequest(), async: false}, + {req: NewSQLParserDebugSetRequest(false), async: false}, + {req: NewSQLParserDebugGetRequest(), async: false}, + {req: NewSQLRecursiveTriggersSetRequest(false), async: false}, + {req: NewSQLRecursiveTriggersGetRequest(), async: false}, + {req: NewSQLReverseUnorderedSelectsSetRequest(false), async: false}, + {req: NewSQLReverseUnorderedSelectsGetRequest(), async: false}, + {req: NewSQLSelectDebugSetRequest(false), async: false}, + {req: NewSQLSelectDebugGetRequest(), async: false}, + {req: NewSQLVDBEDebugSetRequest(false), async: false}, + {req: NewSQLVDBEDebugGetRequest(), async: false}, + } + + for _, test := range tests { + if async := test.req.Async(); async != test.async { + t.Errorf("An invalid async %t, expected %t", async, test.async) + } + } +} + +// runTestMain is a body of TestMain function +// (see https://pkg.go.dev/testing#hdr-Main). +// Using defer + os.Exit is not works so TestMain body +// is a separate function, see +// https://stackoverflow.com/questions/27629380/how-to-exit-a-go-program-honoring-deferred-calls +func runTestMain(m *testing.M) int { + isLess, err := test_helpers.IsTarantoolVersionLess(2, 3, 1) + if err != nil { + log.Fatalf("Failed to extract tarantool version: %s", err) + } + + if isLess { + log.Println("Skipping session settings tests...") + isSettingsSupported = false + return m.Run() + } + + isSettingsSupported = true + + inst, err := test_helpers.StartTarantool(test_helpers.StartOpts{ + InitScript: "config.lua", + Listen: server, + User: opts.User, + Pass: opts.Pass, + WaitStart: 100 * time.Millisecond, + ConnectRetry: 3, + RetryTimeout: 500 * time.Millisecond, + }) + + if err != nil { + log.Fatalf("Failed to prepare test tarantool: %s", err) + } + + defer test_helpers.StopTarantoolWithCleanup(inst) + + return m.Run() +} + +func TestMain(m *testing.M) { + code := runTestMain(m) + os.Exit(code) +}