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

tarantool / crud / 6615352971

23 Oct 2023 03:42PM UTC coverage: 89.145% (-0.08%) from 89.22%
6615352971

push

github

DifferentialOrange
crud: allow tuple fields of select options to be cdata in type checks

In scope of tarantool/tarantool#8147, box tuples returned from remote
procedure calls our now encoded as a new extension, `MP_TUPLE`, and, as,
the results of such calls are now decoded as box tuples, i.e., cdata. For
compatibility with this feature, we need to allow tuple fields of select
options, which are returned from a remote procedure calls, to be cdata in
option type checks.

Needed for tarantool/tarantool#8147

4673 of 5242 relevant lines covered (89.15%)

14936.72 hits per line

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

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

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

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

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

28
local utils = {}
463✔
29

30
--- Returns a full call string for a storage function name.
31
--
32
--  @param string name a base name of the storage function.
33
--
34
--  @return a full string for the call.
35
function utils.get_storage_call(name)
463✔
36
    dev_checks('string')
12,117✔
37

38
    return '_crud.' .. name
12,117✔
39
end
40

41
local CRUD_STORAGE_INFO_FUNC_NAME = utils.get_storage_call('storage_info_on_storage')
463✔
42

43
local space_format_cache = setmetatable({}, {__mode = 'k'})
463✔
44

45
-- copy from LuaJIT lj_char.c
46
local lj_char_bits = {
463✔
47
    0,
48
    1,  1,  1,  1,  1,  1,  1,  1,  1,  3,  3,  3,  3,  3,  1,  1,
49
    1,  1,  1,  1,  1,  1,  1,  1,  1,  1,  1,  1,  1,  1,  1,  1,
50
    2,  4,  4,  4,  4,  4,  4,  4,  4,  4,  4,  4,  4,  4,  4,  4,
51
    152,152,152,152,152,152,152,152,152,152,  4,  4,  4,  4,  4,  4,
52
    4,176,176,176,176,176,176,160,160,160,160,160,160,160,160,160,
53
    160,160,160,160,160,160,160,160,160,160,160,  4,  4,  4,  4,132,
54
    4,208,208,208,208,208,208,192,192,192,192,192,192,192,192,192,
55
    192,192,192,192,192,192,192,192,192,192,192,  4,  4,  4,  4,  1,
56
    128,128,128,128,128,128,128,128,128,128,128,128,128,128,128,128,
57
    128,128,128,128,128,128,128,128,128,128,128,128,128,128,128,128,
58
    128,128,128,128,128,128,128,128,128,128,128,128,128,128,128,128,
59
    128,128,128,128,128,128,128,128,128,128,128,128,128,128,128,128,
60
    128,128,128,128,128,128,128,128,128,128,128,128,128,128,128,128,
61
    128,128,128,128,128,128,128,128,128,128,128,128,128,128,128,128,
62
    128,128,128,128,128,128,128,128,128,128,128,128,128,128,128,128,
63
    128,128,128,128,128,128,128,128,128,128,128,128,128,128,128,128
64
}
65

66
local LJ_CHAR_IDENT = 0x80
463✔
67
local LJ_CHAR_DIGIT = 0x08
463✔
68

69
local LUA_KEYWORDS = {
463✔
70
    ['and'] = true,
71
    ['end'] = true,
72
    ['in'] = true,
73
    ['repeat'] = true,
74
    ['break'] = true,
75
    ['false'] = true,
76
    ['local'] = true,
77
    ['return'] = true,
78
    ['do'] = true,
79
    ['for'] = true,
80
    ['nil'] = true,
81
    ['then'] = true,
82
    ['else'] = true,
83
    ['function'] = true,
84
    ['not'] = true,
85
    ['true'] = true,
86
    ['elseif'] = true,
87
    ['if'] = true,
88
    ['or'] = true,
89
    ['until'] = true,
90
    ['while'] = true,
91
}
92

93
function utils.table_count(table)
463✔
94
    dev_checks("table")
4✔
95

96
    local cnt = 0
4✔
97
    for _, _ in pairs(table) do
23✔
98
        cnt = cnt + 1
15✔
99
    end
100

101
    return cnt
4✔
102
end
103

104
function utils.format_replicaset_error(replicaset_uuid, msg, ...)
463✔
105
    dev_checks("string", "string")
629✔
106

107
    return string.format(
629✔
108
        "Failed for %s: %s",
629✔
109
        replicaset_uuid,
629✔
110
        string.format(msg, ...)
629✔
111
    )
629✔
112
end
113

114
local function get_replicaset_by_replica_uuid(replicasets, uuid)
115
    for replicaset_uuid, replicaset in pairs(replicasets) do
54✔
116
        for replica_uuid, _ in pairs(replicaset.replicas) do
126✔
117
            if replica_uuid == uuid then
72✔
118
                return replicasets[replicaset_uuid]
18✔
119
            end
120
        end
121
    end
122

123
    return nil
×
124
end
125

126
function utils.get_spaces(vshard_router, timeout, replica_uuid)
463✔
127
    local replicasets, replicaset
128
    timeout = timeout or const.DEFAULT_VSHARD_CALL_TIMEOUT
148,920✔
129
    local deadline = fiber.clock() + timeout
297,840✔
130
    while (
131
        -- Break if the deadline condition is exceeded.
132
        -- Handling for deadline errors are below in the code.
133
        fiber.clock() < deadline
297,840✔
134
    ) do
148,920✔
135
        -- Try to get master with timeout.
136
        fiber.yield()
148,920✔
137
        replicasets = vshard_router:routeall()
297,840✔
138
        if replica_uuid ~= nil then
148,920✔
139
            -- Get the same replica on which the last DML operation was performed.
140
            -- This approach is temporary and is related to [1], [2].
141
            -- [1] https://github.com/tarantool/crud/issues/236
142
            -- [2] https://github.com/tarantool/crud/issues/361
143
            replicaset = get_replicaset_by_replica_uuid(replicasets, replica_uuid)
36✔
144
            break
