• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

tarantool / crud / 6302316016

25 Sep 2023 04:58PM UTC coverage: 92.988% (-0.08%) from 93.065%
6302316016

push

github

DifferentialOrange
[WIP] api: get relevant schema

37 of 37 new or added lines in 3 files covered. (100.0%)

4575 of 4920 relevant lines covered (92.99%)

14729.26 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

86.72
/crud/common/utils.lua
1
local errors = require('errors')
383✔
2
local ffi = require('ffi')
383✔
3
local vshard = require('vshard')
383✔
4
local fun = require('fun')
383✔
5
local bit = require('bit')
383✔
6
local log = require('log')
383✔
7
local checks = require('checks')
383✔
8

9
local is_cartridge, cartridge = pcall(require, 'cartridge')
383✔
10
local is_cartridge_hotreload, cartridge_hotreload = pcall(require, 'cartridge.hotreload')
383✔
11

12
local const = require('crud.common.const')
383✔
13
local schema = require('crud.common.schema')
383✔
14
local dev_checks = require('crud.common.dev_checks')
383✔
15

16
local FlattenError = errors.new_class("FlattenError", {capture_stack = false})
383✔
17
local UnflattenError = errors.new_class("UnflattenError", {capture_stack = false})
383✔
18
local ParseOperationsError = errors.new_class('ParseOperationsError', {capture_stack = false})
383✔
19
local ShardingError = errors.new_class('ShardingError', {capture_stack = false})
383✔
20
local GetSpaceError = errors.new_class('GetSpaceError')
383✔
21
local GetSpaceFormatError = errors.new_class('GetSpaceFormatError', {capture_stack = false})
383✔
22
local FilterFieldsError = errors.new_class('FilterFieldsError', {capture_stack = false})
383✔
23
local NotInitializedError = errors.new_class('NotInitialized')
383✔
24
local StorageInfoError = errors.new_class('StorageInfoError')
383✔
25
local VshardRouterError = errors.new_class('VshardRouterError', {capture_stack = false})
383✔
26
local UtilsInternalError = errors.new_class('UtilsInternalError', {capture_stack = false})
383✔
27
local GetSchemaError = errors.new_class('GetSchemaError', {capture_stack = false})
383✔
28
local fiber = require('fiber')
383✔
29

30
local utils = {}
383✔
31

32
local space_format_cache = setmetatable({}, {__mode = 'k'})
383✔
33

34
-- copy from LuaJIT lj_char.c
35
local lj_char_bits = {
383✔
36
    0,
37
    1,  1,  1,  1,  1,  1,  1,  1,  1,  3,  3,  3,  3,  3,  1,  1,
38
    1,  1,  1,  1,  1,  1,  1,  1,  1,  1,  1,  1,  1,  1,  1,  1,
39
    2,  4,  4,  4,  4,  4,  4,  4,  4,  4,  4,  4,  4,  4,  4,  4,
40
    152,152,152,152,152,152,152,152,152,152,  4,  4,  4,  4,  4,  4,
41
    4,176,176,176,176,176,176,160,160,160,160,160,160,160,160,160,
42
    160,160,160,160,160,160,160,160,160,160,160,  4,  4,  4,  4,132,
43
    4,208,208,208,208,208,208,192,192,192,192,192,192,192,192,192,
44
    192,192,192,192,192,192,192,192,192,192,192,  4,  4,  4,  4,  1,
45
    128,128,128,128,128,128,128,128,128,128,128,128,128,128,128,128,
46
    128,128,128,128,128,128,128,128,128,128,128,128,128,128,128,128,
47
    128,128,128,128,128,128,128,128,128,128,128,128,128,128,128,128,
48
    128,128,128,128,128,128,128,128,128,128,128,128,128,128,128,128,
49
    128,128,128,128,128,128,128,128,128,128,128,128,128,128,128,128,
50
    128,128,128,128,128,128,128,128,128,128,128,128,128,128,128,128,
51
    128,128,128,128,128,128,128,128,128,128,128,128,128,128,128,128,
52
    128,128,128,128,128,128,128,128,128,128,128,128,128,128,128,128
53
}
54

55
local LJ_CHAR_IDENT = 0x80
383✔
56
local LJ_CHAR_DIGIT = 0x08
383✔
57

58
local LUA_KEYWORDS = {
383✔
59
    ['and'] = true,
60
    ['end'] = true,
61
    ['in'] = true,
62
    ['repeat'] = true,
63
    ['break'] = true,
64
    ['false'] = true,
65
    ['local'] = true,
66
    ['return'] = true,
67
    ['do'] = true,
68
    ['for'] = true,
69
    ['nil'] = true,
70
    ['then'] = true,
71
    ['else'] = true,
72
    ['function'] = true,
73
    ['not'] = true,
74
    ['true'] = true,
75
    ['elseif'] = true,
76
    ['if'] = true,
77
    ['or'] = true,
78
    ['until'] = true,
79
    ['while'] = true,
80
}
81

82
function utils.table_count(table)
383✔
83
    dev_checks("table")
4✔
84

85
    local cnt = 0
4✔
86
    for _, _ in pairs(table) do
23✔
87
        cnt = cnt + 1
15✔
88
    end
89

90
    return cnt
4✔
91
end
92

93
function utils.format_replicaset_error(replicaset_uuid, msg, ...)
383✔
94
    dev_checks("string", "string")
548✔
95

96
    return string.format(
548✔
97
        "Failed for %s: %s",
548✔
98
        replicaset_uuid,
548✔
99
        string.format(msg, ...)
548✔
100
    )
548✔
101
end
102

103
local function get_replicaset_by_replica_uuid(replicasets, uuid)
104
    for replicaset_uuid, replicaset in pairs(replicasets) do
54✔
105
        for replica_uuid, _ in pairs(replicaset.replicas) do
108✔
106
            if replica_uuid == uuid then
54✔
107
                return replicasets[replicaset_uuid]
18✔
108
            end
109
        end
110
    end
111

112
    return nil
×
113
end
114

115
function utils.get_spaces(vshard_router, timeout, replica_uuid)
383✔
116
    local replicasets, replicaset
117
    timeout = timeout or const.DEFAULT_VSHARD_CALL_TIMEOUT
139,230✔
118
    local deadline = fiber.clock() + timeout
278,460✔
119
    while (
120
        -- Break if the deadline condition is exceeded.
121
        -- Handling for deadline errors are below in the code.
122
        fiber.clock() < deadline
278,460✔
123
    ) do
139,230✔
124
        -- Try to get master with timeout.
125
        fiber.yield()
139,230✔
126
        replicasets = vshard_router:routeall()
278,460✔
127
        if replica_uuid ~= nil then
139,230✔
128
            -- Get the same replica on which the last DML operation was performed.
129
            -- This approach is temporary and is related to [1], [2].
130
            -- [1] https://github.com/tarantool/crud/issues/236
131
            -- [2] https://github.com/tarantool/crud/issues/361
132
            replicaset = get_replicaset_by_replica_uuid(replicasets, replica_uuid)
