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..2bec72a0e --- /dev/null +++ b/settings/const.go @@ -0,0 +1,48 @@ +package settings + +const sessionSettingsSpace string = "_session_settings" + +// In Go and IPROTO_UPDATE count starts with 0. +const sessionSettingValueField int = 1 + +const ( + // ErrorMarshalingEnabled defines whether error objects + // have a special structure. Added in Tarantool 2.4.1, dropped + // in Tarantool 2.10.0 in favor of ErrorExtensionFeature protocol + // feature. Default is `false`. + ErrorMarshalingEnabled string = "error_marshaling_enabled" + // SQLDefaultEngine defined default storage engine for + // new SQL tables. Added in Tarantool 2.3.1. Default is `"memtx"`. + SQLDefaultEngine string = "sql_default_engine" + // SQLDeferForeignKeys defines whether foreign-key checks + // can wait till commit. Added in Tarantool 2.3.1, dropped + // in master commit 14618c4 (possible 2.10.5 or 2.11.0). Default is `false`. + SQLDeferForeignKeys string = "sql_defer_foreign_keys" + // SQLFullColumnNames defines whether full column names is displayed + // in SQL result set metadata. Added in Tarantool 2.3.1. Default is `false`. + SQLFullColumnNames string = "sql_full_column_names" + // SQLFullMetadata defines whether SQL result set metadata will have + // more than just name and type. Added in Tarantool 2.3.1. Default is `false`. + SQLFullMetadata string = "sql_full_metadata" + // SQLParserDebug defines whether to show parser steps for following + // statements. Option has no effect unless Tarantool was built with + // `-DCMAKE_BUILD_TYPE=Debug`. Added in Tarantool 2.3.1. Default is `false`. + SQLParserDebug string = "sql_parser_debug" + // SQLParserDebug defines whether a triggered statement can activate + // a trigger. Added in Tarantool 2.3.1. Default is `true`. + SQLRecursiveTriggers string = "sql_recursive_triggers" + // SQLReverseUnorderedSelects defines whether result rows are usually + // in reverse order if there is no ORDER BY clause. Added in Tarantool 2.3.1. + // Default is `false`. + SQLReverseUnorderedSelects string = "sql_reverse_unordered_selects" + // SQLSelectDebug defines whether to show execution steps during SELECT. + // Option has no effect unless Tarantool was built with `-DCMAKE_BUILD_TYPE=Debug`. + // Added in Tarantool 2.3.1. Default is `false`. + SQLSelectDebug string = "sql_select_debug" + // SQLVDBEDebug defines whether VDBE debug mode is enabled. + // Option has no effect unless Tarantool was built with `-DCMAKE_BUILD_TYPE=Debug`. + // Added in Tarantool 2.3.1. Default is `false`. + 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..9f3716bdb --- /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 ExampleSQLFullColumnNames() { + 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.SetSQLFullColumnNames(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.SetSQLFullColumnNames(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_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_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..264f3f8f2 --- /dev/null +++ b/settings/request.go @@ -0,0 +1,165 @@ +package settings + +import ( + "github.com/tarantool/go-tarantool" +) + +func newSetRequest(setting string, value interface{}) *tarantool.UpdateRequest { + return tarantool.NewUpdateRequest(sessionSettingsSpace). + Key(tarantool.StringKey{S: setting}). + Operations(tarantool.NewOperations().Assign(sessionSettingValueField, value)) +} + +func newGetRequest(setting string) *tarantool.SelectRequest { + return tarantool.NewSelectRequest(sessionSettingsSpace). + Key(tarantool.StringKey{S: setting}). + Limit(1) +} + +// SetErrorMarshalingEnabled creates a request to +// update current session ErrorMarshalingEnabled setting. +// Added in 1.10.0. +func SetErrorMarshalingEnabled(value bool) *tarantool.UpdateRequest { + return newSetRequest(ErrorMarshalingEnabled, value) +} + +// GetErrorMarshalingEnabled creates a request to get +// current session ErrorMarshalingEnabled setting in tuple format. +// Added in 1.10.0. +func GetErrorMarshalingEnabled() *tarantool.SelectRequest { + return newGetRequest(ErrorMarshalingEnabled) +} + +// SetSQLDefaultEngine creates a request to +// update current session SQLDefaultEngine setting. +// Added in 1.10.0. +func SetSQLDefaultEngine(value string) *tarantool.UpdateRequest { + return newSetRequest(SQLDefaultEngine, value) +} + +// GetSQLDefaultEngine creates a request to get +// current session SQLDefaultEngine setting in tuple format. +// Added in 1.10.0. +func GetSQLDefaultEngine() *tarantool.SelectRequest { + return newGetRequest(SQLDefaultEngine) +} + +// SetSQLDeferForeignKeys creates a request to +// update current session SQLDeferForeignKeys setting. +// Added in 1.10.0. +func SetSQLDeferForeignKeys(value bool) *tarantool.UpdateRequest { + return newSetRequest(SQLDeferForeignKeys, value) +} + +// GetSQLDeferForeignKeys creates a request to get +// current session SQLDeferForeignKeys setting in tuple format. +// Added in 1.10.0. +func GetSQLDeferForeignKeys() *tarantool.SelectRequest { + return newGetRequest(SQLDeferForeignKeys) +} + +// SetSQLFullColumnNames creates a request to +// update current session SQLFullColumnNames setting. +// Added in 1.10.0. +func SetSQLFullColumnNames(value bool) *tarantool.UpdateRequest { + return newSetRequest(SQLFullColumnNames, value) +} + +// GetSQLFullColumnNames creates a request to get +// current session SQLFullColumnNames setting in tuple format. +// Added in 1.10.0. +func GetSQLFullColumnNames() *tarantool.SelectRequest { + return newGetRequest(SQLFullColumnNames) +} + +// SetSQLFullMetadata creates a request to +// update current session SQLFullMetadata setting. +// Added in 1.10.0. +func SetSQLFullMetadata(value bool) *tarantool.UpdateRequest { + return newSetRequest(SQLFullMetadata, value) +} + +// GetSQLFullMetadata creates a request to get +// current session SQLFullMetadata setting in tuple format. +// Added in 1.10.0. +func GetSQLFullMetadata() *tarantool.SelectRequest { + return newGetRequest(SQLFullMetadata) +} + +// SetSQLParserDebug creates a request to +// update current session SQLParserDebug setting. +// Added in 1.10.0. +func SetSQLParserDebug(value bool) *tarantool.UpdateRequest { + return newSetRequest(SQLParserDebug, value) +} + +// GetSQLParserDebug creates a request to get +// current session SQLParserDebug setting in tuple format. +// Added in 1.10.0. +func GetSQLParserDebug() *tarantool.SelectRequest { + return newGetRequest(SQLParserDebug) +} + +// SetSQLRecursiveTriggers creates a request to +// update current session SQLRecursiveTriggers setting. +// Added in 1.10.0. +func SetSQLRecursiveTriggers(value bool) *tarantool.UpdateRequest { + return newSetRequest(SQLRecursiveTriggers, value) +} + +// GetSQLRecursiveTriggers creates a request to get +// current session SQLRecursiveTriggers setting in tuple format. +// Added in 1.10.0. +func GetSQLRecursiveTriggers() *tarantool.SelectRequest { + return newGetRequest(SQLRecursiveTriggers) +} + +// SetSQLReverseUnorderedSelects creates a request to +// update current session SQLReverseUnorderedSelects setting. +// Added in 1.10.0. +func SetSQLReverseUnorderedSelects(value bool) *tarantool.UpdateRequest { + return newSetRequest(SQLReverseUnorderedSelects, value) +} + +// GetSQLReverseUnorderedSelects creates a request to get +// current session SQLReverseUnorderedSelects setting in tuple format. +// Added in 1.10.0. +func GetSQLReverseUnorderedSelects() *tarantool.SelectRequest { + return newGetRequest(SQLReverseUnorderedSelects) +} + +// SetSQLSelectDebug creates a request to +// update current session SQLSelectDebug setting. +// Added in 1.10.0. +func SetSQLSelectDebug(value bool) *tarantool.UpdateRequest { + return newSetRequest(SQLSelectDebug, value) +} + +// GetSQLSelectDebug creates a request to get +// current session SQLSelectDebug setting in tuple format. +// Added in 1.10.0. +func GetSQLSelectDebug() *tarantool.SelectRequest { + return newGetRequest(SQLSelectDebug) +} + +// SetSQLVDBEDebug creates a request to +// update current session SQLVDBEDebug setting. +// Added in 1.10.0. +func SetSQLVDBEDebug(value bool) *tarantool.UpdateRequest { + return newSetRequest(SQLVDBEDebug, value) +} + +// GetSQLVDBEDebug creates a request to get +// current session SQLVDBEDebug setting in tuple format. +// Added in 1.10.0. +func GetSQLVDBEDebug() *tarantool.SelectRequest { + return newGetRequest(SQLVDBEDebug) +} + +// GetSession creates a request to get all +// current session settings in tuple format. +// Added in 1.10.0. +func GetSession() *tarantool.SelectRequest { + return tarantool.NewSelectRequest(sessionSettingsSpace). + Limit(selectAllLimit) +} diff --git a/settings/request_test.go b/settings/request_test.go new file mode 100644 index 000000000..acb7567cc --- /dev/null +++ b/settings/request_test.go @@ -0,0 +1,672 @@ +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 skipIfErrorMarshalingEnabledUnsupported(t *testing.T) { + t.Helper() + + test_helpers.SkipIfLess(t, "error_marshaling_enabled session setting", 2, 4, 1) + + isLess, err := test_helpers.IsTarantoolVersionLess(2, 10, 0) + if err != nil { + t.Fatalf("Could not check the Tarantool version") + } + + if !isLess { + t.Skipf("Skipping test for Tarantool with error_marshaling_enabled session setting support dropped") + } +} + +func skipIfSQLDeferForeignKeysUnsupported(t *testing.T) { + t.Helper() + + test_helpers.SkipIfLess(t, "sql_defer_foreign_keys session setting", 2, 3, 1) + + isLess, err := test_helpers.IsTarantoolVersionLess(2, 10, 5) + if err != nil { + t.Fatalf("Could not check the Tarantool version") + } + + if !isLess { + t.Skipf("Skipping test for Tarantool with sql_defer_foreign_keys session setting support dropped") + } +} + +func TestErrorMarshalingEnabled(t *testing.T) { + skipIfErrorMarshalingEnabledUnsupported(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(SetErrorMarshalingEnabled(false)).Get() + require.Nil(t, err) + require.NotNil(t, resp) + require.Equal(t, []interface{}{[]interface{}{ErrorMarshalingEnabled, false}}, resp.Data) + + // Fetch current setting value. + resp, err = conn.Do(GetErrorMarshalingEnabled()).Get() + require.Nil(t, err) + require.NotNil(t, resp) + require.Equal(t, []interface{}{[]interface{}{ErrorMarshalingEnabled, 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(SetErrorMarshalingEnabled(true)).Get() + require.Nil(t, err) + require.NotNil(t, resp) + require.Equal(t, []interface{}{[]interface{}{ErrorMarshalingEnabled, true}}, resp.Data) + + // Fetch current setting value. + resp, err = conn.Do(GetErrorMarshalingEnabled()).Get() + require.Nil(t, err) + require.NotNil(t, resp) + require.Equal(t, []interface{}{[]interface{}{ErrorMarshalingEnabled, 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 TestSQLDefaultEngine(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(SetSQLDefaultEngine("vinyl")).Get() + require.Nil(t, err) + require.NotNil(t, resp) + require.EqualValues(t, []interface{}{[]interface{}{SQLDefaultEngine, "vinyl"}}, resp.Data) + + // Fetch current setting value. + resp, err = conn.Do(GetSQLDefaultEngine()).Get() + require.Nil(t, err) + require.NotNil(t, resp) + require.Equal(t, []interface{}{[]interface{}{SQLDefaultEngine, "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(SetSQLDefaultEngine("memtx")).Get() + require.Nil(t, err) + require.NotNil(t, resp) + require.Equal(t, []interface{}{[]interface{}{SQLDefaultEngine, "memtx"}}, resp.Data) + + // Fetch current setting value. + resp, err = conn.Do(GetSQLDefaultEngine()).Get() + require.Nil(t, err) + require.NotNil(t, resp) + require.Equal(t, []interface{}{[]interface{}{SQLDefaultEngine, "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 TestSQLDeferForeignKeys(t *testing.T) { + // https://github.com/tarantool/tarantool/blob/eafadc13425f14446d7aaa49dea67dfc1d5f45e9/test/sql/transitive-transactions.result + skipIfSQLDeferForeignKeysUnsupported(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(SetSQLDeferForeignKeys(false)).Get() + require.Nil(t, err) + require.NotNil(t, resp) + require.Equal(t, []interface{}{[]interface{}{SQLDeferForeignKeys, false}}, resp.Data) + + // Fetch current setting value. + resp, err = conn.Do(GetSQLDeferForeignKeys()).Get() + require.Nil(t, err) + require.NotNil(t, resp) + require.Equal(t, []interface{}{[]interface{}{SQLDeferForeignKeys, 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(SetSQLDeferForeignKeys(true)).Get() + require.Nil(t, err) + require.NotNil(t, resp) + require.Equal(t, []interface{}{[]interface{}{SQLDeferForeignKeys, true}}, resp.Data) + + // Fetch current setting value. + resp, err = conn.Do(GetSQLDeferForeignKeys()).Get() + require.Nil(t, err) + require.NotNil(t, resp) + require.Equal(t, []interface{}{[]interface{}{SQLDeferForeignKeys, 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 TestSQLFullColumnNames(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(SetSQLFullColumnNames(false)).Get() + require.Nil(t, err) + require.NotNil(t, resp) + require.Equal(t, []interface{}{[]interface{}{SQLFullColumnNames, false}}, resp.Data) + + // Fetch current setting value. + resp, err = conn.Do(GetSQLFullColumnNames()).Get() + require.Nil(t, err) + require.NotNil(t, resp) + require.Equal(t, []interface{}{[]interface{}{SQLFullColumnNames, 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(SetSQLFullColumnNames(true)).Get() + require.Nil(t, err) + require.NotNil(t, resp) + require.Equal(t, []interface{}{[]interface{}{SQLFullColumnNames, true}}, resp.Data) + + // Fetch current setting value. + resp, err = conn.Do(GetSQLFullColumnNames()).Get() + require.Nil(t, err) + require.NotNil(t, resp) + require.Equal(t, []interface{}{[]interface{}{SQLFullColumnNames, 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 TestSQLFullMetadata(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(SetSQLFullMetadata(false)).Get() + require.Nil(t, err) + require.Equal(t, []interface{}{[]interface{}{SQLFullMetadata, false}}, resp.Data) + + // Fetch current setting value. + resp, err = conn.Do(GetSQLFullMetadata()).Get() + require.Nil(t, err) + require.NotNil(t, resp) + require.Equal(t, []interface{}{[]interface{}{SQLFullMetadata, 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(SetSQLFullMetadata(true)).Get() + require.Nil(t, err) + require.NotNil(t, resp) + require.Equal(t, []interface{}{[]interface{}{SQLFullMetadata, true}}, resp.Data) + + // Fetch current setting value. + resp, err = conn.Do(GetSQLFullMetadata()).Get() + require.Nil(t, err) + require.NotNil(t, resp) + require.Equal(t, []interface{}{[]interface{}{SQLFullMetadata, 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 TestSQLParserDebug(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(SetSQLParserDebug(false)).Get() + require.Nil(t, err) + require.NotNil(t, resp) + require.Equal(t, []interface{}{[]interface{}{SQLParserDebug, false}}, resp.Data) + + // Fetch current setting value. + resp, err = conn.Do(GetSQLParserDebug()).Get() + require.Nil(t, err) + require.NotNil(t, resp) + require.Equal(t, []interface{}{[]interface{}{SQLParserDebug, false}}, resp.Data) + + // Enable parser debug mode. + resp, err = conn.Do(SetSQLParserDebug(true)).Get() + require.Nil(t, err) + require.NotNil(t, resp) + require.Equal(t, []interface{}{[]interface{}{SQLParserDebug, true}}, resp.Data) + + // Fetch current setting value. + resp, err = conn.Do(GetSQLParserDebug()).Get() + require.Nil(t, err) + require.NotNil(t, resp) + require.Equal(t, []interface{}{[]interface{}{SQLParserDebug, true}}, resp.Data) + + // To test real effect we need a Tarantool instance built with + // `-DCMAKE_BUILD_TYPE=Debug`. +} + +func TestSQLRecursiveTriggers(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(SetSQLRecursiveTriggers(true)).Get() + require.Nil(t, err) + require.NotNil(t, resp) + require.Equal(t, []interface{}{[]interface{}{SQLRecursiveTriggers, true}}, resp.Data) + + // Fetch current setting value. + resp, err = conn.Do(GetSQLRecursiveTriggers()).Get() + require.Nil(t, err) + require.NotNil(t, resp) + require.Equal(t, []interface{}{[]interface{}{SQLRecursiveTriggers, 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(SetSQLRecursiveTriggers(false)).Get() + require.Nil(t, err) + require.NotNil(t, resp) + require.Equal(t, []interface{}{[]interface{}{SQLRecursiveTriggers, false}}, resp.Data) + + // Fetch current setting value. + resp, err = conn.Do(GetSQLRecursiveTriggers()).Get() + require.Nil(t, err) + require.NotNil(t, resp) + require.Equal(t, []interface{}{[]interface{}{SQLRecursiveTriggers, 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 TestSQLReverseUnorderedSelects(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(SetSQLReverseUnorderedSelects(false)).Get() + require.Nil(t, err) + require.NotNil(t, resp) + require.Equal(t, []interface{}{[]interface{}{SQLReverseUnorderedSelects, false}}, resp.Data) + + // Fetch current setting value. + resp, err = conn.Do(GetSQLReverseUnorderedSelects()).Get() + require.Nil(t, err) + require.NotNil(t, resp) + require.Equal(t, []interface{}{[]interface{}{SQLReverseUnorderedSelects, 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(SetSQLReverseUnorderedSelects(true)).Get() + require.Nil(t, err) + require.NotNil(t, resp) + require.Equal(t, []interface{}{[]interface{}{SQLReverseUnorderedSelects, true}}, resp.Data) + + // Fetch current setting value. + resp, err = conn.Do(GetSQLReverseUnorderedSelects()).Get() + require.Nil(t, err) + require.NotNil(t, resp) + require.Equal(t, []interface{}{[]interface{}{SQLReverseUnorderedSelects, 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 TestSQLSelectDebug(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(SetSQLSelectDebug(false)).Get() + require.Nil(t, err) + require.NotNil(t, resp) + require.Equal(t, []interface{}{[]interface{}{SQLSelectDebug, false}}, resp.Data) + + // Fetch current setting value. + resp, err = conn.Do(GetSQLSelectDebug()).Get() + require.Nil(t, err) + require.NotNil(t, resp) + require.Equal(t, []interface{}{[]interface{}{SQLSelectDebug, false}}, resp.Data) + + // Enable select debug mode. + resp, err = conn.Do(SetSQLSelectDebug(true)).Get() + require.Nil(t, err) + require.NotNil(t, resp) + require.Equal(t, []interface{}{[]interface{}{SQLSelectDebug, true}}, resp.Data) + + // Fetch current setting value. + resp, err = conn.Do(GetSQLSelectDebug()).Get() + require.Nil(t, err) + require.NotNil(t, resp) + require.Equal(t, []interface{}{[]interface{}{SQLSelectDebug, true}}, resp.Data) + + // To test real effect we need a Tarantool instance built with + // `-DCMAKE_BUILD_TYPE=Debug`. +} + +func TestSQLVDBEDebug(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(SetSQLVDBEDebug(false)).Get() + require.Nil(t, err) + require.NotNil(t, resp) + require.Equal(t, []interface{}{[]interface{}{SQLVDBEDebug, false}}, resp.Data) + + // Fetch current setting value. + resp, err = conn.Do(GetSQLVDBEDebug()).Get() + require.Nil(t, err) + require.NotNil(t, resp) + require.Equal(t, []interface{}{[]interface{}{SQLVDBEDebug, false}}, resp.Data) + + // Enable VDBE debug mode. + resp, err = conn.Do(SetSQLVDBEDebug(true)).Get() + require.Nil(t, err) + require.NotNil(t, resp) + require.Equal(t, []interface{}{[]interface{}{SQLVDBEDebug, true}}, resp.Data) + + // Fetch current setting value. + resp, err = conn.Do(GetSQLVDBEDebug()).Get() + require.Nil(t, err) + require.NotNil(t, resp) + require.Equal(t, []interface{}{[]interface{}{SQLVDBEDebug, 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(SetSQLDefaultEngine("memtx")).Get() + require.Nil(t, err) + require.NotNil(t, resp) + require.Equal(t, []interface{}{[]interface{}{SQLDefaultEngine, "memtx"}}, resp.Data) + + resp, err = conn.Do(SetSQLFullColumnNames(true)).Get() + require.Nil(t, err) + require.NotNil(t, resp) + require.Equal(t, []interface{}{[]interface{}{SQLFullColumnNames, true}}, resp.Data) + + // Fetch current settings values. + resp, err = conn.Do(GetSession()).Get() + require.Nil(t, err) + require.NotNil(t, resp) + require.Subset(t, resp.Data, + []interface{}{ + []interface{}{SQLDefaultEngine, "memtx"}, + []interface{}{SQLFullColumnNames, true}, + }) +} + +// 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) +}