18✔
145
        else
146
            replicaset = select(2, next(replicasets))
148,902✔
147
        end
148
        if replicaset ~= nil and
148,902✔
149
           replicaset.master ~= nil and
148,902✔
150
           replicaset.master.conn.error == nil then
148,902✔
151
            break
148,902✔
152
        end
153
    end
154

155
    if replicaset == nil then
148,920✔
156
        return nil, GetSpaceError:new(
×
157
            'The router returned empty replicasets: ' ..
×
158
            'perhaps other instances are unavailable or you have configured only the router')
×
159
    end
160

161
    if replicaset.master == nil then
148,920✔
162
        local error_msg = string.format(
×
163
            'The master was not found in replicaset %s, ' ..
×
164
            'check status of the master and repeat the operation later',
165
             replicaset.uuid)
×
166
        return nil, GetSpaceError:new(error_msg)
×
167
    end
168

169
    if replicaset.master.conn.error ~= nil then
148,920✔
170
        local error_msg = string.format(
×
171
            'The connection to the master of replicaset %s is not valid: %s',
172
             replicaset.uuid, replicaset.master.conn.error)
×
173
        return nil, GetSpaceError:new(error_msg)
×
174
    end
175

176
    return replicaset.master.conn.space, nil, replicaset.master.conn.schema_version
148,920✔
177
end
178

179
function utils.get_space(space_name, vshard_router, timeout, replica_uuid)
463✔
180
    local spaces, err, schema_version = utils.get_spaces(vshard_router, timeout, replica_uuid)
148,888✔
181

182
    if spaces == nil then
148,888✔
183
        return nil, err
×
184
    end
185

186
    return spaces[space_name], err, schema_version
148,888✔
187
end
188

189
function utils.get_space_format(space_name, vshard_router)
463✔
190
    local space, err = utils.get_space(space_name, vshard_router)
7,115✔
191
    if err ~= nil then
7,115✔
192
        return nil, GetSpaceFormatError:new("An error occurred during the operation: %s", err)
×
193
    end
194
    if space == nil then
7,115✔
195
        return nil, GetSpaceFormatError:new("Space %q doesn't exist", space_name)
298✔
196
    end
197

198
    local space_format = space:format()
6,966✔
199

200
    return space_format
6,966✔
201
end
202

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

240
    return space
18✔
241
end
242

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

280
    return space
16✔
281
end
282

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

314
    return iter
4✔
315
end
316

317
local function append(lines, s, ...)
318
    table.insert(lines, string.format(s, ...))
4,635✔
319
end
320

321
local flatten_functions_cache = setmetatable({}, {__mode = 'k'})
463✔
322

323
function utils.flatten(object, space_format, bucket_id, skip_nullability_check)
463✔
324
    local flatten_func = flatten_functions_cache[space_format]
7,144✔
325
    if flatten_func ~= nil then
7,144✔
326
        local data, err = flatten_func(object, bucket_id, skip_nullability_check)
6,997✔
327
        if err ~= nil then
6,997✔
328
            return nil, FlattenError:new(err)
1,354✔
329
        end
330
        return data
6,320✔
331
    end
332

333
    local lines = {}
147✔
334
    append(lines, 'local object, bucket_id, skip_nullability_check = ...')
147✔
335

336
    append(lines, 'for k in pairs(object) do')
147✔
337
    append(lines, '    if fieldmap[k] == nil then')
147✔
338
    append(lines, '        return nil, format(\'Unknown field %%q is specified\', k)')
147✔
339
    append(lines, '    end')
147✔
340
    append(lines, 'end')
147✔
341

342
    local len = #space_format
147✔
343
    append(lines, 'local result = {%s}', string.rep('NULL,', len))
147✔
344

345
    local fieldmap = {}
147✔
346

347
    for i, field in ipairs(space_format) do
866✔
348
        fieldmap[field.name] = true
719✔
349
        if field.name ~= 'bucket_id' then
719✔
350
            append(lines, 'if object[%q] ~= nil then', field.name)
572✔
351
            append(lines, '    result[%d] = object[%q]', i, field.name)
572✔
352
            if field.is_nullable ~= true then
572✔
353
                append(lines, 'elseif skip_nullability_check ~= true then')
504✔
354
                append(lines, '    return nil, \'Field %q isn\\\'t nullable' ..
1,008✔
355
                              ' (set skip_nullability_check_on_flatten option to true to skip check)\'',
504✔
356
                              field.name)
504✔
357
            end
358
            append(lines, 'end')
1,144✔
359
        else
360
            append(lines, 'if bucket_id ~= nil then')
147✔
361
            append(lines, '    result[%d] = bucket_id', i, field.name)
147✔
362
            append(lines, 'else')
147✔
363
            append(lines, '    result[%d] = object[%q]', i, field.name)
147✔
364
            append(lines, 'end')
147✔
365
        end
366
    end
367
    append(lines, 'return result')
147✔
368

369
    local code = table.concat(lines, '\n')
147✔
370
    local env = {
147✔
371
        pairs = pairs,
147✔
372
        format = string.format,
147✔
373
        fieldmap = fieldmap,
147✔
374
        NULL = box.NULL,
147✔
375
    }
376
    flatten_func = assert(load(code, nil, 't', env))
147✔
377

378
    flatten_functions_cache[space_format] = flatten_func
147✔
379
    local data, err = flatten_func(object, bucket_id, skip_nullability_check)
147✔
380
    if err ~= nil then
147✔
381
        return nil, FlattenError:new(err)
18✔
382
    end
383
    return data
138✔
384
end
385

386
function utils.unflatten(tuple, space_format)
463✔
387
    if tuple == nil then return nil end
11,437✔
388

389
    local object = {}
11,437✔
390

391
    for fieldno, field_format in ipairs(space_format) do
77,626✔
392
        local value = tuple[fieldno]
66,190✔
393

394
        if not field_format.is_nullable and value == nil then
66,190✔
395
            return nil, UnflattenError:new("Field %s isn't nullable", fieldno)
