diff --git a/.luacheckrc b/.luacheckrc index c4bc116..c2e0d0f 100644 --- a/.luacheckrc +++ b/.luacheckrc @@ -1,4 +1,6 @@ redefined = false +globals = {"box", "_TARANTOOL", "tonumber64", "utf8", "table"} include_files = {"**/*.lua", "*.rockspec", "*.luacheckrc"} exclude_files = {".rocks/", "tmp/", ".history/"} max_line_length = 120 +max_comment_line_length = 150 diff --git a/CHANGELOG.md b/CHANGELOG.md index 2f5745d..8936b57 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,12 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). +## Unreleased + +### Added + +- Added support of sequences in schema and space indexes (#122). + ## [1.6.5] - 2023-10-23 ### Added diff --git a/README.md b/README.md index 7392ec1..ca8bcf8 100644 --- a/README.md +++ b/README.md @@ -205,17 +205,16 @@ format = { }, ... }, - sequences = { -- Not implemented yet - [seqence_name] = { - start - min - max - cycle - cache - step - - } - } + sequences = { + [sequence_name] = { + start = start, + min = min, + max = max, + cycle = cycle, + cache = cache, + step = step, + }, + }, } ``` @@ -292,8 +291,52 @@ local schema = { }}, sharding_key = {'customer_id'}, sharding_func = 'vshard.router.bucket_id_mpcrc32', - } - } + }, + tickets = { + engine = 'memtx', + is_local = false, + temporary = false, + format = { + {name = 'ticket_id', is_nullable = false, type = 'unsigned'}, + {name = 'customer_id', is_nullable = false, type = 'unsigned'}, + {name = 'bucket_id', is_nullable = false, type = 'unsigned'}, + {name = 'contents', is_nullable = false, type = 'string'}, + }, + indexes = {{ + name = 'ticket_id', + type = 'TREE', + unique = true, + parts = { + {path = 'ticket_id', is_nullable = false, type = 'unsigned'} + }, + sequence = 'ticket_seq', + }, {, + name = 'customer_id', + type = 'TREE', + unique = false, + parts = { + {path = 'customer_id', is_nullable = false, type = 'unsigned'} + } + }, { + name = 'bucket_id', + type = 'TREE', + unique = false, + parts = { + {path = 'bucket_id', is_nullable = false, type = 'unsigned'} + } + }}, + sharding_key = {'customer_id'}, + sharding_func = 'vshard.router.bucket_id_mpcrc32', + }, + }, + sequences = { + ticket_seq = { + start = 1, + min = 1, + max = 10000, + cycle = false, + }, + }, } ``` diff --git a/ddl.lua b/ddl.lua index 6027739..294299f 100755 --- a/ddl.lua +++ b/ddl.lua @@ -2,6 +2,7 @@ local ddl_get = require('ddl.get') local ddl_set = require('ddl.set') local ddl_check = require('ddl.check') local ddl_db = require('ddl.db') +local ddl_compare = require('ddl.compare') local utils = require('ddl.utils') local function check_schema_format(schema) @@ -22,12 +23,14 @@ local function check_schema_format(schema) return nil, string.format("functions: not supported") end - if type(schema.sequences) ~= 'nil' then - return nil, string.format("sequences: not supported") + if type(schema.sequences) ~= 'nil' and type(schema.sequences) ~= 'table' then + return nil, string.format( + "sequences: must be a table or nil, got %s", type(schema.sequences) + ) end do -- check redundant keys - local k = utils.redundant_key(schema, {'spaces'}) + local k = utils.redundant_key(schema, {'spaces', 'sequences'}) if k ~= nil then return nil, string.format( "Invalid schema: redundant key %q", k @@ -39,11 +42,44 @@ local function check_schema_format(schema) end local function _check_schema(schema) + -- Create sequences before spaces since space indexes use sequences. + local sequences = schema.sequences or {} + for sequence_name, sequence_schema in pairs(sequences) do + local ok, err = ddl_check.check_sequence(sequence_name, sequence_schema) + if not ok then + return nil, err + end + + if box.sequence[sequence_name] ~= nil then + local current_schema = ddl_get.get_sequence_schema(sequence_name) + local _, err = ddl_compare.assert_equiv_sequence_schema(sequence_schema, current_schema) + if err ~= nil then + return nil, string.format( + "Incompatible schema: sequences[%q] %s", sequence_name, err) + end + else + local ok, err = pcall( + ddl_set.create_sequence, + sequence_name, sequence_schema, {dummy = true} + ) + + local dummy = box.sequence['_ddl_dummy'] + if dummy then + pcall(box.schema.sequence.drop, dummy.id) + end + + if not ok then + return nil, tostring(err):gsub('_ddl_dummy', sequence_name) + end + end + end + for space_name, space_schema in pairs(schema.spaces) do - local ok, err = ddl_check.check_space(space_name, space_schema) + local ok, err = ddl_check.check_space(space_name, space_schema, sequences) if not ok then return nil, err end + if box.space[space_name] ~= nil then local diff = {} local current_schema = ddl_get.get_space_schema(space_name) @@ -127,6 +163,12 @@ local function _set_schema(schema) } ) + for sequence_name, sequence_schema in pairs(schema.sequences or {}) do + if box.sequence[sequence_name] == nil then + ddl_set.create_sequence(sequence_name, sequence_schema) + end + end + for space_name, space_schema in pairs(schema.spaces) do if box.space[space_name] == nil then ddl_set.create_space(space_name, space_schema) @@ -153,8 +195,22 @@ local function get_schema() end end + local sequences = {} + for _, sequence in box.space._sequence:pairs() do + sequences[sequence.name] = ddl_get.get_sequence_schema(sequence.name) + end + + local next_k = next(sequences) + local no_sequences = next_k == nil + + if no_sequences then + -- For backward compatibility. + sequences = nil + end + return { spaces = spaces, + sequences = sequences, } end diff --git a/ddl/check.lua b/ddl/check.lua index fe3af3b..7079b66 100644 --- a/ddl/check.lua +++ b/ddl/check.lua @@ -489,7 +489,9 @@ local function check_index_parts(index, space) return true end -local function check_index(i, index, space) +local function check_index(i, index, space, sequences) + sequences = sequences or {} + if type(index) ~= 'table' then return nil, string.format( "spaces[%q].indexes[%d]: bad value" .. @@ -709,9 +711,30 @@ local function check_index(i, index, space) end end - local keys = {'type', 'name', 'unique', 'parts', 'field'} + do + local index_sequence_name = index.sequence + + if index_sequence_name ~= nil then + if type(index_sequence_name) ~= 'string' then + return nil, string.format( + "spaces[%q].indexes[%q].sequence: incorrect value (string expected, got %s)", + space.name, index.name, type(index_sequence_name) + ) + end + + local sequence = sequences[index_sequence_name] + if sequence == nil then + return nil, string.format( + "spaces[%q].indexes[%q].sequence: missing sequence %q in sequences section", + space.name, index.name, index_sequence_name + ) + end + end + end + + local keys = {'type', 'name', 'unique', 'parts', 'field', 'sequence'} if index.type == 'RTREE' then - keys = {'type', 'name', 'unique', 'parts', 'field', 'dimension', 'distance'} + keys = {'type', 'name', 'unique', 'parts', 'field', 'dimension', 'distance', 'sequence'} end local k = utils.redundant_key(index, keys) @@ -961,7 +984,7 @@ local function check_sharding_metadata(space) end -local function check_space(space_name, space) +local function check_space(space_name, space, sequences) if type(space_name) ~= 'string' then return nil, string.format( "spaces[%s]: invalid space name (string expected, got %s)", @@ -1074,7 +1097,7 @@ local function check_space(space_name, space) name = space_name, engine = space.engine, fields = space_fields, - }) + }, sequences) if not ok then return nil, err @@ -1118,6 +1141,94 @@ local function check_space(space_name, space) return true end +local function check_sequence_nullable_option_type(sequence_name, sequence, + option_name, type_checker) + local option = sequence[option_name] + if option == nil then + return true + end + + local _, err = type_checker(option) + if err ~= nil then + return nil, string.format( + "sequences[%q].%s: bad value (%s)", + sequence_name, option_name, err + ) + end + + return true +end + +local function check_sequence_multiple_nullable_options_type(sequence_name, sequence, + option_names_array, type_checker) + for _, option_name in ipairs(option_names_array) do + local _, err = check_sequence_nullable_option_type(sequence_name, sequence, + option_name, type_checker) + if err ~= nil then + return err + end + end + + return true +end + +local function check_sequence(sequence_name, sequence) + if type(sequence_name) ~= 'string' then + return nil, string.format( + "sequences[%s]: invalid sequence name (string expected, got %s)", + sequence_name, type(sequence_name) + ) + end + + if type(sequence) ~= 'table' then + return nil, string.format( + "sequences[%q]: bad value (table expected, got %s)", + sequence_name, type(sequence) + ) + end + + local number_nullable_options = {'start', 'min', 'max', 'cache', 'step'} + local number_checker = function(v) + if not utils.is_number(v) then + local actual_type = type(v) + return nil, ('number expected, got %s'):format(actual_type) + else + return true + end + end + local _, err = check_sequence_multiple_nullable_options_type(sequence_name, sequence, + number_nullable_options, number_checker) + if err ~= nil then + return nil, err + end + + local boolean_nullable_options = {'cycle'} + local boolean_checker = function(v) + if type(v) ~= 'boolean' then + local actual_type = type(v) + return nil, ('boolean expected, got %s'):format(actual_type) + else + return true + end + end + local _, err = check_sequence_multiple_nullable_options_type(sequence_name, sequence, + boolean_nullable_options, boolean_checker) + if err ~= nil then + return nil, err + end + + local allowed_options = utils.concat_arrays(number_nullable_options, boolean_nullable_options) + local k = utils.redundant_key(sequence, allowed_options) + if k ~= nil then + return nil, string.format( + "sequences[%q]: redundant key %q", + sequence_name, k + ) + end + + return true +end + return { check_space = check_space, @@ -1128,7 +1239,8 @@ return { check_index_parts = check_index_parts, check_index = check_index, check_field = check_field, + check_sequence = check_sequence, internal = { is_callable = is_callable, - } + }, } diff --git a/ddl/compare.lua b/ddl/compare.lua new file mode 100644 index 0000000..cc34502 --- /dev/null +++ b/ddl/compare.lua @@ -0,0 +1,40 @@ +local INT64_MIN = tonumber64('-9223372036854775808') +local INT64_MAX = tonumber64('9223372036854775807') + +local function get_sequence_defaults(opts) + -- https://github.com/tarantool/tarantool/blob/05e69108076ae22f33f8fc55b463c6babb6478fe/src/box/lua/schema.lua#L2847-L2855 + local ascending = not opts.step or opts.step > 0 + return { + step = 1, + min = ascending and 1 or INT64_MIN, + max = ascending and INT64_MAX or -1, + start = ascending and (opts.min or 1) or (opts.max or -1), + cache = 0, + cycle = false, + } +end + +local function assert_equiv_sequence_schema(sequence_schema, current_schema) + local defaults = get_sequence_defaults(sequence_schema) + + for key, current_value in pairs(current_schema) do + local ddl_value = sequence_schema[key] + + if ddl_value ~= nil then + if ddl_value ~= current_value then + return nil, ('%s (expected %s, got %s)'):format(key, current_value, ddl_value) + end + else + local default_value = defaults[key] + if default_value ~= current_value then + return nil, ('%s (expected %s, got nil and default is %s)'):format(key, current_value, default_value) + end + end + end + + return true +end + +return { + assert_equiv_sequence_schema = assert_equiv_sequence_schema, +} diff --git a/ddl/get.lua b/ddl/get.lua index c9a3d07..cdde806 100644 --- a/ddl/get.lua +++ b/ddl/get.lua @@ -37,6 +37,25 @@ local function _get_index_parts(space, index_part) return ddl_index_part end +local function get_sequence_by_id(id) + if box.sequence[id] ~= nil then + return box.sequence[id] + end + + -- For some reason, Tarantool 1.10 indexes box.sequence only by name. + local sequence_tuple = box.space._sequence:get(id) + if sequence_tuple == nil then + return nil + end + + local name = sequence_tuple.name + if name == nil then + return nil + end + + return box.sequence[name] +end + local function _get_index(box_space, box_index) local ddl_index = {} ddl_index.name = box_index.name @@ -53,6 +72,16 @@ local function _get_index(box_space, box_index) ddl_index.distance = box.space._index:get({box_space.id, box_index.id}).opts.distance or 'euclid' end + local sequence_id = box_index.sequence_id + if sequence_id ~= nil then + local sequence = get_sequence_by_id(sequence_id) + + -- box should keep there two consistent, so assert here. + assert(sequence ~= nil, "missing sequence") + + ddl_index.sequence = sequence.name + end + return ddl_index end @@ -168,8 +197,24 @@ local function bucket_id(space_name, sharding_key) return id end +local function get_sequence_schema(sequence_name) + local box_sequence = box.sequence[sequence_name] + assert(box_sequence ~= nil) + + return { + start = box_sequence.start, + min = box_sequence.min, + max = box_sequence.max, + cycle = box_sequence.cycle, + cache = box_sequence.cache, + step = box_sequence.step, + } +end + return { get_space_schema = get_space_schema, + get_sequence_schema = get_sequence_schema, + get_sequence_by_id = get_sequence_by_id, internal = { bucket_id = bucket_id, } diff --git a/ddl/set.lua b/ddl/set.lua index 954b255..1d4d730 100644 --- a/ddl/set.lua +++ b/ddl/set.lua @@ -1,3 +1,20 @@ +local function create_sequence(sequence_name, sequence_schema, opts) + local is_dummy = opts and opts.dummy + if is_dummy then + sequence_name = '_ddl_dummy' + end + + local ok, err = pcall(box.schema.sequence.create, sequence_name, sequence_schema) + + if not ok then + error( + string.format("sequences[%q]: %s", sequence_name, err), 0 + ) + end + + return true +end + local function create_index(box_space, ddl_index) if ddl_index.parts == nil then error("index parts is nil") @@ -14,17 +31,11 @@ local function create_index(box_space, ddl_index) table.insert(index_parts, index_part) end - local sequence_name = nil - if ddl_index.sequence ~= nil then - local sequence = box.schema.sequence.create(ddl_index.sequence) - sequence_name = sequence.name - end - box_space:create_index(ddl_index.name, { type = ddl_index.type, unique = ddl_index.unique, parts = index_parts, - sequence = sequence_name, + sequence = ddl_index.sequence, dimension = ddl_index.dimension, distance = ddl_index.distance, func = ddl_index.func and ddl_index.func.name, @@ -69,7 +80,19 @@ local function create_space(space_name, space_schema, opts) end local box_space = data + + local dummy_sequences = {} + for i, index in ipairs(space_schema.indexes) do + if is_dummy and index.sequence ~= nil then + index = table.deepcopy(index) + + local next_dummy_sequence = ('_dummy_seq_%d'):format(#dummy_sequences + 1) + box.schema.sequence.create(next_dummy_sequence) + index.sequence = next_dummy_sequence + table.insert(dummy_sequences, next_dummy_sequence) + end + local ok, data = pcall(create_index, box_space, index) if not ok then error(string.format( @@ -79,6 +102,17 @@ local function create_space(space_name, space_schema, opts) end end + if is_dummy and #dummy_sequences > 0 then + -- Indexes reference sequences. + for i, index in ipairs(space_schema.indexes) do + box_space.index[index.name or i]:drop() + end + + for _, name in ipairs(dummy_sequences) do + box.schema.sequence.drop(name) + end + end + if not is_dummy then local ok, err = pcall(create_metadata, space_name, {space_schema.sharding_key}, "sharding_key") if not ok then @@ -105,4 +139,5 @@ end return { create_space = create_space, + create_sequence = create_sequence, } diff --git a/ddl/utils.lua b/ddl/utils.lua index 0f6b4f3..fc7d4b9 100644 --- a/ddl/utils.lua +++ b/ddl/utils.lua @@ -49,12 +49,22 @@ local LUA_KEYWORDS = { ['while'] = true, } +local function is_number(v) + local is_lua_number = type(v) == 'number' + + local is_lj_uint = type(v) == 'cdata' and ffi.istype('uint64_t', v) + local is_lj_int = type(v) == 'cdata' and ffi.istype('int64_t', v) + local is_lj_number = is_lj_uint or is_lj_int + + return is_lua_number or is_lj_number +end + local function deepcmp(got, expected, extra) if extra == nil then extra = {} end - if type(expected) == "number" or type(got) == "number" then + if is_number(expected) or is_number(got) then extra.got = got extra.expected = expected if got ~= got and expected ~= expected then @@ -213,13 +223,25 @@ local function get_G_function(func_name) return sharding_func end +local function concat_arrays(arr1, arr2) + local res = table.deepcopy(arr1) + + for _, v in ipairs(arr2) do + table.insert(res, table.deepcopy(v)) + end + + return res +end + return { deepcmp = deepcmp, is_array = is_array, + is_number = is_number, redundant_key = redundant_key, find_first_duplicate = find_first_duplicate, lj_char_isident = lj_char_isident, lj_char_isdigit = lj_char_isdigit, LUA_KEYWORDS = LUA_KEYWORDS, get_G_function = get_G_function, + concat_arrays = concat_arrays, } diff --git a/test/check_schema_test.lua b/test/check_schema_test.lua index 17113e8..4388bac 100644 --- a/test/check_schema_test.lua +++ b/test/check_schema_test.lua @@ -1608,3 +1608,143 @@ g.test_exclude_null_unsupported = function() t.assert_str_contains(err, 'spaces["my_space"].indexes["nullable_index"].parts[1]: ' .. 'exclude_null isn\'t allowed in your Tarantool version') end + +g.test_index_invalid_sequence = function() + local schema = { + spaces = { + ['with_sequence'] = { + engine = 'memtx', + is_local = false, + temporary = false, + format = { + {name = 'seq_id', type = 'unsigned', is_nullable = false}, + {name = 'first', type = 'string', is_nullable = false}, + {name = 'second', type = 'string', is_nullable = false}, + }, + indexes = { + { + name = 'seq_index', + type = 'TREE', + unique = true, + parts = {{is_nullable = false, path = 'seq_id', type = 'unsigned'}}, + sequence = true, + }, + }, + }, + }, + } + + local _, err = ddl.set_schema(schema) + t.assert_str_contains(err, 'spaces["with_sequence"].indexes["seq_index"].sequence: ' .. + 'incorrect value (string expected, got boolean)') +end + +g.test_index_sequence_is_not_in_schema = function() + local schema = { + spaces = { + ['with_sequence'] = { + engine = 'memtx', + is_local = false, + temporary = false, + format = { + {name = 'seq_id', type = 'unsigned', is_nullable = false}, + {name = 'first', type = 'string', is_nullable = false}, + {name = 'second', type = 'string', is_nullable = false}, + }, + indexes = { + { + name = 'seq_index', + type = 'TREE', + unique = true, + parts = {{is_nullable = false, path = 'seq_id', type = 'unsigned'}}, + sequence = 'seq1', + }, + }, + }, + }, + sequences = { + ['seq2'] = {}, + }, + } + + local _, err = ddl.set_schema(schema) + t.assert_str_contains(err, 'spaces["with_sequence"].indexes["seq_index"].sequence: ' .. + 'missing sequence "seq1" in sequences section') +end + +g.test_sequences_invalid_key = function() + local schema = { + spaces = {}, + sequences = { + [1] = {}, + }, + } + + local _, err = ddl.set_schema(schema) + t.assert_str_contains(err, 'sequences[1]: invalid sequence name (string expected, got number)') +end + +g.test_sequences_invalid_options = function() + local schema = { + spaces = {}, + sequences = { + ['seq'] = true, + }, + } + + local _, err = ddl.set_schema(schema) + t.assert_str_contains(err, 'sequences["seq"]: bad value (table expected, got boolean)') +end + +local sequence_invalid_option_cases = { + start = { + err = "sequences[\"seq\"]: Illegal parameters, options parameter 'start' should be of type number", + }, + min = { + err = "sequences[\"seq\"]: Illegal parameters, options parameter 'min' should be of type number", + }, + max = { + err = "sequences[\"seq\"]: Illegal parameters, options parameter 'max' should be of type number", + }, + cache = { + err = "sequences[\"seq\"]: Illegal parameters, options parameter 'cache' should be of type number", + }, + step = { + err = "sequences[\"seq\"]: Illegal parameters, options parameter 'step' should be of type number", + }, + cycle = { + err = "sequences[\"seq\"]: Illegal parameters, options parameter 'cycle' should be of type boolean", + }, +} + +for option_key, case in pairs(sequence_invalid_option_cases) do + local test_name = ('test_sequences_invalid_option_%s_value'):format(option_key) + + g[test_name] = function() + local schema = { + spaces = {}, + sequences = { + ['seq'] = { + [option_key] = 'invalid', + }, + }, + } + + local _, err = ddl.set_schema(schema) + t.assert_str_contains(err, case.err) + end +end + +g.test_sequences_unknown_option = function() + local schema = { + spaces = {}, + sequences = { + ['seq'] = { + random = true, + }, + }, + } + + local _, err = ddl.set_schema(schema) + t.assert_str_contains(err, 'sequences["seq"]: redundant key "random"') +end diff --git a/test/db.lua b/test/db.lua index dc8d7ab..c2404b4 100644 --- a/test/db.lua +++ b/test/db.lua @@ -16,12 +16,24 @@ local function init() } end -local function drop_all() +local function drop_all_non_system_spaces() for _, space in box.space._space:pairs({box.schema.SYSTEM_ID_MAX}, {iterator = "GT"}) do box.space[space.name]:drop() end end +local function drop_all_sequences() + for _, seq in box.space._sequence:pairs() do + print(('dropping %q'):format(seq.name)) + box.sequence[seq.name]:drop() + end +end + +local function drop_all() + drop_all_non_system_spaces() + drop_all_sequences() +end + -- Check if tarantool version >= required local function v(req_major, req_minor) req_minor = req_minor or 0 diff --git a/test/get_schema_test.lua b/test/get_schema_test.lua index 7d0f871..446fc0d 100644 --- a/test/get_schema_test.lua +++ b/test/get_schema_test.lua @@ -547,7 +547,6 @@ function g.test_with_function_index() end function g.test_sequence_index() - t.skip('Not implemented yet') g.space = box.schema.space.create('with_sequence') g.space:format({ {name = 'seq_id', type = 'unsigned', is_nullable = false}, @@ -555,6 +554,7 @@ function g.test_sequence_index() {name = 'second', type = 'string', is_nullable = false}, }) + local seq_name = 'seq' local seq_opts = { start = 1, min = 0, @@ -563,37 +563,41 @@ function g.test_sequence_index() cache = 0, step = 5, } - box.schema.sequence.create('seq', seq_opts) + box.schema.sequence.create(seq_name, seq_opts) + g.space:create_index('seq_index', { type = 'TREE', unique = true, - sequence = 'seq' + sequence = seq_name, }) local res = ddl.get_schema() - local seq_info = seq_opts - seq_info.name = 'seq' - t.assert_equals(res.spaces, { - ['with_sequence'] = { - engine = 'memtx', - is_local = false, - temporary = false, - format = { - {name = 'seq_id', type = 'unsigned', is_nullable = false}, - {name = 'first', type = 'string', is_nullable = false}, - {name = 'second', type = 'string', is_nullable = false}, - }, - indexes = { - { - name = 'seq_index', - type = 'TREE', - unique = true, - parts = {{is_nullable = false, path = 'seq_id', type = 'unsigned'}}, - sequence = seq_info, + t.assert_equals(res, { + spaces = { + ['with_sequence'] = { + engine = 'memtx', + is_local = false, + temporary = false, + format = { + {name = 'seq_id', type = 'unsigned', is_nullable = false}, + {name = 'first', type = 'string', is_nullable = false}, + {name = 'second', type = 'string', is_nullable = false}, + }, + indexes = { + { + name = 'seq_index', + type = 'TREE', + unique = true, + parts = {{is_nullable = false, path = 'seq_id', type = 'unsigned'}}, + sequence = seq_name, + } } } - } + }, + sequences = { + [seq_name] = seq_opts, + }, }) end diff --git a/test/set_schema_test.lua b/test/set_schema_test.lua index dd224eb..f8fc8ff 100644 --- a/test/set_schema_test.lua +++ b/test/set_schema_test.lua @@ -117,10 +117,10 @@ function g.test_invalid_schema() 'functions: not supported' ) - local res, err = ddl.set_schema({spaces = {}, sequences = {}}) + local res, err = ddl.set_schema({spaces = {}, sequences = true}) t.assert_not(res) t.assert_equals(err, - 'sequences: not supported' + 'sequences: must be a table or nil, got boolean' ) local res, err = ddl.set_schema({spaces = {}, meta = {}}) diff --git a/test/set_schema_with_sequence_test.lua b/test/set_schema_with_sequence_test.lua new file mode 100644 index 0000000..7a86b6d --- /dev/null +++ b/test/set_schema_with_sequence_test.lua @@ -0,0 +1,250 @@ +#!/usr/bin/env tarantool + +local t = require('luatest') +local db = require('test.db') +local ddl = require('ddl') +local ddl_get = require('ddl.get') + +local g = t.group() +g.before_all(db.init) +g.before_each(db.drop_all) + +local seq_start = 1 +local seq_step = 5 + +local test_schema = { + spaces = { + ['with_sequence'] = { + engine = 'memtx', + is_local = false, + temporary = false, + format = { + {name = 'seq_id', type = 'unsigned', is_nullable = false}, + {name = 'first', type = 'string', is_nullable = false}, + {name = 'second', type = 'string', is_nullable = false}, + }, + indexes = { + { + name = 'seq_index', + type = 'TREE', + unique = true, + parts = { + {is_nullable = false, path = 'seq_id', type = 'unsigned'}, + }, + sequence = 'seq', + }, + }, + }, + }, + sequences = { + ['seq'] = { + start = seq_start, + min = 0, + max = 1000000000000ULL, + cycle = true, + cache = 0, + step = seq_step, + }, + }, +} + +local function assert_test_schema_applied() + t.assert_not_equals(box.space['with_sequence'], nil, 'space exists') + t.assert_not_equals(box.space['with_sequence'].index['seq_index'], nil, 'space index exists') + + local index_seq_id = box.space['with_sequence'].index['seq_index'].sequence_id + t.assert_type(index_seq_id, 'number', 'space index uses sequence') + + local seq = box.sequence['seq'] + t.assert_not_equals(seq, nil, 'sequence exists') + t.assert_equals(seq.start, seq_start, 'sequence is configured with proper values') + t.assert_equals(seq.min, 0, 'sequence is configured with proper values') + t.assert_equals(seq.max, 1000000000000ULL, 'sequence is configured with proper values') + t.assert_equals(seq.cycle, true, 'sequence is configured with proper values') + t.assert_equals(seq.cache, 0, 'sequence is configured with proper values') + t.assert_equals(seq.step, seq_step, 'sequence is configured with proper values') + + local index_seq = ddl_get.get_sequence_by_id(index_seq_id) + t.assert_equals(seq, index_seq, 'index uses expected sequence') +end + +local function assert_two_test_records_can_be_inserted(opts) + assert(type(opts.steps_before) == 'number') + + local start_id = seq_start + opts.steps_before * seq_step + + t.assert_equals( + box.space['with_sequence']:insert{box.NULL, 'val1', 'val2'}, + {start_id, 'val1', 'val2'}, + 'autoincrement space works fine for inserts' + ) + t.assert_equals( + box.space['with_sequence']:insert{box.NULL, 'val1', 'val2'}, + {start_id + seq_step, 'val1', 'val2'}, + 'autoincrement space works fine for inserts' + ) +end + +g.test_sequence_index_schema_applies_on_clean_instance = function() + local _, err = ddl.set_schema(test_schema) + t.assert_equals(err, nil) + + assert_test_schema_applied() + assert_two_test_records_can_be_inserted{steps_before = 0} +end + +g.test_sequence_index_schema_reapply = function() + local _, err = ddl.set_schema(test_schema) + t.assert_equals(err, nil) + + assert_test_schema_applied() + assert_two_test_records_can_be_inserted{steps_before = 0} + + local _, err = ddl.set_schema(ddl.get_schema()) + t.assert_equals(err, nil) + + assert_test_schema_applied() + assert_two_test_records_can_be_inserted{steps_before = 2} +end + +function g.test_sequence_index_schema_applies_if_same_setup_already_exists() + g.space = box.schema.space.create('with_sequence') + g.space:format({ + {name = 'seq_id', type = 'unsigned', is_nullable = false}, + {name = 'first', type = 'string', is_nullable = false}, + {name = 'second', type = 'string', is_nullable = false}, + }) + + local seq_name = 'seq' + local seq_opts = { + start = 1, + min = 0, + max = 1000000000000ULL, + cycle = true, + cache = 0, + step = 5, + } + box.schema.sequence.create(seq_name, seq_opts) + + g.space:create_index('seq_index', { + type = 'TREE', + unique = true, + parts = {{is_nullable = false, field = 'seq_id', type = 'unsigned'}}, + sequence = seq_name, + }) + + assert_two_test_records_can_be_inserted{steps_before = 0} + + local _, err = ddl.set_schema(test_schema) + t.assert_equals(err, nil) + + assert_test_schema_applied() + assert_two_test_records_can_be_inserted{steps_before = 2} +end + +function g.test_sequence_index_schema_apply_fails_if_existing_space_index_does_not_use_sequence() + g.space = box.schema.space.create('with_sequence') + g.space:format({ + {name = 'seq_id', type = 'unsigned', is_nullable = false}, + {name = 'first', type = 'string', is_nullable = false}, + {name = 'second', type = 'string', is_nullable = false}, + }) + + g.space:create_index('seq_index', { + type = 'TREE', + unique = true, + parts = {{is_nullable = false, field = 'seq_id', type = 'unsigned'}}, + -- no sequence + }) + + local _, err = ddl.set_schema(test_schema) + t.assert_str_contains(err, 'Incompatible schema: spaces["with_sequence"] //indexes/1/sequence ' .. + '(expected nil, got string)') +end + +function g.test_sequence_index_schema_apply_fails_if_existing_space_index_uses_different_sequence() + g.space = box.schema.space.create('with_sequence') + g.space:format({ + {name = 'seq_id', type = 'unsigned', is_nullable = false}, + {name = 'first', type = 'string', is_nullable = false}, + {name = 'second', type = 'string', is_nullable = false}, + }) + + box.schema.sequence.create('different_sequence') + + g.space:create_index('seq_index', { + type = 'TREE', + unique = true, + parts = {{is_nullable = false, field = 'seq_id', type = 'unsigned'}}, + sequence = 'different_sequence', + }) + + local _, err = ddl.set_schema(test_schema) + t.assert_str_contains(err, 'Incompatible schema: spaces["with_sequence"] //indexes/1/sequence ' .. + '(expected different_sequence, got seq)') +end + +function g.test_sequence_schema_apply_fails_if_existing_sequence_has_different_options() + g.space = box.schema.space.create('with_sequence') + g.space:format({ + {name = 'seq_id', type = 'unsigned', is_nullable = false}, + {name = 'first', type = 'string', is_nullable = false}, + {name = 'second', type = 'string', is_nullable = false}, + }) + + local seq_name = 'seq' + local seq_opts = { + start = 10, + min = 10, + max = 1000, + cycle = false, + cache = 0, + step = 5, + } + box.schema.sequence.create(seq_name, seq_opts) + + g.space:create_index('seq_index', { + type = 'TREE', + unique = true, + parts = {{is_nullable = false, field = 'seq_id', type = 'unsigned'}}, + sequence = seq_name, + }) + + local _, err = ddl.set_schema(test_schema) + t.assert_str_contains(err, 'Incompatible schema: sequences["seq"] max ' .. + '(expected 1000, got 1000000000000ULL)') +end + +function g.test_sequence_schema_with_defaults_apply_fails_if_existing_sequence_has_different_options() + g.space = box.schema.space.create('with_sequence') + g.space:format({ + {name = 'seq_id', type = 'unsigned', is_nullable = false}, + {name = 'first', type = 'string', is_nullable = false}, + {name = 'second', type = 'string', is_nullable = false}, + }) + + local seq_name = 'seq' + local seq_opts = { + start = 10, + min = 10, + max = 1000, + cycle = false, + cache = 0, + step = 5, + } + box.schema.sequence.create(seq_name, seq_opts) + + g.space:create_index('seq_index', { + type = 'TREE', + unique = true, + parts = {{is_nullable = false, field = 'seq_id', type = 'unsigned'}}, + sequence = seq_name, + }) + + local test_schema = table.deepcopy(test_schema) + test_schema.sequences['seq'] = {} -- Fill with defaults. + + local _, err = ddl.set_schema(test_schema) + t.assert_str_contains(err, 'Incompatible schema: sequences["seq"] max '.. + '(expected 1000, got nil and default is 9223372036854775807ULL)') +end diff --git a/test/utils_test.lua b/test/utils_test.lua index 31ad01f..88fcc8e 100644 --- a/test/utils_test.lua +++ b/test/utils_test.lua @@ -23,3 +23,26 @@ function g.test_find_first_duplicate() t.assert_equals(ddl_utils.find_first_duplicate(objects, 'key'), 4) t.assert_not(ddl_utils.find_first_duplicate(objects, 'value')) end + +function g.test_is_number() + t.assert(ddl_utils.is_number(0)) + t.assert(ddl_utils.is_number(1)) + t.assert(ddl_utils.is_number(1.213)) + t.assert(ddl_utils.is_number(-1.213)) + t.assert(ddl_utils.is_number(math.huge)) + t.assert(ddl_utils.is_number(0 / 0)) + t.assert(ddl_utils.is_number(1LL)) + t.assert(ddl_utils.is_number(1ULL)) + + t.assert_not(ddl_utils.is_number(nil)) + t.assert_not(ddl_utils.is_number(box.NULL)) + t.assert_not(ddl_utils.is_number('')) + t.assert_not(ddl_utils.is_number('1.234')) + t.assert_not(ddl_utils.is_number({1, 2,})) +end + +function g.test_concat_arrays() + t.assert(ddl_utils.concat_arrays({1}, {2, 3}), {1, 2, 3}) + t.assert(ddl_utils.concat_arrays({}, {2, 3}), {2, 3}) + t.assert(ddl_utils.concat_arrays({1}, {}), {1}) +end