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

tarantool / crud / 21364862659

26 Jan 2026 04:13PM UTC coverage: 73.492% (-15.0%) from 88.463%
21364862659

push

github

web-flow
Merge f981517ee into a84e19f3e

4253 of 5787 relevant lines covered (73.49%)

55.69 hits per line

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

72.01
/crud/common/utils.lua
1
local bit = require('bit')
37✔
2
local errors = require('errors')
37✔
3
local fiber = require('fiber')
37✔
4
local ffi = require('ffi')
37✔
5
local fun = require('fun')
37✔
6
local vshard = require('vshard')
37✔
7
local log = require('log')
37✔
8
local tarantool = require('tarantool')
37✔
9

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

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

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

28
local utils = {}
37✔
29

30
utils.STORAGE_NAMESPACE = '_crud'
37✔
31

32
--- Returns a full call string for a storage function name.
33
--
34
--  @param string name a base name of the storage function.
35
--
36
--  @return a full string for the call.
37
function utils.get_storage_call(name)
37✔
38
    dev_checks('string')
1,260✔
39

40
    return ('%s.%s'):format(utils.STORAGE_NAMESPACE, name)
1,260✔
41
end
42

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

45
-- copy from LuaJIT lj_char.c
46
local lj_char_bits = {
37✔
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
37✔
67
local LJ_CHAR_DIGIT = 0x08
37✔
68

69
local LUA_KEYWORDS = {
37✔
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)
37✔
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_id, msg, ...)
37✔
105
    dev_checks("string", "string")
62✔
106

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

114
local function get_replicaset_by_replica_id(replicasets, id)
115
    for replicaset_id, replicaset in pairs(replicasets) do
×
116
        for replica_id, _ in pairs(replicaset.replicas) do
×
117
            if replica_id == id then
×
118
                return replicaset_id, replicaset
×
119
            end
120
        end
121
    end
122

123
    return nil, nil
×
124
end
125

126
function utils.get_spaces(vshard_router, timeout, replica_id)
37✔
127
    local replicasets, replicaset, replicaset_id, master
128

129
    timeout = timeout or const.DEFAULT_VSHARD_CALL_TIMEOUT
1,634✔
130
    local deadline = fiber.clock() + timeout
3,268✔
131
    local iter_sleep = math.min(timeout / 100, 0.1)
1,634✔
132
    while (
133
        -- Break if the deadline condition is exceeded.
134
        -- Handling for deadline errors are below in the code.
135
        fiber.clock() < deadline
3,268✔
136
    ) do
1,634✔
137
        -- Try to get master with timeout.
138
        replicasets = vshard_router:routeall()
3,268✔
139
        if replica_id ~= nil then
1,634✔
140
            -- Get the same replica on which the last DML operation was performed.
141
            -- This approach is temporary and is related to [1], [2].
142
            -- [1] https://github.com/tarantool/crud/issues/236
143
            -- [2] https://github.com/tarantool/crud/issues/361
144
            replicaset_id, replicaset = get_replicaset_by_replica_id(replicasets, replica_id)
×
145
            break
146
        else
147
            replicaset_id, replicaset = next(replicasets)
1,634✔
148
        end
149

150
        if replicaset ~= nil then
1,634✔
151
            -- Get cached, reload (if required) will be processed in other place.
152
            master = utils.get_replicaset_master(replicaset, {cached = true})
3,268✔
153
            if master ~= nil and master.conn.error == nil then
1,634✔
154
                break
1,634✔
155
            end
156
        end
157

158
        fiber.sleep(iter_sleep)
×
159
    end
160

161
    if replicaset == nil then
1,634✔
162
        return nil, GetSpaceError:new(
×
163
            'The router returned empty replicasets: ' ..
×
164
            'perhaps other instances are unavailable or you have configured only the router')
×
165
    end
166

167
    master = utils.get_replicaset_master(replicaset, {cached = true})
3,268✔
168

169
    if master == nil then
1,634✔
170
        local error_msg = string.format(
×
171
            'The master was not found in replicaset %s, ' ..
×
172
            'check status of the master and repeat the operation later',
173
             replicaset_id)
×
174
        return nil, GetSpaceError:new(error_msg)
×
175
    end
176

177
    if master.conn.error ~= nil then
1,634✔
178
        local error_msg = string.format(
×
179
            'The connection to the master of replicaset %s is not valid: %s',
180
             replicaset_id, master.conn.error)
×
181
        return nil, GetSpaceError:new(error_msg)
×
182
    end
183

184
    return master.conn.space, nil, master.conn.schema_version
1,634✔
185
end
186

187
function utils.get_space(space_name, vshard_router, timeout, replica_id)
37✔
188
    local spaces, err, schema_version = utils.get_spaces(vshard_router, timeout, replica_id)
1,630✔
189

190
    if spaces == nil then
1,630✔
191
        return nil, err
×
192
    end
193

194
    return spaces[space_name], err, schema_version
1,630✔
195
end
196

197
function utils.get_space_format(space_name, vshard_router)
37✔
198
    local space, err = utils.get_space(space_name, vshard_router)
522✔
199
    if err ~= nil then
522✔
200
        return nil, GetSpaceFormatError:new("An error occurred during the operation: %s", err)
×
201
    end
202
    if space == nil then
522✔
203
        return nil, GetSpaceFormatError:new("Space %q doesn't exist", space_name)
72✔
204
    end
205

206
    local space_format = space:format()
486✔
207

208
    return space_format
486✔
209
end
210

211
function utils.fetch_latest_metadata_when_single_storage(space, space_name, netbox_schema_version,
37✔
212
                                                         vshard_router, opts, storage_info)
213
    -- Checking the relevance of the schema version is necessary
214
    -- to prevent the irrelevant metadata of the DML operation.
215
    -- This approach is temporary and is related to [1], [2].
216
    -- [1] https://github.com/tarantool/crud/issues/236
217
    -- [2] https://github.com/tarantool/crud/issues/361
218
    local latest_space, err
219

220
    assert(storage_info.replica_schema_version ~= nil,
×
221
           'check the replica_schema_version value from storage ' ..
×
222
           'for correct use of the fetch_latest_metadata opt')
×
223

224
    local replica_id
225
    if storage_info.replica_id == nil then -- Backward compatibility.
×
226
        assert(storage_info.replica_uuid ~= nil,
×
227
               'check the replica_uuid value from storage ' ..
×
228
               'for correct use of the fetch_latest_metadata opt')