2✔
396
        end
397

398
        object[field_format.name] = value
66,189✔
399
    end
400

401
    return object
11,436✔
402
end
403

404
function utils.extract_key(tuple, key_parts)
463✔
405
    local key = {}
275,040✔
406
    for i, part in ipairs(key_parts) do
550,800✔
407
        key[i] = tuple[part.fieldno]
275,760✔
408
    end
409
    return key
275,040✔
410
end
411

412
function utils.merge_primary_key_parts(key_parts, pk_parts)
463✔
413
    local merged_parts = {}
40,675✔
414
    local key_fieldnos = {}
40,675✔
415

416
    for _, part in ipairs(key_parts) do
81,569✔
417
        table.insert(merged_parts, part)
40,894✔
418
        key_fieldnos[part.fieldno] = true
40,894✔
419
    end
420

421
    for _, pk_part in ipairs(pk_parts) do
91,629✔
422
        if not key_fieldnos[pk_part.fieldno] then
50,954✔
423
            table.insert(merged_parts, pk_part)
27,980✔
424
        end
425
    end
426

427
    return merged_parts
40,675✔
428
end
429

430
function utils.enrich_field_names_with_cmp_key(field_names, key_parts, space_format)
463✔
431
    if field_names == nil then
29,198✔
432
        return nil
29,113✔
433
    end
434

435
    local enriched_field_names = {}
85✔
436
    local key_field_names = {}
85✔
437

438
    for _, field_name in ipairs(field_names) do
251✔
439
        table.insert(enriched_field_names, field_name)
166✔
440
        key_field_names[field_name] = true
166✔
441
    end
442

443
    for _, part in ipairs(key_parts) do
223✔
444
        local field_name = space_format[part.fieldno].name
138✔
445
        if not key_field_names[field_name] then
138✔
446
            table.insert(enriched_field_names, field_name)
108✔
447
            key_field_names[field_name] = true
108✔
448
        end
449
    end
450

451
    return enriched_field_names
85✔
452
end
453

454

455
local function get_version_suffix(suffix_candidate)
456
    if type(suffix_candidate) ~= 'string' then
1,655✔
457
        return nil
×
458
    end
459

460
    if suffix_candidate:find('^entrypoint$')
1,655✔
461
    or suffix_candidate:find('^alpha%d$')
1,655✔
462
    or suffix_candidate:find('^beta%d$')
1,654✔
463
    or suffix_candidate:find('^rc%d$') then
1,652✔
464
        return suffix_candidate
7✔
465
    end
466

467
    return nil
1,648✔
468
end
469

470
utils.get_version_suffix = get_version_suffix
463✔
471

472

473
local suffix_with_digit_weight = {
463✔
474
    alpha = -3000,
475
    beta  = -2000,
476
    rc    = -1000,
477
}
478

479
local function get_version_suffix_weight(suffix)
480
    if suffix == nil then
8,022✔
481
        return 0
7,979✔
482
    end
483

484
    if suffix:find('^entrypoint$') then
43✔
485
        return -math.huge
9✔
486
    end
487

488
    for header, weight in pairs(suffix_with_digit_weight) do
100✔
489
        local pos, _, digits = suffix:find('^' .. header .. '(%d)$')
64✔
490
        if pos ~= nil then
64✔
491
            return weight + tonumber(digits)
32✔
492
        end
493
    end
494

495
    UtilsInternalError:assert(false,
4✔
496
        'Unexpected suffix %q, parse with "utils.get_version_suffix" first', suffix)
2✔
497
end
498

499
utils.get_version_suffix_weight = get_version_suffix_weight
463✔
500

501

502
local function is_version_ge(major, minor,
503
                             patch, suffix,
504
                             major_to_compare, minor_to_compare,
505
                             patch_to_compare, suffix_to_compare)
506
    major = major or 0
4,006✔
507
    minor = minor or 0
4,006✔
508
    patch = patch or 0
4,006✔
509
    local suffix_weight = get_version_suffix_weight(suffix)
4,006✔
510

511
    major_to_compare = major_to_compare or 0
4,006✔
512
    minor_to_compare = minor_to_compare or 0
4,006✔
513
    patch_to_compare = patch_to_compare or 0
4,006✔
514
    local suffix_weight_to_compare = get_version_suffix_weight(suffix_to_compare)
4,006✔
515

516
    if major > major_to_compare then return true end
4,006✔
517
    if major < major_to_compare then return false end
3,993✔
518

519
    if minor > minor_to_compare then return true end
3,982✔
520
    if minor < minor_to_compare then return false end
552✔
521

522
    if patch > patch_to_compare then return true end
547✔
523
    if patch < patch_to_compare then return false end
11✔
524

525
    if suffix_weight > suffix_weight_to_compare then return true end
10✔
526
    if suffix_weight < suffix_weight_to_compare then return false end
7✔
527

528
    return true
4✔
529
end
530

531
utils.is_version_ge = is_version_ge
463✔
532

533

534
local function is_version_in_range(major, minor,
535
                                   patch, suffix,
536
                                   major_left_side, minor_left_side,
537
                                   patch_left_side, suffix_left_side,
538
                                   major_right_side, minor_right_side,
539
                                   patch_right_side, suffix_right_side)
540
    return is_version_ge(major, minor,
4✔
541
                         patch, suffix,
2✔
542
                         major_left_side, minor_left_side,
2✔
543
                         patch_left_side, suffix_left_side)
2✔
544
       and is_version_ge(major_right_side, minor_right_side,
4✔
545
                         patch_right_side, suffix_right_side,
2✔
546
                         major, minor,
2✔
547
                         patch, suffix)
4✔
548
end
549

550
utils.is_version_in_range = is_version_in_range
463✔
551

552

553
local function get_tarantool_version()
554
    local version_parts = rawget(_G, '_TARANTOOL'):split('-', 1)
1,646✔
555

556
    local major_minor_patch_parts = version_parts[1]:split('.', 2)
1,646✔
557
    local major = tonumber(major_minor_patch_parts[1])
