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

tarantool / crud / 7320500616

25 Dec 2023 08:41AM UTC coverage: 89.158% (+0.01%) from 89.145%
7320500616

Pull #404

github

DifferentialOrange
update: propagate new options if fields not exist

If Tarantool older than 2.8.1 [1] was used to update nullable fields,
the workaround was executed to perform this operation. If `noreturn` or
`fetch_latest_metadata` options were set, they were ignored on storage
side before this patch.

1. https://github.com/tarantool/tarantool/issues/3378
Pull Request #404: Support vshard names as keys

25 of 30 new or added lines in 5 files covered. (83.33%)

1 existing line in 1 file now uncovered.

4679 of 5248 relevant lines covered (89.16%)

5996.97 hits per line

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

87.59
/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")
625✔
106

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

114
local function get_replicaset_by_replica_uuid(replicasets, uuid)
115
    for _, replicaset in pairs(replicasets) do
58✔
116
        for _, replica in pairs(replicaset.replicas) do
132✔
117
            if replica.uuid == uuid then
76✔
118
                return replicaset
20✔
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
80,563✔
129
    local deadline = fiber.clock() + timeout
161,126✔
130
    while (
131
        -- Break if the deadline condition is exceeded.
132
        -- Handling for deadline errors are below in the code.
133
        fiber.clock() < deadline
161,126✔
134
    ) do
80,563✔
135
        -- Try to get master with timeout.
136
        fiber.yield()
80,563✔
137
        replicasets = vshard_router:routeall()
161,126✔
138
        if replica_uuid ~= nil then
80,563✔
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)
40✔
144
            break
20✔
145
        else
146
            replicaset = select(2, next(replicasets))
80,543✔
147
        end
148
        if replicaset ~= nil and
80,543✔
149
           replicaset.master ~= nil and
80,543✔
150
           replicaset.master.conn.error == nil then
80,543✔
151
            break
80,543✔
152
        end
153
    end
154

155
    if replicaset == nil then
80,563✔
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
80,563✔
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
80,563✔
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
80,563✔
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)
80,531✔
181

182
    if spaces == nil then
80,531✔
183
        return nil, err
×
184
    end
185

186
    return spaces[space_name], err, schema_version
80,531✔
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,116✔
191
    if err ~= nil then
7,116✔
192
        return nil, GetSpaceFormatError:new("An error occurred during the operation: %s", err)
×
193
    end
194
    if space == nil then
7,116✔
195
        return nil, GetSpaceFormatError:new("Space %q doesn't exist", space_name)
300✔
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,
40✔
212
           'check the replica_schema_version value from storage ' ..
20✔
213
           'for correct use of the fetch_latest_metadata opt')
20✔
214
    assert(storage_info.replica_uuid ~= nil,
40✔
215
           'check the replica_uuid value from storage ' ..
20✔
216
           'for correct use of the fetch_latest_metadata opt')
20✔
217
    assert(netbox_schema_version ~= nil,
40✔
218
           'check the netbox_schema_version value from net_box conn on router ' ..
20✔
219
           'for correct use of the fetch_latest_metadata opt')
20✔
220
    if storage_info.replica_schema_version ~= netbox_schema_version then
20✔
221
        local ok, reload_schema_err = schema.reload_schema(vshard_router)
20✔
222
        if ok then
20✔
223
            latest_space, err = utils.get_space(space_name, vshard_router,
40✔
224
                                                opts.timeout, storage_info.replica_uuid)
40✔
225
            if err ~= nil then
20✔
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
20✔
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
20✔
237
        space = latest_space
20✔
238
    end
239

240
    return space
20✔
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,666✔
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,996✔
327
        if err ~= nil then
6,996✔
328
            return nil, FlattenError:new(err)
1,354✔
329
        end
330
        return data
6,319✔
331
    end
332

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

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

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

345
    local fieldmap = {}
148✔
346

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

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

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

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

389
    local object = {}