36✔
133
            break
18✔
134
        else
135
            replicaset = select(2, next(replicasets))
139,212✔
136
        end
137
        if replicaset ~= nil and
139,212✔
138
           replicaset.master ~= nil and
139,212✔
139
           replicaset.master.conn.error == nil then
139,212✔
140
            break
139,212✔
141
        end
142
    end
143

144
    if replicaset == nil then
139,230✔
145
        return nil, GetSpaceError:new(
×
146
            'The router returned empty replicasets: ' ..
×
147
            'perhaps other instances are unavailable or you have configured only the router')
×
148
    end
149

150
    if replicaset.master == nil then
139,230✔
151
        local error_msg = string.format(
×
152
            'The master was not found in replicaset %s, ' ..
×
153
            'check status of the master and repeat the operation later',
154
             replicaset.uuid)
×
155
        return nil, GetSpaceError:new(error_msg)
×
156
    end
157

158
    if replicaset.master.conn.error ~= nil then
139,230✔
159
        local error_msg = string.format(
×
160
            'The connection to the master of replicaset %s is not valid: %s',
161
             replicaset.uuid, replicaset.master.conn.error)
×
162
        return nil, GetSpaceError:new(error_msg)
×
163
    end
164

165
    return replicaset.master.conn.space, nil, replicaset.master.conn.schema_version
139,230✔
166
end
167

168
function utils.get_space(space_name, vshard_router, timeout, replica_uuid)
383✔
169
    local spaces, err, schema_version = utils.get_spaces(vshard_router, timeout, replica_uuid)
139,228✔
170
    if spaces == nil then
139,228✔
171
        return nil, err
×
172
    end
173
    return spaces[space_name], nil, schema_version
139,228✔
174
end
175

176
function utils.get_space_format(space_name, vshard_router)
383✔
177
    local space, err = utils.get_space(space_name, vshard_router)
6,601✔
178
    if err ~= nil then
6,601✔
179
        return nil, GetSpaceFormatError:new("An error occurred during the operation: %s", err)
×
180
    end
181
    if space == nil then
6,601✔
182
        return nil, GetSpaceFormatError:new("Space %q doesn't exist", space_name)
298✔
183
    end
184

185
    local space_format = space:format()
6,452✔
186

187
    return space_format
6,452✔
188
end
189

190
function utils.fetch_latest_metadata_when_single_storage(space, space_name, netbox_schema_version,
383✔
191
                                                         vshard_router, opts, storage_info)
192
    -- Checking the relevance of the schema version is necessary
193
    -- to prevent the irrelevant metadata of the DML operation.
194
    -- This approach is temporary and is related to [1], [2].
195
    -- [1] https://github.com/tarantool/crud/issues/236
196
    -- [2] https://github.com/tarantool/crud/issues/361
197
    local latest_space, err
198
    assert(storage_info.replica_schema_version ~= nil,
36✔
199
           'check the replica_schema_version value from storage ' ..
18✔
200
           'for correct use of the fetch_latest_metadata opt')
18✔
201
    assert(storage_info.replica_uuid ~= nil,
36✔
202
           'check the replica_uuid value from storage ' ..
18✔
203
           'for correct use of the fetch_latest_metadata opt')
18✔
204
    assert(netbox_schema_version ~= nil,
36✔
205
           'check the netbox_schema_version value from net_box conn on router ' ..
18✔
206
           'for correct use of the fetch_latest_metadata opt')
18✔
207
    if storage_info.replica_schema_version ~= netbox_schema_version then
18✔
208
        local ok, reload_schema_err = schema.reload_schema(vshard_router)
18✔
209
        if ok then
18✔
210
            latest_space, err = utils.get_space(space_name, vshard_router,
36✔
211
                                                opts.timeout, storage_info.replica_uuid)
36✔
212
            if err ~= nil then
18✔
213
                local warn_msg = "Failed to fetch space for latest schema actualization, metadata may be outdated: %s"
×
214
                log.warn(warn_msg, err)
×
215
            end
216
            if latest_space == nil then
18✔
217
                log.warn("Failed to find space for latest schema actualization, metadata may be outdated")
×
218
            end
219
        else
220
            log.warn("Failed to reload schema, metadata may be outdated: %s", reload_schema_err)
×
221
        end
222
    end
223
    if err == nil and latest_space ~= nil then
18✔
224
        space = latest_space
18✔
225
    end
226

227
    return space
18✔
228
end
229

230
function utils.fetch_latest_metadata_when_map_storages(space, space_name, vshard_router, opts,
383✔
231
                                                       storages_info, netbox_schema_version)
232
    -- Checking the relevance of the schema version is necessary
233
    -- to prevent the irrelevant metadata of the DML operation.
234
    -- This approach is temporary and is related to [1], [2].
235
    -- [1] https://github.com/tarantool/crud/issues/236
236
    -- [2] https://github.com/tarantool/crud/issues/361
237
    local latest_space, err
238
    for _, storage_info in pairs(storages_info) do
32✔
239
        assert(storage_info.replica_schema_version ~= nil,
32✔
240
            'check the replica_schema_version value from storage ' ..
16✔
241
            'for correct use of the fetch_latest_metadata opt')
16✔
242
        assert(netbox_schema_version ~= nil,
32✔
243
               'check the netbox_schema_version value from net_box conn on router ' ..
16✔
244
               'for correct use of the fetch_latest_metadata opt')
16✔
245
        if storage_info.replica_schema_version ~= netbox_schema_version then
16✔
246
            local ok, reload_schema_err = schema.reload_schema(vshard_router)
16✔
247
            if ok then
16✔
248
                latest_space, err = utils.get_space(space_name, vshard_router, opts.timeout)
32✔
249
                if err ~= nil then
16✔
250
                    local warn_msg = "Failed to fetch space for latest schema actualization, " ..
×
251
                                     "metadata may be outdated: %s"
252
                    log.warn(warn_msg, err)
×
253
                end
254
                if latest_space == nil then
16✔
255
                    log.warn("Failed to find space for latest schema actualization, metadata may be outdated")
×
256
                end
257
            else
258
                log.warn("Failed to reload schema, metadata may be outdated: %s", reload_schema_err)
×
259
            end
260
            if err == nil and latest_space ~= nil then
16✔
261
                space = latest_space
16✔
262
            end
263
            break
16✔
264
        end
265
    end
266

267
    return space
16✔
268
end
269

270
function utils.fetch_latest_metadata_for_select(space_name, vshard_router, opts,
383✔
271
                                                storages_info, iter)
272
    -- Checking the relevance of the schema version is necessary
273
    -- to prevent the irrelevant metadata of the DML operation.
274
    -- This approach is temporary and is related to [1], [2].
275
    -- [1] https://github.com/tarantool/crud/issues/236
276
    -- [2] https://github.com/tarantool/crud/issues/361
277
    for _, storage_info in pairs(storages_info) do
8✔
278
        assert(storage_info.replica_schema_version ~= nil,
8✔
279
               'check the replica_schema_version value from storage ' ..
4✔
280
               'for correct use of the fetch_latest_metadata opt')