1,646✔
558
    local minor = tonumber(major_minor_patch_parts[2])
1,646✔
559
    local patch = tonumber(major_minor_patch_parts[3])
1,646✔
560

561
    local suffix = get_version_suffix(version_parts[2])
1,646✔
562

563
    return major, minor, patch, suffix
1,646✔
564
end
565

566
utils.get_tarantool_version = get_tarantool_version
463✔
567

568

569
local function tarantool_version_at_least(wanted_major, wanted_minor, wanted_patch)
570
    local major, minor, patch, suffix = get_tarantool_version()
1,182✔
571

572
    return is_version_ge(major, minor, patch, suffix,
1,182✔
573
                         wanted_major, wanted_minor, wanted_patch, nil)
1,182✔
574
end
575

576
utils.tarantool_version_at_least = tarantool_version_at_least
463✔
577

578

579
local enabled_tarantool_features = {}
463✔
580

581
local function determine_enabled_features()
582
    local major, minor, patch, suffix = get_tarantool_version()
463✔
583

584
    -- since Tarantool 2.3.1
585
    enabled_tarantool_features.fieldpaths = is_version_ge(major, minor, patch, suffix,
926✔
586
                                                          2, 3, 1, nil)
926✔
587

588
    -- since Tarantool 2.4.1
589
    enabled_tarantool_features.uuids = is_version_ge(major, minor, patch, suffix,
926✔
590
                                                     2, 4, 1, nil)
926✔
591

592
    -- since Tarantool 2.6.3 / 2.7.2 / 2.8.1
593
    enabled_tarantool_features.jsonpath_indexes = is_version_ge(major, minor, patch, suffix,
926✔
594
                                                                2, 8, 1, nil)
463✔
595
                                               or is_version_in_range(major, minor, patch, suffix,
463✔
596
                                                                      2, 7, 2, nil,
597
                                                                      2, 7, math.huge, nil)
×
598
                                               or is_version_in_range(major, minor, patch, suffix,
×
599
                                                                      2, 6, 3, nil,
600
                                                                      2, 6, math.huge, nil)
463✔
601

602
    -- The merger module was implemented in 2.2.1, see [1].
603
    -- However it had the critical problem [2], which leads to
604
    -- segfault at attempt to use the module from a fiber serving
605
    -- iproto request. So we don't use it in versions before the
606
    -- fix.
607
    --
608
    -- [1]: https://github.com/tarantool/tarantool/issues/3276
609
    -- [2]: https://github.com/tarantool/tarantool/issues/4954
610
    enabled_tarantool_features.builtin_merger = is_version_ge(major, minor, patch, suffix,
926✔
611
                                                              2, 6, 0, nil)
463✔
612
                                             or is_version_in_range(major, minor, patch, suffix,
463✔
613
                                                                    2, 5, 1, nil,
614
                                                                    2, 5, math.huge, nil)
×
615
                                             or is_version_in_range(major, minor, patch, suffix,
×
616
                                                                    2, 4, 2, nil,
617
                                                                    2, 4, math.huge, nil)
×
618
                                             or is_version_in_range(major, minor, patch, suffix,
×
619
                                                                    2, 3, 3, nil,
620
                                                                    2, 3, math.huge, nil)
463✔
621

622
    -- The external merger module leans on a set of relatively
623
    -- new APIs in tarantool. So it works only on tarantool
624
    -- versions, which offer those APIs.
625
    --
626
    -- See README of the module:
627
    -- https://github.com/tarantool/tuple-merger
628
    enabled_tarantool_features.external_merger = is_version_ge(major, minor, patch, suffix,
926✔
629
                                                               2, 7, 0, nil)
463✔
630
                                              or is_version_in_range(major, minor, patch, suffix,
463✔
631
                                                                     2, 6, 1, nil,
632
                                                                     2, 6, math.huge, nil)
×
633
                                              or is_version_in_range(major, minor, patch, suffix,
×
634
                                                                     2, 5, 2, nil,
635
                                                                     2, 5, math.huge, nil)
×
636
                                              or is_version_in_range(major, minor, patch, suffix,
×
637
                                                                     2, 4, 3, nil,
638
                                                                     2, 4, math.huge, nil)
×
639
                                              or is_version_in_range(major, minor, patch, suffix,
×
640
                                                                     1, 10, 8, nil,
641
                                                                     1, 10, math.huge, nil)
463✔
642

643
    enabled_tarantool_features.netbox_skip_header_option = is_version_ge(major, minor, patch, suffix,
926✔
644
                                                                         2, 2, 0, nil)
926✔
645
end
646

647
function utils.tarantool_supports_fieldpaths()
463✔
648
    if enabled_tarantool_features.fieldpaths == nil then
1,144✔
649
        determine_enabled_features()
×
650
    end
651

652
    return enabled_tarantool_features.fieldpaths
1,144✔
653
end
654

655
function utils.tarantool_supports_uuids()
463✔
656
    if enabled_tarantool_features.uuids == nil then
34✔
657
        determine_enabled_features()
28✔
658
    end
659

660
    return enabled_tarantool_features.uuids
34✔
661
end
662

663
function utils.tarantool_supports_jsonpath_indexes()
463✔
664
    if enabled_tarantool_features.jsonpath_indexes == nil then
38✔
665
        determine_enabled_features()
×
666
    end
667

668
    return enabled_tarantool_features.jsonpath_indexes
38✔
669
end
670

671
function utils.tarantool_has_builtin_merger()
463✔
672
    if enabled_tarantool_features.builtin_merger == nil then
950✔
673
        determine_enabled_features()
×
674
    end
675

676
    return enabled_tarantool_features.builtin_merger
950✔
677
end
678

679
function utils.tarantool_supports_external_merger()
463✔
680
    if enabled_tarantool_features.external_merger == nil then
950✔
681
        determine_enabled_features()
435✔
682
    end
683

684
    return enabled_tarantool_features.external_merger
950✔
685
end
686