×
229
        replica_id = storage_info.replica_uuid
×
230
    else
231
        replica_id = storage_info.replica_id
×
232
    end
233

234
    assert(netbox_schema_version ~= nil,
×
235
           'check the netbox_schema_version value from net_box conn on router ' ..
×
236
           'for correct use of the fetch_latest_metadata opt')
×
237

238
    if storage_info.replica_schema_version ~= netbox_schema_version then
×
239
        local ok, reload_schema_err = schema.reload_schema(vshard_router)
×
240
        if ok then
×
241
            latest_space, err = utils.get_space(space_name, vshard_router,
×
242
                                                opts.timeout, replica_id)
×
243
            if err ~= nil then
×
244
                local warn_msg = "Failed to fetch space for latest schema actualization, metadata may be outdated: %s"
×
245
                log.warn(warn_msg, err)
×
246
            end
247
            if latest_space == nil then
×
248
                log.warn("Failed to find space for latest schema actualization, metadata may be outdated")
×
249
            end
250
        else
251
            log.warn("Failed to reload schema, metadata may be outdated: %s", reload_schema_err)
×
252
        end
253
    end
254
    if err == nil and latest_space ~= nil then
×
255
        space = latest_space
×
256
    end
257

258
    return space
×
259
end
260

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

298
    return space
×
299
end
300

301
function utils.fetch_latest_metadata_for_select(space_name, vshard_router, opts,
37✔
302
                                                storages_info, iter)
303
    -- Checking the relevance of the schema version is necessary
304
    -- to prevent the irrelevant metadata of the DML operation.
305
    -- This approach is temporary and is related to [1], [2].
306
    -- [1] https://github.com/tarantool/crud/issues/236
307
    -- [2] https://github.com/tarantool/crud/issues/361
308
    for _, storage_info in pairs(storages_info) do
×
309
        assert(storage_info.replica_schema_version ~= nil,
×
310
               'check the replica_schema_version value from storage ' ..
×
311
               'for correct use of the fetch_latest_metadata opt')
×
312
        assert(iter.netbox_schema_version ~= nil,
×
313
               'check the netbox_schema_version value from net_box conn on router ' ..
×
314
               'for correct use of the fetch_latest_metadata opt')
×
315
        if storage_info.replica_schema_version ~= iter.netbox_schema_version then
×
316
            local ok, reload_schema_err = schema.reload_schema(vshard_router)
×
317
            if ok then
×
318
                local err
319
                iter.space, err = utils.get_space(space_name, vshard_router, opts.timeout)
×
320
                if err ~= nil then
×
321
                    local warn_msg = "Failed to fetch space for latest schema actualization, " ..
×
322
                                     "metadata may be outdated: %s"
323
                    log.warn(warn_msg, err)
×
324
                end
325
            else
326
                log.warn("Failed to reload schema, metadata may be outdated: %s", reload_schema_err)
×
327
            end
328
            break
329
        end
330
    end
331

332
    return iter
×
333
end
334

335
local function append(lines, s, ...)
336
    table.insert(lines, string.format(s, ...))
442✔
337
end
338

339
local flatten_functions_cache = setmetatable({}, {__mode = 'k'})
37✔
340

341
function utils.flatten(object, space_format, bucket_id, skip_nullability_check)
37✔
342
    local flatten_func = flatten_functions_cache[space_format]
492✔
343
    if flatten_func ~= nil then
492✔
344
        local data, err = flatten_func(object, bucket_id, skip_nullability_check)
479✔
345
        if err ~= nil then
479✔
346
            return nil, FlattenError:new(err)
358✔
347
        end
348
        return data
300✔
349
    end
350

351
    local lines = {}
13✔
352
    append(lines, 'local object, bucket_id, skip_nullability_check = ...')
13✔
353

354
    append(lines, 'for k in pairs(object) do')
13✔
355
    append(lines, '    if fieldmap[k] == nil then')
13✔
356
    append(lines, '        return nil, format(\'Unknown field %%q is specified\', k)')
13✔
357
    append(lines, '    end')
13✔
358
    append(lines, 'end')
13✔
359

360
    local len = #space_format
13✔
361
    append(lines, 'local result = {%s}', string.rep('NULL,', len))
13✔
362

363
    local fieldmap = {}
13✔
364

365
    for i, field in ipairs(space_format) do
81✔
366
        fieldmap[field.name] = true
68✔
367
        if field.name ~= 'bucket_id' then
68✔
368
            append(lines, 'if object[%q] ~= nil then', field.name)
55✔
369
            append(lines, '    result[%d] = object[%q]', i, field.name)
55✔
370
            if field.is_nullable ~= true then
55✔
371
                append(lines, 'elseif skip_nullability_check ~= true then')
54✔
372
                append(lines, '    return nil, \'Field %q isn\\\'t nullable' ..
108✔
373
                              ' (set skip_nullability_check_on_flatten option to true to skip check)\'',
54✔
374
                              field.name)
54✔
375
            end
376
            append(lines, 'end')
110✔
377
        else
378
            append(lines, 'if bucket_id ~= nil then')
13✔
379
            append(lines, '    result[%d] = bucket_id', i, field.name)
13✔
380
            append(lines, 'else')
13✔
381
            append(lines, '    result[%d] = object[%q]', i, field.name)
13✔
382
            append(lines, 'end')
13✔
383
        end
384
    end
385
    append(lines, 'return result')
13✔
386

387
    local code = table.concat(lines, '\n')
13✔
388
    local env = {
13✔
389
        pairs = pairs,
13✔
390
        format = string.format,
13✔
391
        fieldmap = fieldmap,
13✔
392
        NULL = box.NULL,
13✔
393
    }
394
    flatten_func = assert(load(code, nil, 't', env))
13✔
395

396
    flatten_functions_cache[space_format] = flatten_func
13✔
397
    local data, err = flatten_func(object, bucket_id, skip_nullability_check)
13✔
398
    if err ~= nil then
13✔
399
        return nil, FlattenError:new(err)
6✔
400
    end
401
    return data
10✔
402
end
403

404
function utils.unflatten(tuple, space_format)
37✔
405
    if tuple == nil then return nil end
147✔
406

407
    local object = {}
147✔
408

409
    for fieldno, field_format in ipairs(space_format) do
1,018✔
410
        local value = tuple[fieldno]
872✔
411