4✔
281
        assert(iter.netbox_schema_version ~= nil,
8✔
282
               'check the netbox_schema_version value from net_box conn on router ' ..
4✔
283
               'for correct use of the fetch_latest_metadata opt')
4✔
284
        if storage_info.replica_schema_version ~= iter.netbox_schema_version then
4✔
285
            local ok, reload_schema_err = schema.reload_schema(vshard_router)
4✔
286
            if ok then
4✔
287
                local err
288
                iter.space, err = utils.get_space(space_name, vshard_router, opts.timeout)
8✔
289
                if err ~= nil then
4✔
290
                    local warn_msg = "Failed to fetch space for latest schema actualization, " ..
×
291
                                     "metadata may be outdated: %s"
292
                    log.warn(warn_msg, err)
×
293
                end
294
            else
295
                log.warn("Failed to reload schema, metadata may be outdated: %s", reload_schema_err)
×
296
            end
297
            break
298
        end
299
    end
300

301
    return iter
4✔
302
end
303

304
local function append(lines, s, ...)
305
    table.insert(lines, string.format(s, ...))
4,000✔
306
end
307

308
local flatten_functions_cache = setmetatable({}, {__mode = 'k'})
383✔
309

310
function utils.flatten(object, space_format, bucket_id, skip_nullability_check)
383✔
311
    local flatten_func = flatten_functions_cache[space_format]
6,544✔
312
    if flatten_func ~= nil then
6,544✔
313
        local data, err = flatten_func(object, bucket_id, skip_nullability_check)
6,418✔
314
        if err ~= nil then
6,418✔
315
            return nil, FlattenError:new(err)
1,000✔
316
        end
317
        return data
5,918✔
318
    end
319

320
    local lines = {}
126✔
321
    append(lines, 'local object, bucket_id, skip_nullability_check = ...')
126✔
322

323
    append(lines, 'for k in pairs(object) do')
126✔
324
    append(lines, '    if fieldmap[k] == nil then')
126✔
325
    append(lines, '        return nil, format(\'Unknown field %%q is specified\', k)')
126✔
326
    append(lines, '    end')
126✔
327
    append(lines, 'end')
126✔
328

329
    local len = #space_format
126✔
330
    append(lines, 'local result = {%s}', string.rep('NULL,', len))
126✔
331

332
    local fieldmap = {}
126✔
333

334
    for i, field in ipairs(space_format) do
752✔
335
        fieldmap[field.name] = true
626✔
336
        if field.name ~= 'bucket_id' then
626✔
337
            append(lines, 'if object[%q] ~= nil then', field.name)
500✔
338
            append(lines, '    result[%d] = object[%q]', i, field.name)
500✔
339
            if field.is_nullable ~= true then
500✔
340
                append(lines, 'elseif skip_nullability_check ~= true then')
431✔
341
                append(lines, '    return nil, \'Field %q isn\\\'t nullable' ..
862✔
342
                              ' (set skip_nullability_check_on_flatten option to true to skip check)\'',
431✔
343
                              field.name)
431✔
344
            end
345
            append(lines, 'end')
1,000✔
346
        else
347
            append(lines, 'if bucket_id ~= nil then')
126✔
348
            append(lines, '    result[%d] = bucket_id', i, field.name)
126✔
349
            append(lines, 'else')
126✔
350
            append(lines, '    result[%d] = object[%q]', i, field.name)
126✔
351
            append(lines, 'end')
126✔
352
        end
353
    end
354
    append(lines, 'return result')
126✔
355

356
    local code = table.concat(lines, '\n')
126✔
357
    local env = {
126✔
358
        pairs = pairs,
126✔
359
        format = string.format,
126✔
360
        fieldmap = fieldmap,
126✔
361
        NULL = box.NULL,
126✔
362
    }
363
    flatten_func = assert(load(code, nil, 't', env))
126✔
364

365
    flatten_functions_cache[space_format] = flatten_func
126✔
366
    local data, err = flatten_func(object, bucket_id, skip_nullability_check)
126✔
367
    if err ~= nil then
126✔
368
        return nil, FlattenError:new(err)
12✔
369
    end
370
    return data
120✔
371
end
372

373
function utils.unflatten(tuple, space_format)
383✔
374
    if tuple == nil then return nil end
5,836✔
375

376
    local object = {}
5,836✔
377

378
    for fieldno, field_format in ipairs(space_format) do
39,565✔
379
        local value = tuple[fieldno]
33,730✔
380

381
        if not field_format.is_nullable and value == nil then
33,730✔
382
            return nil, UnflattenError:new("Field %s isn't nullable", fieldno)
2✔
383
        end
384

385
        object[field_format.name] = value
33,729✔
386
    end
387

388
    return object
5,835✔
389
end
390

391
function utils.extract_key(tuple, key_parts)
383✔
392
    local key = {}
260,423✔
393
    for i, part in ipairs(key_parts) do
521,566✔
394
        key[i] = tuple[part.fieldno]
261,143✔
395
    end
396
    return key
260,423✔
397
end
398

399
function utils.merge_primary_key_parts(key_parts, pk_parts)
383✔
400
    local merged_parts = {}
37,080✔
401
    local key_fieldnos = {}
37,080✔
402

403
    for _, part in ipairs(key_parts) do
74,379✔
404
        table.insert(merged_parts, part)
37,299✔
405
        key_fieldnos[part.fieldno] = true
37,299✔
406
    end
407

408
    for _, pk_part in ipairs(pk_parts) do
83,607✔
409
        if not key_fieldnos[pk_part.fieldno] then
46,527✔
410
            table.insert(merged_parts, pk_part)
25,953✔
411
        end
412
    end
413

414
    return merged_parts
37,080✔
415
end
416

417
function utils.enrich_field_names_with_cmp_key(field_names, key_parts, space_format)
383✔
418
    if field_names == nil then
26,277✔
419
        return nil
26,192✔
420
    end
421

422
    local enriched_field_names = {}
85✔
423
    local key_field_names = {}
85✔
424

425
    for _, field_name in ipairs(field_names) do
251✔
426
        table.insert(enriched_field_names, field_name)
166✔
427
        key_field_names[field_name] = true
166✔
428
    end
429

430
    for _, part in ipairs(key_parts) do
223✔
431
        local field_name = space_format[part.fieldno].name
138✔
432
        if not key_field_names[field_name] then
138✔
433
            table.insert(enriched_field_names, field_name)
108✔
434
            key_field_names[field_name] = true
108✔
435
        end
436
    end
437

438
    return enriched_field_names
85✔
439
end
440

441

442
local function get_version_suffix(suffix_candidate)
443
    if type(suffix_candidate) ~= 'string' then
844✔
444
        return nil
×
445
    end
446

447
    if suffix_candidate:find('^entrypoint$')
844✔
448
    or suffix_candidate:find('^alpha%d$')
844✔
449
    or suffix_candidate:find('^beta%d$')
843✔
450
    or suffix_candidate:find('^rc%d$') then