11,443✔
390

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

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

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

401
    return object
11,442✔
402
end
403

404
function utils.extract_key(tuple, key_parts)
463✔
405
    local key = {}
182,753✔
406
    for i, part in ipairs(key_parts) do
366,226✔
407
        key[i] = tuple[part.fieldno]
183,473✔
408
    end
409
    return key
182,753✔
410
end
411

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

416
    for _, part in ipairs(key_parts) do
10,406✔
417
        table.insert(merged_parts, part)
5,311✔
418
        key_fieldnos[part.fieldno] = true
5,311✔
419
    end
420

421
    for _, pk_part in ipairs(pk_parts) do
11,268✔
422
        if not key_fieldnos[pk_part.fieldno] then
6,173✔
423
            table.insert(merged_parts, pk_part)
3,115✔
424
        end
425
    end
426

427
    return merged_parts
5,095✔
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
2,993✔
432
        return nil
2,908✔
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
8,845✔
689
        determine_enabled_features()
×
690
    end
691

692
    return enabled_tarantool_features.netbox_skip_header_option
8,845✔
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'
183,096✔
827
    local bucket_id_index = space.index[shard_index_name]
183,096✔
828
    if bucket_id_index == nil then
183,096✔
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
183,084✔
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')
3,811✔
853

854
    local fieldno_map = {}
3,811✔
855
    for fieldno, field_format in ipairs(space_format) do
20,793✔
856
        fieldno_map[field_format.name] = fieldno
16,982✔
857
    end
858

859
    return fieldno_map
3,811✔
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')
2,546✔
902

903
    if field_names == nil then
2,546✔
904
        return table.copy(space_format)
2,482✔
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 = {}
67,584✔
918
    local err
919
    local space_format = space:format()
67,584✔
920
    result.rows = rows
67,584✔
921

922
    if field_names == nil then
67,584✔
923
        result.metadata = table.copy(space_format)
134,932✔
924
        return result
67,466✔
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,116✔
995
    if err ~= nil then
7,116✔
996
        return nil, FlattenError:new("Failed to get space format: %s", err), const.NEED_SCHEMA_RELOAD
300✔
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
674✔
1058
        return nil
×
1059
    end
1060

1061
    if (err.type == 'ClientError' or err.type == 'AccessDeniedError')
1,176✔
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
674✔
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')
2,778✔
1093

1094
    for _, value in ipairs(values) do
50,888✔
1095
        table.insert(list, value)
48,110✔
1096
    end
1097

1098
    return list
2,778✔
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 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)
×
NEW
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')
73,476✔
1267

1268
    local router_instance
1269

1270
    if type(router) == 'string' then
73,476✔
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
73,344✔
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')
73,132✔
1291
        router_instance = vshard.router.static
73,132✔
1292

1293
        if router_instance == nil then
73,132✔
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
73,300✔
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
function utils.get_self_vshard_replicaset()
463✔
1318
    local box_info = box.info()
196✔
1319

1320
    local ok, storage_info = pcall(vshard.storage.info)
196✔
1321
    assert(ok, 'vshard.storage.cfg() must be called first')
196✔
1322

1323
    local replicaset_uuid
1324
    if box_info.replicaset ~= nil then
194✔
NEW
1325
        replicaset_uuid = box_info.replicaset.uuid
×
1326
    else
1327
        replicaset_uuid = box_info.cluster.uuid
194✔
1328
    end
1329

1330
    local replicaset
1331
    -- Identification key may be name since vshard 0.1.25.
1332
    -- See also https://github.com/tarantool/vshard/issues/460.
1333
    for _, v in pairs(storage_info.replicasets) do
779✔
1334
        if v.uuid == replicaset_uuid then
391✔
1335
            replicaset = v
194✔
1336
        end
1337
    end
1338

1339
    return replicaset_uuid, replicaset
194✔
1340
end
1341

1342
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

© 2025 Coveralls, Inc