412
        if not field_format.is_nullable and value == nil then
872✔
413
            return nil, UnflattenError:new("Field %s isn't nullable", fieldno)
2✔
414
        end
415

416
        object[field_format.name] = value
871✔
417
    end
418

419
    return object
146✔
420
end
421

422
function utils.extract_key(tuple, key_parts)
37✔
423
    local key = {}
569✔
424
    for i, part in ipairs(key_parts) do
1,140✔
425
        key[i] = tuple[part.fieldno]
571✔
426
    end
427
    return key
569✔
428
end
429

430
function utils.merge_primary_key_parts(key_parts, pk_parts)
37✔
431
    local merged_parts = {}
311✔
432
    local key_fieldnos = {}
311✔
433

434
    for _, part in ipairs(key_parts) do
633✔
435
        table.insert(merged_parts, part)
322✔
436
        key_fieldnos[part.fieldno] = true
322✔
437
    end
438

439
    for _, pk_part in ipairs(pk_parts) do
630✔
440
        if not key_fieldnos[pk_part.fieldno] then
319✔
441
            table.insert(merged_parts, pk_part)
101✔
442
        end
443
    end
444

445
    return merged_parts
311✔
446
end
447

448
function utils.enrich_field_names_with_cmp_key(field_names, key_parts, space_format)
37✔
449
    if field_names == nil then
275✔
450
        return nil
274✔
451
    end
452

453
    local enriched_field_names = {}
1✔
454
    local key_field_names = {}
1✔
455

456
    for _, field_name in ipairs(field_names) do
3✔
457
        table.insert(enriched_field_names, field_name)
2✔
458
        key_field_names[field_name] = true
2✔
459
    end
460

461
    for _, part in ipairs(key_parts) do
3✔
462
        local field_name = space_format[part.fieldno].name
2✔
463
        if not key_field_names[field_name] then
2✔
464
            table.insert(enriched_field_names, field_name)
2✔
465
            key_field_names[field_name] = true
2✔
466
        end
467
    end
468

469
    return enriched_field_names
1✔
470
end
471

472

473
local function get_version_suffix(suffix_candidate)
474
    if type(suffix_candidate) ~= 'string' then
151✔
475
        return nil
×
476
    end
477

478
    if suffix_candidate:find('^entrypoint$')
151✔
479
    or suffix_candidate:find('^alpha%d$')
151✔
480
    or suffix_candidate:find('^beta%d$')
150✔
481
    or suffix_candidate:find('^rc%d$') then
148✔
482
        return suffix_candidate
7✔
483
    end
484

485
    return nil
144✔
486
end
487

488
local function get_commits_since_from_version_part(commits_since_candidate)
489
    if commits_since_candidate == nil then
142✔
490
        return 0
×
491
    end
492

493
    local ok, val = pcall(tonumber, commits_since_candidate)
142✔
494
    if ok then
142✔
495
        return val
142✔
496
    else
497
        -- It may be unknown suffix instead.
498
        -- Since suffix already unknown, there is no way to properly compare versions.
499
        return 0
×
500
    end
501
end
502

503
local function get_commits_since(suffix, commits_since_candidate_1, commits_since_candidate_2)
504
    -- x.x.x.-candidate_1-candidate_2
505

506
    if suffix ~= nil then
142✔
507
        -- X.Y.Z-suffix-N
508
        return get_commits_since_from_version_part(commits_since_candidate_2)
×
509
    else
510
        -- X.Y.Z-N
511
        -- Possibly X.Y.Z-suffix-N with unknown suffix
512
        return get_commits_since_from_version_part(commits_since_candidate_1)
142✔
513
    end
514
end
515

516
utils.get_version_suffix = get_version_suffix
37✔
517

518

519
local suffix_with_digit_weight = {
37✔
520
    alpha = -3000,
521
    beta  = -2000,
522
    rc    = -1000,
523
}
524

525
local function get_version_suffix_weight(suffix)
526
    if suffix == nil then
1,452✔
527
        return 0
1,215✔
528
    end
529

530
    if suffix:find('^entrypoint$') then
237✔
531
        return -math.huge
83✔
532
    end
533

534
    for header, weight in pairs(suffix_with_digit_weight) do
500✔
535
        local pos, _, digits = suffix:find('^' .. header .. '(%d)$')
344✔
536
        if pos ~= nil then
344✔
537
            return weight + tonumber(digits)
152✔
538
        end
539
    end
540

541
    UtilsInternalError:assert(false,
4✔
542
        'Unexpected suffix %q, parse with "utils.get_version_suffix" first', suffix)
2✔
543
end
544

545
utils.get_version_suffix_weight = get_version_suffix_weight
37✔
546

547

548
local function is_version_ge(major, minor,
549
                             patch, suffix, commits_since,
550
                             major_to_compare, minor_to_compare,
551
                             patch_to_compare, suffix_to_compare, commits_since_to_compare)
552
    major = major or 0
721✔
553
    minor = minor or 0
721✔
554
    patch = patch or 0
721✔
555
    local suffix_weight = get_version_suffix_weight(suffix)
721✔
556
    commits_since = commits_since or 0
721✔
557

558
    major_to_compare = major_to_compare or 0
721✔
559
    minor_to_compare = minor_to_compare or 0
721✔
560
    patch_to_compare = patch_to_compare or 0
721✔
561
    local suffix_weight_to_compare = get_version_suffix_weight(suffix_to_compare)
721✔
562
    commits_since_to_compare = commits_since_to_compare or 0
721✔
563

564
    if major > major_to_compare then return true end
721✔
565
    if major < major_to_compare then return false end
706✔
566

567
    if minor > minor_to_compare then return true end
509✔
568
    if minor < minor_to_compare then return false end
92✔
569

570
    if patch > patch_to_compare then return true end
85✔
571
    if patch < patch_to_compare then return false end
20✔
572

573
    if suffix_weight > suffix_weight_to_compare then return true end
18✔
574
    if suffix_weight < suffix_weight_to_compare then return false end
13✔
575

576
    if commits_since > commits_since_to_compare then return true end
8✔
577
    if commits_since < commits_since_to_compare then return false end
7✔
578

579
    return true
6✔
580
end
581

582
utils.is_version_ge = is_version_ge
37✔
583

584