841✔
451
        return suffix_candidate
7✔
452
    end
453

454
    return nil
837✔
455
end
456

457
utils.get_version_suffix = get_version_suffix
383✔
458

459

460
local suffix_with_digit_weight = {
383✔
461
    alpha = -3000,
462
    beta  = -2000,
463
    rc    = -1000,
464
}
465

466
local function get_version_suffix_weight(suffix)
467
    if suffix == nil then
4,834✔
468
        return 0
4,791✔
469
    end
470

471
    if suffix:find('^entrypoint$') then
43✔
472
        return -math.huge
9✔
473
    end
474

475
    for header, weight in pairs(suffix_with_digit_weight) do
100✔
476
        local pos, _, digits = suffix:find('^' .. header .. '(%d)$')
64✔
477
        if pos ~= nil then
64✔
478
            return weight + tonumber(digits)
32✔
479
        end
480
    end
481

482
    UtilsInternalError:assert(false,
4✔
483
        'Unexpected suffix %q, parse with "utils.get_version_suffix" first', suffix)
2✔
484
end
485

486
utils.get_version_suffix_weight = get_version_suffix_weight
383✔
487

488

489
local function is_version_ge(major, minor,
490
                             patch, suffix,
491
                             major_to_compare, minor_to_compare,
492
                             patch_to_compare, suffix_to_compare)
493
    major = major or 0
2,412✔
494
    minor = minor or 0
2,412✔
495
    patch = patch or 0
2,412✔
496
    local suffix_weight = get_version_suffix_weight(suffix)
2,412✔
497

498
    major_to_compare = major_to_compare or 0
2,412✔
499
    minor_to_compare = minor_to_compare or 0
2,412✔
500
    patch_to_compare = patch_to_compare or 0
2,412✔
501
    local suffix_weight_to_compare = get_version_suffix_weight(suffix_to_compare)
2,412✔
502

503
    if major > major_to_compare then return true end
2,412✔
504
    if major < major_to_compare then return false end
2,399✔
505

506
    if minor > minor_to_compare then return true end
2,388✔
507
    if minor < minor_to_compare then return false end
17✔
508

509
    if patch > patch_to_compare then return true end
12✔
510
    if patch < patch_to_compare then return false end
11✔
511

512
    if suffix_weight > suffix_weight_to_compare then return true end
10✔
513
    if suffix_weight < suffix_weight_to_compare then return false end
7✔
514

515
    return true
4✔
516
end
517

518
utils.is_version_ge = is_version_ge
383✔
519

520

521
local function is_version_in_range(major, minor,
522
                                   patch, suffix,
523
                                   major_left_side, minor_left_side,
524
                                   patch_left_side, suffix_left_side,
525
                                   major_right_side, minor_right_side,
526
                                   patch_right_side, suffix_right_side)
527
    return is_version_ge(major, minor,
4✔
528
                         patch, suffix,
2✔
529
                         major_left_side, minor_left_side,
2✔
530
                         patch_left_side, suffix_left_side)
2✔
531
       and is_version_ge(major_right_side, minor_right_side,
4✔
532
                         patch_right_side, suffix_right_side,
2✔
533
                         major, minor,
2✔
534
                         patch, suffix)
4✔
535
end
536

537
utils.is_version_in_range = is_version_in_range
383✔
538

539

540
local function get_tarantool_version()
541
    local version_parts = rawget(_G, '_TARANTOOL'):split('-', 1)
835✔
542

543
    local major_minor_patch_parts = version_parts[1]:split('.', 2)
835✔
544
    local major = tonumber(major_minor_patch_parts[1])
835✔
545
    local minor = tonumber(major_minor_patch_parts[2])
835✔
546
    local patch = tonumber(major_minor_patch_parts[3])
835✔
547

548
    local suffix = get_version_suffix(version_parts[2])
835✔
549

550
    return major, minor, patch, suffix
835✔
551
end
552

553
utils.get_tarantool_version = get_tarantool_version
383✔
554

555

556
local function tarantool_version_at_least(wanted_major, wanted_minor, wanted_patch)
557
    local major, minor, patch, suffix = get_tarantool_version()
451✔
558

559
    return is_version_ge(major, minor, patch, suffix,
451✔
560
                         wanted_major, wanted_minor, wanted_patch, nil)
451✔
561
end
562

563
utils.tarantool_version_at_least = tarantool_version_at_least
383✔
564

565

566
local enabled_tarantool_features = {}
383✔
567

568
local function determine_enabled_features()
569
    local major, minor, patch, suffix = get_tarantool_version()
383✔
570

571
    -- since Tarantool 2.3.1
572
    enabled_tarantool_features.fieldpaths = is_version_ge(major, minor, patch, suffix,
766✔
573
                                                          2, 3, 1, nil)
766✔
574

575
    -- since Tarantool 2.4.1
576
    enabled_tarantool_features.uuids = is_version_ge(major, minor, patch, suffix,
766✔
577
                                                     2, 4, 1, nil)
766✔
578

579
    -- since Tarantool 2.6.3 / 2.7.2 / 2.8.1
580
    enabled_tarantool_features.jsonpath_indexes = is_version_ge(major, minor, patch, suffix,
766✔
581
                                                                2, 8, 1, nil)
383✔
582
                                               or is_version_in_range(major, minor, patch, suffix,
383✔
583
                                                                      2, 7, 2, nil,
584
                                                                      2, 7, math.huge, nil)
×
585
                                               or is_version_in_range(major, minor, patch, suffix,
×
586
                                                                      2, 6, 3, nil,
587
                                                                      2, 6, math.huge, nil)
383✔
588

589
    -- The merger module was implemented in 2.2.1, see [1].
590
    -- However it had the critical problem [2], which leads to
591
    -- segfault at attempt to use the module from a fiber serving
592
    -- iproto request. So we don't use it in versions before the
593
    -- fix.
594
    --
595
    -- [1]: https://github.com/tarantool/tarantool/issues/3276
596
    -- [2]: https://github.com/tarantool/tarantool/issues/4954
597
    enabled_tarantool_features.builtin_merger = is_version_ge(major, minor, patch, suffix,
766✔
598
                                                              2, 6, 0, nil)
383✔
599
                                             or is_version_in_range(major, minor, patch, suffix,
383✔
600
                                                                    2, 5, 1, nil,
601
                                                                    2, 5, math.huge, nil)
×
602
                                             or is_version_in_range(major, minor, patch, suffix,
×
603
                                                                    2, 4, 2, nil,
604
                                                                    2, 4, math.huge, nil)
×
605
                                             or is_version_in_range(major, minor, patch, suffix,
×
606
                                                                    2, 3, 3, nil,
607
                                                                    2, 3, math.huge, nil)
383✔
608

609
    -- The external merger module leans on a set of relatively
610
    -- new APIs in tarantool. So it works only on tarantool
611
    -- versions, which offer those APIs.
612
    --
613
    -- See README of the module:
614
    -- https://github.com/tarantool/tuple-merger