687
function utils.tarantool_supports_netbox_skip_header_option()
463✔
688
    if enabled_tarantool_features.netbox_skip_header_option == nil then
113,692✔
689
        determine_enabled_features()
×
690
    end
691

692
    return enabled_tarantool_features.netbox_skip_header_option
113,692✔
693
end
694

695
local function add_nullable_fields_recursive(operations, operations_map, space_format, tuple, id)
696
    if id < 2 or tuple[id - 1] ~= box.NULL then
×
697
        return operations
×
698
    end
699

700
    if space_format[id - 1].is_nullable and not operations_map[id - 1] then
×
701
        table.insert(operations, {'=', id - 1, box.NULL})
×
702
        return add_nullable_fields_recursive(operations, operations_map, space_format, tuple, id - 1)
×
703
    end
704

705
    return operations
×
706
end
707

708
-- Tarantool < 2.1 has no fields `box.error.NO_SUCH_FIELD_NO` and `box.error.NO_SUCH_FIELD_NAME`.
709
if tarantool_version_at_least(2, 1, 0, nil) then
926✔
710
    function utils.is_field_not_found(err_code)
463✔
711
        return err_code == box.error.NO_SUCH_FIELD_NO or err_code == box.error.NO_SUCH_FIELD_NAME
51✔
712
    end
713
else
714
    function utils.is_field_not_found(err_code)
×
715
        return err_code == box.error.NO_SUCH_FIELD
×
716
    end
717
end
718

719
local function get_operations_map(operations)
720
    local map = {}
×
721
    for _, operation in ipairs(operations) do
×
722
        map[operation[2]] = true
×
723
    end
724

725
    return map
×
726
end
727

728
function utils.add_intermediate_nullable_fields(operations, space_format, tuple)
463✔
729
    if tuple == nil then
2✔
730
        return operations
×
731
    end
732

733
    -- If tarantool doesn't supports the fieldpaths, we already
734
    -- have converted operations (see this function call in update.lua)
735
    if utils.tarantool_supports_fieldpaths() then
4✔
736
        local formatted_operations, err = utils.convert_operations(operations, space_format)
2✔
737
        if err ~= nil then
2✔
738
            return operations
2✔
739
        end
740

741
        operations = formatted_operations
×
742
    end
743

744
    -- We need this map to check if there is a field update
745
    -- operation with constant complexity
746
    local operations_map = get_operations_map(operations)
×
747
    for _, operation in ipairs(operations) do
×
748
        operations = add_nullable_fields_recursive(
×
749
            operations, operations_map,
750
            space_format, tuple, operation[2]
×
751
        )
752
    end
753

754
    table.sort(operations, function(v1, v2) return v1[2] < v2[2] end)
×
755
    return operations
×
756
end
757

758
function utils.convert_operations(user_operations, space_format)
463✔
759
    local converted_operations = {}
2✔
760

761
    for _, operation in ipairs(user_operations) do
2✔
762
        if type(operation[2]) == 'string' then
2✔
763
            local field_id
764
            for fieldno, field_format in ipairs(space_format) do
10✔
765
                if field_format.name == operation[2] then
8✔
766
                    field_id = fieldno
×
767
                    break
768
                end
769
            end
770

771
            if field_id == nil then
2✔
772
                return nil, ParseOperationsError:new(
4✔
773
                        "Space format doesn't contain field named %q", operation[2])
4✔
774
            end
775

776
            table.insert(converted_operations, {
×
777
                operation[1], field_id, operation[3]
×
778
            })
779
        else
780
            table.insert(converted_operations, operation)
×
781
        end
782
    end
783

784
    return converted_operations
×
785
end
786

787
function utils.unflatten_rows(rows, metadata)
463✔
788
    if metadata == nil then
10,545✔
789
        return nil, UnflattenError:new('Metadata is not provided')
×
790
    end
791