585
local function is_version_in_range(major, minor,
586
                                   patch, suffix, commits_since,
587
                                   major_left_side, minor_left_side,
588
                                   patch_left_side, suffix_left_side, commits_since_left_side,
589
                                   major_right_side, minor_right_side,
590
                                   patch_right_side, suffix_right_side, commits_since_right_side)
591
    return is_version_ge(major, minor,
154✔
592
                         patch, suffix, commits_since,
77✔
593
                         major_left_side, minor_left_side,
77✔
594
                         patch_left_side, suffix_left_side, commits_since_left_side)
77✔
595
       and is_version_ge(major_right_side, minor_right_side,
80✔
596
                         patch_right_side, suffix_right_side, commits_since_right_side,
3✔
597
                         major, minor,
3✔
598
                         patch, suffix, commits_since)
80✔
599
end
600

601
utils.is_version_in_range = is_version_in_range
37✔
602

603

604
local function get_tarantool_version()
605
    local version_parts = rawget(_G, '_TARANTOOL'):split('-', 3)
142✔
606

607
    local major_minor_patch_parts = version_parts[1]:split('.', 2)
142✔
608
    local major = tonumber(major_minor_patch_parts[1])
142✔
609
    local minor = tonumber(major_minor_patch_parts[2])
142✔
610
    local patch = tonumber(major_minor_patch_parts[3])
142✔
611

612
    local suffix = get_version_suffix(version_parts[2])
142✔
613

614
    local commits_since = get_commits_since(suffix, version_parts[2], version_parts[3])
142✔
615

616
    return major, minor, patch, suffix, commits_since
142✔
617
end
618

619
utils.get_tarantool_version = get_tarantool_version
37✔
620

621

622
local function tarantool_version_at_least(wanted_major, wanted_minor,
623
                                          wanted_patch, wanted_suffix, wanted_commits_since)
624
    local major, minor, patch, suffix, commits_since = get_tarantool_version()
104✔
625

626
    return is_version_ge(major, minor, patch, suffix, commits_since,
104✔
627
                         wanted_major, wanted_minor, wanted_patch, wanted_suffix, wanted_commits_since)
104✔
628
end
629

630
utils.tarantool_version_at_least = tarantool_version_at_least
37✔
631

632
function utils.is_enterprise_package()
37✔
633
    return tarantool.package == 'Tarantool Enterprise'
339✔
634
end
635

636

637
local enabled_tarantool_features = {}
37✔
638

639
local function determine_enabled_features()
640
    local major, minor, patch, suffix, commits_since = get_tarantool_version()
37✔
641

642
    -- since Tarantool 2.3.1
643
    enabled_tarantool_features.fieldpaths = is_version_ge(major, minor, patch, suffix, commits_since,
74✔
644
                                                          2, 3, 1, nil, nil)
74✔
645

646
    -- Full support (Lua type, space format type and indexes) for decimal type
647
    -- is since Tarantool 2.3.1 [1]
648
    --
649
    -- [1] https://github.com/tarantool/tarantool/commit/485439e33196e26d120e622175f88b4edc7a5aa1
650
    enabled_tarantool_features.decimals = is_version_ge(major, minor, patch, suffix, commits_since,
74✔
651
                                                        2, 3, 1, nil, nil)
74✔
652

653
    -- Full support (Lua type, space format type and indexes) for uuid type
654
    -- is since Tarantool 2.4.1 [1]
655
    --
656
    -- [1] https://github.com/tarantool/tarantool/commit/b238def8065d20070dcdc50b54c2536f1de4c7c7
657
    enabled_tarantool_features.uuids = is_version_ge(major, minor, patch, suffix, commits_since,
74✔
658
                                                     2, 4, 1, nil, nil)
74✔
659

660
    -- Full support (Lua type, space format type and indexes) for datetime type
661
    -- is since Tarantool 2.10.0-beta2 [1]
662
    --
663
    -- [1] https://github.com/tarantool/tarantool/commit/3bd870261c462416c29226414fe0a2d79aba0c74
664
    enabled_tarantool_features.datetimes = is_version_ge(major, minor, patch, suffix, commits_since,
74✔
665
                                                         2, 10, 0, 'beta2', nil)
74✔
666

667
    -- Full support (Lua type, space format type and indexes) for datetime type
668
    -- is since Tarantool 2.10.0-rc1 [1]
669
    --
670
    -- [1] https://github.com/tarantool/tarantool/commit/38f0c904af4882756c6dc802f1895117d3deae6a
671
    enabled_tarantool_features.intervals = is_version_ge(major, minor, patch, suffix, commits_since,
74✔
672
                                                         2, 10, 0, 'rc1', nil)
74✔
673

674
    -- since Tarantool 2.6.3 / 2.7.2 / 2.8.1
675
    enabled_tarantool_features.jsonpath_indexes = is_version_ge(major, minor, patch, suffix, commits_since,
74✔
676
                                                                2, 8, 1, nil, nil)
37✔
677
                                               or is_version_in_range(major, minor, patch, suffix, commits_since,
37✔
678
                                                                      2, 7, 2, nil, nil,
679
                                                                      2, 7, math.huge, nil, nil)
×
680
                                               or is_version_in_range(major, minor, patch, suffix, commits_since,
×
681
                                                                      2, 6, 3, nil, nil,
682
                                                                      2, 6, math.huge, nil, nil)
37✔
683

684
    -- The merger module was implemented in 2.2.1, see [1].
685
    -- However it had the critical problem [2], which leads to
686
    -- segfault at attempt to use the module from a fiber serving
687
    -- iproto request. So we don't use it in versions before the
688
    -- fix.
689
    --
690
    -- [1]: https://github.com/tarantool/tarantool/issues/3276
691
    -- [2]: https://github.com/tarantool/tarantool/issues/4954
692
    enabled_tarantool_features.builtin_merger = is_version_ge(major, minor, patch, suffix, commits_since,
74✔
693
                                                              2, 6, 0, nil, nil)
37✔
694
                                             or is_version_in_range(major, minor, patch, suffix, commits_since,
37✔
695
                                                                    2, 5, 1, nil, nil,
696
                                                                    2, 5, math.huge, nil, nil)
×
697
                                             or is_version_in_range(major, minor, patch, suffix, commits_since,
×
698
                                                                    2, 4, 2, nil, nil,
699
                                                                    2, 4, math.huge, nil, nil)
×
700
                                             or is_version_in_range(major, minor, patch, suffix, commits_since,
×
701
                                                                    2, 3, 3, nil, nil,
702
                                                                    2, 3, math.huge, nil, nil)