615
    enabled_tarantool_features.external_merger = is_version_ge(major, minor, patch, suffix,
766✔
616
                                                               2, 7, 0, nil)
383✔
617
                                              or is_version_in_range(major, minor, patch, suffix,
383✔
618
                                                                     2, 6, 1, nil,
619
                                                                     2, 6, math.huge, nil)
×
620
                                              or is_version_in_range(major, minor, patch, suffix,
×
621
                                                                     2, 5, 2, nil,
622
                                                                     2, 5, math.huge, nil)
×
623
                                              or is_version_in_range(major, minor, patch, suffix,
×
624
                                                                     2, 4, 3, nil,
625
                                                                     2, 4, math.huge, nil)
×
626
                                              or is_version_in_range(major, minor, patch, suffix,
×
627
                                                                     1, 10, 8, nil,
628
                                                                     1, 10, math.huge, nil)
383✔
629
end
630

631
function utils.tarantool_supports_fieldpaths()
383✔
632
    if enabled_tarantool_features.fieldpaths == nil then
906✔
633
        determine_enabled_features()
×
634
    end
635

636
    return enabled_tarantool_features.fieldpaths
906✔
637
end
638

639
function utils.tarantool_supports_uuids()
383✔
640
    if enabled_tarantool_features.uuids == nil then
55✔
641
        determine_enabled_features()
×
642
    end
643

644
    return enabled_tarantool_features.uuids
55✔
645
end
646

647
function utils.tarantool_supports_jsonpath_indexes()
383✔
648
    if enabled_tarantool_features.jsonpath_indexes == nil then
55✔
649
        determine_enabled_features()
×
650
    end
651

652
    return enabled_tarantool_features.jsonpath_indexes
55✔
653
end
654

655
function utils.tarantool_has_builtin_merger()
383✔
656
    if enabled_tarantool_features.builtin_merger == nil then
404✔
657
        determine_enabled_features()
×
658
    end
659

660
    return enabled_tarantool_features.builtin_merger
404✔
661
end
662

663
function utils.tarantool_supports_external_merger()
383✔
664
    if enabled_tarantool_features.external_merger == nil then
404✔
665
        determine_enabled_features()
383✔
666
    end
667

668
    return enabled_tarantool_features.external_merger
404✔
669
end
670

671
local function add_nullable_fields_recursive(operations, operations_map, space_format, tuple, id)
672
    if id < 2 or tuple[id - 1] ~= box.NULL then
×
673
        return operations
×
674
    end
675

676
    if space_format[id - 1].is_nullable and not operations_map[id - 1] then
×
677
        table.insert(operations, {'=', id - 1, box.NULL})
×
678
        return add_nullable_fields_recursive(operations, operations_map, space_format, tuple, id - 1)
×
679
    end
680

681
    return operations
×
682
end
683

684
-- Tarantool < 2.1 has no fields `box.error.NO_SUCH_FIELD_NO` and `box.error.NO_SUCH_FIELD_NAME`.
685
if tarantool_version_at_least(2, 1, 0, nil) then
766✔
686
    function utils.is_field_not_found(err_code)
383✔
687
        return err_code == box.error.NO_SUCH_FIELD_NO or err_code == box.error.NO_SUCH_FIELD_NAME
36✔
688
    end
689
else
690
    function utils.is_field_not_found(err_code)
×
691
        return err_code == box.error.NO_SUCH_FIELD
×
692
    end
693
end
694

695
local function get_operations_map(operations)
696
    local map = {}
×
697
    for _, operation in ipairs(operations) do
×
698
        map[operation[2]] = true
×
699
    end
700

701
    return map
×
702
end
703

704
function utils.add_intermediate_nullable_fields(operations, space_format, tuple)
383✔
705
    if tuple == nil then
2✔
706
        return operations
×
707
    end
708

709
    -- If tarantool doesn't supports the fieldpaths, we already
710
    -- have converted operations (see this function call in update.lua)
711
    if utils.tarantool_supports_fieldpaths() then
4✔
712
        local formatted_operations, err = utils.convert_operations(operations, space_format)
2✔
713
        if err ~= nil then
2✔
714
            return operations
2✔
715
        end
716

717
        operations = formatted_operations
×
718
    end
719

720
    -- We need this map to check if there is a field update
721
    -- operation with constant complexity
722
    local operations_map = get_operations_map(operations)
×
723
    for _, operation in ipairs(operations) do
×
724
        operations = add_nullable_fields_recursive(
×
725
            operations, operations_map,
726
            space_format, tuple, operation[2]
×
727
        )
728
    end
729

730
    table.sort(operations, function(v1, v2) return v1[2] < v2[2] end)
×
731
    return operations
×
732
end
733

734
function utils.convert_operations(user_operations, space_format)
383✔
735
    local converted_operations = {}
2✔
736

737
    for _, operation in ipairs(user_operations) do
2✔
738
        if type(operation[2]) == 'string' then
2✔
739
            local field_id
740
            for fieldno, field_format in ipairs(space_format) do
10✔
741
                if field_format.name == operation[2] then
8✔
742
                    field_id = fieldno
×
743
                    break
744
                end
745
            end
746

747
            if field_id == nil then
2✔
748
                return nil, ParseOperationsError:new(
4✔
749
                        "Space format doesn't contain field named %q", operation[2])
4✔
750
            end
751

752
            table.insert(converted_operations, {
×
753
                operation[1], field_id, operation[3]
×
754
            })
755
        else
756
            table.insert(converted_operations, operation)
×
757
        end
758
    end
759

760
    return converted_operations
×
761
end
762

763
function utils.unflatten_rows(rows, metadata)
383✔
764
    if metadata == nil then
5,344✔
765
        return nil, UnflattenError:new('Metadata is not provided')
×
766
    end
767