792
    local result = table.new(#rows, 0)
10,545✔
793
    local err
794
    for i, row in ipairs(rows) do
21,890✔
795
        result[i], err = utils.unflatten(row, metadata)
22,690✔
796
        if err ~= nil then
11,345✔
797
            return nil, err
×
798
        end
799
    end
800
    return result
10,545✔
801
end
802

803
local inverted_tarantool_iters = {
463✔
804
    [box.index.EQ] = box.index.REQ,
463✔
805
    [box.index.GT] = box.index.LT,
463✔
806
    [box.index.GE] = box.index.LE,
463✔
807
    [box.index.LT] = box.index.GT,
463✔
808
    [box.index.LE] = box.index.GE,
463✔
809
    [box.index.REQ] = box.index.EQ,
463✔
810
}
811

812
function utils.invert_tarantool_iter(iter)
463✔
813
    local inverted_iter = inverted_tarantool_iters[iter]
44✔
814
    assert(inverted_iter ~= nil, "Unsupported Tarantool iterator: " .. tostring(iter))
44✔
815
    return inverted_iter
44✔
816
end
817

818
function utils.reverse_inplace(t)
463✔
819
    for i = 1,math.floor(#t / 2) do
91✔
820
        t[i], t[#t - i + 1] = t[#t - i + 1], t[i]
43✔
821
    end
822
    return t
48✔
823
end
824

825
function utils.get_bucket_id_fieldno(space, shard_index_name)
463✔
826
    shard_index_name = shard_index_name or 'bucket_id'
275,341✔
827
    local bucket_id_index = space.index[shard_index_name]
275,341✔
828
    if bucket_id_index == nil then
275,341✔
829
        return nil, ShardingError:new('%q index is not found', shard_index_name)
24✔
830
    end
831

832
    return bucket_id_index.parts[1].fieldno
275,329✔
833
end
834

835
-- Build a map with field number as a keys and part number
836
-- as a values using index parts as a source.
837
function utils.get_index_fieldno_map(index_parts)
463✔
838
    dev_checks('table')
111✔
839

840
    local fieldno_map = {}
111✔
841
    for i, part in ipairs(index_parts) do
284✔
842
        local fieldno = part.fieldno
173✔
843
        fieldno_map[fieldno] = i
173✔
844
    end
845

846
    return fieldno_map
111✔
847
end
848

849
-- Build a map with field names as a keys and fieldno's
850
-- as a values using space format as a source.
851
function utils.get_format_fieldno_map(space_format)
463✔
852
    dev_checks('table')
30,016✔
853

854
    local fieldno_map = {}
30,016✔
855
    for fieldno, field_format in ipairs(space_format) do
151,818✔
856
        fieldno_map[field_format.name] = fieldno
121,802✔
857
    end
858

859
    return fieldno_map
30,016✔
860
end
861

862
local uuid_t = ffi.typeof('struct tt_uuid')
463✔
863
function utils.is_uuid(value)
463✔
864
    return ffi.istype(uuid_t, value)
5✔
865
end
866

867
local function get_field_format(space_format, field_name)
868
    dev_checks('table', 'string')
466✔
869

870
    local metadata = space_format_cache[space_format]
466✔
871
    if metadata ~= nil then
466✔
872
        return metadata[field_name]
446✔
873
    end
874

875
    space_format_cache[space_format] = {}
20✔
876
    for _, field in ipairs(space_format) do
134✔
877
        space_format_cache[space_format][field.name] = field
114✔
878
    end
879

880
    return space_format_cache[space_format][field_name]
20✔
881
end
882

883
local function filter_format_fields(space_format, field_names)
884
    dev_checks('table', 'table')
182✔
885

886
    local filtered_space_format = {}
182✔
887

888
    for i, field_name in ipairs(field_names) do
618✔
889
        filtered_space_format[i] = get_field_format(space_format, field_name)
932✔
890
        if filtered_space_format[i] == nil then
466✔
891
            return nil, FilterFieldsError:new(
60✔
892
                    'Space format doesn\'t contain field named %q', field_name
30✔
893
            )
60✔
894
        end
895
    end
896

897
    return filtered_space_format
152✔
898
end
899

900
function utils.get_fields_format(space_format, field_names)
463✔
901
    dev_checks('table', '?table')
28,751✔
902

903
    if field_names == nil then
28,751✔
904
        return table.copy(space_format)
28,687✔
905
    end
906

907
    local filtered_space_format, err = filter_format_fields(space_format, field_names)
64✔
908

909
    if err ~= nil then
64✔
910
        return nil, err
2✔
911
    end
912

913
    return filtered_space_format
62✔
914
end
915

916
function utils.format_result(rows, space, field_names)
463✔
917
    local result = {}
98,041✔
918
    local err
919
    local space_format = space:format()
98,041✔
920
    result.rows = rows
98,041✔
921

922
    if field_names == nil then
98,041✔
923
        result.metadata = table.copy(space_format)
195,846✔
924
        return result
97,923✔
925
    end
926

927
    result.metadata, err = filter_format_fields(space_format, field_names)
236✔
928

929
    if err ~= nil then
118✔
930
        return nil, err
28✔
931
    end
932

933
    return result
90✔
934
end
935

936
local function truncate_tuple_metadata(tuple_metadata, field_names)
937
    dev_checks('?table', 'table')
31✔
938

939
    if tuple_metadata == nil then
31✔
940
        return nil
3✔
941
    end
942

943
    local truncated_metadata = {}
28✔
944

945
    if #tuple_metadata < #field_names then
28✔
946
        return nil, FilterFieldsError:new(
×
947
                'Field names don\'t match to tuple metadata'
948
        )
949
    end
950

951
    for i, name in ipairs(field_names) do
79✔
952
        if tuple_metadata[i].name ~= name then
53✔
953
            return nil, FilterFieldsError:new(
4✔
954
                    'Field names don\'t match to tuple metadata'
955
            )
4✔
956
        end
957

958
        table.insert(truncated_metadata, tuple_metadata[i])
51✔
959
    end
960

961
    return truncated_metadata
26✔
962
end
963

964
function utils.cut_objects(objs, field_names)
463✔
965
    dev_checks('table', 'table')
5✔
966

967
    for i, obj in ipairs(objs) do
20✔
968
        objs[i] = schema.filter_obj_fields(obj, field_names)
30✔
969
    end
970

971
    return objs
5✔
972
end
973

974
function utils.cut_rows(rows, metadata, field_names)
463✔
975
    dev_checks('table', '?table', 'table')
31✔
976

977
    local truncated_metadata, err = truncate_tuple_metadata(metadata, field_names)
31✔
978

979
    if err ~= nil then
31✔
980
        return nil, err
2✔
981
    end
982

983
    for i, row in ipairs(rows) do
72✔
984
        rows[i] = schema.truncate_row_trailing_fields(row, field_names)
86✔
985
    end
986

987
    return {
29✔
988
        metadata = truncated_metadata,
29✔
989
        rows = rows,
29✔
990
    }
29✔
991
end
992

993
local function flatten_obj(vshard_router, space_name, obj, skip_nullability_check)
994
    local space_format, err = utils.get_space_format(space_name, vshard_router)
7,115✔
995
    if err ~= nil then
7,115✔
996
        return nil, FlattenError:new("Failed to get space format: %s", err), const.NEED_SCHEMA_RELOAD
298✔
997
    end
998

999
    local tuple, err = utils.flatten(obj, space_format, nil, skip_nullability_check)
6,966✔
1000
    if err ~= nil then
6,966✔
1001
        return nil, FlattenError:new("Object is specified in bad format: %s", err), const.NEED_SCHEMA_RELOAD
1,368✔
1002
    end
1003

1004
    return tuple
6,282✔
1005
end
1006

1007
function utils.flatten_obj_reload(vshard_router, space_name, obj, skip_nullability_check)
463✔
1008
    return schema.wrap_func_reload(vshard_router, flatten_obj, space_name, obj, skip_nullability_check)
6,692✔
1009
end
1010

1011
-- Merge two options map.
1012
--
1013
-- `opts_a` and/or `opts_b` can be `nil`.
1014
--
1015
-- If `opts_a.foo` and `opts_b.foo` exists, prefer `opts_b.foo`.
1016
function utils.merge_options(opts_a, opts_b)
463✔
1017
    return fun.chain(opts_a or {}, opts_b or {}):tomap()
12,278✔
1018
end
1019

1020
local function lj_char_isident(n)
1021
    return bit.band(lj_char_bits[n + 2], LJ_CHAR_IDENT) == LJ_CHAR_IDENT
11,959✔
1022
end
1023

1024
local function lj_char_isdigit(n)
1025
    return bit.band(lj_char_bits[n + 2], LJ_CHAR_DIGIT) == LJ_CHAR_DIGIT
728✔
1026
end
1027

1028
function utils.check_name_isident(name)
463✔
1029
    dev_checks('string')
729✔
1030

1031
    -- sharding function name cannot
1032
    -- be equal to lua keyword
1033
    if LUA_KEYWORDS[name] then
729✔
1034
        return false
1✔
1035
    end
1036

1037
    -- sharding function name cannot
1038
    -- begin with a digit
1039
    local char_number = string.byte(name:sub(1,1))
1,456✔
1040
    if lj_char_isdigit(char_number) then
1,456✔
1041
        return false
1✔
1042
    end
1043

1044
    -- sharding func name must be sequence
1045
    -- of letters, digits, or underscore symbols
1046
    for i = 1, #name do
12,685✔
1047
        local char_number = string.byte(name:sub(i,i))
23,918✔
1048
        if not lj_char_isident(char_number) then
23,918✔
1049
            return false
1✔
1050
        end
1051
    end
1052

1053
    return true
726✔
1054
end
1055

1056
function utils.update_storage_call_error_description(err, func_name, replicaset_uuid)
463✔
1057
    if err == nil then
678✔
1058
        return nil
×
1059
    end
1060

1061
    if (err.type == 'ClientError' or err.type == 'AccessDeniedError')
1,186✔
1062
        and type(err.message) == 'string' then
993✔
1063
        local not_defined_str = string.format("Procedure '%s' is not defined", func_name)
499✔
1064
        local access_denied_str = string.format("Execute access to function '%s' is denied", func_name)
499✔
1065
        if err.message == not_defined_str or err.message:startswith(access_denied_str) then
1,977✔
1066
            if func_name:startswith('_crud.') then
16✔
1067
                err = NotInitializedError:new("Function %s is not registered: " ..
12✔
1068
                    "crud isn't initialized on replicaset %q or crud module versions mismatch " ..
6✔
1069
                    "between router and storage",
6✔
1070
                    func_name, replicaset_uuid or "Unknown")
12✔
1071
            else
1072
                err = NotInitializedError:new("Function %s is not registered", func_name)
4✔
1073
            end
1074
        end
1075
    end
1076
    return err
678✔
1077
end
1078

1079
--- Insert each value from values to list
1080
--
1081
-- @function list_extend
1082
--
1083
-- @param table list
1084
--  List to be extended
1085
--
1086
-- @param table values
1087
--  Values to be inserted to list
1088
--
1089
-- @return[1] list
1090
--  List with old values and inserted values
1091
function utils.list_extend(list, values)
463✔
1092
    dev_checks('table', 'table')
28,969✔
1093

1094
    for _, value in ipairs(values) do
112,529✔
1095
        table.insert(list, value)
83,560✔
1096
    end
1097

1098
    return list
28,969✔
1099
end
1100

1101
function utils.list_slice(list, start_index, end_index)
463✔
1102
    dev_checks('table', 'number', '?number')
48✔
1103

1104
    if end_index == nil then
48✔
1105
        end_index = table.maxn(list)
48✔
1106
    end
1107

1108
    local slice = {}
48✔
1109
    for i = start_index, end_index do
120✔
1110
        table.insert(slice, list[i])
72✔
1111
    end
1112

1113
    return slice
48✔
1114
end
1115

1116
--- Polls replicas for storage state
1117
--
1118
-- @function storage_info
1119
--
1120
-- @tparam ?number opts.timeout
1121
--  Function call timeout
1122
--
1123
-- @tparam ?string|table opts.vshard_router
1124
--  Cartridge vshard group name or vshard router instance.
1125
--
1126
-- @return a table of storage states by replica uuid.
1127
function utils.storage_info(opts)
463✔
1128
    opts = opts or {}
5✔
1129

1130
    local vshard_router, err = utils.get_vshard_router_instance(opts.vshard_router)
5✔
1131
    if err ~= nil then
5✔
1132
        return nil, StorageInfoError:new(err)
×
1133
    end
1134

1135
    local replicasets, err = vshard_router:routeall()
5✔
1136
    if replicasets == nil then
5✔
1137
        return nil, StorageInfoError:new("Failed to get router replicasets: %s", err.err)
×
1138
    end
1139

1140
    local futures_by_replicas = {}
5✔
1141
    local replica_state_by_uuid = {}
5✔
1142
    local async_opts = {is_async = true}
5✔
1143
    local timeout = opts.timeout or const.DEFAULT_VSHARD_CALL_TIMEOUT
5✔
1144

1145
    for _, replicaset in pairs(replicasets) do
20✔
1146
        for replica_uuid, replica in pairs(replicaset.replicas) do
40✔
1147
            replica_state_by_uuid[replica_uuid] = {
20✔
1148
                status = "error",
1149
                is_master = replicaset.master == replica
20✔
1150
            }
20✔
1151
            local ok, res = pcall(replica.conn.call, replica.conn, CRUD_STORAGE_INFO_FUNC_NAME,
40✔
1152
                                  {}, async_opts)
20✔
1153
            if ok then
20✔
1154
                futures_by_replicas[replica_uuid] = res
19✔
1155
            else
1156
                local err_msg = string.format("Error getting storage info for %s", replica_uuid)
1✔
1157
                if res ~= nil then
1✔
1158
                    log.error("%s: %s", err_msg, res)
1✔
1159
                    replica_state_by_uuid[replica_uuid].message = tostring(res)
2✔
1160
                else
1161
                    log.error(err_msg)
×
1162
                    replica_state_by_uuid[replica_uuid].message = err_msg
×
1163
                end
1164
            end
1165
        end
1166
    end
1167

1168
    local deadline = fiber.clock() + timeout
10✔
1169
    for replica_uuid, future in pairs(futures_by_replicas) do
29✔
1170
        local wait_timeout = deadline - fiber.clock()
38✔
1171
        if wait_timeout < 0 then
19✔
1172
            wait_timeout = 0
×
1173
        end
1174

1175
        local result, err = future:wait_result(wait_timeout)
19✔
1176
        if result == nil then
19✔
1177
            future:discard()
1✔
1178
            local err_msg = string.format("Error getting storage info for %s", replica_uuid)
1✔
1179
            if err ~= nil then
1✔
1180
                if err.type == 'ClientError' and err.code == box.error.NO_SUCH_PROC then
2✔
1181
                    replica_state_by_uuid[replica_uuid].status = "uninitialized"
×
1182
                else
1183
                    log.error("%s: %s", err_msg, err)
1✔
1184
                    replica_state_by_uuid[replica_uuid].message = tostring(err)
2✔
1185
                end
1186
            else
1187
                log.error(err_msg)
×
1188
                replica_state_by_uuid[replica_uuid].message = err_msg
×
1189
            end
1190
        else
1191
            replica_state_by_uuid[replica_uuid].status = result[1].status or "uninitialized"
18✔
1192
        end
1193
    end
1194

1195
    return replica_state_by_uuid
5✔
1196
end
1197

1198
--- Storage status information.
1199
--
1200
-- @function storage_info_on_storage
1201
--
1202
-- @return a table with storage status.
1203
function utils.storage_info_on_storage()
463✔
1204
    return {status = "running"}
18✔
1205
end
1206

1207
--- Initializes a storage function by its name.
1208
--
1209
--  It adds the function into the global scope by its name and required
1210
--  access to a vshard storage user.
1211
--
1212
--  @function init_storage_call
1213
--
1214
--  @param string name of a user or nil if there is no need to setup access.
1215
--  @param string name a name of the function.
1216
--  @param function func the function.
1217
--
1218
--  @return nil
1219
function utils.init_storage_call(user, name, func)
463✔
1220
    dev_checks('?string', 'string', 'function')
5,904✔
1221

1222
    rawset(_G['_crud'], name, func)
5,904✔
1223

1224
    if user ~= nil then
5,904✔
1225
        name = utils.get_storage_call(name)
6,208✔
1226
        box.schema.func.create(name, {setuid = true, if_not_exists = true})
3,104✔
1227
        box.schema.user.grant(user, 'execute', 'function', name, {if_not_exists=true})
3,104✔
1228
    end
1229
end
1230

1231
local expected_vshard_api = {
463✔
1232
    'routeall', 'route', 'bucket_id_strcrc32',
1233
    'callrw', 'callro', 'callbro', 'callre',
1234
    'callbre', 'map_callrw'
1235
}
1236

1237
--- Verifies that a table has expected vshard
1238
--  router handles.
1239
local function verify_vshard_router(router)
1240
    dev_checks("table")
212✔
1241

1242
    for _, func_name in ipairs(expected_vshard_api) do
1,328✔
1243
        if type(router[func_name]) ~= 'function' then
1,204✔
1244
            return false
88✔
1245
        end
1246
    end
1247

1248
    return true
124✔
1249
end
1250

1251
--- Get a vshard router instance from a parameter.
1252
--
1253
--  If a string passed, extract router instance from
1254
--  Cartridge vshard groups. If table passed, verifies
1255
--  that a table is a vshard router instance.
1256
--
1257
-- @function get_vshard_router_instance
1258
--
1259
-- @param[opt] router name of a vshard group or a vshard router
1260
--  instance
1261
--
1262
-- @return[1] table vshard router instance
1263
-- @treturn[2] nil
1264
-- @treturn[2] table Error description
1265
function utils.get_vshard_router_instance(router)
463✔
1266
    dev_checks('?string|table')
141,840✔
1267

1268
    local router_instance
1269

1270
    if type(router) == 'string' then
141,840✔
1271
        if not is_cartridge then
132✔
1272
            return nil, VshardRouterError:new("Vshard groups are supported only in Tarantool Cartridge")
×
1273
        end
1274

1275
        local router_service = cartridge.service_get('vshard-router')
132✔
1276
        assert(router_service ~= nil)
132✔
1277

1278
        router_instance = router_service.get(router)
264✔
1279
        if router_instance == nil then
132✔
1280
            return nil, VshardRouterError:new("Vshard group %s is not found", router)
×
1281
        end
1282
    elseif type(router) == 'table' then
141,708✔
1283
        if not verify_vshard_router(router) then
424✔
1284
            return nil, VshardRouterError:new("Invalid opts.vshard_router table value, " ..
176✔
1285
                                              "a vshard router instance has been expected")
176✔
1286
        end
1287

1288
        router_instance = router
124✔
1289
    else
1290
        assert(type(router) == 'nil')
141,496✔
1291
        router_instance = vshard.router.static
141,496✔
1292

1293
        if router_instance == nil then
141,496✔
1294
            return nil, VshardRouterError:new("Default vshard group is not found and custom " ..
176✔
1295
                                              "is not specified with opts.vshard_router")
176✔
1296
        end
1297
    end
1298

1299
    return router_instance
141,664✔
1300
end
1301

1302
--- Check if Tarantool Cartridge hotreload supported
1303
--  and get its implementaion.
1304
--
1305
-- @function is_cartridge_hotreload_supported
1306
--
1307
-- @return[1] true or false
1308
-- @return[1] module table, if supported
1309
function utils.is_cartridge_hotreload_supported()
463✔
1310
    if not is_cartridge_hotreload then
273✔
1311
        return false
×
1312
    end
1313

1314
    return true, cartridge_hotreload
273✔
1315
end
1316

1317
return utils
463✔
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