37✔
703

704
    -- The external merger module leans on a set of relatively
705
    -- new APIs in tarantool. So it works only on tarantool
706
    -- versions, which offer those APIs.
707
    --
708
    -- See README of the module:
709
    -- https://github.com/tarantool/tuple-merger
710
    enabled_tarantool_features.external_merger = is_version_ge(major, minor, patch, suffix, commits_since,
74✔
711
                                                               2, 7, 0, nil, nil)
37✔
712
                                              or is_version_in_range(major, minor, patch, suffix, commits_since,
37✔
713
                                                                     2, 6, 1, nil, nil,
714
                                                                     2, 6, math.huge, nil, nil)
×
715
                                              or is_version_in_range(major, minor, patch, suffix, commits_since,
×
716
                                                                     2, 5, 2, nil, nil,
717
                                                                     2, 5, math.huge, nil, nil)
×
718
                                              or is_version_in_range(major, minor, patch, suffix, commits_since,
×
719
                                                                     2, 4, 3, nil, nil,
720
                                                                     2, 4, math.huge, nil, nil)
×
721
                                              or is_version_in_range(major, minor, patch, suffix, commits_since,
×
722
                                                                     1, 10, 8, nil, nil,
723
                                                                     1, 10, math.huge, nil, nil)
37✔
724

725
    enabled_tarantool_features.netbox_skip_header_option = is_version_ge(major, minor, patch, suffix, commits_since,
74✔
726
                                                                         2, 2, 0, nil, nil)
74✔
727

728
    -- https://github.com/tarantool/tarantool/commit/11f2d999a92e45ee41b8c8d0014d8a09290fef7b
729
    enabled_tarantool_features.box_watch = is_version_ge(major, minor, patch, suffix, commits_since,
74✔
730
                                                         2, 10, 0, 'beta2', nil)
74✔
731

732
    enabled_tarantool_features.tarantool_3 = is_version_ge(major, minor, patch, suffix, commits_since,
74✔
733
                                                           3, 0, 0, nil, nil)
74✔
734

735
    enabled_tarantool_features.config_get_inside_roles = (
37✔
736
        -- https://github.com/tarantool/tarantool/commit/ebb170cb8cf2b9c4634bcf0178665909f578c335
737
        not utils.is_enterprise_package()
74✔
738
        and is_version_ge(major, minor, patch, suffix, commits_since,
74✔
739
                          3, 1, 0, 'entrypoint', 77)
37✔
740
    ) or (
37✔
741
        -- https://github.com/tarantool/tarantool/commit/e0e1358cb60d6749c34daf508e05586e0959bf89
742
        not utils.is_enterprise_package()
74✔
743
        and is_version_in_range(major, minor, patch, suffix, commits_since,
74✔
744
                                3, 0, 1, nil, 10,
37✔
745
                                3, 0, math.huge, nil, nil)
37✔
746
    ) or (
37✔
747
        -- https://github.com/tarantool/tarantool-ee/commit/368cc4007727af30ae3ca3a3cdfc7065f34e02aa
748
        utils.is_enterprise_package()
37✔
749
        and is_version_ge(major, minor, patch, suffix, commits_since,
37✔
750
                          3, 1, 0, 'entrypoint', 44)
×
751
    ) or (
×
752
        -- https://github.com/tarantool/tarantool-ee/commit/1dea81bed4cbe4856a0fc77dcc548849a2dabf45
753
        utils.is_enterprise_package()
37✔
754
        and is_version_in_range(major, minor, patch, suffix, commits_since,
37✔
755
                                3, 0, 1, nil, 10,
756
                                3, 0, math.huge, nil, nil)
×
757
    )
37✔
758

759
    enabled_tarantool_features.role_privileges_not_revoked = (
37✔
760
        -- https://github.com/tarantool/tarantool/commit/b982b46442e62e05ab6340343233aa766ad5e52c
761
        not utils.is_enterprise_package()
74✔
762
        and is_version_ge(major, minor, patch, suffix, commits_since,
74✔
763
                          3, 1, 0, 'entrypoint', 179)
37✔
764
    ) or (
37✔
765
        -- https://github.com/tarantool/tarantool/commit/ee2faf7c328abc54631233342cb9b88e4ce8cae4
766
        not utils.is_enterprise_package()
74✔
767
        and is_version_in_range(major, minor, patch, suffix, commits_since,
74✔
768
                                3, 0, 1, nil, 57,
37✔
769
                                3, 0, math.huge, nil, nil)
37✔
770
    ) or (
37✔
771
        -- https://github.com/tarantool/tarantool-ee/commit/5388e9d0f40d86226dc15bb27d85e63b0198e789
772
        utils.is_enterprise_package()
37✔
773
        and is_version_ge(major, minor, patch, suffix, commits_since,
37✔
774
                          3, 1, 0, 'entrypoint', 82)
×
775
    ) or (
×
776
        -- https://github.com/tarantool/tarantool-ee/commit/83d378d01bf2761da8ec684b6afe5683d38faeae
777
        utils.is_enterprise_package()
37✔
778
        and is_version_in_range(major, minor, patch, suffix, commits_since,
37✔
779
                                3, 0, 1, nil, 35,
780
                                3, 0, math.huge, nil, nil)
×
781
    )
37✔
782
end
783

784
determine_enabled_features()
37✔
785

786
for feature_name, feature_enabled in pairs(enabled_tarantool_features) do
555✔
787
    local util_name
788
    if feature_name == 'tarantool_3' then
481✔
789
        util_name = ('is_%s'):format(feature_name)
37✔
790
    elseif feature_name == 'builtin_merger' then
444✔
791
        util_name = ('tarantool_has_%s'):format(feature_name)
37✔
792
    elseif feature_name == 'role_privileges_not_revoked' then
407✔
793
        util_name = ('tarantool_%s'):format(feature_name)
37✔
794
    else
795
        util_name = ('tarantool_supports_%s'):format(feature_name)
370✔
796
    end
797

798
    local util_func = function() return feature_enabled end
1,425✔
799

800
    utils[util_name] = util_func
481✔
801
end
802

803
local function add_nullable_fields_recursive(operations, operations_map, space_format, tuple, id)
804
    if id < 2 or tuple[id - 1] ~= box.NULL then
×
805
        return operations
×
806
    end
807