768
    local result = table.new(#rows, 0)
5,344✔
769
    local err
770
    for i, row in ipairs(rows) do
11,088✔
771
        result[i], err = utils.unflatten(row, metadata)
11,488✔
772
        if err ~= nil then
5,744✔
773
            return nil, err
×
774
        end
775
    end
776
    return result
5,344✔
777
end
778

779
local inverted_tarantool_iters = {
383✔
780
    [box.index.EQ] = box.index.REQ,
383✔
781
    [box.index.GT] = box.index.LT,
383✔
782
    [box.index.GE] = box.index.LE,
383✔
783
    [box.index.LT] = box.index.GT,
383✔
784
    [box.index.LE] = box.index.GE,
383✔
785
    [box.index.REQ] = box.index.EQ,
383✔
786
}
787

788
function utils.invert_tarantool_iter(iter)
383✔
789
    local inverted_iter = inverted_tarantool_iters[iter]
44✔
790
    assert(inverted_iter ~= nil, "Unsupported Tarantool iterator: " .. tostring(iter))
44✔
791
    return inverted_iter
44✔
792
end
793

794
function utils.reverse_inplace(t)
383✔
795
    for i = 1,math.floor(#t / 2) do
91✔
796
        t[i], t[#t - i + 1] = t[#t - i + 1], t[i]
43✔
797
    end
798
    return t
48✔
799
end
800

801
function utils.get_bucket_id_fieldno(space, shard_index_name)
383✔
802
    shard_index_name = shard_index_name or 'bucket_id'
260,735✔
803
    local bucket_id_index = space.index[shard_index_name]
260,735✔
804
    if bucket_id_index == nil then
260,735✔
805
        return nil, ShardingError:new('%q index is not found', shard_index_name)
24✔
806
    end
807

808
    return bucket_id_index.parts[1].fieldno
260,723✔
809
end
810

811
-- Build a map with field number as a keys and part number
812
-- as a values using index parts as a source.
813
function utils.get_index_fieldno_map(index_parts)
383✔
814
    dev_checks('table')
87✔
815

816
    local fieldno_map = {}
87✔
817
    for i, part in ipairs(index_parts) do
236✔
818
        local fieldno = part.fieldno
149✔
819
        fieldno_map[fieldno] = i
149✔
820
    end
821

822
    return fieldno_map
87✔
823
end
824

825
-- Build a map with field names as a keys and fieldno's
826
-- as a values using space format as a source.
827
function utils.get_format_fieldno_map(space_format)
383✔
828
    dev_checks('table')
26,851✔
829

830
    local fieldno_map = {}
26,851✔
831
    for fieldno, field_format in ipairs(space_format) do
135,665✔
832
        fieldno_map[field_format.name] = fieldno
108,814✔
833
    end
834

835
    return fieldno_map
26,851✔
836
end
837

838
local uuid_t = ffi.typeof('struct tt_uuid')
383✔
839
function utils.is_uuid(value)
383✔
840
    return ffi.istype(uuid_t, value)
5✔
841
end
842

843
local function get_field_format(space_format, field_name)
844
    dev_checks('table', 'string')
466✔
845

846
    local metadata = space_format_cache[space_format]
466✔
847
    if metadata ~= nil then
466✔
848
        return metadata[field_name]
446✔
849
    end
850

851
    space_format_cache[space_format] = {}
20✔
852
    for _, field in ipairs(space_format) do
134✔
853
        space_format_cache[space_format][field.name] = field
114✔
854
    end
855

856
    return space_format_cache[space_format][field_name]
20✔
857
end
858

859
local function filter_format_fields(space_format, field_names)
860
    dev_checks('table', 'table')
182✔
861

862
    local filtered_space_format = {}
182✔
863

864
    for i, field_name in ipairs(field_names) do
618✔
865
        filtered_space_format[i] = get_field_format(space_format, field_name)
932✔
866
        if filtered_space_format[i] == nil then
466✔
867
            return nil, FilterFieldsError:new(
60✔
868
                    'Space format doesn\'t contain field named %q', field_name
30✔
869
            )
60✔
870
        end
871
    end
872

873
    return filtered_space_format
152✔
874
end
875

876
function utils.get_fields_format(space_format, field_names)
383✔
877
    dev_checks('table', '?table')
25,917✔
878

879
    if field_names == nil then
25,917✔
880
        return table.copy(space_format)
25,853✔
881
    end
882

883
    local filtered_space_format, err = filter_format_fields(space_format, field_names)
64✔
884

885
    if err ~= nil then
64✔
886
        return nil, err
2✔
887
    end
888

889
    return filtered_space_format
62✔
890
end
891

892
function utils.format_result(rows, space, field_names)
383✔
893
    local result = {}
93,811✔
894
    local err
895
    local space_format = space:format()
93,811✔
896
    result.rows = rows
93,811✔
897

898
    if field_names == nil then
93,811✔
899
        result.metadata = table.copy(space_format)
187,386✔
900
        return result
93,693✔
901
    end
902

903
    result.metadata, err = filter_format_fields(space_format, field_names)
236✔
904

905
    if err ~= nil then
118✔
906
        return nil, err
28✔
907
    end
908

909
    return result
90✔
910
end
911

912
local function truncate_tuple_metadata(tuple_metadata, field_names)
913
    dev_checks('?table', 'table')
31✔
914

915
    if tuple_metadata == nil then
31✔
916
        return nil
3✔
917
    end
918

919
    local truncated_metadata = {}
28✔
920

921
    if #tuple_metadata < #field_names then
28✔
922
        return nil, FilterFieldsError:new(
×
923
                'Field names don\'t match to tuple metadata'
924
        )
925
    end
926

927
    for i, name in ipairs(field_names) do
79✔
928
        if tuple_metadata[i].name ~= name then
53✔
929
            return nil, FilterFieldsError:new(
4✔
930
                    'Field names don\'t match to tuple metadata'
931
            )
4✔
932
        end
933

934
        table.insert(truncated_metadata, tuple_metadata[i])
51✔
935
    end
936

937
    return truncated_metadata
26✔
938
end
939

940
function utils.cut_objects(objs, field_names)
383✔
941
    dev_checks('table', 'table')
5✔
942

943
    for i, obj in ipairs(objs) do
20✔
944
        objs[i] = schema.filter_obj_fields(obj, field_names)
30✔
945
    end
946

947
    return objs
5✔
948
end
949

950
function utils.cut_rows(rows, metadata, field_names)
383✔
951
    dev_checks('table', '?table', 'table')
31✔
952

953
    local truncated_metadata, err = truncate_tuple_metadata(metadata, field_names)
31✔
954

955
    if err ~= nil then
31✔
956
        return nil, err
2✔
957
    end
958

959
    for i, row in ipairs(rows) do
72✔
960
        rows[i] = schema.truncate_row_trailing_fields(row, field_names)
86✔
961
    end
962

963
    return {
29✔
964
        metadata = truncated_metadata,
29✔
965
        rows = rows,
29✔
966
    }
29✔
967
end
968

969
local function flatten_obj(vshard_router, space_name, obj, skip_nullability_check)
970
    local space_format, err = utils.get_space_format(space_name, vshard_router)
6,601✔
971
    if err ~= nil then
6,601✔
972
        return nil, FlattenError:new("Failed to get space format: %s", err), const.NEED_SCHEMA_RELOAD
298✔
973
    end
974

975
    local tuple, err = utils.flatten(obj, space_format, nil, skip_nullability_check)
6,452✔
976
    if err ~= nil then
6,452✔
977
        return nil, FlattenError:new("Object is specified in bad format: %s", err), const.NEED_SCHEMA_RELOAD
1,008✔
978
    end
979

980
    return tuple
5,948✔
981
end
982

983
function utils.flatten_obj_reload(vshard_router, space_name, obj, skip_nullability_check)
383✔
984
    return schema.wrap_func_reload(vshard_router, flatten_obj, space_name, obj, skip_nullability_check)
6,268✔
985
end
986

987
-- Merge two options map.
988
--
989
-- `opts_a` and/or `opts_b` can be `nil`.
990
--
991
-- If `opts_a.foo` and `opts_b.foo` exists, prefer `opts_b.foo`.
992
function utils.merge_options(opts_a, opts_b)
383✔
993
    return fun.chain(opts_a or {}, opts_b or {}):tomap()
11,592✔
994
end
995

996
local function lj_char_isident(n)
997
    return bit.band(lj_char_bits[n + 2], LJ_CHAR_IDENT) == LJ_CHAR_IDENT
7,071✔
998
end
999

1000
local function lj_char_isdigit(n)
1001
    return bit.band(lj_char_bits[n + 2], LJ_CHAR_DIGIT) == LJ_CHAR_DIGIT
456✔
1002
end
1003

1004
function utils.check_name_isident(name)
383✔
1005
    dev_checks('string')
457✔
1006

1007
    -- sharding function name cannot
1008
    -- be equal to lua keyword
1009
    if LUA_KEYWORDS[name] then
457✔
1010
        return false
1✔
1011
    end
1012

1013
    -- sharding function name cannot
1014
    -- begin with a digit
1015
    local char_number = string.byte(name:sub(1,1))
912✔
1016
    if lj_char_isdigit(char_number) then
912✔
1017
        return false
1✔
1018
    end
1019

1020
    -- sharding func name must be sequence
1021
    -- of letters, digits, or underscore symbols
1022
    for i = 1, #name do
7,525✔
1023
        local char_number = string.byte(name:sub(i,i))
14,142✔
1024
        if not lj_char_isident(char_number) then
14,142✔
1025
            return false
1✔
1026
        end
1027
    end
1028

1029
    return true
454✔
1030
end
1031

1032
function utils.update_storage_call_error_description(err, func_name, replicaset_uuid)
383✔
1033
    if err == nil then
582✔
1034
        return nil
×
1035
    end
1036

1037
    if err.type == 'ClientError' and type(err.message) == 'string' then
1,427✔
1038
        if err.message == string.format("Procedure '%s' is not defined", func_name) then
843✔
1039
            if func_name:startswith('_crud.') then
16✔
1040
                err = NotInitializedError:new("Function %s is not registered: " ..
12✔
1041
                    "crud isn't initialized on replicaset %q or crud module versions mismatch " ..
6✔
1042
                    "between router and storage",
6✔
1043
                    func_name, replicaset_uuid or "Unknown")
12✔
1044
            else
1045
                err = NotInitializedError:new("Function %s is not registered", func_name)
4✔
1046
            end
1047
        end
1048
    end
1049
    return err
582✔
1050
end
1051

1052
--- Insert each value from values to list
1053
--
1054
-- @function list_extend
1055
--
1056
-- @param table list
1057
--  List to be extended
1058
--
1059
-- @param table values
1060
--  Values to be inserted to list
1061
--
1062
-- @return[1] list
1063
--  List with old values and inserted values
1064
function utils.list_extend(list, values)
383✔
1065
    dev_checks('table', 'table')
25,554✔
1066

1067
    for _, value in ipairs(values) do
104,272✔
1068
        table.insert(list, value)
78,718✔
1069
    end
1070

1071
    return list
25,554✔
1072
end
1073

1074
function utils.list_slice(list, start_index, end_index)
383✔
1075
    dev_checks('table', 'number', '?number')
48✔
1076

1077
    if end_index == nil then
48✔
1078
        end_index = table.maxn(list)
48✔
1079
    end
1080

1081
    local slice = {}
48✔
1082
    for i = start_index, end_index do
120✔
1083
        table.insert(slice, list[i])
72✔
1084
    end
1085

1086
    return slice
48✔
1087
end
1088

1089
--- Polls replicas for storage state
1090
--
1091
-- @function storage_info
1092
--
1093
-- @tparam ?number opts.timeout
1094
--  Function call timeout
1095
--
1096
-- @tparam ?string|table opts.vshard_router
1097
--  Cartridge vshard group name or vshard router instance.
1098
--
1099
-- @return a table of storage states by replica uuid.
1100
function utils.storage_info(opts)
383✔
1101
    opts = opts or {}
7✔
1102

1103
    local vshard_router, err = utils.get_vshard_router_instance(opts.vshard_router)
7✔
1104
    if err ~= nil then
7✔
1105
        return nil, StorageInfoError:new(err)
×
1106
    end
1107

1108
    local replicasets, err = vshard_router:routeall()
7✔
1109
    if replicasets == nil then
7✔
1110
        return nil, StorageInfoError:new("Failed to get router replicasets: %s", err.err)
×
1111
    end
1112

1113
    local futures_by_replicas = {}
7✔
1114
    local replica_state_by_uuid = {}
7✔
1115
    local async_opts = {is_async = true}
7✔
1116
    local timeout = opts.timeout or const.DEFAULT_VSHARD_CALL_TIMEOUT
7✔
1117

1118
    for _, replicaset in pairs(replicasets) do
28✔
1119
        for replica_uuid, replica in pairs(replicaset.replicas) do
56✔
1120
            replica_state_by_uuid[replica_uuid] = {
28✔
1121
                status = "error",
1122
                is_master = replicaset.master == replica
28✔
1123
            }
28✔
1124
            local ok, res = pcall(replica.conn.call, replica.conn, "_crud.storage_info_on_storage",
56✔
1125
                                  {}, async_opts)
28✔
1126
            if ok then
28✔
1127
                futures_by_replicas[replica_uuid] = res
26✔
1128
            else
1129
                local err_msg = string.format("Error getting storage info for %s", replica_uuid)
2✔
1130
                if res ~= nil then
2✔
1131
                    log.error("%s: %s", err_msg, res)
2✔
1132
                    replica_state_by_uuid[replica_uuid].message = tostring(res)
4✔
1133
                else
1134
                    log.error(err_msg)
×
1135
                    replica_state_by_uuid[replica_uuid].message = err_msg
×
1136
                end
1137
            end
1138
        end
1139
    end
1140

1141
    local deadline = fiber.clock() + timeout
14✔
1142
    for replica_uuid, future in pairs(futures_by_replicas) do
40✔
1143
        local wait_timeout = deadline - fiber.clock()
52✔
1144
        if wait_timeout < 0 then
26✔
1145
            wait_timeout = 0
×
1146
        end
1147

1148
        local result, err = future:wait_result(wait_timeout)
26✔
1149
        if result == nil then
26✔
1150
            future:discard()
2✔
1151
            local err_msg = string.format("Error getting storage info for %s", replica_uuid)
2✔
1152
            if err ~= nil then
2✔
1153
                if err.type == 'ClientError' and err.code == box.error.NO_SUCH_PROC then
4✔
1154
                    replica_state_by_uuid[replica_uuid].status = "uninitialized"
1✔
1155
                else
1156
                    log.error("%s: %s", err_msg, err)
1✔
1157
                    replica_state_by_uuid[replica_uuid].message = tostring(err)
2✔
1158
                end
1159
            else
1160
                log.error(err_msg)
×
1161
                replica_state_by_uuid[replica_uuid].message = err_msg
×
1162
            end
1163
        else
1164
            replica_state_by_uuid[replica_uuid].status = result[1].status or "uninitialized"
24✔
1165
        end
1166
    end
1167

1168
    return replica_state_by_uuid
7✔
1169
end
1170

1171
--- Storage status information.
1172
--
1173
-- @function storage_info_on_storage
1174
--
1175
-- @return a table with storage status.
1176
function utils.storage_info_on_storage()
383✔
1177
    return {status = "running"}
24✔
1178
end
1179

1180
local expected_vshard_api = {
383✔
1181
    'routeall', 'route', 'bucket_id_strcrc32',
1182
    'callrw', 'callro', 'callbro', 'callre',
1183
    'callbre', 'map_callrw'
1184
}
1185

1186
--- Verifies that a table has expected vshard
1187
--  router handles.
1188
local function verify_vshard_router(router)
1189
    dev_checks("table")
212✔
1190

1191
    for _, func_name in ipairs(expected_vshard_api) do
1,328✔
1192
        if type(router[func_name]) ~= 'function' then
1,204✔
1193
            return false
88✔
1194
        end
1195
    end
1196

1197
    return true
124✔
1198
end
1199

1200
--- Get a vshard router instance from a parameter.
1201
--
1202
--  If a string passed, extract router instance from
1203
--  Cartridge vshard groups. If table passed, verifies
1204
--  that a table is a vshard router instance.
1205
--
1206
-- @function get_vshard_router_instance
1207
--
1208
-- @param[opt] router name of a vshard group or a vshard router
1209
--  instance
1210
--
1211
-- @return[1] table vshard router instance
1212
-- @treturn[2] nil
1213
-- @treturn[2] table Error description
1214
function utils.get_vshard_router_instance(router)
383✔
1215
    dev_checks('?string|table')
132,712✔
1216

1217
    local router_instance
1218

1219
    if type(router) == 'string' then
132,712✔
1220
        if not is_cartridge then
124✔
1221
            return nil, VshardRouterError:new("Vshard groups are supported only in Tarantool Cartridge")
×
1222
        end
1223

1224
        local router_service = cartridge.service_get('vshard-router')
124✔
1225
        assert(router_service ~= nil)
124✔
1226

1227
        router_instance = router_service.get(router)
248✔
1228
        if router_instance == nil then
124✔
1229
            return nil, VshardRouterError:new("Vshard group %s is not found", router)
×
1230
        end
1231
    elseif type(router) == 'table' then
132,588✔
1232
        if not verify_vshard_router(router) then
424✔
1233
            return nil, VshardRouterError:new("Invalid opts.vshard_router table value, " ..
176✔
1234
                                              "a vshard router instance has been expected")
176✔
1235
        end
1236

1237
        router_instance = router
124✔
1238
    else
1239
        assert(type(router) == 'nil')
132,376✔
1240
        router_instance = vshard.router.static
132,376✔
1241

1242
        if router_instance == nil then
132,376✔
1243
            return nil, VshardRouterError:new("Default vshard group is not found and custom " ..
176✔
1244
                                              "is not specified with opts.vshard_router")
176✔
1245
        end
1246
    end
1247

1248
    return router_instance
132,536✔
1249
end
1250

1251
--- Check if Tarantool Cartridge hotreload supported
1252
--  and get its implementaion.
1253
--
1254
-- @function is_cartridge_hotreload_supported
1255
--
1256
-- @return[1] true or false
1257
-- @return[1] module table, if supported
1258
function utils.is_cartridge_hotreload_supported()
383✔
1259
    if not is_cartridge_hotreload then
516✔
1260
        return false
×
1261
    end
1262

1263
    return true, cartridge_hotreload
516✔
1264
end
1265

1266
local system_spaces = {
383✔
1267
    -- https://github.com/tarantool/tarantool/blob/3240201a2f5bac3bddf8a74015db9b351954e0b5/src/box/schema_def.h#L77-L127
1268
    ['_vinyl_deferred_delete'] = true,
1269
    ['_schema'] = true,
1270
    ['_collation'] = true,
1271
    ['_vcollation'] = true,
1272
    ['_space'] = true,
1273
    ['_vspace'] = true,
1274
    ['_sequence'] = true,
1275
    ['_sequence_data'] = true,
1276
    ['_vsequence'] = true,
1277
    ['_index'] = true,
1278
    ['_vindex'] = true,
1279
    ['_func'] = true,
1280
    ['_vfunc'] = true,
1281
    ['_user'] = true,
1282
    ['_vuser'] = true,
1283
    ['_priv'] = true,
1284
    ['_vpriv'] = true,
1285
    ['_cluster'] = true,
1286
    ['_trigger'] = true,
1287
    ['_truncate'] = true,
1288
    ['_space_sequence'] = true,
1289
    ['_vspace_sequence'] = true,
1290
    ['_fk_constraint'] = true,
1291
    ['_ck_constraint'] = true,
1292
    ['_func_index'] = true,
1293
    ['_session_settings'] = true,
1294
    -- https://github.com/tarantool/vshard/blob/b3c27b32637863e9a03503e641bb7c8c69779a00/vshard/storage/init.lua#L752
1295
    ['_bucket'] = true,
1296
    -- https://github.com/tarantool/ddl/blob/b55d0ff7409f32e4d527e2d25444d883bce4163b/test/set_sharding_metadata_test.lua#L92-L98
1297
    ['_ddl_sharding_key'] = true,
1298
    ['_ddl_sharding_func'] = true,
1299
}
1300

1301
utils.get_schema = function(space_name, opts)
1302
    checks('?string', {
2✔
1303
        vshard_router = '?string|table',
1304
        timeout = '?number',
1305
        cached = '?boolean',
1306
    })
1307

1308
    opts = opts or {}
2✔
1309

1310
    local vshard_router, err = utils.get_vshard_router_instance(opts.vshard_router)
2✔
1311
    if err ~= nil then
2✔
1312
        return nil, GetSchemaError:new(err)
×
1313
    end
1314

1315
    if opts.cached ~= true then
2✔
1316
        local _, err = schema.reload_schema(vshard_router)
2✔
1317
        if err ~= nil then
2✔
1318
            return nil, GetSchemaError:new(err)
×
1319
        end
1320
    end
1321

1322
    local spaces, err = utils.get_spaces(vshard_router, opts.timeout)
2✔
1323
    if err ~= nil then
2✔
1324
        return nil, GetSchemaError:new(err)
×
1325
    end
1326

1327
    if space_name ~= nil then
2✔
1328
        local space = spaces[space_name]
×
1329
        if space == nil then
×
1330
            return nil, GetSchemaError:new("Space %q doesn't exist", space_name)
×
1331
        end
1332
        return schema.get_normalized_space_schema(space)
×
1333
    else
1334
        local resp = {}
2✔
1335

1336
        for name, space in pairs(spaces) do
128✔
1337
            -- Can be indexed by space id and space name,
1338
            -- so we need to be careful with duplicates.
1339
            if type(name) == 'string' and system_spaces[name] == nil then
124✔
1340
                resp[name] = schema.get_normalized_space_schema(space)
16✔
1341
            end
1342
        end
1343

1344
        return resp
2✔
1345
    end
1346
end
1347

1348
return utils
383✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc