diff --git a/README.md b/README.md index a0eac7e09..7a165f24c 100644 --- a/README.md +++ b/README.md @@ -22,6 +22,7 @@ It also provides the `crud-storage` and `crud-router` roles for - [Delete](#delete) - [Replace](#replace) - [Upsert](#upsert) + - [Upsert many](#upsert-many) - [Select](#select) - [Select conditions](#select-conditions) - [Pairs](#pairs) @@ -524,6 +525,100 @@ crud.upsert_object('customers', ... ``` +### Upsert many + +```lua +-- Upsert batch of tuples +local result, err = crud.upsert_many(space_name, tuples, operations, opts) +-- Upsert batch of objects +local result, err = crud.upsert_object_many(space_name, objects, operations, opts) +``` + +where: + +* `space_name` (`string`) - name of the space to insert an object +* `tuples` / `objects` (`table`) - array of tuples/objects to insert +* `operations` (`table`) - update [operations](https://www.tarantool.io/en/doc/latest/reference/reference_lua/box_space/#box-space-update) if there is an existing tuple which matches the key fields of tuple +* `opts`: + * `timeout` (`?number`) - `vshard.call` timeout (in seconds) + * `fields` (`?table`) - field names for getting only a subset of fields + * `stop_on_error` (`?boolean`) - stop on a first error and report error + regarding the failed operation and error about what tuples were not + performed, default is `false` + * `rollback_on_error` (`?boolean`) - any failed operation will lead to + rollback on a storage, where the operation is failed, report error + about what tuples were rollback, default is `false` + +Returns metadata and array of empty arrays, array of errors. +Error object can contain field `operation_data`. + +This field can contain: +* tuple for which the error occurred; +* object with an incorrect format; +* tuple the operation on which was performed but + operation was rollback; +* tuple the operation on which was not performed + because operation was stopped by error. + +Right now CRUD cannot provide batch upsert with full consistency. +CRUD offers batch upsert with partial consistency. That means +that full consistency can be provided only on single replicaset +using `box` transactions. + +**Example:** + +```lua +crud.upsert_many('customers', { + {1, box.NULL, 'Elizabeth', 23}, + {2, box.NULL, 'Anastasia', 22}, +},{{'+', 'age', 1}, {'+', 'age', 2}}) +--- +- metadata: + - {'name': 'id', 'type': 'unsigned'} + - {'name': 'bucket_id', 'type': 'unsigned'} + - {'name': 'name', 'type': 'string'} + - {'name': 'age', 'type': 'number'} + rows: + - [] + - [] +... +crud.upsert_object_many('customers', { + {id = 3, name = 'Elizabeth', age = 24}, + {id = 10, name = 'Anastasia', age = 21}, +}, {{'+', 'age', 1}, {'+', 'age', 2}}) +--- +- metadata: + - {'name': 'id', 'type': 'unsigned'} + - {'name': 'bucket_id', 'type': 'unsigned'} + - {'name': 'name', 'type': 'string'} + - {'name': 'age', 'type': 'number'} + rows: + - [] + - [] + +-- Partial success +local res, errs = crud.upsert_object_many('customers', { + {id = 22, name = 'Alex', age = 34}, + {id = 3, name = 'Anastasia', age = 22}, + {id = 5, name = 'Sergey', age = 25}, +}, {{'+', 'age', 12}, {'=', 'age', 'invalid type'}, {'+', 'age', 10}}) +--- +res +- metadata: + - {'name': 'id', 'type': 'unsigned'} + - {'name': 'bucket_id', 'type': 'unsigned'} + - {'name': 'name', 'type': 'string'} + - {'name': 'age', 'type': 'number'} + rows: + - [], + - [], + +#errs -- 1 +errs[1].class_name -- BatchUpsertError +errs[1].err -- 'Tuple field 4 (age) type does not match one required by operation <...>' +errs[1].tuple -- {3, 2804, 'Anastasia', 22} +... +``` ### Select diff --git a/crud.lua b/crud.lua index 9a12115e2..82665de35 100644 --- a/crud.lua +++ b/crud.lua @@ -9,6 +9,7 @@ local replace = require('crud.replace') local get = require('crud.get') local update = require('crud.update') local upsert = require('crud.upsert') +local upsert_many = require('crud.upsert_many') local delete = require('crud.delete') local select = require('crud.select') local truncate = require('crud.truncate') @@ -60,6 +61,14 @@ crud.update = stats.wrap(update.call, stats.op.UPDATE) -- @function upsert crud.upsert = stats.wrap(upsert.tuple, stats.op.UPSERT) +-- @refer upsert_many.tuples +-- @function upsert_many +crud.upsert_many = upsert_many.tuples + +-- @refer upsert_many.objects +-- @function upsert_object_many +crud.upsert_object_many = upsert_many.objects + -- @refer upsert.object -- @function upsert crud.upsert_object = stats.wrap(upsert.object, stats.op.UPSERT) @@ -138,6 +147,7 @@ function crud.init_storage() replace.init() update.init() upsert.init() + upsert_many.init() delete.init() select.init() truncate.init() diff --git a/crud/common/map_call_cases/batch_insert_iter.lua b/crud/common/map_call_cases/batch_insert_iter.lua index 24e07a878..637bce22f 100644 --- a/crud/common/map_call_cases/batch_insert_iter.lua +++ b/crud/common/map_call_cases/batch_insert_iter.lua @@ -70,7 +70,7 @@ function BatchInsertIterator:get() local replicaset = self.next_index local func_args = { self.space_name, - self.next_batch, + self.next_batch.tuples, self.opts, } diff --git a/crud/common/map_call_cases/batch_upsert_iter.lua b/crud/common/map_call_cases/batch_upsert_iter.lua new file mode 100644 index 000000000..ad349b8bc --- /dev/null +++ b/crud/common/map_call_cases/batch_upsert_iter.lua @@ -0,0 +1,88 @@ +local errors = require('errors') + +local dev_checks = require('crud.common.dev_checks') +local sharding = require('crud.common.sharding') + +local BaseIterator = require('crud.common.map_call_cases.base_iter') + +local SplitTuplesError = errors.new_class('SplitTuplesError') + +local BatchUpsertIterator = {} +-- inheritance from BaseIterator +setmetatable(BatchUpsertIterator, {__index = BaseIterator}) + +--- Create new batch upsert iterator for map call +-- +-- @function new +-- +-- @tparam[opt] table opts +-- Options of BatchUpsertIterator:new +-- @tparam[opt] table opts.tuples +-- Tuples to be upserted +-- @tparam[opt] table opts.space +-- Space to be upserted into +-- @tparam[opt] table opts.operations +-- Operations to be performed on tuples +-- @tparam[opt] table opts.execute_on_storage_opts +-- Additional opts for call on storage +-- +-- @return[1] table iterator +-- @treturn[2] nil +-- @treturn[2] table of tables Error description +function BatchUpsertIterator:new(opts) + dev_checks('table', { + tuples = 'table', + space = 'table', + operations = 'table', + execute_on_storage_opts = 'table', + }) + + local sharding_data, err = sharding.split_tuples_by_replicaset(opts.tuples, opts.space, { + operations = opts.operations, + }) + if err ~= nil then + return nil, SplitTuplesError:new("Failed to split tuples by replicaset: %s", err.err) + end + + local next_replicaset, next_batch = next(sharding_data.batches) + + local execute_on_storage_opts = opts.execute_on_storage_opts + execute_on_storage_opts.sharding_func_hash = sharding_data.sharding_func_hash + execute_on_storage_opts.sharding_key_hash = sharding_data.sharding_key_hash + execute_on_storage_opts.skip_sharding_hash_check = sharding_data.skip_sharding_hash_check + + local iter = { + space_name = opts.space.name, + opts = execute_on_storage_opts, + batches_by_replicasets = sharding_data.batches, + next_index = next_replicaset, + next_batch = next_batch, + } + + setmetatable(iter, self) + self.__index = self + + return iter +end + +--- Get function arguments and next replicaset +-- +-- @function get +-- +-- @return[1] table func_args +-- @return[2] table replicaset +function BatchUpsertIterator:get() + local replicaset = self.next_index + local func_args = { + self.space_name, + self.next_batch.tuples, + self.next_batch.operations, + self.opts, + } + + self.next_index, self.next_batch = next(self.batches_by_replicasets, self.next_index) + + return func_args, replicaset +end + +return BatchUpsertIterator diff --git a/crud/common/sharding/init.lua b/crud/common/sharding/init.lua index db3b9b463..95bfecab1 100644 --- a/crud/common/sharding/init.lua +++ b/crud/common/sharding/init.lua @@ -209,8 +209,12 @@ end -- @return[1] batches -- Map where key is a replicaset and value -- is table of tuples related to this replicaset -function sharding.split_tuples_by_replicaset(tuples, space) - dev_checks('table', 'table') +function sharding.split_tuples_by_replicaset(tuples, space, opts) + dev_checks('table', 'table', { + operations = '?table', + }) + + opts = opts or {} local batches = {} @@ -219,7 +223,7 @@ function sharding.split_tuples_by_replicaset(tuples, space) local skip_sharding_hash_check local sharding_data local err - for _, tuple in ipairs(tuples) do + for i, tuple in ipairs(tuples) do sharding_data, err = sharding.tuple_set_and_return_bucket_id(tuple, space) if err ~= nil then return nil, BucketIDError:new("Failed to get bucket ID: %s", err) @@ -244,9 +248,15 @@ function sharding.split_tuples_by_replicaset(tuples, space) sharding_data.bucket_id, err.err) end - local tuples_by_replicaset = batches[replicaset] or {} - table.insert(tuples_by_replicaset, tuple) - batches[replicaset] = tuples_by_replicaset + local record_by_replicaset = batches[replicaset] or {tuples = {}} + table.insert(record_by_replicaset.tuples, tuple) + + if opts.operations ~= nil then + record_by_replicaset.operations = record_by_replicaset.operations or {} + table.insert(record_by_replicaset.operations, opts.operations[i]) + end + + batches[replicaset] = record_by_replicaset end return { diff --git a/crud/upsert_many.lua b/crud/upsert_many.lua new file mode 100644 index 000000000..b965cf07e --- /dev/null +++ b/crud/upsert_many.lua @@ -0,0 +1,305 @@ +local checks = require('checks') +local errors = require('errors') +local vshard = require('vshard') + +local call = require('crud.common.call') +local const = require('crud.common.const') +local utils = require('crud.common.utils') +local batching_utils = require('crud.common.batching_utils') +local sharding = require('crud.common.sharding') +local dev_checks = require('crud.common.dev_checks') +local schema = require('crud.common.schema') + +local BatchUpsertIterator = require('crud.common.map_call_cases.batch_upsert_iter') +local BatchPostprocessor = require('crud.common.map_call_cases.batch_postprocessor') + +local UpsertManyError = errors.new_class('UpsertManyError', {capture_stack = false}) + +local upsert_many = {} + +local UPSERT_MANY_FUNC_NAME = '_crud.upsert_many_on_storage' + +local function upsert_many_on_storage(space_name, tuples, operations, opts) + dev_checks('string', 'table', 'table', { + add_space_schema_hash = '?boolean', + stop_on_error = '?boolean', + rollback_on_error = '?boolean', + sharding_key_hash = '?number', + sharding_func_hash = '?number', + skip_sharding_hash_check = '?boolean', + }) + + opts = opts or {} + + local space = box.space[space_name] + if space == nil then + return nil, UpsertManyError:new("Space %q doesn't exist", space_name) + end + + local _, err = sharding.check_sharding_hash(space_name, + opts.sharding_func_hash, + opts.sharding_key_hash, + opts.skip_sharding_hash_check) + + if err ~= nil then + return nil, batching_utils.construct_sharding_hash_mismatch_errors(err.err, tuples) + end + + local processed_tuples = {} + local errs = {} + + box.begin() + for i, tuple in ipairs(tuples) do + -- add_space_schema_hash is true only in case of upsert_object_many + -- the only one case when reloading schema can avoid upsert error + -- is flattening object on router + local insert_result = schema.wrap_box_space_func_result(space, 'upsert', {tuple, operations[i]}, { + add_space_schema_hash = opts.add_space_schema_hash, + }) + + if insert_result.err ~= nil then + local err = { + err = insert_result.err, + space_schema_hash = insert_result.space_schema_hash, + operation_data = tuple, + } + + table.insert(errs, err) + + if opts.stop_on_error == true then + local left_tuples = utils.list_slice(tuples, i + 1) + if next(left_tuples) then + errs = batching_utils.complement_batching_errors(errs, + batching_utils.stop_on_error_msg, left_tuples) + end + + if opts.rollback_on_error == true then + box.rollback() + if next(processed_tuples) then + errs = batching_utils.complement_batching_errors(errs, + batching_utils.rollback_on_error_msg, processed_tuples) + end + + return nil, errs + end + + box.commit() + + return nil, errs + end + else + table.insert(processed_tuples, tuple) + end + end + + if next(errs) ~= nil then + if opts.rollback_on_error == true then + box.rollback() + if next(processed_tuples) then + errs = batching_utils.complement_batching_errors(errs, + batching_utils.rollback_on_error_msg, processed_tuples) + end + + return nil, errs + end + + box.commit() + + return nil, errs + end + + box.commit() + + return nil +end + +function upsert_many.init() + _G._crud.upsert_many_on_storage = upsert_many_on_storage +end + +-- returns result, err, need_reload +-- need_reload indicates if reloading schema could help +-- see crud.common.schema.wrap_func_reload() +local function call_upsert_many_on_router(space_name, original_tuples_operation_data, opts) + dev_checks('string', 'table', { + timeout = '?number', + fields = '?table', + add_space_schema_hash = '?boolean', + stop_on_error = '?boolean', + rollback_on_error = '?boolean', + }) + + opts = opts or {} + + local space = utils.get_space(space_name, vshard.router.routeall()) + if space == nil then + return nil, {UpsertManyError:new("Space %q doesn't exist", space_name)}, const.NEED_SCHEMA_RELOAD + end + + local space_format = space:format() + local tuples = {} + local operations = {} + for _, tuple_operation_data in ipairs(original_tuples_operation_data) do + local tuple = table.deepcopy(tuple_operation_data[1]) + local operations_by_tuple = tuple_operation_data[2] + + if not utils.tarantool_supports_fieldpaths() then + local converted_operations, err = utils.convert_operations(operations_by_tuple, space_format) + if err ~= nil then + return nil, {UpsertManyError:new("Wrong operations are specified: %s", err)}, const.NEED_SCHEMA_RELOAD + end + + operations_by_tuple = converted_operations + end + + table.insert(tuples, tuple) + table.insert(operations, operations_by_tuple) + end + + local upsert_many_on_storage_opts = { + add_space_schema_hash = opts.add_space_schema_hash, + stop_on_error = opts.stop_on_error, + rollback_on_error = opts.rollback_on_error, + } + + local iter, err = BatchUpsertIterator:new({ + tuples = tuples, + space = space, + operations = operations, + execute_on_storage_opts = upsert_many_on_storage_opts, + }) + if err ~= nil then + return nil, {err}, const.NEED_SCHEMA_RELOAD + end + + local postprocessor = BatchPostprocessor:new() + + local _, errs = call.map(UPSERT_MANY_FUNC_NAME, nil, { + timeout = opts.timeout, + mode = 'write', + iter = iter, + postprocessor = postprocessor, + }) + + if errs ~= nil then + local tuples_count = utils.table_count(tuples) + if sharding.batching_result_needs_sharding_reload(errs, tuples_count) then + return nil, errs, const.NEED_SHARDING_RELOAD + end + + if schema.batching_result_needs_reload(space, errs, tuples_count) then + return nil, errs, const.NEED_SCHEMA_RELOAD + end + + if utils.table_count(tuples) == utils.table_count(errs) then + return nil, errs + end + end + + local res, err = utils.format_result(nil, space, opts.fields) + if err ~= nil then + errs = errs or {} + table.insert(errs, err) + return nil, errs + end + + return res, errs +end + +--- Update or insert batch of tuples to the specified space +-- +-- @function tuples +-- +-- @param string space_name +-- A space name +-- +-- @param table tuples_operation_data +-- Tuples and operations in format +-- {{tuple_1, operation_1}, ..., {tuple_n, operation_n}} +-- +-- @tparam ?table opts +-- Options of batch_upsert.tuples_batch +-- +-- @return[1] tuples +-- @treturn[2] nil +-- @treturn[2] table of tables Error description + +function upsert_many.tuples(space_name, tuples_operation_data, opts) + checks('string', 'table', { + timeout = '?number', + fields = '?table', + add_space_schema_hash = '?boolean', + stop_on_error = '?boolean', + rollback_on_error = '?boolean', + }) + + return schema.wrap_func_reload(sharding.wrap_method, + call_upsert_many_on_router, space_name, tuples_operation_data, opts) +end + +--- Update or insert batch of objects to the specified space +-- +-- @function objects +-- +-- @param string space_name +-- A space name +-- +-- @param table objs_operation_data +-- Objects and operations in format +-- {{obj_1, operation_1}, ..., {obj_n, operation_n}} +-- +-- @tparam ?table opts +-- Options of batch_upsert.tuples_batch +-- +-- @return[1] objects +-- @treturn[2] nil +-- @treturn[2] table of tables Error description + +function upsert_many.objects(space_name, objs_operation_data, opts) + checks('string', 'table', { + timeout = '?number', + fields = '?table', + stop_on_error = '?boolean', + rollback_on_error = '?boolean', + }) + + -- upsert can fail if router uses outdated schema to flatten object + opts = utils.merge_options(opts, {add_space_schema_hash = true}) + + local tuples_operation_data = {} + local format_errs = {} + + for _, obj_operation_data in ipairs(objs_operation_data) do + local tuple, err = utils.flatten_obj_reload(space_name, obj_operation_data[1]) + if err ~= nil then + local err_obj = UpsertManyError:new("Failed to flatten object: %s", err) + err_obj.operation_data = obj_operation_data[1] + + if opts.stop_on_error == true then + return nil, {err_obj} + end + + table.insert(format_errs, err_obj) + else + table.insert(tuples_operation_data, {tuple, obj_operation_data[2]}) + end + end + + if next(tuples_operation_data) == nil then + return nil, format_errs + end + + local res, errs = upsert_many.tuples(space_name, tuples_operation_data, opts) + + if next(format_errs) ~= nil then + if errs == nil then + errs = format_errs + else + errs = utils.list_extend(errs, format_errs) + end + end + + return res, errs +end + +return upsert_many diff --git a/test/helper.lua b/test/helper.lua index 46743999a..99a83a13b 100644 --- a/test/helper.lua +++ b/test/helper.lua @@ -522,4 +522,14 @@ function helpers.fflush_main_server_stdout(cluster, capture) return captured end +function helpers.complement_tuples_batch_with_operations(tuples, operations) + + local tuples_operation_data = {} + for i, tuple in ipairs(tuples) do + table.insert(tuples_operation_data, {tuple, operations[i]}) + end + + return tuples_operation_data +end + return helpers diff --git a/test/integration/ddl_sharding_func_test.lua b/test/integration/ddl_sharding_func_test.lua index d062290cb..f5c8cc996 100644 --- a/test/integration/ddl_sharding_func_test.lua +++ b/test/integration/ddl_sharding_func_test.lua @@ -325,6 +325,86 @@ pgroup.test_upsert = function(g) t.assert_equals(result, {14, 4, 'John', 51}) end +pgroup.test_upsert_object_many = function(g) + -- Upsert an object first time. + local result, err = g.cluster.main_server.net_box:call( + 'crud.upsert_object_many', + {g.params.space_name, { { {id = 66, name = 'Jack Sparrow', age = 25}, {{'+', 'age', 26}} } }, } + ) + t.assert_equals(result.rows, nil) + t.assert_equals(result.metadata, { + {is_nullable = false, name = 'id', type = 'unsigned'}, + {is_nullable = false, name = 'bucket_id', type = 'unsigned'}, + {is_nullable = false, name = 'name', type = 'string'}, + {is_nullable = false, name = 'age', type = 'number'}, + }) + t.assert_equals(err, nil) + + -- There is no tuple on s1 replicaset. + local conn_s1 = g.cluster:server('s1-master').net_box + local result = conn_s1.space[g.params.space_name]:get({66, 'Jack Sparrow'}) + t.assert_equals(result, nil) + + -- There is a tuple on s2 replicaset. + local conn_s2 = g.cluster:server('s2-master').net_box + local result = conn_s2.space[g.params.space_name]:get({66, 'Jack Sparrow'}) + t.assert_equals(result, {66, 6, 'Jack Sparrow', 25}) + + -- Upsert the same query second time when tuple exists. + local result, err = g.cluster.main_server.net_box:call( + 'crud.upsert_object_many', + {g.params.space_name, { { {id = 66, name = 'Jack Sparrow', age = 25}, {{'+', 'age', 26}} } }, } + ) + t.assert_equals(result.rows, nil) + t.assert_equals(err, nil) + + -- There is no tuple on s2 replicaset. + local result = conn_s1.space[g.params.space_name]:get({66, 'Jack Sparrow'}) + t.assert_equals(result, nil) + + -- There is an updated tuple on s1 replicaset. + local result = conn_s2.space[g.params.space_name]:get({66, 'Jack Sparrow'}) + t.assert_equals(result, {66, 6, 'Jack Sparrow', 51}) +end + +pgroup.test_upsert_many = function(g) + local tuple = {14, box.NULL, 'John', 25} + + -- Upsert an object first time. + local result, err = g.cluster.main_server.net_box:call('crud.upsert_many', { + g.params.space_name, { {tuple, {}} }, + }) + t.assert_equals(err, nil) + t.assert_not_equals(result, nil) + t.assert_equals(result.rows, nil) + + -- There is no tuple on s2 replicaset. + local conn_s1 = g.cluster:server('s1-master').net_box + local result = conn_s1.space[g.params.space_name]:get({14, 'John'}) + t.assert_equals(result, nil) + + -- There is a tuple on s1 replicaset. + local conn_s2 = g.cluster:server('s2-master').net_box + local result = conn_s2.space[g.params.space_name]:get({14, 'John'}) + t.assert_equals(result, {14, 4, 'John', 25}) + + -- Upsert the same query second time when tuple exists. + local result, err = g.cluster.main_server.net_box:call( + 'crud.upsert_many', + {g.params.space_name, { {tuple, {{'+', 'age', 26}}} }, } + ) + t.assert_equals(result.rows, nil) + t.assert_equals(err, nil) + + -- There is no tuple on s2 replicaset. + local result = conn_s1.space[g.params.space_name]:get({14, 'John'}) + t.assert_equals(result, nil) + + -- There is an updated tuple on s1 replicaset. + local result = conn_s2.space[g.params.space_name]:get({14, 'John'}) + t.assert_equals(result, {14, 4, 'John', 51}) +end + pgroup.test_select = function(g) -- bucket_id is id % 10 = 8 local tuple = {18, 8, 'Ptolemy', 25} diff --git a/test/integration/ddl_sharding_info_reload_test.lua b/test/integration/ddl_sharding_info_reload_test.lua index 83ed6822e..732181dd7 100644 --- a/test/integration/ddl_sharding_info_reload_test.lua +++ b/test/integration/ddl_sharding_info_reload_test.lua @@ -303,6 +303,14 @@ local test_objects_batch = { { id = 3, bucket_id = box.NULL, name = 'Petra', age = 27 }, } +local upsert_many_operations = { {}, {}, {} } +local test_tuples_operation_batch = helpers.complement_tuples_batch_with_operations( + test_tuples_batch, + upsert_many_operations) +local test_objects_operation_batch = helpers.complement_tuples_batch_with_operations( + test_objects_batch, + upsert_many_operations) + -- Sharded by "name" and computed with custom sharding function. local test_customers_new_result = { s1 = {{1, 2861, 'Emma', 22}}, @@ -355,6 +363,16 @@ local new_space_cases = { input = {'customers_new', test_object, {}}, result = test_customers_new_result, }, + upsert_many = { + func = 'crud.upsert_many', + input = {'customers_new', test_tuples_operation_batch}, + result = test_customers_new_batching_result, + }, + upsert_object_many = { + func = 'crud.upsert_object_many', + input = {'customers_new', test_objects_operation_batch}, + result = test_customers_new_batching_result, + }, } for name, case in pairs(new_space_cases) do @@ -449,6 +467,16 @@ local schema_change_sharding_key_cases = { input = {'customers', test_object, {}}, result = test_customers_age_result, }, + upsert_many = { + func = 'crud.upsert_many', + input = {'customers', test_tuples_operation_batch}, + result = test_customers_age_batching_result, + }, + upsert_object_many = { + func = 'crud.upsert_object_many', + input = {'customers', test_objects_operation_batch}, + result = test_customers_age_batching_result, + }, } for name, case in pairs(schema_change_sharding_key_cases) do @@ -622,6 +650,16 @@ local schema_change_sharding_func_cases = { input = {'customers_pk', test_object, {}}, result = test_customers_pk_func, }, + upsert_many = { + func = 'crud.upsert_many', + input = {'customers_pk', test_tuples_operation_batch}, + result = test_customers_pk_batching_func, + }, + upsert_object_many = { + func = 'crud.upsert_object_many', + input = {'customers_pk', test_objects_operation_batch}, + result = test_customers_pk_batching_func, + }, delete = { before_test = setup_customers_pk_migrated_data, func = 'crud.delete', diff --git a/test/integration/ddl_sharding_key_test.lua b/test/integration/ddl_sharding_key_test.lua index ac7b831b4..2ddf65b4e 100644 --- a/test/integration/ddl_sharding_key_test.lua +++ b/test/integration/ddl_sharding_key_test.lua @@ -302,6 +302,85 @@ pgroup.test_upsert = function(g) t.assert_equals(result, nil) end +pgroup.test_upsert_object_many = function(g) + -- Upsert an object first time. + local result, err = g.cluster.main_server.net_box:call( + 'crud.upsert_object_many', {'customers_name_key', + { { {id = 66, name = 'Jack Sparrow', age = 25}, {{'+', 'age', 25}} } }, + }) + + t.assert_equals(result.rows, nil) + t.assert_equals(result.metadata, { + {is_nullable = false, name = 'id', type = 'unsigned'}, + {is_nullable = false, name = 'bucket_id', type = 'unsigned'}, + {is_nullable = false, name = 'name', type = 'string'}, + {is_nullable = false, name = 'age', type = 'number'}, + }) + t.assert_equals(err, nil) + + -- There is a tuple on s1 replicaset. + local conn_s1 = g.cluster:server('s1-master').net_box + local result = conn_s1.space['customers_name_key']:get({66, 'Jack Sparrow'}) + t.assert_equals(result, {66, 2719, 'Jack Sparrow', 25}) + + -- There is no tuple on s2 replicaset. + local conn_s2 = g.cluster:server('s2-master').net_box + local result = conn_s2.space['customers_name_key']:get({66, 'Jack Sparrow'}) + t.assert_equals(result, nil) + + -- Upsert the same query second time when tuple exists. + local result, err = g.cluster.main_server.net_box:call( + 'crud.upsert_object_many', {'customers_name_key', + { {{id = 66, name = 'Jack Sparrow', age = 25}, {{'+', 'age', 25}}} }, + }) + t.assert_equals(result.rows, nil) + t.assert_equals(err, nil) + + -- There is an updated tuple on s1 replicaset. + local result = conn_s1.space['customers_name_key']:get({66, 'Jack Sparrow'}) + t.assert_equals(result, {66, 2719, 'Jack Sparrow', 50}) + + -- There is no tuple on s2 replicaset. + local result = conn_s2.space['customers_name_key']:get({66, 'Jack Sparrow'}) + t.assert_equals(result, nil) +end + +pgroup.test_upsert_many = function(g) + local tuple = {1, box.NULL, 'John', 25} + + -- Upsert an object first time. + local result, err = g.cluster.main_server.net_box:call('crud.upsert_many', { + 'customers_name_key', { {tuple, {}} }, + }) + t.assert_equals(err, nil) + t.assert_not_equals(result, nil) + t.assert_equals(result.rows, nil) + + -- There is a tuple on s1 replicaset. + local conn_s1 = g.cluster:server('s1-master').net_box + local result = conn_s1.space['customers_name_key']:get({1, 'John'}) + t.assert_equals(result, {1, 2699, 'John', 25}) + + -- There is no tuple on s2 replicaset. + local conn_s2 = g.cluster:server('s2-master').net_box + local result = conn_s2.space['customers_name_key']:get({1, 'John'}) + t.assert_equals(result, nil) + + -- Upsert the same query second time when tuple exists. + local result, err = g.cluster.main_server.net_box:call( + 'crud.upsert_many', {'customers_name_key', { {tuple, {{'+', 'age', 25}}} }, }) + t.assert_equals(result.rows, nil) + t.assert_equals(err, nil) + + -- There is an updated tuple on s1 replicaset. + local result = conn_s1.space['customers_name_key']:get({1, 'John'}) + t.assert_equals(result, {1, 2699, 'John', 50}) + + -- There is no tuple on s2 replicaset. + local result = conn_s2.space['customers_name_key']:get({1, 'John'}) + t.assert_equals(result, nil) +end + -- The main purpose of testcase is to verify that CRUD will calculate bucket_id -- using secondary sharding key (name) correctly and will get tuple on storage -- in replicaset s2. diff --git a/test/integration/upsert_many_test.lua b/test/integration/upsert_many_test.lua new file mode 100644 index 000000000..5d6086239 --- /dev/null +++ b/test/integration/upsert_many_test.lua @@ -0,0 +1,1966 @@ +local fio = require('fio') + +local t = require('luatest') + +local helpers = require('test.helper') + +local batching_utils = require('crud.common.batching_utils') + +local pgroup = t.group('upsert_many', { + {engine = 'memtx'}, + {engine = 'vinyl'}, +}) + +pgroup.before_all(function(g) + g.cluster = helpers.Cluster:new({ + datadir = fio.tempdir(), + server_command = helpers.entrypoint('srv_batch_operations'), + use_vshard = true, + replicasets = helpers.get_test_replicasets(), + env = { + ['ENGINE'] = g.params.engine, + }, + }) + + g.cluster:start() +end) + +pgroup.after_all(function(g) helpers.stop_cluster(g.cluster) end) + +pgroup.before_each(function(g) + helpers.truncate_space_on_cluster(g.cluster, 'customers') +end) + +pgroup.test_non_existent_space = function(g) + -- upsert_many + local result, errs = g.cluster.main_server.net_box:call('crud.upsert_many', { + 'non_existent_space', + { + {{1, box.NULL, 'Alex', 59}, {{'+', 'age', 1}}}, + {{2, box.NULL, 'Anna', 23}, {{'+', 'age', 1}}}, + {{3, box.NULL, 'Daria', 18}, {{'+', 'age', 1}}} + }, + }) + + t.assert_equals(result, nil) + t.assert_not_equals(errs, nil) + t.assert_equals(#errs, 1) + t.assert_str_contains(errs[1].err, 'Space "non_existent_space" doesn\'t exist') + + -- upsert_object_many + local result, errs = g.cluster.main_server.net_box:call('crud.upsert_object_many', { + 'non_existent_space', + { + {{id = 1, name = 'Fedor', age = 59}, {{'+', 'age', 1}}}, + {{id = 2, name = 'Anna', age = 23}, {{'+', 'age', 1}}}, + {{id = 3, name = 'Daria', age = 18}, {{'+', 'age', 1}}}, + }, + }) + + t.assert_equals(result, nil) + t.assert_not_equals(errs, nil) + + -- we got 3 errors about non existent space, because it caused by flattening objects + t.assert_equals(#errs, 3) + t.assert_str_contains(errs[1].err, 'Space "non_existent_space" doesn\'t exist') + t.assert_str_contains(errs[2].err, 'Space "non_existent_space" doesn\'t exist') + t.assert_str_contains(errs[3].err, 'Space "non_existent_space" doesn\'t exist') + + -- upsert_object_many + local result, errs = g.cluster.main_server.net_box:call('crud.upsert_object_many', { + 'non_existent_space', + { + {{id = 1, name = 'Fedor', age = 59}, {{'+', 'age', 1}}}, + {{id = 2, name = 'Anna', age = 23}, {{'+', 'age', 1}}}, + {{id = 3, name = 'Daria', age = 18}, {{'+', 'age', 1}}}, + }, + { + stop_on_error = true, + } + }) + + t.assert_equals(result, nil) + t.assert_not_equals(errs, nil) + + -- we got 1 error about non existent space, because stop_on_error == true + t.assert_equals(#errs, 1) + t.assert_str_contains(errs[1].err, 'Space "non_existent_space" doesn\'t exist') +end + +pgroup.test_object_bad_format = function(g) + -- bad format + local result, errs = g.cluster.main_server.net_box:call('crud.upsert_object_many', { + 'customers', + { + {{id = 1, name = 'Fedor', age = 59}, {{'+', 'age', 12}}}, + {{id = 2, name = 'Anna'}, {{'+', 'age', 12}}}, + }, + }) + + t.assert_not_equals(errs, nil) + t.assert_equals(#errs, 1) + t.assert_str_contains(errs[1].err, 'Field \"age\" isn\'t nullable') + t.assert_equals(errs[1].operation_data, {id = 2, name = 'Anna'}) + + t.assert_equals(result.metadata, { + {name = 'id', type = 'unsigned'}, + {name = 'bucket_id', type = 'unsigned'}, + {name = 'name', type = 'string'}, + {name = 'age', type = 'number'}, + }) + t.assert_equals(result.rows, nil) + + -- get + -- primary key = 1 -> bucket_id = 477 -> s2-master + local conn_s2 = g.cluster:server('s2-master').net_box + local result = conn_s2.space['customers']:get(1) + t.assert_equals(result, {1, 477, 'Fedor', 59}) + + -- bad format + local result, errs = g.cluster.main_server.net_box:call('crud.upsert_object_many', { + 'customers', + { + {{id = 1, name = 'Fedor', age = 59}, {{'+', 'age', '1'}}}, + {{id = 2, name = 'Anna'}, {{'+', 'age', 12}}} + }, + }) + + t.assert_equals(result, nil) + t.assert_not_equals(errs, nil) + t.assert_equals(#errs, 2) + + if helpers.tarantool_version_at_least(2, 3) then + t.assert_str_contains(errs[1].err, + 'Argument type in operation \'+\' on field \'age\' does not match field type') + else + t.assert_str_contains(errs[1].err, + 'Argument type in operation \'+\' on field 4 does not match field type') + end + t.assert_equals(errs[1].operation_data, {1, 477, "Fedor", 59}) + + t.assert_str_contains(errs[2].err, 'Field \"age\" isn\'t nullable') + t.assert_equals(errs[2].operation_data, {id = 2, name = 'Anna'}) + + -- bad format + -- two errors, default: stop_on_error == false + local result, errs = g.cluster.main_server.net_box:call('crud.upsert_object_many', { + 'customers', + { + {{id = 2, name = 'Anna'}, {{'+', 'age', 25}, {'=', 'name', 'Leo Tolstoy'},}}, + {{id = 3, name = 'Inga'}, {{'+', 'age', 12}}} + }, + }) + + t.assert_equals(result, nil) + t.assert_not_equals(errs, nil) + t.assert_equals(#errs, 2) + + t.assert_str_contains(errs[1].err, 'Field \"age\" isn\'t nullable') + t.assert_equals(errs[1].operation_data, {id = 2, name = 'Anna'}) + + t.assert_str_contains(errs[2].err, 'Field \"age\" isn\'t nullable') + t.assert_equals(errs[2].operation_data, {id = 3, name = 'Inga'}) +end + +pgroup.test_all_success = function(g) + -- upsert_many + local result, errs = g.cluster.main_server.net_box:call('crud.upsert_many', { + 'customers', + { + {{1, box.NULL, 'Fedor', 59}, {{'+', 'age', 25}, {'=', 'name', 'Leo Tolstoy'},}}, + {{2, box.NULL, 'Anna', 23}, {{'+', 'age', 12}}}, + {{3, box.NULL, 'Daria', 18}, {{'=', 'name', 'Jane'}}} + }, + }) + + t.assert_equals(errs, nil) + t.assert_equals(result.metadata, { + {name = 'id', type = 'unsigned'}, + {name = 'bucket_id', type = 'unsigned'}, + {name = 'name', type = 'string'}, + {name = 'age', type = 'number'}, + }) + t.assert_equals(result.rows, nil) + + -- get + -- primary key = 1 -> bucket_id = 477 -> s2-master + local conn_s2 = g.cluster:server('s2-master').net_box + local result = conn_s2.space['customers']:get(1) + t.assert_equals(result, {1, 477, 'Fedor', 59}) + + -- primary key = 2 -> bucket_id = 401 -> s2-master + local conn_s2 = g.cluster:server('s2-master').net_box + local result = conn_s2.space['customers']:get(2) + t.assert_equals(result, {2, 401, 'Anna', 23}) + + -- primary key = 3 -> bucket_id = 2804 -> s1-master + local conn_s1 = g.cluster:server('s1-master').net_box + local result = conn_s1.space['customers']:get(3) + t.assert_equals(result, {3, 2804, 'Daria', 18}) +end + +pgroup.test_object_all_success = function(g) + -- upsert_object_many + local result, errs = g.cluster.main_server.net_box:call('crud.upsert_object_many', { + 'customers', + { + {{id = 1, name = 'Fedor', age = 59}, {{'+', 'age', 25}, {'=', 'name', 'Leo Tolstoy'},}}, + {{id = 2, name = 'Anna', age = 23}, {{'+', 'age', 12}}}, + {{id = 3, name = 'Daria', age = 18}, {{'=', 'name', 'Jane'}}} + }, + }) + + t.assert_equals(errs, nil) + t.assert_equals(result.metadata, { + {name = 'id', type = 'unsigned'}, + {name = 'bucket_id', type = 'unsigned'}, + {name = 'name', type = 'string'}, + {name = 'age', type = 'number'}, + }) + t.assert_equals(result.rows, nil) + + -- get + -- primary key = 1 -> bucket_id = 477 -> s2-master + local conn_s2 = g.cluster:server('s2-master').net_box + local result = conn_s2.space['customers']:get(1) + t.assert_equals(result, {1, 477, 'Fedor', 59}) + + -- primary key = 2 -> bucket_id = 401 -> s2-master + local conn_s2 = g.cluster:server('s2-master').net_box + local result = conn_s2.space['customers']:get(2) + t.assert_equals(result, {2, 401, 'Anna', 23}) + + -- primary key = 3 -> bucket_id = 2804 -> s1-master + local conn_s1 = g.cluster:server('s1-master').net_box + local result = conn_s1.space['customers']:get(3) + t.assert_equals(result, {3, 2804, 'Daria', 18}) +end + +pgroup.test_one_error = function(g) + -- primary key = 3 -> bucket_id = 2804 -> s1-master + local conn_s1 = g.cluster:server('s1-master').net_box + local result = conn_s1.space['customers']:insert({3, 2804, 'Daria', 18}) + t.assert_equals(result, {3, 2804, 'Daria', 18}) + + -- primary key = 5 -> bucket_id = 1172 -> s2-master + local conn_s2 = g.cluster:server('s2-master').net_box + local result = conn_s2.space['customers']:insert({5, 1172, 'Sergey', 20}) + t.assert_equals(result, {5, 1172, 'Sergey', 20}) + + -- upsert_many + -- failed for s1-master + local result, errs = g.cluster.main_server.net_box:call('crud.upsert_many', { + 'customers', + { + {{22, box.NULL, 'Alex', 34}, {{'=', 'name', 'Peter'},}}, + {{3, box.NULL, 'Anastasia', 22}, {{'=', 'age', 'invalid type'}, {'=', 'name', 'Leo Tolstoy'},}}, + {{5, box.NULL, 'Peter', 27}, {{'+', 'age', 5}}} + }, + }) + + t.assert_not_equals(errs, nil) + t.assert_equals(#errs, 1) + if helpers.tarantool_version_at_least(2, 8) then + t.assert_str_contains(errs[1].err, 'Tuple field 4 (age) type does not match one required by operation') + else + t.assert_str_contains(errs[1].err, 'Tuple field 4 type does not match one required by operation') + end + t.assert_equals(errs[1].operation_data, {3, 2804, 'Anastasia', 22}) + t.assert_equals(result.metadata, { + {name = 'id', type = 'unsigned'}, + {name = 'bucket_id', type = 'unsigned'}, + {name = 'name', type = 'string'}, + {name = 'age', type = 'number'}, + }) + t.assert_equals(result.rows, nil) + + -- get + -- primary key = 22 -> bucket_id = 655 -> s2-master + local conn_s2 = g.cluster:server('s2-master').net_box + local result = conn_s2.space['customers']:get(22) + t.assert_equals(result, {22, 655, 'Alex', 34}) + + -- primary key = 5 -> bucket_id = 1172 -> s2-master + local conn_s2 = g.cluster:server('s2-master').net_box + local result = conn_s2.space['customers']:get(5) + t.assert_equals(result, {5, 1172, 'Sergey', 25}) + + -- primary key = 3 -> bucket_id = 2804 -> s1-master + local conn_s1 = g.cluster:server('s1-master').net_box + local result = conn_s1.space['customers']:get(3) + t.assert_equals(result, {3, 2804, 'Daria', 18}) +end + +pgroup.test_object_one_error = function(g) + -- primary key = 3 -> bucket_id = 2804 -> s1-master + local conn_s1 = g.cluster:server('s1-master').net_box + local result = conn_s1.space['customers']:insert({3, 2804, 'Daria', 18}) + t.assert_equals(result, {3, 2804, 'Daria', 18}) + + -- primary key = 5 -> bucket_id = 1172 -> s2-master + local conn_s2 = g.cluster:server('s2-master').net_box + local result = conn_s2.space['customers']:insert({5, 1172, 'Sergey', 20}) + t.assert_equals(result, {5, 1172, 'Sergey', 20}) + + -- upsert_object_many + -- failed for s1-master + local result, errs = g.cluster.main_server.net_box:call('crud.upsert_object_many', { + 'customers', + { + {{id = 22, name = 'Alex', age = 34}, {{'=', 'name', 'Peter'},}}, + {{id = 3, name = 'Anastasia', age = 22}, {{'=', 'age', 'invalid type'}, {'=', 'name', 'Leo Tolstoy'},}}, + {{id = 5, name = 'Peter', age = 27}, {{'+', 'age', 5}}} + }, + }) + + t.assert_not_equals(errs, nil) + t.assert_equals(#errs, 1) + if helpers.tarantool_version_at_least(2, 8) then + t.assert_str_contains(errs[1].err, 'Tuple field 4 (age) type does not match one required by operation') + else + t.assert_str_contains(errs[1].err, 'Tuple field 4 type does not match one required by operation') + end + t.assert_equals(errs[1].operation_data, {3, 2804, 'Anastasia', 22}) + t.assert_equals(result.metadata, { + {name = 'id', type = 'unsigned'}, + {name = 'bucket_id', type = 'unsigned'}, + {name = 'name', type = 'string'}, + {name = 'age', type = 'number'}, + }) + t.assert_equals(result.rows, nil) + + -- get + -- primary key = 22 -> bucket_id = 655 -> s2-master + local conn_s2 = g.cluster:server('s2-master').net_box + local result = conn_s2.space['customers']:get(22) + t.assert_equals(result, {22, 655, 'Alex', 34}) + + -- primary key = 5 -> bucket_id = 1172 -> s2-master + local conn_s2 = g.cluster:server('s2-master').net_box + local result = conn_s2.space['customers']:get(5) + t.assert_equals(result, {5, 1172, 'Sergey', 25}) + + -- primary key = 3 -> bucket_id = 2804 -> s1-master + local conn_s1 = g.cluster:server('s1-master').net_box + local result = conn_s1.space['customers']:get(3) + t.assert_equals(result, {3, 2804, 'Daria', 18}) +end + +pgroup.test_many_errors = function(g) + -- primary key = 3 -> bucket_id = 2804 -> s1-master + local conn_s1 = g.cluster:server('s1-master').net_box + local result = conn_s1.space['customers']:insert({3, 2804, 'Daria', 18}) + t.assert_equals(result, {3, 2804, 'Daria', 18}) + + -- primary key = 5 -> bucket_id = 1172 -> s2-master + local conn_s2 = g.cluster:server('s2-master').net_box + local result = conn_s2.space['customers']:insert({5, 1172, 'Sergey', 20}) + t.assert_equals(result, {5, 1172, 'Sergey', 20}) + + -- upsert_many + -- failed for both: s1-master s2-master + local result, errs = g.cluster.main_server.net_box:call('crud.upsert_many', { + 'customers', + { + {{22, box.NULL, 'Alex', 34}, {{'=', 'name', 'Peter'},}}, + {{3, box.NULL, 'Anastasia', 22}, {{'=', 'age', 'invalid type'}, {'=', 'name', 'Leo Tolstoy'},}}, + {{5, box.NULL, 'Peter', 27}, {{'+', 'age', '5'}}} + }, + }) + + t.assert_not_equals(errs, nil) + t.assert_equals(#errs, 2) + + table.sort(errs, function(err1, err2) return err1.operation_data[1] < err2.operation_data[1] end) + + if helpers.tarantool_version_at_least(2, 8) then + t.assert_str_contains(errs[1].err, 'Tuple field 4 (age) type does not match one required by operation') + else + t.assert_str_contains(errs[1].err, 'Tuple field 4 type does not match one required by operation') + end + t.assert_equals(errs[1].operation_data, {3, 2804, 'Anastasia', 22}) + + if helpers.tarantool_version_at_least(2, 3) then + t.assert_str_contains(errs[2].err, + 'Argument type in operation \'+\' on field \'age\' does not match field type') + else + t.assert_str_contains(errs[2].err, + 'Argument type in operation \'+\' on field 4 does not match field type') + end + t.assert_equals(errs[2].operation_data, {5, 1172, 'Peter', 27}) + + t.assert_equals(result.metadata, { + {name = 'id', type = 'unsigned'}, + {name = 'bucket_id', type = 'unsigned'}, + {name = 'name', type = 'string'}, + {name = 'age', type = 'number'}, + }) + t.assert_equals(result.rows, nil) + + -- get + -- primary key = 22 -> bucket_id = 655 -> s2-master + local conn_s2 = g.cluster:server('s2-master').net_box + local result = conn_s2.space['customers']:get(22) + t.assert_equals(result, {22, 655, 'Alex', 34}) + + -- primary key = 5 -> bucket_id = 1172 -> s2-master + local conn_s2 = g.cluster:server('s2-master').net_box + local result = conn_s2.space['customers']:get(5) + t.assert_equals(result, {5, 1172, 'Sergey', 20}) + + -- primary key = 3 -> bucket_id = 2804 -> s1-master + local conn_s1 = g.cluster:server('s1-master').net_box + local result = conn_s1.space['customers']:get(3) + t.assert_equals(result, {3, 2804, 'Daria', 18}) +end + +pgroup.test_object_many_errors = function(g) + -- primary key = 3 -> bucket_id = 2804 -> s1-master + local conn_s1 = g.cluster:server('s1-master').net_box + local result = conn_s1.space['customers']:insert({3, 2804, 'Daria', 18}) + t.assert_equals(result, {3, 2804, 'Daria', 18}) + + -- primary key = 5 -> bucket_id = 1172 -> s2-master + local conn_s2 = g.cluster:server('s2-master').net_box + local result = conn_s2.space['customers']:insert({5, 1172, 'Sergey', 20}) + t.assert_equals(result, {5, 1172, 'Sergey', 20}) + + -- upsert_object_many + -- failed for both: s1-master s2-master + local result, errs = g.cluster.main_server.net_box:call('crud.upsert_object_many', { + 'customers', + { + {{id = 22, name = 'Alex', age = 34}, {{'=', 'name', 'Peter'},}}, + {{id = 3, name = 'Anastasia', age = 22}, {{'=', 'age', 'invalid type'}, {'=', 'name', 'Leo Tolstoy'},}}, + {{id = 5, name = 'Peter', age = 27}, {{'+', 'age', '5'}}} + }, + }) + + t.assert_not_equals(errs, nil) + t.assert_equals(#errs, 2) + + table.sort(errs, function(err1, err2) return err1.operation_data[1] < err2.operation_data[1] end) + + if helpers.tarantool_version_at_least(2, 8) then + t.assert_str_contains(errs[1].err, 'Tuple field 4 (age) type does not match one required by operation') + else + t.assert_str_contains(errs[1].err, 'Tuple field 4 type does not match one required by operation') + end + t.assert_equals(errs[1].operation_data, {3, 2804, 'Anastasia', 22}) + + if helpers.tarantool_version_at_least(2, 3) then + t.assert_str_contains(errs[2].err, + 'Argument type in operation \'+\' on field \'age\' does not match field type') + else + t.assert_str_contains(errs[2].err, + 'Argument type in operation \'+\' on field 4 does not match field type') + end + t.assert_equals(errs[2].operation_data, {5, 1172, 'Peter', 27}) + + t.assert_equals(result.metadata, { + {name = 'id', type = 'unsigned'}, + {name = 'bucket_id', type = 'unsigned'}, + {name = 'name', type = 'string'}, + {name = 'age', type = 'number'}, + }) + t.assert_equals(result.rows, nil) + + -- get + -- primary key = 22 -> bucket_id = 655 -> s2-master + local conn_s2 = g.cluster:server('s2-master').net_box + local result = conn_s2.space['customers']:get(22) + t.assert_equals(result, {22, 655, 'Alex', 34}) + + -- primary key = 5 -> bucket_id = 1172 -> s2-master + local conn_s2 = g.cluster:server('s2-master').net_box + local result = conn_s2.space['customers']:get(5) + t.assert_equals(result, {5, 1172, 'Sergey', 20}) + + -- primary key = 3 -> bucket_id = 2804 -> s1-master + local conn_s1 = g.cluster:server('s1-master').net_box + local result = conn_s1.space['customers']:get(3) + t.assert_equals(result, {3, 2804, 'Daria', 18}) +end + +pgroup.test_no_success = function(g) + -- primary key = 22 -> bucket_id = 655 -> s2-master + local conn_s2 = g.cluster:server('s2-master').net_box + local result = conn_s2.space['customers']:insert({22, 655, 'Roman', 30}) + t.assert_equals(result, {22, 655, 'Roman', 30}) + + -- primary key = 3 -> bucket_id = 2804 -> s1-master + local conn_s1 = g.cluster:server('s1-master').net_box + local result = conn_s1.space['customers']:insert({3, 2804, 'Daria', 18}) + t.assert_equals(result, {3, 2804, 'Daria', 18}) + + -- primary key = 5 -> bucket_id = 1172 -> s2-master + local conn_s2 = g.cluster:server('s2-master').net_box + local result = conn_s2.space['customers']:insert({5, 1172, 'Sergey', 20}) + t.assert_equals(result, {5, 1172, 'Sergey', 20}) + + -- upsert_many + -- failed for both: s1-master s2-master + local result, errs = g.cluster.main_server.net_box:call('crud.upsert_many', { + 'customers', + { + {{22, box.NULL, 'Alex', 34}, {{'=', 'name', 5},}}, + {{3, box.NULL, 'Anastasia', 22}, {{'=', 'age', 'invalid type'}, {'=', 'name', 'Leo Tolstoy'},}}, + {{5, box.NULL, 'Peter', 27}, {{'+', 'age', '5'}}} + }, + }) + + t.assert_equals(result, nil) + t.assert_not_equals(errs, nil) + t.assert_equals(#errs, 3) + + table.sort(errs, function(err1, err2) return err1.operation_data[1] < err2.operation_data[1] end) + + if helpers.tarantool_version_at_least(2, 8) then + t.assert_str_contains(errs[1].err, 'Tuple field 4 (age) type does not match one required by operation') + else + t.assert_str_contains(errs[1].err, 'Tuple field 4 type does not match one required by operation') + end + t.assert_equals(errs[1].operation_data, {3, 2804, 'Anastasia', 22}) + + if helpers.tarantool_version_at_least(2, 3) then + t.assert_str_contains(errs[2].err, + 'Argument type in operation \'+\' on field \'age\' does not match field type') + else + t.assert_str_contains(errs[2].err, + 'Argument type in operation \'+\' on field 4 does not match field type') + end + t.assert_equals(errs[2].operation_data, {5, 1172, 'Peter', 27}) + + if helpers.tarantool_version_at_least(2, 8) then + t.assert_str_contains(errs[3].err, 'Tuple field 3 (name) type does not match one required by operation') + else + t.assert_str_contains(errs[3].err, 'Tuple field 3 type does not match one required by operation') + end + t.assert_equals(errs[3].operation_data, {22, 655, 'Alex', 34}) + + -- get + -- primary key = 22 -> bucket_id = 655 -> s2-master + local conn_s2 = g.cluster:server('s2-master').net_box + local result = conn_s2.space['customers']:get(22) + t.assert_equals(result, {22, 655, 'Roman', 30}) + + -- primary key = 3 -> bucket_id = 2804 -> s1-master + local conn_s1 = g.cluster:server('s1-master').net_box + local result = conn_s1.space['customers']:get(3) + t.assert_equals(result, {3, 2804, 'Daria', 18}) + + -- primary key = 5 -> bucket_id = 1172 -> s2-master + local conn_s2 = g.cluster:server('s2-master').net_box + local result = conn_s2.space['customers']:get(5) + t.assert_equals(result, {5, 1172, 'Sergey', 20}) +end + +pgroup.test_object_no_success = function(g) + -- primary key = 22 -> bucket_id = 655 -> s2-master + local conn_s2 = g.cluster:server('s2-master').net_box + local result = conn_s2.space['customers']:insert({22, 655, 'Roman', 30}) + t.assert_equals(result, {22, 655, 'Roman', 30}) + + -- primary key = 3 -> bucket_id = 2804 -> s1-master + local conn_s1 = g.cluster:server('s1-master').net_box + local result = conn_s1.space['customers']:insert({3, 2804, 'Daria', 18}) + t.assert_equals(result, {3, 2804, 'Daria', 18}) + + -- primary key = 5 -> bucket_id = 1172 -> s2-master + local conn_s2 = g.cluster:server('s2-master').net_box + local result = conn_s2.space['customers']:insert({5, 1172, 'Sergey', 20}) + t.assert_equals(result, {5, 1172, 'Sergey', 20}) + + -- upsert_object_many + -- failed for both: s1-master s2-master + local result, errs = g.cluster.main_server.net_box:call('crud.upsert_object_many', { + 'customers', + { + {{id = 22, name = 'Alex', age = 34}, {{'=', 'name', 5},}}, + {{id = 3, name = 'Anastasia', age = 22}, {{'=', 'age', 'invalid type'}, {'=', 'name', 'Leo Tolstoy'},}}, + {{id = 5, name = 'Peter', age = 27}, {{'+', 'age', '5'}}} + }, + }) + + t.assert_equals(result, nil) + t.assert_not_equals(errs, nil) + t.assert_equals(#errs, 3) + + table.sort(errs, function(err1, err2) return err1.operation_data[1] < err2.operation_data[1] end) + + if helpers.tarantool_version_at_least(2, 8) then + t.assert_str_contains(errs[1].err, 'Tuple field 4 (age) type does not match one required by operation') + else + t.assert_str_contains(errs[1].err, 'Tuple field 4 type does not match one required by operation') + end + t.assert_equals(errs[1].operation_data, {3, 2804, 'Anastasia', 22}) + + if helpers.tarantool_version_at_least(2, 3) then + t.assert_str_contains(errs[2].err, + 'Argument type in operation \'+\' on field \'age\' does not match field type') + else + t.assert_str_contains(errs[2].err, + 'Argument type in operation \'+\' on field 4 does not match field type') + end + t.assert_equals(errs[2].operation_data, {5, 1172, 'Peter', 27}) + + if helpers.tarantool_version_at_least(2, 8) then + t.assert_str_contains(errs[3].err, 'Tuple field 3 (name) type does not match one required by operation') + else + t.assert_str_contains(errs[3].err, 'Tuple field 3 type does not match one required by operation') + end + t.assert_equals(errs[3].operation_data, {22, 655, 'Alex', 34}) + + -- get + -- primary key = 22 -> bucket_id = 655 -> s2-master + local conn_s2 = g.cluster:server('s2-master').net_box + local result = conn_s2.space['customers']:get(22) + t.assert_equals(result, {22, 655, 'Roman', 30}) + + -- primary key = 3 -> bucket_id = 2804 -> s1-master + local conn_s1 = g.cluster:server('s1-master').net_box + local result = conn_s1.space['customers']:get(3) + t.assert_equals(result, {3, 2804, 'Daria', 18}) + + -- primary key = 5 -> bucket_id = 1172 -> s2-master + local conn_s2 = g.cluster:server('s2-master').net_box + local result = conn_s2.space['customers']:get(5) + t.assert_equals(result, {5, 1172, 'Sergey', 20}) +end + +pgroup.test_object_bad_format_stop_on_error = function(g) + -- bad format + -- two errors, stop_on_error == true + local result, errs = g.cluster.main_server.net_box:call('crud.upsert_object_many', { + 'customers', + { + {{id = 1, name = 'Fedor'}, {{'+', 'age', 12}}}, + {{id = 2, name = 'Anna'}, {{'+', 'age', 12}}} + }, + { + stop_on_error = true, + } + }) + + t.assert_equals(result, nil) + t.assert_not_equals(errs, nil) + t.assert_equals(#errs, 1) + t.assert_str_contains(errs[1].err, 'Field \"age\" isn\'t nullable') + t.assert_equals(errs[1].operation_data, {id = 1, name = 'Fedor'}) +end + +pgroup.test_all_success_stop_on_error = function(g) + -- upsert_many + local result, errs = g.cluster.main_server.net_box:call('crud.upsert_many', { + 'customers', + { + {{1, box.NULL, 'Fedor', 59}, {{'+', 'age', 25}, {'=', 'name', 'Leo Tolstoy'},}}, + {{2, box.NULL, 'Anna', 23}, {{'+', 'age', 12}}}, + {{3, box.NULL, 'Daria', 18}, {{'=', 'name', 'Jane'}}} + }, + { + stop_on_error = true, + } + }) + + t.assert_equals(errs, nil) + t.assert_equals(result.metadata, { + {name = 'id', type = 'unsigned'}, + {name = 'bucket_id', type = 'unsigned'}, + {name = 'name', type = 'string'}, + {name = 'age', type = 'number'}, + }) + t.assert_equals(result.rows, nil) + + -- get + -- primary key = 1 -> bucket_id = 477 -> s2-master + local conn_s2 = g.cluster:server('s2-master').net_box + local result = conn_s2.space['customers']:get(1) + t.assert_equals(result, {1, 477, 'Fedor', 59}) + + -- primary key = 2 -> bucket_id = 401 -> s2-master + local conn_s2 = g.cluster:server('s2-master').net_box + local result = conn_s2.space['customers']:get(2) + t.assert_equals(result, {2, 401, 'Anna', 23}) + + -- primary key = 3 -> bucket_id = 2804 -> s1-master + local conn_s1 = g.cluster:server('s1-master').net_box + local result = conn_s1.space['customers']:get(3) + t.assert_equals(result, {3, 2804, 'Daria', 18}) +end + +pgroup.test_object_all_success_stop_on_error = function(g) + -- upsert_object_many + local result, errs = g.cluster.main_server.net_box:call('crud.upsert_object_many', { + 'customers', + { + {{id = 1, name = 'Fedor', age = 59}, {{'+', 'age', 25}, {'=', 'name', 'Leo Tolstoy'},}}, + {{id = 2, name = 'Anna', age = 23}, {{'+', 'age', 12}}}, + {{id = 3, name = 'Daria', age = 18}, {{'=', 'name', 'Jane'}}} + }, + { + stop_on_error = true, + } + }) + + t.assert_equals(errs, nil) + t.assert_equals(result.metadata, { + {name = 'id', type = 'unsigned'}, + {name = 'bucket_id', type = 'unsigned'}, + {name = 'name', type = 'string'}, + {name = 'age', type = 'number'}, + }) + t.assert_equals(result.rows, nil) + + -- get + -- primary key = 1 -> bucket_id = 477 -> s2-master + local conn_s2 = g.cluster:server('s2-master').net_box + local result = conn_s2.space['customers']:get(1) + t.assert_equals(result, {1, 477, 'Fedor', 59}) + + -- primary key = 2 -> bucket_id = 401 -> s2-master + local conn_s2 = g.cluster:server('s2-master').net_box + local result = conn_s2.space['customers']:get(2) + t.assert_equals(result, {2, 401, 'Anna', 23}) + + -- primary key = 3 -> bucket_id = 2804 -> s1-master + local conn_s1 = g.cluster:server('s1-master').net_box + local result = conn_s1.space['customers']:get(3) + t.assert_equals(result, {3, 2804, 'Daria', 18}) +end + +pgroup.test_object_partial_success_stop_on_error = function(g) + -- insert + -- primary key = 3 -> bucket_id = 2804 -> s1-master + local conn_s1 = g.cluster:server('s1-master').net_box + local result = conn_s1.space['customers']:insert({3, 2804, 'Daria', 18}) + t.assert_equals(result, {3, 2804, 'Daria', 18}) + + -- primary key = 9 -> bucket_id = 1644 -> s1-master + local conn_s1 = g.cluster:server('s1-master').net_box + local result = conn_s1.space['customers']:insert({9, 1644, 'Nicolo', 35}) + t.assert_equals(result, {9, 1644, 'Nicolo', 35}) + + -- upsert_object_many + -- stop_on_error = true, rollback_on_error = false + -- one error on one storage without rollback, inserts stop by error on this storage + -- inserts before error are successful + local result, errs = g.cluster.main_server.net_box:call('crud.upsert_object_many', { + 'customers', + { + {{id = 22, name = 'Alex', age = 34}, {{'+', 'age', 1}}}, + {{id = 92, name = 'Artur', age = 29}, {{'+', 'age', 2}}}, + {{id = 3, name = 'Anastasia', age = 22}, {{'+', 'age', '3'}}}, + {{id = 5, name = 'Sergey', age = 25}, {{'+', 'age', 4}}}, + {{id = 9, name = 'Anna', age = 30}, {{'+', 'age', '5'}}} + }, + { + stop_on_error = true, + } + }) + + t.assert_equals(#errs, 2) + + table.sort(errs, function(err1, err2) return err1.operation_data[1] < err2.operation_data[1] end) + + if helpers.tarantool_version_at_least(2, 3) then + t.assert_str_contains(errs[1].err, + 'Argument type in operation \'+\' on field \'age\' does not match field type') + else + t.assert_str_contains(errs[1].err, + 'Argument type in operation \'+\' on field 4 does not match field type') + end + t.assert_equals(errs[1].operation_data, {3, 2804, 'Anastasia', 22}) + + t.assert_str_contains(errs[2].err, batching_utils.stop_on_error_msg) + t.assert_equals(errs[2].operation_data, {9, 1644, "Anna", 30}) + + t.assert_equals(result.metadata, { + {name = 'id', type = 'unsigned'}, + {name = 'bucket_id', type = 'unsigned'}, + {name = 'name', type = 'string'}, + {name = 'age', type = 'number'}, + }) + t.assert_equals(result.rows, nil) + + -- get + -- primary key = 22 -> bucket_id = 655 -> s2-master + local conn_s2 = g.cluster:server('s2-master').net_box + local result = conn_s2.space['customers']:get(22) + t.assert_equals(result, {22, 655, 'Alex', 34}) + + -- primary key = 92 -> bucket_id = 2040 -> s1-master + local conn_s1 = g.cluster:server('s1-master').net_box + local result = conn_s1.space['customers']:get(92) + t.assert_equals(result, {92, 2040, 'Artur', 29}) + + -- primary key = 5 -> bucket_id = 1172 -> s2-master + local conn_s2 = g.cluster:server('s2-master').net_box + local result = conn_s2.space['customers']:get(5) + t.assert_equals(result, {5, 1172, 'Sergey', 25}) + + -- primary key = 3 -> bucket_id = 2804 -> s1-master + local conn_s1 = g.cluster:server('s1-master').net_box + local result = conn_s1.space['customers']:get(3) + t.assert_equals(result, {3, 2804, 'Daria', 18}) + + -- primary key = 9 -> bucket_id = 1644 -> s1-master + local conn_s1 = g.cluster:server('s1-master').net_box + local result = conn_s1.space['customers']:get(9) + t.assert_equals(result, {9, 1644, 'Nicolo', 35}) +end + +pgroup.test_partial_success_stop_on_error = function(g) + -- insert + -- primary key = 3 -> bucket_id = 2804 -> s1-master + local conn_s1 = g.cluster:server('s1-master').net_box + local result = conn_s1.space['customers']:insert({3, 2804, 'Daria', 18}) + t.assert_equals(result, {3, 2804, 'Daria', 18}) + + -- primary key = 9 -> bucket_id = 1644 -> s1-master + local conn_s1 = g.cluster:server('s1-master').net_box + local result = conn_s1.space['customers']:insert({9, 1644, 'Nicolo', 35}) + t.assert_equals(result, {9, 1644, 'Nicolo', 35}) + + -- upsert_many + -- stop_on_error = true, rollback_on_error = false + -- one error on one storage without rollback, inserts stop by error on this storage + -- inserts before error are successful + local result, errs = g.cluster.main_server.net_box:call('crud.upsert_many', { + 'customers', + { + {{22, box.NULL, 'Alex', 34}, {{'+', 'age', 1}}}, + {{92, box.NULL, 'Artur', 29}, {{'+', 'age', 2}}}, + {{3, box.NULL, 'Anastasia', 22}, {{'+', 'age', '3'}}}, + {{5, box.NULL, 'Sergey', 25}, {{'+', 'age', 4}}}, + {{9, box.NULL, 'Anna', 30}, {{'+', 'age', '5'}}} + }, + { + stop_on_error = true, + } + }) + + t.assert_equals(#errs, 2) + + table.sort(errs, function(err1, err2) return err1.operation_data[1] < err2.operation_data[1] end) + + if helpers.tarantool_version_at_least(2, 3) then + t.assert_str_contains(errs[1].err, + 'Argument type in operation \'+\' on field \'age\' does not match field type') + else + t.assert_str_contains(errs[1].err, + 'Argument type in operation \'+\' on field 4 does not match field type') + end + t.assert_equals(errs[1].operation_data, {3, 2804, 'Anastasia', 22}) + + t.assert_str_contains(errs[2].err, batching_utils.stop_on_error_msg) + t.assert_equals(errs[2].operation_data, {9, 1644, "Anna", 30}) + + t.assert_equals(result.metadata, { + {name = 'id', type = 'unsigned'}, + {name = 'bucket_id', type = 'unsigned'}, + {name = 'name', type = 'string'}, + {name = 'age', type = 'number'}, + }) + t.assert_equals(result.rows, nil) + + -- get + -- primary key = 22 -> bucket_id = 655 -> s2-master + local conn_s2 = g.cluster:server('s2-master').net_box + local result = conn_s2.space['customers']:get(22) + t.assert_equals(result, {22, 655, 'Alex', 34}) + + -- primary key = 92 -> bucket_id = 2040 -> s1-master + local conn_s1 = g.cluster:server('s1-master').net_box + local result = conn_s1.space['customers']:get(92) + t.assert_equals(result, {92, 2040, 'Artur', 29}) + + -- primary key = 5 -> bucket_id = 1172 -> s2-master + local conn_s2 = g.cluster:server('s2-master').net_box + local result = conn_s2.space['customers']:get(5) + t.assert_equals(result, {5, 1172, 'Sergey', 25}) + + -- primary key = 3 -> bucket_id = 2804 -> s1-master + local conn_s1 = g.cluster:server('s1-master').net_box + local result = conn_s1.space['customers']:get(3) + t.assert_equals(result, {3, 2804, 'Daria', 18}) + + -- primary key = 9 -> bucket_id = 1644 -> s1-master + local conn_s1 = g.cluster:server('s1-master').net_box + local result = conn_s1.space['customers']:get(9) + t.assert_equals(result, {9, 1644, 'Nicolo', 35}) +end + +pgroup.test_no_success_stop_on_error = function(g) + -- insert + -- primary key = 2 -> bucket_id = 401 -> s2-master + local conn_s2 = g.cluster:server('s2-master').net_box + local result = conn_s2.space['customers']:insert({2, 401, 'Anna', 23}) + t.assert_equals(result, {2, 401, 'Anna', 23}) + + -- primary key = 3 -> bucket_id = 2804 -> s1-master + local conn_s1 = g.cluster:server('s1-master').net_box + local result = conn_s1.space['customers']:insert({3, 2804, 'Daria', 18}) + t.assert_equals(result, {3, 2804, 'Daria', 18}) + + -- primary key = 92 -> bucket_id = 2040 -> s1-master + local conn_s1 = g.cluster:server('s1-master').net_box + local result = conn_s1.space['customers']:insert({92, 2040, 'Artur', 29}) + t.assert_equals(result, {92, 2040, 'Artur', 29}) + + -- upsert_many + -- fails for both: s1-master s2-master + -- one error on each storage, all inserts stop by error + local result, errs = g.cluster.main_server.net_box:call('crud.upsert_many', { + 'customers', + { + {{2, box.NULL, 'Alex', 34}, {{'+', 'age', '1'}}}, + {{3, box.NULL, 'Anastasia', 22}, {{'+', 'age', '2'}}}, + {{10, box.NULL, 'Sergey', 25}, {{'+', 'age', 3}}}, + {{9, box.NULL, 'Anna', 30}, {{'+', 'age', '4'}}}, + {{92, box.NULL, 'Leo', 29}, {{'+', 'age', 5}}} + }, + { + stop_on_error = true, + } + }) + + t.assert_equals(result, nil) + t.assert_not_equals(errs, nil) + t.assert_equals(#errs, 5) + + table.sort(errs, function(err1, err2) return err1.operation_data[1] < err2.operation_data[1] end) + + if helpers.tarantool_version_at_least(2, 3) then + t.assert_str_contains(errs[1].err, + 'Argument type in operation \'+\' on field \'age\' does not match field type') + else + t.assert_str_contains(errs[1].err, + 'Argument type in operation \'+\' on field 4 does not match field type') + end + t.assert_equals(errs[1].operation_data, {2, 401, 'Alex', 34}) + + if helpers.tarantool_version_at_least(2, 3) then + t.assert_str_contains(errs[2].err, + 'Argument type in operation \'+\' on field \'age\' does not match field type') + else + t.assert_str_contains(errs[2].err, + 'Argument type in operation \'+\' on field 4 does not match field type') + end + t.assert_equals(errs[2].operation_data, {3, 2804, 'Anastasia', 22}) + + t.assert_str_contains(errs[3].err, batching_utils.stop_on_error_msg) + t.assert_equals(errs[3].operation_data, {9, 1644, "Anna", 30}) + + t.assert_str_contains(errs[4].err, batching_utils.stop_on_error_msg) + t.assert_equals(errs[4].operation_data, {10, 569, "Sergey", 25}) + + t.assert_str_contains(errs[5].err, batching_utils.stop_on_error_msg) + t.assert_equals(errs[5].operation_data, {92, 2040, "Leo", 29}) + + -- primary key = 2 -> bucket_id = 401 -> s2-master + local conn_s2 = g.cluster:server('s2-master').net_box + local result = conn_s2.space['customers']:get(2) + t.assert_equals(result, {2, 401, 'Anna', 23}) + + -- primary key = 3 -> bucket_id = 2804 -> s1-master + local conn_s1 = g.cluster:server('s1-master').net_box + local result = conn_s1.space['customers']:get(3) + t.assert_equals(result, {3, 2804, 'Daria', 18}) + + -- primary key = 10 -> bucket_id = 569 -> s2-master + local conn_s2 = g.cluster:server('s2-master').net_box + local result = conn_s2.space['customers']:get(10) + t.assert_equals(result, nil) + + -- primary key = 9 -> bucket_id = 1644 -> s1-master + local conn_s1 = g.cluster:server('s1-master').net_box + local result = conn_s1.space['customers']:get(9) + t.assert_equals(result, nil) + + -- primary key = 92 -> bucket_id = 2040 -> s1-master + local conn_s1 = g.cluster:server('s1-master').net_box + local result = conn_s1.space['customers']:get(92) + t.assert_equals(result, {92, 2040, 'Artur', 29}) +end + +pgroup.test_object_no_success_stop_on_error = function(g) + -- insert + -- primary key = 2 -> bucket_id = 401 -> s2-master + local conn_s2 = g.cluster:server('s2-master').net_box + local result = conn_s2.space['customers']:insert({2, 401, 'Anna', 23}) + t.assert_equals(result, {2, 401, 'Anna', 23}) + + -- primary key = 3 -> bucket_id = 2804 -> s1-master + local conn_s1 = g.cluster:server('s1-master').net_box + local result = conn_s1.space['customers']:insert({3, 2804, 'Daria', 18}) + t.assert_equals(result, {3, 2804, 'Daria', 18}) + + -- primary key = 92 -> bucket_id = 2040 -> s1-master + local conn_s1 = g.cluster:server('s1-master').net_box + local result = conn_s1.space['customers']:insert({92, 2040, 'Artur', 29}) + t.assert_equals(result, {92, 2040, 'Artur', 29}) + + -- upsert_object_many + -- fails for both: s1-master s2-master + -- one error on each storage, all inserts stop by error + local result, errs = g.cluster.main_server.net_box:call('crud.upsert_object_many', { + 'customers', + { + {{id = 2, name = 'Alex', age = 34}, {{'+', 'age', '1'}}}, + {{id = 3, name = 'Anastasia', age = 22}, {{'+', 'age', '2'}}}, + {{id = 10, name = 'Sergey', age = 25}, {{'+', 'age', 3}}}, + {{id = 9, name = 'Anna', age = 30}, {{'+', 'age', '4'}}}, + {{id = 92, name = 'Leo', age = 29}, {{'+', 'age', 5}}}, + }, + { + stop_on_error = true, + } + }) + + t.assert_equals(result, nil) + t.assert_not_equals(errs, nil) + t.assert_equals(#errs, 5) + + table.sort(errs, function(err1, err2) return err1.operation_data[1] < err2.operation_data[1] end) + + if helpers.tarantool_version_at_least(2, 3) then + t.assert_str_contains(errs[1].err, + 'Argument type in operation \'+\' on field \'age\' does not match field type') + else + t.assert_str_contains(errs[1].err, + 'Argument type in operation \'+\' on field 4 does not match field type') + end + t.assert_equals(errs[1].operation_data, {2, 401, 'Alex', 34}) + + if helpers.tarantool_version_at_least(2, 3) then + t.assert_str_contains(errs[2].err, + 'Argument type in operation \'+\' on field \'age\' does not match field type') + else + t.assert_str_contains(errs[2].err, + 'Argument type in operation \'+\' on field 4 does not match field type') + end + t.assert_equals(errs[2].operation_data, {3, 2804, 'Anastasia', 22}) + + t.assert_str_contains(errs[3].err, batching_utils.stop_on_error_msg) + t.assert_equals(errs[3].operation_data, {9, 1644, "Anna", 30}) + + t.assert_str_contains(errs[4].err, batching_utils.stop_on_error_msg) + t.assert_equals(errs[4].operation_data, {10, 569, "Sergey", 25}) + + t.assert_str_contains(errs[5].err, batching_utils.stop_on_error_msg) + t.assert_equals(errs[5].operation_data, {92, 2040, "Leo", 29}) + + -- primary key = 2 -> bucket_id = 401 -> s2-master + local conn_s2 = g.cluster:server('s2-master').net_box + local result = conn_s2.space['customers']:get(2) + t.assert_equals(result, {2, 401, 'Anna', 23}) + + -- primary key = 3 -> bucket_id = 2804 -> s1-master + local conn_s1 = g.cluster:server('s1-master').net_box + local result = conn_s1.space['customers']:get(3) + t.assert_equals(result, {3, 2804, 'Daria', 18}) + + -- primary key = 10 -> bucket_id = 569 -> s2-master + local conn_s2 = g.cluster:server('s2-master').net_box + local result = conn_s2.space['customers']:get(10) + t.assert_equals(result, nil) + + -- primary key = 9 -> bucket_id = 1644 -> s1-master + local conn_s1 = g.cluster:server('s1-master').net_box + local result = conn_s1.space['customers']:get(9) + t.assert_equals(result, nil) + + -- primary key = 92 -> bucket_id = 2040 -> s1-master + local conn_s1 = g.cluster:server('s1-master').net_box + local result = conn_s1.space['customers']:get(92) + t.assert_equals(result, {92, 2040, 'Artur', 29}) +end + +pgroup.test_all_success_rollback_on_error = function(g) + -- upsert_many + local result, errs = g.cluster.main_server.net_box:call('crud.upsert_many', { + 'customers', + { + {{1, box.NULL, 'Fedor', 59}, {{'+', 'age', 25}, {'=', 'name', 'Leo Tolstoy'},}}, + {{2, box.NULL, 'Anna', 23}, {{'+', 'age', 12}}}, + {{3, box.NULL, 'Daria', 18}, {{'=', 'name', 'Jane'}}} + }, + { + rollback_on_error = true, + } + }) + + t.assert_equals(errs, nil) + t.assert_equals(result.metadata, { + {name = 'id', type = 'unsigned'}, + {name = 'bucket_id', type = 'unsigned'}, + {name = 'name', type = 'string'}, + {name = 'age', type = 'number'}, + }) + t.assert_equals(result.rows, nil) + + -- get + -- primary key = 1 -> bucket_id = 477 -> s2-master + local conn_s2 = g.cluster:server('s2-master').net_box + local result = conn_s2.space['customers']:get(1) + t.assert_equals(result, {1, 477, 'Fedor', 59}) + + -- primary key = 2 -> bucket_id = 401 -> s2-master + local conn_s2 = g.cluster:server('s2-master').net_box + local result = conn_s2.space['customers']:get(2) + t.assert_equals(result, {2, 401, 'Anna', 23}) + + -- primary key = 3 -> bucket_id = 2804 -> s1-master + local conn_s1 = g.cluster:server('s1-master').net_box + local result = conn_s1.space['customers']:get(3) + t.assert_equals(result, {3, 2804, 'Daria', 18}) +end + +pgroup.test_object_all_success_rollback_on_error = function(g) + -- upsert_object_many + local result, errs = g.cluster.main_server.net_box:call('crud.upsert_object_many', { + 'customers', + { + {{id = 1, name = 'Fedor', age = 59}, {{'+', 'age', 25}, {'=', 'name', 'Leo Tolstoy'},}}, + {{id = 2, name = 'Anna', age = 23}, {{'+', 'age', 12}}}, + {{id = 3, name = 'Daria', age = 18}, {{'=', 'name', 'Jane'}}} + }, + { + rollback_on_error = true, + } + }) + + t.assert_equals(errs, nil) + t.assert_equals(result.metadata, { + {name = 'id', type = 'unsigned'}, + {name = 'bucket_id', type = 'unsigned'}, + {name = 'name', type = 'string'}, + {name = 'age', type = 'number'}, + }) + t.assert_equals(result.rows, nil) + + -- get + -- primary key = 1 -> bucket_id = 477 -> s2-master + local conn_s2 = g.cluster:server('s2-master').net_box + local result = conn_s2.space['customers']:get(1) + t.assert_equals(result, {1, 477, 'Fedor', 59}) + + -- primary key = 2 -> bucket_id = 401 -> s2-master + local conn_s2 = g.cluster:server('s2-master').net_box + local result = conn_s2.space['customers']:get(2) + t.assert_equals(result, {2, 401, 'Anna', 23}) + + -- primary key = 3 -> bucket_id = 2804 -> s1-master + local conn_s1 = g.cluster:server('s1-master').net_box + local result = conn_s1.space['customers']:get(3) + t.assert_equals(result, {3, 2804, 'Daria', 18}) +end + +pgroup.test_object_partial_success_rollback_on_error = function(g) + -- insert + -- primary key = 22 -> bucket_id = 655 -> s2-master + local conn_s2 = g.cluster:server('s2-master').net_box + local result = conn_s2.space['customers']:insert({22, 655, 'Alex', 34}) + t.assert_equals(result, {22, 655, 'Alex', 34}) + + -- primary key = 3 -> bucket_id = 2804 -> s1-master + local conn_s1 = g.cluster:server('s1-master').net_box + local result = conn_s1.space['customers']:insert({3, 2804, 'Daria', 18}) + t.assert_equals(result, {3, 2804, 'Daria', 18}) + + -- primary key = 9 -> bucket_id = 1644 -> s1-master + local conn_s1 = g.cluster:server('s1-master').net_box + local result = conn_s1.space['customers']:insert({9, 1644, 'Nicolo', 35}) + t.assert_equals(result, {9, 1644, 'Nicolo', 35}) + + -- upsert_object_many + -- stop_on_error = false, rollback_on_error = true + -- two error on one storage with rollback + local result, errs = g.cluster.main_server.net_box:call('crud.upsert_object_many', { + 'customers', + { + {{id = 22, name = 'Alex', age = 34}, {{'+', 'age', 1}}}, + {{id = 92, name = 'Artur', age = 29}, {{'+', 'age', 2}}}, + {{id = 3, name = 'Anastasia', age = 22}, {{'+', 'age', '3'}}}, + {{id = 5, name = 'Sergey', age = 25}, {{'+', 'age', 4}}}, + {{id = 9, name = 'Anna', age = 30}, {{'+', 'age', '5'}}} + }, + { + rollback_on_error = true, + } + }) + + t.assert_not_equals(errs, nil) + t.assert_equals(#errs, 3) + + table.sort(errs, function(err1, err2) return err1.operation_data[1] < err2.operation_data[1] end) + + if helpers.tarantool_version_at_least(2, 3) then + t.assert_str_contains(errs[1].err, + 'Argument type in operation \'+\' on field \'age\' does not match field type') + else + t.assert_str_contains(errs[1].err, + 'Argument type in operation \'+\' on field 4 does not match field type') + end + t.assert_equals(errs[1].operation_data, {3, 2804, 'Anastasia', 22}) + + if helpers.tarantool_version_at_least(2, 3) then + t.assert_str_contains(errs[2].err, + 'Argument type in operation \'+\' on field \'age\' does not match field type') + else + t.assert_str_contains(errs[2].err, + 'Argument type in operation \'+\' on field 4 does not match field type') + end + t.assert_equals(errs[2].operation_data, {9, 1644, 'Anna', 30}) + + t.assert_str_contains(errs[3].err, batching_utils.rollback_on_error_msg) + t.assert_equals(errs[3].operation_data, {92, 2040, "Artur", 29}) + + t.assert_equals(result.metadata, { + {name = 'id', type = 'unsigned'}, + {name = 'bucket_id', type = 'unsigned'}, + {name = 'name', type = 'string'}, + {name = 'age', type = 'number'}, + }) + t.assert_equals(result.rows, nil) + + -- get + -- primary key = 22 -> bucket_id = 655 -> s2-master + local conn_s2 = g.cluster:server('s2-master').net_box + local result = conn_s2.space['customers']:get(22) + t.assert_equals(result, {22, 655, 'Alex', 35}) + + -- primary key = 92 -> bucket_id = 2040 -> s1-master + local conn_s1 = g.cluster:server('s1-master').net_box + local result = conn_s1.space['customers']:get(92) + t.assert_equals(result, nil) + + -- primary key = 5 -> bucket_id = 1172 -> s2-master + local conn_s2 = g.cluster:server('s2-master').net_box + local result = conn_s2.space['customers']:get(5) + t.assert_equals(result, {5, 1172, 'Sergey', 25}) + + -- primary key = 3 -> bucket_id = 2804 -> s1-master + local conn_s1 = g.cluster:server('s1-master').net_box + local result = conn_s1.space['customers']:get(3) + t.assert_equals(result, {3, 2804, 'Daria', 18}) + + -- primary key = 9 -> bucket_id = 1644 -> s1-master + local conn_s1 = g.cluster:server('s1-master').net_box + local result = conn_s1.space['customers']:get(9) + t.assert_equals(result, {9, 1644, 'Nicolo', 35}) +end + +pgroup.test_partial_success_rollback_on_error = function(g) + -- insert + -- primary key = 22 -> bucket_id = 655 -> s2-master + local conn_s2 = g.cluster:server('s2-master').net_box + local result = conn_s2.space['customers']:insert({22, 655, 'Alex', 34}) + t.assert_equals(result, {22, 655, 'Alex', 34}) + + -- primary key = 3 -> bucket_id = 2804 -> s1-master + local conn_s1 = g.cluster:server('s1-master').net_box + local result = conn_s1.space['customers']:insert({3, 2804, 'Daria', 18}) + t.assert_equals(result, {3, 2804, 'Daria', 18}) + + -- primary key = 9 -> bucket_id = 1644 -> s1-master + local conn_s1 = g.cluster:server('s1-master').net_box + local result = conn_s1.space['customers']:insert({9, 1644, 'Nicolo', 35}) + t.assert_equals(result, {9, 1644, 'Nicolo', 35}) + + -- upsert_many + -- stop_on_error = false, rollback_on_error = true + -- two error on one storage with rollback + local result, errs = g.cluster.main_server.net_box:call('crud.upsert_many', { + 'customers', + { + {{22, box.NULL, 'Peter', 24}, {{'+', 'age', 1}}}, + {{92, box.NULL, 'Artur', 29}, {{'+', 'age', 2}}}, + {{3, box.NULL, 'Anastasia', 22}, {{'+', 'age', '3'}}}, + {{5, box.NULL, 'Sergey', 25}, {{'+', 'age', 4}}}, + {{9, box.NULL, 'Anna', 30}, {{'+', 'age', '5'}}} + }, + { + rollback_on_error = true, + } + }) + + t.assert_not_equals(errs, nil) + t.assert_equals(#errs, 3) + + table.sort(errs, function(err1, err2) return err1.operation_data[1] < err2.operation_data[1] end) + + if helpers.tarantool_version_at_least(2, 3) then + t.assert_str_contains(errs[1].err, + 'Argument type in operation \'+\' on field \'age\' does not match field type') + else + t.assert_str_contains(errs[1].err, + 'Argument type in operation \'+\' on field 4 does not match field type') + end + t.assert_equals(errs[1].operation_data, {3, 2804, 'Anastasia', 22}) + + if helpers.tarantool_version_at_least(2, 3) then + t.assert_str_contains(errs[2].err, + 'Argument type in operation \'+\' on field \'age\' does not match field type') + else + t.assert_str_contains(errs[2].err, + 'Argument type in operation \'+\' on field 4 does not match field type') + end + t.assert_equals(errs[2].operation_data, {9, 1644, 'Anna', 30}) + + t.assert_str_contains(errs[3].err, batching_utils.rollback_on_error_msg) + t.assert_equals(errs[3].operation_data, {92, 2040, "Artur", 29}) + + t.assert_equals(result.metadata, { + {name = 'id', type = 'unsigned'}, + {name = 'bucket_id', type = 'unsigned'}, + {name = 'name', type = 'string'}, + {name = 'age', type = 'number'}, + }) + t.assert_equals(result.rows, nil) + + -- get + -- primary key = 22 -> bucket_id = 655 -> s2-master + local conn_s2 = g.cluster:server('s2-master').net_box + local result = conn_s2.space['customers']:get(22) + t.assert_equals(result, {22, 655, 'Alex', 35}) + + -- primary key = 92 -> bucket_id = 2040 -> s1-master + local conn_s1 = g.cluster:server('s1-master').net_box + local result = conn_s1.space['customers']:get(92) + t.assert_equals(result, nil) + + -- primary key = 5 -> bucket_id = 1172 -> s2-master + local conn_s2 = g.cluster:server('s2-master').net_box + local result = conn_s2.space['customers']:get(5) + t.assert_equals(result, {5, 1172, 'Sergey', 25}) + + -- primary key = 3 -> bucket_id = 2804 -> s1-master + local conn_s1 = g.cluster:server('s1-master').net_box + local result = conn_s1.space['customers']:get(3) + t.assert_equals(result, {3, 2804, 'Daria', 18}) + + -- primary key = 9 -> bucket_id = 1644 -> s1-master + local conn_s1 = g.cluster:server('s1-master').net_box + local result = conn_s1.space['customers']:get(9) + t.assert_equals(result, {9, 1644, 'Nicolo', 35}) +end + +pgroup.test_no_success_rollback_on_error = function(g) + -- insert + -- primary key = 2 -> bucket_id = 401 -> s2-master + local conn_s2 = g.cluster:server('s2-master').net_box + local result = conn_s2.space['customers']:insert({2, 401, 'Anna', 23}) + t.assert_equals(result, {2, 401, 'Anna', 23}) + + -- primary key = 3 -> bucket_id = 2804 -> s1-master + local conn_s1 = g.cluster:server('s1-master').net_box + local result = conn_s1.space['customers']:insert({3, 2804, 'Daria', 18}) + t.assert_equals(result, {3, 2804, 'Daria', 18}) + + -- primary key = 5 -> bucket_id = 1172 -> s2-master + local conn_s2 = g.cluster:server('s2-master').net_box + local result = conn_s2.space['customers']:insert({5, 1172, 'Sergey', 25}) + t.assert_equals(result, {5, 1172, 'Sergey', 25}) + + -- primary key = 71 -> bucket_id = 1802 -> s1-master + local conn_s1 = g.cluster:server('s1-master').net_box + local result = conn_s1.space['customers']:insert({71, 1802, 'Oleg', 32}) + t.assert_equals(result, {71, 1802, 'Oleg', 32}) + + -- upsert_many + -- fails for both: s1-master s2-master + -- two errors on each storage with rollback + local result, errs = g.cluster.main_server.net_box:call('crud.upsert_many', { + 'customers', + { + {{1, box.NULL, 'Olga', 27}, {{'+', 'age', 1}}}, + {{92, box.NULL, 'Oleg', 32}, {{'+', 'age', 2}}}, + {{71, box.NULL, 'Sergey', 25}, {{'+', 'age', '3'}}}, + {{5, box.NULL, 'Anna', 30}, {{'+', 'age', '4'}}}, + {{2, box.NULL, 'Alex', 34}, {{'+', 'age', '5'}}}, + {{3, box.NULL, 'Anastasia', 22}, {{'+', 'age', '6'}}} + }, + { + rollback_on_error = true, + } + }) + + t.assert_equals(result, nil) + t.assert_not_equals(errs, nil) + t.assert_equals(#errs, 6) + + table.sort(errs, function(err1, err2) return err1.operation_data[1] < err2.operation_data[1] end) + + t.assert_str_contains(errs[1].err, batching_utils.rollback_on_error_msg) + t.assert_equals(errs[1].operation_data, {1, 477, "Olga", 27}) + + if helpers.tarantool_version_at_least(2, 3) then + t.assert_str_contains(errs[2].err, + 'Argument type in operation \'+\' on field \'age\' does not match field type') + else + t.assert_str_contains(errs[2].err, + 'Argument type in operation \'+\' on field 4 does not match field type') + end + t.assert_equals(errs[2].operation_data, {2, 401, 'Alex', 34}) + + if helpers.tarantool_version_at_least(2, 3) then + t.assert_str_contains(errs[3].err, + 'Argument type in operation \'+\' on field \'age\' does not match field type') + else + t.assert_str_contains(errs[3].err, + 'Argument type in operation \'+\' on field 4 does not match field type') + end + t.assert_equals(errs[3].operation_data, {3, 2804, 'Anastasia', 22}) + + if helpers.tarantool_version_at_least(2, 3) then + t.assert_str_contains(errs[4].err, + 'Argument type in operation \'+\' on field \'age\' does not match field type') + else + t.assert_str_contains(errs[4].err, + 'Argument type in operation \'+\' on field 4 does not match field type') + end + t.assert_equals(errs[4].operation_data, {5, 1172, "Anna", 30}) + + if helpers.tarantool_version_at_least(2, 3) then + t.assert_str_contains(errs[5].err, + 'Argument type in operation \'+\' on field \'age\' does not match field type') + else + t.assert_str_contains(errs[5].err, + 'Argument type in operation \'+\' on field 4 does not match field type') + end + t.assert_equals(errs[5].operation_data, {71, 1802, "Sergey", 25}) + + t.assert_str_contains(errs[6].err, batching_utils.rollback_on_error_msg) + t.assert_equals(errs[6].operation_data, {92, 2040, "Oleg", 32}) + + -- primary key = 1 -> bucket_id = 477 -> s2-master + local conn_s2 = g.cluster:server('s2-master').net_box + local result = conn_s2.space['customers']:get(1) + t.assert_equals(result, nil) + + -- primary key = 92 -> bucket_id = 2040 -> s1-master + local conn_s1 = g.cluster:server('s1-master').net_box + local result = conn_s1.space['customers']:get(92) + t.assert_equals(result, nil) + + -- primary key = 2 -> bucket_id = 401 -> s2-master + local conn_s2 = g.cluster:server('s2-master').net_box + local result = conn_s2.space['customers']:get(2) + t.assert_equals(result, {2, 401, 'Anna', 23}) + + -- primary key = 3 -> bucket_id = 2804 -> s1-master + local conn_s1 = g.cluster:server('s1-master').net_box + local result = conn_s1.space['customers']:get(3) + t.assert_equals(result, {3, 2804, 'Daria', 18}) + + -- primary key = 5 -> bucket_id = 1172 -> s2-master + local conn_s2 = g.cluster:server('s2-master').net_box + local result = conn_s2.space['customers']:get(5) + t.assert_equals(result, {5, 1172, 'Sergey', 25}) + + -- primary key = 71 -> bucket_id = 1802 -> s1-master + local conn_s1 = g.cluster:server('s1-master').net_box + local result = conn_s1.space['customers']:get(71) + t.assert_equals(result, {71, 1802, 'Oleg', 32}) +end + +pgroup.test_object_no_success_rollback_on_error = function(g) + -- insert + -- primary key = 2 -> bucket_id = 401 -> s2-master + local conn_s2 = g.cluster:server('s2-master').net_box + local result = conn_s2.space['customers']:insert({2, 401, 'Anna', 23}) + t.assert_equals(result, {2, 401, 'Anna', 23}) + + -- primary key = 3 -> bucket_id = 2804 -> s1-master + local conn_s1 = g.cluster:server('s1-master').net_box + local result = conn_s1.space['customers']:insert({3, 2804, 'Daria', 18}) + t.assert_equals(result, {3, 2804, 'Daria', 18}) + + -- primary key = 5 -> bucket_id = 1172 -> s2-master + local conn_s2 = g.cluster:server('s2-master').net_box + local result = conn_s2.space['customers']:insert({5, 1172, 'Sergey', 25}) + t.assert_equals(result, {5, 1172, 'Sergey', 25}) + + -- primary key = 71 -> bucket_id = 1802 -> s1-master + local conn_s1 = g.cluster:server('s1-master').net_box + local result = conn_s1.space['customers']:insert({71, 1802, 'Oleg', 32}) + t.assert_equals(result, {71, 1802, 'Oleg', 32}) + + -- upsert_object_many + -- fails for both: s1-master s2-master + -- two errors on each storage with rollback + local result, errs = g.cluster.main_server.net_box:call('crud.upsert_object_many', { + 'customers', + { + {{id = 1, name = 'Olga', age = 27}, {{'+', 'age', 1}}}, + {{id = 92, name = 'Oleg', age = 32}, {{'+', 'age', 2}}}, + {{id = 71, name = 'Sergey', age = 25}, {{'+', 'age', '3'}}}, + {{id = 5, name = 'Anna', age = 30}, {{'+', 'age', '4'}}}, + {{id = 2, name = 'Alex', age = 34}, {{'+', 'age', '5'}}}, + {{id = 3, name = 'Anastasia', age = 22}, {{'+', 'age', '6'}}} + }, + { + rollback_on_error = true, + } + }) + + t.assert_equals(result, nil) + t.assert_not_equals(errs, nil) + t.assert_equals(#errs, 6) + + table.sort(errs, function(err1, err2) return err1.operation_data[1] < err2.operation_data[1] end) + + t.assert_str_contains(errs[1].err, batching_utils.rollback_on_error_msg) + t.assert_equals(errs[1].operation_data, {1, 477, "Olga", 27}) + + if helpers.tarantool_version_at_least(2, 3) then + t.assert_str_contains(errs[2].err, + 'Argument type in operation \'+\' on field \'age\' does not match field type') + else + t.assert_str_contains(errs[2].err, + 'Argument type in operation \'+\' on field 4 does not match field type') + end + t.assert_equals(errs[2].operation_data, {2, 401, 'Alex', 34}) + + if helpers.tarantool_version_at_least(2, 3) then + t.assert_str_contains(errs[3].err, + 'Argument type in operation \'+\' on field \'age\' does not match field type') + else + t.assert_str_contains(errs[3].err, + 'Argument type in operation \'+\' on field 4 does not match field type') + end + t.assert_equals(errs[3].operation_data, {3, 2804, 'Anastasia', 22}) + + if helpers.tarantool_version_at_least(2, 3) then + t.assert_str_contains(errs[4].err, + 'Argument type in operation \'+\' on field \'age\' does not match field type') + else + t.assert_str_contains(errs[4].err, + 'Argument type in operation \'+\' on field 4 does not match field type') + end + t.assert_equals(errs[4].operation_data, {5, 1172, "Anna", 30}) + + if helpers.tarantool_version_at_least(2, 3) then + t.assert_str_contains(errs[5].err, + 'Argument type in operation \'+\' on field \'age\' does not match field type') + else + t.assert_str_contains(errs[5].err, + 'Argument type in operation \'+\' on field 4 does not match field type') + end + t.assert_equals(errs[5].operation_data, {71, 1802, "Sergey", 25}) + + t.assert_str_contains(errs[6].err, batching_utils.rollback_on_error_msg) + t.assert_equals(errs[6].operation_data, {92, 2040, "Oleg", 32}) + + -- primary key = 1 -> bucket_id = 477 -> s2-master + local conn_s2 = g.cluster:server('s2-master').net_box + local result = conn_s2.space['customers']:get(1) + t.assert_equals(result, nil) + + -- primary key = 92 -> bucket_id = 2040 -> s1-master + local conn_s1 = g.cluster:server('s1-master').net_box + local result = conn_s1.space['customers']:get(92) + t.assert_equals(result, nil) + + -- primary key = 2 -> bucket_id = 401 -> s2-master + local conn_s2 = g.cluster:server('s2-master').net_box + local result = conn_s2.space['customers']:get(2) + t.assert_equals(result, {2, 401, 'Anna', 23}) + + -- primary key = 3 -> bucket_id = 2804 -> s1-master + local conn_s1 = g.cluster:server('s1-master').net_box + local result = conn_s1.space['customers']:get(3) + t.assert_equals(result, {3, 2804, 'Daria', 18}) + + -- primary key = 5 -> bucket_id = 1172 -> s2-master + local conn_s2 = g.cluster:server('s2-master').net_box + local result = conn_s2.space['customers']:get(5) + t.assert_equals(result, {5, 1172, 'Sergey', 25}) + + -- primary key = 71 -> bucket_id = 1802 -> s1-master + local conn_s1 = g.cluster:server('s1-master').net_box + local result = conn_s1.space['customers']:get(71) + t.assert_equals(result, {71, 1802, 'Oleg', 32}) +end + +pgroup.test_all_success_rollback_and_stop_on_error = function(g) + -- upsert_many + -- all success + local result, errs = g.cluster.main_server.net_box:call('crud.upsert_many', { + 'customers', + { + {{2, box.NULL, 'Anna', 23}, {{'+', 'age', 1}}}, + {{3, box.NULL, 'Daria', 18}, {{'+', 'age', 1}}}, + {{71, box.NULL, 'Oleg', 32}, {{'+', 'age', 1}}} + }, + { + stop_on_error = true, + rollback_on_error = true, + } + }) + + t.assert_equals(errs, nil) + t.assert_equals(result.metadata, { + {name = 'id', type = 'unsigned'}, + {name = 'bucket_id', type = 'unsigned'}, + {name = 'name', type = 'string'}, + {name = 'age', type = 'number'}, + }) + t.assert_equals(result.rows, nil) + + -- get + -- primary key = 2 -> bucket_id = 401 -> s2-master + local conn_s2 = g.cluster:server('s2-master').net_box + local result = conn_s2.space['customers']:get(2) + t.assert_equals(result, {2, 401, 'Anna', 23}) + + -- primary key = 3 -> bucket_id = 2804 -> s1-master + local conn_s1 = g.cluster:server('s1-master').net_box + local result = conn_s1.space['customers']:get(3) + t.assert_equals(result, {3, 2804, 'Daria', 18}) + + -- primary key = 71 -> bucket_id = 1802 -> s1-master + local conn_s1 = g.cluster:server('s1-master').net_box + local result = conn_s1.space['customers']:get(71) + t.assert_equals(result, {71, 1802, 'Oleg', 32}) +end + +pgroup.test_object_all_success_rollback_and_stop_on_error = function(g) + -- upsert_object_many + -- all success + local result, errs = g.cluster.main_server.net_box:call('crud.upsert_object_many', { + 'customers', + { + {{id = 2, name = 'Anna', age = 23}, {{'+', 'age', 1}}}, + {{id = 3, name = 'Daria', age = 18}, {{'+', 'age', 1}}}, + {{id = 71, name = 'Oleg', age = 32}, {{'+', 'age', 1}}} + }, + { + stop_on_error = true, + rollback_on_error = true, + } + }) + + t.assert_equals(errs, nil) + t.assert_equals(result.metadata, { + {name = 'id', type = 'unsigned'}, + {name = 'bucket_id', type = 'unsigned'}, + {name = 'name', type = 'string'}, + {name = 'age', type = 'number'}, + }) + t.assert_equals(result.rows, nil) + + -- get + -- primary key = 2 -> bucket_id = 401 -> s2-master + local conn_s2 = g.cluster:server('s2-master').net_box + local result = conn_s2.space['customers']:get(2) + t.assert_equals(result, {2, 401, 'Anna', 23}) + + -- primary key = 3 -> bucket_id = 2804 -> s1-master + local conn_s1 = g.cluster:server('s1-master').net_box + local result = conn_s1.space['customers']:get(3) + t.assert_equals(result, {3, 2804, 'Daria', 18}) + + -- primary key = 71 -> bucket_id = 1802 -> s1-master + local conn_s1 = g.cluster:server('s1-master').net_box + local result = conn_s1.space['customers']:get(71) + t.assert_equals(result, {71, 1802, 'Oleg', 32}) +end + +pgroup.test_partial_success_rollback_and_stop_on_error = function(g) + -- insert + -- primary key = 3 -> bucket_id = 2804 -> s1-master + local conn_s1 = g.cluster:server('s1-master').net_box + local result = conn_s1.space['customers']:insert({3, 2804, 'Daria', 18}) + t.assert_equals(result, {3, 2804, 'Daria', 18}) + + -- primary key = 71 -> bucket_id = 1802 -> s1-master + local conn_s1 = g.cluster:server('s1-master').net_box + local result = conn_s1.space['customers']:insert({71, 1802, 'Oleg', 32}) + t.assert_equals(result, {71, 1802, 'Oleg', 32}) + + -- upsert_many + -- stop_on_error = true, rollback_on_error = true + -- two error on one storage with rollback, inserts stop by error on this storage + -- inserts before error are rollbacked + local result, errs = g.cluster.main_server.net_box:call('crud.upsert_many', { + 'customers', + { + {{22, box.NULL, 'Alex', 34}, {{'+', 'age', 1}}}, + {{92, box.NULL, 'Artur', 29}, {{'+', 'age', 2}}}, + {{3, box.NULL, 'Anastasia', 22}, {{'+', 'age', '3'}}}, + {{5, box.NULL, 'Sergey', 25}, {{'+', 'age', 4}}}, + {{9, box.NULL, 'Anna', 30}, {{'+', 'age', 5}}}, + {{71, box.NULL, 'Oksana', 29}, {{'+', 'age', '6'}}}, + }, + { + stop_on_error = true, + rollback_on_error = true, + } + }) + + t.assert_not_equals(errs, nil) + t.assert_equals(#errs, 4) + + table.sort(errs, function(err1, err2) return err1.operation_data[1] < err2.operation_data[1] end) + + if helpers.tarantool_version_at_least(2, 3) then + t.assert_str_contains(errs[1].err, + 'Argument type in operation \'+\' on field \'age\' does not match field type') + else + t.assert_str_contains(errs[1].err, + 'Argument type in operation \'+\' on field 4 does not match field type') + end + t.assert_equals(errs[1].operation_data, {3, 2804, 'Anastasia', 22}) + + t.assert_str_contains(errs[2].err, batching_utils.stop_on_error_msg) + t.assert_equals(errs[2].operation_data, {9, 1644, "Anna", 30}) + + t.assert_str_contains(errs[3].err, batching_utils.stop_on_error_msg) + t.assert_equals(errs[3].operation_data, {71, 1802, "Oksana", 29}) + + t.assert_str_contains(errs[4].err, batching_utils.rollback_on_error_msg) + t.assert_equals(errs[4].operation_data, {92, 2040, "Artur", 29}) + + t.assert_equals(result.metadata, { + {name = 'id', type = 'unsigned'}, + {name = 'bucket_id', type = 'unsigned'}, + {name = 'name', type = 'string'}, + {name = 'age', type = 'number'}, + }) + t.assert_equals(result.rows, nil) + + -- get + -- primary key = 22 -> bucket_id = 655 -> s2-master + local conn_s2 = g.cluster:server('s2-master').net_box + local result = conn_s2.space['customers']:get(22) + t.assert_equals(result, {22, 655, 'Alex', 34}) + + -- primary key = 92 -> bucket_id = 2040 -> s1-master + local conn_s1 = g.cluster:server('s1-master').net_box + local result = conn_s1.space['customers']:get(92) + t.assert_equals(result, nil) + + -- primary key = 5 -> bucket_id = 1172 -> s2-master + local conn_s2 = g.cluster:server('s2-master').net_box + local result = conn_s2.space['customers']:get(5) + t.assert_equals(result, {5, 1172, 'Sergey', 25}) + + -- primary key = 3 -> bucket_id = 2804 -> s1-master + local conn_s1 = g.cluster:server('s1-master').net_box + local result = conn_s1.space['customers']:get(3) + t.assert_equals(result, {3, 2804, 'Daria', 18}) + + -- primary key = 9 -> bucket_id = 1644 -> s1-master + local conn_s1 = g.cluster:server('s1-master').net_box + local result = conn_s1.space['customers']:get(9) + t.assert_equals(result, nil) + + -- primary key = 71 -> bucket_id = 1802 -> s1-master + local conn_s1 = g.cluster:server('s1-master').net_box + local result = conn_s1.space['customers']:get(71) + t.assert_equals(result, {71, 1802, 'Oleg', 32}) +end + +pgroup.test_object_partial_success_rollback_and_stop_on_error = function(g) + -- insert + -- primary key = 3 -> bucket_id = 2804 -> s1-master + local conn_s1 = g.cluster:server('s1-master').net_box + local result = conn_s1.space['customers']:insert({3, 2804, 'Daria', 18}) + t.assert_equals(result, {3, 2804, 'Daria', 18}) + + -- primary key = 71 -> bucket_id = 1802 -> s1-master + local conn_s1 = g.cluster:server('s1-master').net_box + local result = conn_s1.space['customers']:insert({71, 1802, 'Oleg', 32}) + t.assert_equals(result, {71, 1802, 'Oleg', 32}) + + -- upsert_object_many + -- stop_on_error = true, rollback_on_error = true + -- two error on one storage with rollback, inserts stop by error on this storage + -- inserts before error are rollbacked + local result, errs = g.cluster.main_server.net_box:call('crud.upsert_object_many', { + 'customers', + { + {{id = 22, name = 'Alex', age = 34}, {{'+', 'age', 1}}}, + {{id = 92, name = 'Artur', age = 29}, {{'+', 'age', 2}}}, + {{id = 3, name = 'Anastasia', age = 22}, {{'+', 'age', '3'}}}, + {{id = 5, name = 'Sergey', age = 25}, {{'+', 'age', 4}}}, + {{id = 9, name = 'Anna', age = 30}, {{'+', 'age', 5}}}, + {{id = 71, name = 'Oksana', age = 29}, {{'+', 'age', '6'}}}, + }, + { + stop_on_error = true, + rollback_on_error = true, + } + }) + + t.assert_not_equals(errs, nil) + t.assert_equals(#errs, 4) + + table.sort(errs, function(err1, err2) return err1.operation_data[1] < err2.operation_data[1] end) + + if helpers.tarantool_version_at_least(2, 3) then + t.assert_str_contains(errs[1].err, + 'Argument type in operation \'+\' on field \'age\' does not match field type') + else + t.assert_str_contains(errs[1].err, + 'Argument type in operation \'+\' on field 4 does not match field type') + end + t.assert_equals(errs[1].operation_data, {3, 2804, 'Anastasia', 22}) + + t.assert_str_contains(errs[2].err, batching_utils.stop_on_error_msg) + t.assert_equals(errs[2].operation_data, {9, 1644, "Anna", 30}) + + t.assert_str_contains(errs[3].err, batching_utils.stop_on_error_msg) + t.assert_equals(errs[3].operation_data, {71, 1802, "Oksana", 29}) + + t.assert_str_contains(errs[4].err, batching_utils.rollback_on_error_msg) + t.assert_equals(errs[4].operation_data, {92, 2040, "Artur", 29}) + + t.assert_equals(result.metadata, { + {name = 'id', type = 'unsigned'}, + {name = 'bucket_id', type = 'unsigned'}, + {name = 'name', type = 'string'}, + {name = 'age', type = 'number'}, + }) + t.assert_equals(result.rows, nil) + + -- get + -- primary key = 22 -> bucket_id = 655 -> s2-master + local conn_s2 = g.cluster:server('s2-master').net_box + local result = conn_s2.space['customers']:get(22) + t.assert_equals(result, {22, 655, 'Alex', 34}) + + -- primary key = 92 -> bucket_id = 2040 -> s1-master + local conn_s1 = g.cluster:server('s1-master').net_box + local result = conn_s1.space['customers']:get(92) + t.assert_equals(result, nil) + + -- primary key = 5 -> bucket_id = 1172 -> s2-master + local conn_s2 = g.cluster:server('s2-master').net_box + local result = conn_s2.space['customers']:get(5) + t.assert_equals(result, {5, 1172, 'Sergey', 25}) + + -- primary key = 3 -> bucket_id = 2804 -> s1-master + local conn_s1 = g.cluster:server('s1-master').net_box + local result = conn_s1.space['customers']:get(3) + t.assert_equals(result, {3, 2804, 'Daria', 18}) + + -- primary key = 9 -> bucket_id = 1644 -> s1-master + local conn_s1 = g.cluster:server('s1-master').net_box + local result = conn_s1.space['customers']:get(9) + t.assert_equals(result, nil) + + -- primary key = 71 -> bucket_id = 1802 -> s1-master + local conn_s1 = g.cluster:server('s1-master').net_box + local result = conn_s1.space['customers']:get(71) + t.assert_equals(result, {71, 1802, 'Oleg', 32}) +end + +pgroup.test_partial_result = function(g) + -- bad fields format + local result, errs = g.cluster.main_server.net_box:call('crud.upsert_many', { + 'customers', + { + {{15, box.NULL, 'Fedor', 59}, {{'+', 'age', 1}}}, + {{25, box.NULL, 'Anna', 23}, {{'+', 'age', 1}}}, + }, + {fields = {'id', 'invalid'}}, + }) + + t.assert_equals(result, nil) + t.assert_not_equals(errs, nil) + t.assert_equals(#errs, 1) + t.assert_str_contains(errs[1].err, 'Space format doesn\'t contain field named "invalid"') + + -- upsert_many + local result, errs = g.cluster.main_server.net_box:call('crud.upsert_many', { + 'customers', + { + {{1, box.NULL, 'Fedor', 59}, {{'+', 'age', 1}}}, + {{2, box.NULL, 'Anna', 23}, {{'+', 'age', 1}}}, + {{3, box.NULL, 'Daria', 18}, {{'+', 'age', 1}}}, + }, + {fields = {'id', 'name'}}, + }) + + t.assert_equals(errs, nil) + t.assert_equals(result.metadata, { + {name = 'id', type = 'unsigned'}, + {name = 'name', type = 'string'}, + }) + t.assert_equals(result.rows, nil) +end + +pgroup.test_object_partial_result = function(g) + -- bad fields format + local result, errs = g.cluster.main_server.net_box:call('crud.upsert_object_many', { + 'customers', + { + {{id = 15, name = 'Fedor', age = 59}, {{'+', 'age', 1}}}, + {{id = 25, name = 'Anna', age = 23}, {{'+', 'age', 1}}}, + }, + {fields = {'id', 'invalid'}}, + }) + + t.assert_equals(result, nil) + t.assert_not_equals(errs, nil) + t.assert_equals(#errs, 1) + t.assert_str_contains(errs[1].err, 'Space format doesn\'t contain field named "invalid"') + + -- upsert_object_many + local result, errs = g.cluster.main_server.net_box:call('crud.upsert_object_many', { + 'customers', + { + {{id = 1, name = 'Fedor', age = 59}, {{'+', 'age', 1}}}, + {{id = 2, name = 'Anna', age = 23}, {{'+', 'age', 1}}}, + {{id = 3, name = 'Daria', age = 18}, {{'+', 'age', 1}}}, + }, + {fields = {'id', 'name'}}, + }) + + t.assert_equals(errs, nil) + t.assert_equals(result.metadata, { + {name = 'id', type = 'unsigned'}, + {name = 'name', type = 'string'}, + }) + t.assert_equals(result.rows, nil) +end + +pgroup.test_opts_not_damaged = function(g) + -- upsert_many + local batch_upsert_opts = {timeout = 1, fields = {'name', 'age'}} + local new_batch_upsert_opts, err = g.cluster.main_server:eval([[ + local crud = require('crud') + + local batch_upsert_opts = ... + + local _, err = crud.upsert_many('customers', { + {{1, box.NULL, 'Alex', 59}, {{'+', 'age', 1}},} + }, batch_upsert_opts) + + return batch_upsert_opts, err + ]], {batch_upsert_opts}) + + t.assert_equals(err, nil) + t.assert_equals(new_batch_upsert_opts, batch_upsert_opts) + + -- upsert_object_many + local batch_upsert_opts = {timeout = 1, fields = {'name', 'age'}} + local new_batch_upsert_opts, err = g.cluster.main_server:eval([[ + local crud = require('crud') + + local batch_upsert_opts = ... + + local _, err = crud.upsert_object_many('customers', { + {{id = 2, name = 'Fedor', age = 59}, {{'+', 'age', 1}},} + }, batch_upsert_opts) + + return batch_upsert_opts, err + ]], {batch_upsert_opts}) + + t.assert_equals(err, nil) + t.assert_equals(new_batch_upsert_opts, batch_upsert_opts) +end