808
    if space_format[id - 1].is_nullable and not operations_map[id - 1] then
×
809
        table.insert(operations, {'=', id - 1, box.NULL})
×
810
        return add_nullable_fields_recursive(operations, operations_map, space_format, tuple, id - 1)
×
811
    end
812

813
    return operations
×
814
end
815

816
-- Tarantool < 2.1 has no fields `box.error.NO_SUCH_FIELD_NO` and `box.error.NO_SUCH_FIELD_NAME`.
817
if tarantool_version_at_least(2, 1, 0, nil) then
74✔
818
    function utils.is_field_not_found(err_code)
37✔
819
        return err_code == box.error.NO_SUCH_FIELD_NO or err_code == box.error.NO_SUCH_FIELD_NAME
15✔
820
    end
821
else
822
    function utils.is_field_not_found(err_code)
×
823
        return err_code == box.error.NO_SUCH_FIELD
×
824
    end
825
end
826

827
local function get_operations_map(operations)
828
    local map = {}
×
829
    for _, operation in ipairs(operations) do
×
830
        map[operation[2]] = true
×
831
    end
832

833
    return map
×
834
end
835

836
function utils.add_intermediate_nullable_fields(operations, space_format, tuple)
37✔
837
    if tuple == nil then
×
838
        return operations
×
839
    end
840

841
    -- If tarantool doesn't supports the fieldpaths, we already
842
    -- have converted operations (see this function call in update.lua)
843
    if utils.tarantool_supports_fieldpaths() then
×
844
        local formatted_operations, err = utils.convert_operations(operations, space_format)
×
845
        if err ~= nil then
×
846
            return operations
×
847
        end
848

849
        operations = formatted_operations
×
850
    end
851

852
    -- We need this map to check if there is a field update
853
    -- operation with constant complexity
854
    local operations_map = get_operations_map(operations)
×
855
    for _, operation in ipairs(operations) do
×
856
        operations = add_nullable_fields_recursive(
×
857
            operations, operations_map,
858
            space_format, tuple, operation[2]
×
859
        )
860
    end
861

862
    table.sort(operations, function(v1, v2) return v1[2] < v2[2] end)
×
863
    return operations
×
864
end
865

866
function utils.convert_operations(user_operations, space_format)
37✔
867
    local converted_operations = {}
×
868

869
    for _, operation in ipairs(user_operations) do
×
870
        if type(operation[2]) == 'string' then
×
871
            local field_id
872
            for fieldno, field_format in ipairs(space_format) do
×
873
                if field_format.name == operation[2] then
×
874
                    field_id = fieldno
×
875
                    break
876
                end
877
            end
878

879
            if field_id == nil then
×
880
                return nil, ParseOperationsError:new(
×
881
                        "Space format doesn't contain field named %q", operation[2])
×
882
            end
883

884
            table.insert(converted_operations, {
×
885
                operation[1], field_id, operation[3]
×
886
            })
887
        else
888
            table.insert(converted_operations, operation)
×
889
        end
890
    end
891

892
    return converted_operations
×
893
end
894

895
function utils.unflatten_rows(rows, metadata)
37✔
896
    if metadata == nil then
143✔
897
        return nil, UnflattenError:new('Metadata is not provided')
×
898
    end
899

900
    local result = table.new(#rows, 0)
143✔
901
    local err
902
    for i, row in ipairs(rows) do
286✔
903
        result[i], err = utils.unflatten(row, metadata)
286✔
904
        if err ~= nil then
143✔
905
            return nil, err
×
906
        end
907
    end
908
    return result
143✔
909
end
910

911
local inverted_tarantool_iters = {
37✔
912
    [box.index.EQ] = box.index.REQ,
37✔
913
    [box.index.GT] = box.index.LT,
37✔
914
    [box.index.GE] = box.index.LE,
37✔
915
    [box.index.LT] = box.index.GT,
37✔
916
    [box.index.LE] = box.index.GE,
37✔
917
    [box.index.REQ] = box.index.EQ,
37✔
918
}
919

920
function utils.invert_tarantool_iter(iter)
37✔
921
    local inverted_iter = inverted_tarantool_iters[iter]
7✔
922
    assert(inverted_iter ~= nil, "Unsupported Tarantool iterator: " .. tostring(iter))
7✔
923
    return inverted_iter
7✔
924
end
925

926
function utils.reverse_inplace(t)
37✔
927
    for i = 1,math.floor(#t / 2) do
19✔
928
        t[i], t[#t - i + 1] = t[#t - i + 1], t[i]
13✔
929
    end
930
    return t
6✔
931
end
932

933
function utils.get_bucket_id_fieldno(space, shard_index_name)
37✔
934
    shard_index_name = shard_index_name or 'bucket_id'
1,625✔
935
    local bucket_id_index = space.index[shard_index_name]
1,625✔
936
    if bucket_id_index == nil then
1,625✔
937
        return nil, ShardingError:new('%q index is not found', shard_index_name)
×
938
    end
939

940
    return bucket_id_index.parts[1].fieldno
1,625✔
941
end
942

943
-- Build a map with field number as a keys and part number
944
-- as a values using index parts as a source.
945
function utils.get_index_fieldno_map(index_parts)
37✔
946
    dev_checks('table')
15✔
947

948
    local fieldno_map = {}
15✔
949
    for i, part in ipairs(index_parts) do
46✔
950
        local fieldno = part.fieldno
31✔
951
        fieldno_map[fieldno] = i
31✔
952
    end
953

954
    return fieldno_map
15✔
955
end
956

957
-- Build a map with field names as a keys and fieldno's
958
-- as a values using space format as a source.
959
function utils.get_format_fieldno_map(space_format)
37✔
960
    dev_checks('table')
266✔
961

962
    local fieldno_map = {}
266✔
963
    for fieldno, field_format in ipairs(space_format) do
1,852✔
964
        fieldno_map[field_format.name] = fieldno
1,586✔
965
    end
966

967
    return fieldno_map
266✔
968
end
969

970
local uuid_t = ffi.typeof('struct tt_uuid')
37✔
971
function utils.is_uuid(value)
37✔
972
    return ffi.istype(uuid_t, value)
10✔
973
end
974

975
local function get_field_format(space_format, field_name)
976
    dev_checks('table', 'string')
×
977

978
    local metadata = space_format_cache[space_format]
×
979
    if metadata ~= nil then
×
980
        return metadata[field_name]
×
981
    end
982

983
    space_format_cache[space_format] = {}
×
984
    for _, field in ipairs(space_format) do
×
985
        space_format_cache[space_format][field.name] = field
×
986
    end
987

988
    return space_format_cache[space_format][field_name]
×
989
end
990

991
local function filter_format_fields(space_format, field_names)
992
    dev_checks('table', 'table')
×
993

994
    local filtered_space_format = {}
×
995

996
    for i, field_name in ipairs(field_names) do
×
997
        filtered_space_format[i] = get_field_format(space_format, field_name)
×
998
        if filtered_space_format[i] == nil then
×
999
            return nil, FilterFieldsError:new(
×
1000
                    'Space format doesn\'t contain field named %q', field_name
×
1001
            )
1002
        end
1003
    end
1004

1005
    return filtered_space_format
×
1006
end
1007

1008
function utils.get_fields_format(space_format, field_names)
37✔
1009
    dev_checks('table', '?table')
131✔
1010

1011
    if field_names == nil then
131✔
1012
        return table.copy(space_format)
131✔
1013
    end
1014

1015
    local filtered_space_format, err = filter_format_fields(space_format, field_names)
×
1016

1017
    if err ~= nil then
×
1018
        return nil, err
×
1019
    end
1020

1021
    return filtered_space_format
×
1022
end
1023

1024
function utils.format_result(rows, space, field_names)
37✔
1025
    local result = {}
649✔
1026
    local err
1027
    local space_format = space:format()
649✔
1028
    result.rows = rows
649✔
1029

1030
    if field_names == nil then
649✔
1031
        result.metadata = table.copy(space_format)
1,298✔
1032
        return result
649✔
1033
    end
1034

1035
    result.metadata, err = filter_format_fields(space_format, field_names)
×
1036

1037
    if err ~= nil then
×
1038
        return nil, err
×
1039
    end
1040

1041
    return result
×
1042
end
1043

1044
local function truncate_tuple_metadata(tuple_metadata, field_names)
1045
    dev_checks('?table', 'table')
5✔
1046

1047
    if tuple_metadata == nil then
5✔
1048
        return nil
1✔
1049
    end
1050

1051
    local truncated_metadata = {}
4✔
1052

1053
    if #tuple_metadata < #field_names then
4✔
1054
        return nil, FilterFieldsError:new(
×
1055
                'Field names don\'t match to tuple metadata'
1056
        )
1057
    end
1058

1059
    for i, name in ipairs(field_names) do
9✔
1060
        if tuple_metadata[i].name ~= name then
7✔
1061
            return nil, FilterFieldsError:new(
4✔
1062
                    'Field names don\'t match to tuple metadata'
1063
            )
4✔
1064
        end
1065

1066
        table.insert(truncated_metadata, tuple_metadata[i])
5✔
1067
    end
1068

1069
    return truncated_metadata
2✔
1070
end
1071

1072
function utils.cut_objects(objs, field_names)
37✔
1073
    dev_checks('table', 'table')
3✔
1074

1075
    for i, obj in ipairs(objs) do
12✔
1076
        objs[i] = schema.filter_obj_fields(obj, field_names)
18✔
1077
    end
1078

1079
    return objs
3✔
1080
end
1081

1082
function utils.cut_rows(rows, metadata, field_names)
37✔
1083
    dev_checks('table', '?table', 'table')
5✔
1084

1085
    local truncated_metadata, err = truncate_tuple_metadata(metadata, field_names)
5✔
1086

1087
    if err ~= nil then
5✔
1088
        return nil, err
2✔
1089
    end
1090

1091
    for i, row in ipairs(rows) do
12✔
1092
        rows[i] = schema.truncate_row_trailing_fields(row, field_names)
18✔
1093
    end
1094

1095
    return {
3✔
1096
        metadata = truncated_metadata,
3✔
1097
        rows = rows,
3✔
1098
    }
3✔
1099
end
1100

1101
local function flatten_obj(vshard_router, space_name, obj, skip_nullability_check)
1102
    local space_format, err = utils.get_space_format(space_name, vshard_router)
522✔
1103
    if err ~= nil then
522✔
1104
        return nil, FlattenError:new("Failed to get space format: %s", err), const.NEED_SCHEMA_RELOAD
72✔
1105
    end
1106

1107
    local tuple, err = utils.flatten(obj, space_format, nil, skip_nullability_check)
486✔
1108
    if err ~= nil then
486✔
1109
        return nil, FlattenError:new("Object is specified in bad format: %s", err), const.NEED_SCHEMA_RELOAD
360✔
1110
    end
1111

1112
    return tuple
306✔
1113
end
1114

1115
function utils.flatten_obj_reload(vshard_router, space_name, obj, skip_nullability_check)
37✔
1116
    return schema.wrap_func_reload(vshard_router, flatten_obj, space_name, obj, skip_nullability_check)
414✔
1117
end
1118

1119
-- Merge two options map.
1120
--
1121
-- `opts_a` and/or `opts_b` can be `nil`.
1122
--
1123
-- If `opts_a.foo` and `opts_b.foo` exists, prefer `opts_b.foo`.
1124
function utils.merge_options(opts_a, opts_b)
37✔
1125
    return fun.chain(opts_a or {}, opts_b or {}):tomap()
702✔
1126
end
1127

1128
local function lj_char_isident(n)
1129
    return bit.band(lj_char_bits[n + 2], LJ_CHAR_IDENT) == LJ_CHAR_IDENT
58✔
1130
end
1131

1132
local function lj_char_isdigit(n)
1133
    return bit.band(lj_char_bits[n + 2], LJ_CHAR_DIGIT) == LJ_CHAR_DIGIT
6✔
1134
end
1135

1136
function utils.check_name_isident(name)
37✔
1137
    dev_checks('string')
7✔
1138

1139
    -- sharding function name cannot
1140
    -- be equal to lua keyword
1141
    if LUA_KEYWORDS[name] then
7✔
1142
        return false
1✔
1143
    end
1144

1145
    -- sharding function name cannot
1146
    -- begin with a digit
1147
    local char_number = string.byte(name:sub(1,1))
12✔
1148
    if lj_char_isdigit(char_number) then
12✔
1149
        return false
1✔
1150
    end
1151

1152
    -- sharding func name must be sequence
1153
    -- of letters, digits, or underscore symbols
1154
    for i = 1, #name do
62✔
1155
        local char_number = string.byte(name:sub(i,i))
116✔
1156
        if not lj_char_isident(char_number) then
116✔
1157
            return false
1✔
1158
        end
1159
    end
1160

1161
    return true
4✔
1162
end
1163

1164
function utils.update_storage_call_error_description(err, func_name, replicaset_id)
37✔
1165
    if err == nil then
77✔
1166
        return nil
×
1167
    end
1168

1169
    if (err.type == 'ClientError' or err.type == 'AccessDeniedError' or err.type == 'LuajitError')
152✔
1170
        and type(err.message) == 'string' then
150✔
1171
        local not_defined_str = string.format("Procedure '%s' is not defined", func_name)
75✔
1172
        local not_registered_str = string.format("Function '%s' is not registered", func_name)
75✔
1173
        local access_denied_str = string.format("Execute access to function '%s' is denied", func_name)
75✔
1174
        if err.message == not_defined_str or err.message:startswith(access_denied_str)
225✔
1175
                or err.message:find(not_registered_str)
150✔
1176
                or err.message == "Procedure '_crud.call_on_storage' is not defined"
150✔
1177
                or err.message:startswith("Execute access to function '_crud.call_on_storage' is denied") then
225✔
1178
            if func_name:startswith('_crud.') then
×
1179
                err = NotInitializedError:new("Function '%s' is not registered: " ..
×
1180
                    "crud isn't initialized on replicaset %q or crud module versions mismatch " ..
×
1181
                    "between router and storage",
1182
                    func_name, replicaset_id or "Unknown")
×
1183
            else
1184
                err = NotInitializedError:new("Function '%s' is not registered", func_name)
×
1185
            end
1186
        end
1187
    end
1188
    return err
77✔
1189
end
1190

1191
--- Insert each value from values to list
1192
--
1193
-- @function list_extend
1194
--
1195
-- @param table list
1196
--  List to be extended
1197
--
1198
-- @param table values
1199
--  Values to be inserted to list
1200
--
1201
-- @return[1] list
1202
--  List with old values and inserted values
1203
function utils.list_extend(list, values)
37✔
1204
    dev_checks('table', 'table')
118✔
1205

1206
    for _, value in ipairs(values) do
270✔
1207
        table.insert(list, value)
152✔
1208
    end
1209

1210
    return list
118✔
1211
end
1212

1213
function utils.list_slice(list, start_index, end_index)
37✔
1214
    dev_checks('table', 'number', '?number')
×
1215

1216
    if end_index == nil then
×
1217
        end_index = table.maxn(list)
×
1218
    end
1219

1220
    local slice = {}
×
1221
    for i = start_index, end_index do
×
1222
        table.insert(slice, list[i])
×
1223
    end
1224

1225
    return slice
×
1226
end
1227

1228
local expected_vshard_api = {
37✔
1229
    'routeall', 'route', 'bucket_id_strcrc32',
1230
    'callrw', 'callro', 'callbro', 'callre',
1231
    'callbre', 'map_callrw'
1232
}
1233

1234
--- Verifies that a table has expected vshard
1235
--  router handles.
1236
local function verify_vshard_router(router)
1237
    dev_checks("table")
106✔
1238

1239
    for _, func_name in ipairs(expected_vshard_api) do
664✔
1240
        if type(router[func_name]) ~= 'function' then
602✔
1241
            return false
44✔
1242
        end
1243
    end
1244

1245
    return true
62✔
1246
end
1247

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

1265
    local router_instance
1266

1267
    if type(router) == 'string' then
1,283✔
1268
        if not is_cartridge then
70✔
1269
            return nil, VshardRouterError:new("Vshard groups are supported only in Tarantool Cartridge")
×
1270
        end
1271

1272
        local router_service = cartridge.service_get('vshard-router')
70✔
1273
        assert(router_service ~= nil)
70✔
1274

1275
        router_instance = router_service.get(router)
140✔
1276
        if router_instance == nil then
70✔
1277
            return nil, VshardRouterError:new("Vshard group %s is not found", router)
×
1278
        end
1279
    elseif type(router) == 'table' then
1,213✔
1280
        if not verify_vshard_router(router) then
212✔
1281
            return nil, VshardRouterError:new("Invalid opts.vshard_router table value, " ..
88✔
1282
                                              "a vshard router instance has been expected")
88✔
1283
        end
1284

1285
        router_instance = router
62✔
1286
    else
1287
        assert(type(router) == 'nil')
1,107✔
1288
        router_instance = vshard.router.static
1,107✔
1289

1290
        if router_instance == nil then
1,107✔
1291
            return nil, VshardRouterError:new("Default vshard group is not found and custom " ..
88✔
1292
                                              "is not specified with opts.vshard_router")
88✔
1293
        end
1294
    end
1295

1296
    return router_instance
1,195✔
1297
end
1298

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

1311
    return true, cartridge_hotreload
73✔
1312
end
1313

1314
if utils.tarantool_supports_intervals() then
74✔
1315
    -- https://github.com/tarantool/tarantool/blob/0510ffa07afd84a70c9c6f1a4c28aacd73a393d6/src/lua/datetime.lua#L175-179
1316
    local interval_t = ffi.typeof('struct interval')
37✔
1317

1318
    utils.is_interval = function(o)
1319
        return ffi.istype(interval_t, o)
×
1320
    end
1321
else
1322
    utils.is_interval = function()
1323
        return false
×
1324
    end
1325
end
1326

1327
for k, v in pairs(require('crud.common.vshard_utils')) do
444✔
1328
    utils[k] = v
333✔
1329
end
1330

1331
function utils.append_array(array_src, array_dst)
37✔
1332
    if not array_dst then
1,229✔
1333
        return array_src
1✔
1334
    end
1335

1336
    table.move(array_dst, 1, #array_dst, #array_src + 1, array_src)
1,228✔
1337

1338
    return array_src
1,228✔
1339
end
1340

1341
function utils.is_uint(value)
37✔
1342
    if type(value) == 'number' then
220✔
1343
        return value >= 0 and math.floor(value) == value
204✔
1344
    elseif type(value) == 'cdata' then
16✔
1345
        local ok, casted = pcall(tonumber, value)
8✔
1346
        return ok and type(casted) == 'number' and casted >= 0 and math.floor(casted) == casted
8✔
1347
    end
1348

1349
    return false
8✔
1350
end
1351

1352
return utils
37✔
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