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

tarantool / crud / 28752680257

05 Jul 2026 07:44PM UTC coverage: 88.2% (-0.1%) from 88.313%
28752680257

push

github

p0rtale
feat: add crud.locate method

Introduced `crud.locate(space_name, key, opts)` to detect whether
a tuple is located in 'memtx' or 'vinyl' engine.
This works exclusively for spaces managed by the enterprise 'cooler' module.

86 of 104 new or added lines in 2 files covered. (82.69%)

1 existing line in 1 file now uncovered.

5337 of 6051 relevant lines covered (88.2%)

12320.31 hits per line

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

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

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

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

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

28
local utils = {}
791✔
29

30
utils.STORAGE_NAMESPACE = '_crud'
791✔
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)
791✔
38
    dev_checks('string')
23,667✔
39

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

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

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

69
local LUA_KEYWORDS = {
791✔
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)
791✔
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, ...)
791✔
105
    dev_checks("string", "string")
750✔
106

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

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

123
    return nil, nil
×
124
end
125

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

129
    timeout = timeout or const.DEFAULT_VSHARD_CALL_TIMEOUT
154,167✔
130
    local deadline = fiber.clock() + timeout
308,334✔
131
    local iter_sleep = math.min(timeout / 100, 0.1)
154,167✔
132
    while (
133
        -- Break if the deadline condition is exceeded.
134
        -- Handling for deadline errors are below in the code.
135
        fiber.clock() < deadline
308,334✔
136
    ) do
154,167✔
137
        -- Try to get master with timeout.
138
        replicasets = vshard_router:routeall()
308,334✔
139
        if replica_id ~= nil then
154,167✔
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)
40✔
145
            break
20✔
146
        else
147
            replicaset_id, replicaset = next(replicasets)
154,147✔
148
        end
149

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

158
        fiber.sleep(iter_sleep)
×
159
    end
160

161
    if replicaset == nil then
154,167✔
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})
308,334✔
168

169
    if master == nil then
154,167✔
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
154,167✔
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
154,167✔
185
end
186

187
function utils.get_space(space_name, vshard_router, timeout, replica_id)
791✔
188
    local spaces, err, schema_version = utils.get_spaces(vshard_router, timeout, replica_id)
154,135✔
189

190
    if spaces == nil then
154,135✔
191
        return nil, err
×
192
    end
193

194
    return spaces[space_name], err, schema_version
154,135✔
195
end
196

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

206
    local space_format = space:format()
10,678✔
207

208
    return space_format
10,678✔
209
end
210

211
function utils.fetch_latest_metadata_when_single_storage(space, space_name, netbox_schema_version,
791✔
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,
40✔
221
           'check the replica_schema_version value from storage ' ..
20✔
222
           'for correct use of the fetch_latest_metadata opt')
20✔
223

224
    local replica_id
225
    if storage_info.replica_id == nil then -- Backward compatibility.
20✔
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
20✔
232
    end
233

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

238
    if storage_info.replica_schema_version ~= netbox_schema_version then
20✔
239
        local ok, reload_schema_err = schema.reload_schema(vshard_router)
20✔
240
        if ok then
20✔
241
            latest_space, err = utils.get_space(space_name, vshard_router,
40✔
242
                                                opts.timeout, replica_id)
40✔
243
            if err ~= nil then
20✔
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
20✔
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
20✔
255
        space = latest_space
20✔
256
    end
257

258
    return space
20✔
259
end
260

261
function utils.fetch_latest_metadata_when_map_storages(space, space_name, vshard_router, opts,
791✔
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
32✔
270
        assert(storage_info.replica_schema_version ~= nil,
32✔
271
            'check the replica_schema_version value from storage ' ..
16✔
272
            'for correct use of the fetch_latest_metadata opt')
16✔
273
        assert(netbox_schema_version ~= nil,
32✔
274
               'check the netbox_schema_version value from net_box conn on router ' ..
16✔
275
               'for correct use of the fetch_latest_metadata opt')
16✔
276
        if storage_info.replica_schema_version ~= netbox_schema_version then
16✔
277
            local ok, reload_schema_err = schema.reload_schema(vshard_router)
16✔
278
            if ok then
16✔
279
                latest_space, err = utils.get_space(space_name, vshard_router, opts.timeout)
32✔
280
                if err ~= nil then
16✔
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
16✔
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
16✔
292
                space = latest_space
16✔
293
            end
294
            break
16✔
295
        end
296
    end
297

298
    return space
16✔
299
end
300

301
function utils.fetch_latest_metadata_for_select(space_name, vshard_router, opts,
791✔
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
8✔
309
        assert(storage_info.replica_schema_version ~= nil,
8✔
310
               'check the replica_schema_version value from storage ' ..
4✔
311
               'for correct use of the fetch_latest_metadata opt')
4✔
312
        assert(iter.netbox_schema_version ~= nil,
8✔
313
               'check the netbox_schema_version value from net_box conn on router ' ..
4✔
314
               'for correct use of the fetch_latest_metadata opt')
4✔
315
        if storage_info.replica_schema_version ~= iter.netbox_schema_version then
4✔
316
            local ok, reload_schema_err = schema.reload_schema(vshard_router)
4✔
317
            if ok then
4✔
318
                local err
319
                iter.space, err = utils.get_space(space_name, vshard_router, opts.timeout)
8✔
320
                if err ~= nil then
4✔
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
4✔
333
end
334

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

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

341
function utils.flatten(object, space_format, bucket_id, skip_nullability_check)
791✔
342
    local flatten_func = flatten_functions_cache[space_format]
10,856✔
343
    if flatten_func ~= nil then
10,856✔
344
        local data, err = flatten_func(object, bucket_id, skip_nullability_check)
10,579✔
345
        if err ~= nil then
10,579✔
346
            return nil, FlattenError:new(err)
2,092✔
347
        end
348
        return data
9,533✔
349
    end
350

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

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

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

363
    local fieldmap = {}
277✔
364

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

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

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

404
function utils.unflatten(tuple, space_format)
791✔
405
    if tuple == nil then return nil end
20,261✔
406

407
    local object = {}
20,261✔
408

409
    for fieldno, field_format in ipairs(space_format) do
114,758✔
410
        local value = tuple[fieldno]
94,498✔
411

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

416
        object[field_format.name] = value
94,497✔
417
    end
418

419
    return object
20,260✔
420
end
421

422
function utils.extract_key(tuple, key_parts)
791✔
423
    local key = {}
360,675✔
424
    for i, part in ipairs(key_parts) do
722,770✔
425
        key[i] = tuple[part.fieldno]
362,095✔
426
    end
427
    return key
360,675✔
428
end
429

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

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

439
    for _, pk_part in ipairs(pk_parts) do
15,505✔
440
        if not key_fieldnos[pk_part.fieldno] then
8,282✔
441
            table.insert(merged_parts, pk_part)
3,188✔
442
        end
443
    end
444

445
    return merged_parts
7,223✔
446
end
447

448
function utils.enrich_field_names_with_cmp_key(field_names, key_parts, space_format)
791✔
449
    if field_names == nil then
7,100✔
450
        return nil
7,015✔
451
    end
452

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

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

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

469
    return enriched_field_names
85✔
470
end
471

472

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

478
    if suffix_candidate:find('^entrypoint$')
2,732✔
479
    or suffix_candidate:find('^alpha%d$')
2,732✔
480
    or suffix_candidate:find('^beta%d$')
2,731✔
481
    or suffix_candidate:find('^rc%d$') then
2,729✔
482
        return suffix_candidate
7✔
483
    end
484

485
    return nil
2,725✔
486
end
487

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

493
    local ok, val = pcall(tonumber, commits_since_candidate)
2,723✔
494
    if ok then
2,723✔
495
        return val
2,723✔
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
2,723✔
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)
2,723✔
513
    end
514
end
515

516
utils.get_version_suffix = get_version_suffix
791✔
517

518

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

525
local function get_version_suffix_weight(suffix)
526
    if suffix == nil then
29,308✔
527
        return 0
25,301✔
528
    end
529

530
    if suffix:find('^entrypoint$') then
4,007✔
531
        return -math.huge
1,591✔
532
    end
533

534
    for header, weight in pairs(suffix_with_digit_weight) do
8,040✔
535
        local pos, _, digits = suffix:find('^' .. header .. '(%d)$')
5,622✔
536
        if pos ~= nil then
5,622✔
537
            return weight + tonumber(digits)
2,414✔
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
791✔
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
14,649✔
553
    minor = minor or 0
14,649✔
554
    patch = patch or 0
14,649✔
555
    local suffix_weight = get_version_suffix_weight(suffix)
14,649✔
556
    commits_since = commits_since or 0
14,649✔
557

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

564
    if major > major_to_compare then return true end
14,649✔
565
    if major < major_to_compare then return false end
14,634✔
566

567
    if minor > minor_to_compare then return true end
10,618✔
568
    if minor < minor_to_compare then return false end
940✔
569

570
    if patch > patch_to_compare then return true end
933✔
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
791✔
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,
3,170✔
592
                         patch, suffix, commits_since,
1,585✔
593
                         major_left_side, minor_left_side,
1,585✔
594
                         patch_left_side, suffix_left_side, commits_since_left_side)
1,585✔
595
       and is_version_ge(major_right_side, minor_right_side,
1,588✔
596
                         patch_right_side, suffix_right_side, commits_since_right_side,
3✔
597
                         major, minor,
3✔
598
                         patch, suffix, commits_since)
1,588✔
599
end
600

601
utils.is_version_in_range = is_version_in_range
791✔
602

603

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

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

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

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

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

619
utils.get_tarantool_version = get_tarantool_version
791✔
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()
1,931✔
625

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

630
utils.tarantool_version_at_least = tarantool_version_at_least
791✔
631

632
function utils.is_enterprise_package()
791✔
633
    return tarantool.package == 'Tarantool Enterprise'
7,135✔
634
end
635

636

637
local enabled_tarantool_features = {}
791✔
638

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

642
    -- since Tarantool 2.3.1
643
    enabled_tarantool_features.fieldpaths = is_version_ge(major, minor, patch, suffix, commits_since,
1,582✔
644
                                                          2, 3, 1, nil, nil)
1,582✔
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,
1,582✔
651
                                                        2, 3, 1, nil, nil)
1,582✔
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,
1,582✔
658
                                                     2, 4, 1, nil, nil)
1,582✔
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,
1,582✔
665
                                                         2, 10, 0, 'beta2', nil)
1,582✔
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,
1,582✔
672
                                                         2, 10, 0, 'rc1', nil)
1,582✔
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,
1,582✔
676
                                                                2, 8, 1, nil, nil)
791✔
677
                                               or is_version_in_range(major, minor, patch, suffix, commits_since,
791✔
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)
791✔
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,
1,582✔
693
                                                              2, 6, 0, nil, nil)
791✔
694
                                             or is_version_in_range(major, minor, patch, suffix, commits_since,
791✔
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)
791✔
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,
1,582✔
711
                                                               2, 7, 0, nil, nil)
791✔
712
                                              or is_version_in_range(major, minor, patch, suffix, commits_since,
791✔
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)
791✔
724

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

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

732
    -- Native `after` option in index:pairs() and index:select() for O(1) cursor positioning
733
    -- Available since Tarantool 2.10
734
    enabled_tarantool_features.index_pairs_after = is_version_ge(major, minor, patch, suffix, commits_since,
1,582✔
735
                                                                 2, 10, 0, nil, nil)
1,582✔
736

737
    enabled_tarantool_features.tarantool_3 = is_version_ge(major, minor, patch, suffix, commits_since,
1,582✔
738
                                                           3, 0, 0, nil, nil)
1,582✔
739

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

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

789
determine_enabled_features()
791✔
790

791
for feature_name, feature_enabled in pairs(enabled_tarantool_features) do
12,656✔
792
    local util_name
793
    if feature_name == 'tarantool_3' then
11,074✔
794
        util_name = ('is_%s'):format(feature_name)
791✔
795
    elseif feature_name == 'builtin_merger' then
10,283✔
796
        util_name = ('tarantool_has_%s'):format(feature_name)
791✔
797
    elseif feature_name == 'role_privileges_not_revoked' then
9,492✔
798
        util_name = ('tarantool_%s'):format(feature_name)
791✔
799
    else
800
        util_name = ('tarantool_supports_%s'):format(feature_name)
8,701✔
801
    end
802

803
    local util_func = function() return feature_enabled end
46,063✔
804

805
    utils[util_name] = util_func
11,074✔
806
end
807

808
local function add_nullable_fields_recursive(operations, operations_map, space_format, tuple, id)
809
    if id < 2 or tuple[id - 1] ~= box.NULL then
×
810
        return operations
×
811
    end
812

813
    if space_format[id - 1].is_nullable and not operations_map[id - 1] then
×
814
        table.insert(operations, {'=', id - 1, box.NULL})
×
815
        return add_nullable_fields_recursive(operations, operations_map, space_format, tuple, id - 1)
×
816
    end
817

818
    return operations
×
819
end
820

821
-- Tarantool < 2.1 has no fields `box.error.NO_SUCH_FIELD_NO` and `box.error.NO_SUCH_FIELD_NAME`.
822
if tarantool_version_at_least(2, 1, 0, nil) then
1,582✔
823
    function utils.is_field_not_found(err_code)
791✔
824
        return err_code == box.error.NO_SUCH_FIELD_NO or err_code == box.error.NO_SUCH_FIELD_NAME
81✔
825
    end
826
else
827
    function utils.is_field_not_found(err_code)
×
828
        return err_code == box.error.NO_SUCH_FIELD
×
829
    end
830
end
831

832
local function get_operations_map(operations)
833
    local map = {}
×
834
    for _, operation in ipairs(operations) do
×
835
        map[operation[2]] = true
×
836
    end
837

838
    return map
×
839
end
840

841
function utils.add_intermediate_nullable_fields(operations, space_format, tuple)
791✔
842
    if tuple == nil then
2✔
843
        return operations
×
844
    end
845

846
    -- If tarantool doesn't supports the fieldpaths, we already
847
    -- have converted operations (see this function call in update.lua)
848
    if utils.tarantool_supports_fieldpaths() then
4✔
849
        local formatted_operations, err = utils.convert_operations(operations, space_format)
2✔
850
        if err ~= nil then
2✔
851
            return operations
2✔
852
        end
853

854
        operations = formatted_operations
×
855
    end
856

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

867
    table.sort(operations, function(v1, v2) return v1[2] < v2[2] end)
×
868
    return operations
×
869
end
870

871
function utils.convert_operations(user_operations, space_format)
791✔
872
    local converted_operations = {}
2✔
873

874
    for _, operation in ipairs(user_operations) do
2✔
875
        if type(operation[2]) == 'string' then
2✔
876
            local field_id
877
            for fieldno, field_format in ipairs(space_format) do
10✔
878
                if field_format.name == operation[2] then
8✔
879
                    field_id = fieldno
×
880
                    break
881
                end
882
            end
883

884
            if field_id == nil then
2✔
885
                return nil, ParseOperationsError:new(
4✔
886
                        "Space format doesn't contain field named %q", operation[2])
4✔
887
            end
888

889
            table.insert(converted_operations, {
×
890
                operation[1], field_id, operation[3]
×
891
            })
892
        else
893
            table.insert(converted_operations, operation)
×
894
        end
895
    end
896

897
    return converted_operations
×
898
end
899

900
function utils.unflatten_rows(rows, metadata)
791✔
901
    if metadata == nil then
17,779✔
902
        return nil, UnflattenError:new('Metadata is not provided')
×
903
    end
904

905
    local result = table.new(#rows, 0)
17,779✔
906
    local err
907
    for i, row in ipairs(rows) do
37,006✔
908
        result[i], err = utils.unflatten(row, metadata)
38,454✔
909
        if err ~= nil then
19,227✔
910
            return nil, err
×
911
        end
912
    end
913
    return result
17,779✔
914
end
915

916
local inverted_tarantool_iters = {
791✔
917
    [box.index.EQ] = box.index.REQ,
791✔
918
    [box.index.GT] = box.index.LT,
791✔
919
    [box.index.GE] = box.index.LE,
791✔
920
    [box.index.LT] = box.index.GT,
791✔
921
    [box.index.LE] = box.index.GE,
791✔
922
    [box.index.REQ] = box.index.EQ,
791✔
923
}
924

925
function utils.invert_tarantool_iter(iter)
791✔
926
    local inverted_iter = inverted_tarantool_iters[iter]
49✔
927
    assert(inverted_iter ~= nil, "Unsupported Tarantool iterator: " .. tostring(iter))
49✔
928
    return inverted_iter
49✔
929
end
930

931
function utils.reverse_inplace(t)
791✔
932
    for i = 1,math.floor(#t / 2) do
91✔
933
        t[i], t[#t - i + 1] = t[#t - i + 1], t[i]
43✔
934
    end
935
    return t
48✔
936
end
937

938
function utils.get_bucket_id_fieldno(space, shard_index_name)
791✔
939
    shard_index_name = shard_index_name or 'bucket_id'
722,413✔
940
    local bucket_id_index = space.index[shard_index_name]
722,413✔
941
    if bucket_id_index == nil then
722,413✔
942
        return nil, ShardingError:new('%q index is not found', shard_index_name)
24✔
943
    end
944

945
    return bucket_id_index.parts[1].fieldno
722,401✔
946
end
947

948
-- Build a map with field number as a keys and part number
949
-- as a values using index parts as a source.
950
function utils.get_index_fieldno_map(index_parts)
791✔
951
    dev_checks('table')
159✔
952

953
    local fieldno_map = {}
159✔
954
    for i, part in ipairs(index_parts) do
428✔
955
        local fieldno = part.fieldno
269✔
956
        fieldno_map[fieldno] = i
269✔
957
    end
958

959
    return fieldno_map
159✔
960
end
961

962
-- Build a map with field names as a keys and fieldno's
963
-- as a values using space format as a source.
964
function utils.get_format_fieldno_map(space_format)
791✔
965
    dev_checks('table')
7,881✔
966

967
    local fieldno_map = {}
7,881✔
968
    for fieldno, field_format in ipairs(space_format) do
40,008✔
969
        fieldno_map[field_format.name] = fieldno
32,127✔
970
    end
971

972
    return fieldno_map
7,881✔
973
end
974

975
local uuid_t = ffi.typeof('struct tt_uuid')
791✔
976
function utils.is_uuid(value)
791✔
977
    return ffi.istype(uuid_t, value)
780✔
978
end
979

980
local function get_field_format(space_format, field_name)
981
    dev_checks('table', 'string')
466✔
982

983
    local metadata = space_format_cache[space_format]
466✔
984
    if metadata ~= nil then
466✔
985
        return metadata[field_name]
446✔
986
    end
987

988
    space_format_cache[space_format] = {}
20✔
989
    for _, field in ipairs(space_format) do
134✔
990
        space_format_cache[space_format][field.name] = field
114✔
991
    end
992

993
    return space_format_cache[space_format][field_name]
20✔
994
end
995

996
local function filter_format_fields(space_format, field_names)
997
    dev_checks('table', 'table')
182✔
998

999
    local filtered_space_format = {}
182✔
1000

1001
    for i, field_name in ipairs(field_names) do
618✔
1002
        filtered_space_format[i] = get_field_format(space_format, field_name)
932✔
1003
        if filtered_space_format[i] == nil then
466✔
1004
            return nil, FilterFieldsError:new(
60✔
1005
                    'Space format doesn\'t contain field named %q', field_name
30✔
1006
            )
60✔
1007
        end
1008
    end
1009

1010
    return filtered_space_format
152✔
1011
end
1012

1013
function utils.get_fields_format(space_format, field_names)
791✔
1014
    dev_checks('table', '?table')
5,799✔
1015

1016
    if field_names == nil then
5,799✔
1017
        return table.copy(space_format)
5,735✔
1018
    end
1019

1020
    local filtered_space_format, err = filter_format_fields(space_format, field_names)
64✔
1021

1022
    if err ~= nil then
64✔
1023
        return nil, err
2✔
1024
    end
1025

1026
    return filtered_space_format
62✔
1027
end
1028

1029
function utils.format_result(rows, space, field_names)
791✔
1030
    local result = {}
131,489✔
1031
    local err
1032
    local space_format = space:format()
131,489✔
1033
    result.rows = rows
131,489✔
1034

1035
    if field_names == nil then
131,489✔
1036
        result.metadata = table.copy(space_format)
262,742✔
1037
        return result
131,371✔
1038
    end
1039

1040
    result.metadata, err = filter_format_fields(space_format, field_names)
236✔
1041

1042
    if err ~= nil then
118✔
1043
        return nil, err
28✔
1044
    end
1045

1046
    return result
90✔
1047
end
1048

1049
local function truncate_tuple_metadata(tuple_metadata, field_names)
1050
    dev_checks('?table', 'table')
31✔
1051

1052
    if tuple_metadata == nil then
31✔
1053
        return nil
3✔
1054
    end
1055

1056
    local truncated_metadata = {}
28✔
1057

1058
    if #tuple_metadata < #field_names then
28✔
1059
        return nil, FilterFieldsError:new(
×
1060
                'Field names don\'t match to tuple metadata'
1061
        )
1062
    end
1063

1064
    for i, name in ipairs(field_names) do
79✔
1065
        if tuple_metadata[i].name ~= name then
53✔
1066
            return nil, FilterFieldsError:new(
4✔
1067
                    'Field names don\'t match to tuple metadata'
1068
            )
4✔
1069
        end
1070

1071
        table.insert(truncated_metadata, tuple_metadata[i])
51✔
1072
    end
1073

1074
    return truncated_metadata
26✔
1075
end
1076

1077
function utils.cut_objects(objs, field_names)
791✔
1078
    dev_checks('table', 'table')
5✔
1079

1080
    for i, obj in ipairs(objs) do
20✔
1081
        objs[i] = schema.filter_obj_fields(obj, field_names)
30✔
1082
    end
1083

1084
    return objs
5✔
1085
end
1086

1087
function utils.cut_rows(rows, metadata, field_names)
791✔
1088
    dev_checks('table', '?table', 'table')
31✔
1089

1090
    local truncated_metadata, err = truncate_tuple_metadata(metadata, field_names)
31✔
1091

1092
    if err ~= nil then
31✔
1093
        return nil, err
2✔
1094
    end
1095

1096
    for i, row in ipairs(rows) do
72✔
1097
        rows[i] = schema.truncate_row_trailing_fields(row, field_names)
86✔
1098
    end
1099

1100
    return {
29✔
1101
        metadata = truncated_metadata,
29✔
1102
        rows = rows,
29✔
1103
    }
29✔
1104
end
1105

1106
local function flatten_obj(vshard_router, space_name, obj, skip_nullability_check)
1107
    local space_format, err = utils.get_space_format(space_name, vshard_router)
10,851✔
1108
    if err ~= nil then
10,851✔
1109
        return nil, FlattenError:new("Failed to get space format: %s", err), const.NEED_SCHEMA_RELOAD
346✔
1110
    end
1111

1112
    local tuple, err = utils.flatten(obj, space_format, nil, skip_nullability_check)
10,678✔
1113
    if err ~= nil then
10,678✔
1114
        return nil, FlattenError:new("Object is specified in bad format: %s", err), const.NEED_SCHEMA_RELOAD
2,088✔
1115
    end
1116

1117
    return tuple
9,634✔
1118
end
1119

1120
function utils.flatten_obj_reload(vshard_router, space_name, obj, skip_nullability_check)
791✔
1121
    return schema.wrap_func_reload(vshard_router, flatten_obj, space_name, obj, skip_nullability_check)
10,206✔
1122
end
1123

1124
-- Merge two options map.
1125
--
1126
-- `opts_a` and/or `opts_b` can be `nil`.
1127
--
1128
-- If `opts_a.foo` and `opts_b.foo` exists, prefer `opts_b.foo`.
1129
function utils.merge_options(opts_a, opts_b)
791✔
1130
    return fun.chain(opts_a or {}, opts_b or {}):tomap()
19,114✔
1131
end
1132

1133
local function lj_char_isident(n)
1134
    return bit.band(lj_char_bits[n + 2], LJ_CHAR_IDENT) == LJ_CHAR_IDENT
12,030✔
1135
end
1136

1137
local function lj_char_isdigit(n)
1138
    return bit.band(lj_char_bits[n + 2], LJ_CHAR_DIGIT) == LJ_CHAR_DIGIT
734✔
1139
end
1140

1141
function utils.check_name_isident(name)
791✔
1142
    dev_checks('string')
735✔
1143

1144
    -- sharding function name cannot
1145
    -- be equal to lua keyword
1146
    if LUA_KEYWORDS[name] then
735✔
1147
        return false
1✔
1148
    end
1149

1150
    -- sharding function name cannot
1151
    -- begin with a digit
1152
    local char_number = string.byte(name:sub(1,1))
1,468✔
1153
    if lj_char_isdigit(char_number) then
1,468✔
1154
        return false
1✔
1155
    end
1156

1157
    -- sharding func name must be sequence
1158
    -- of letters, digits, or underscore symbols
1159
    for i = 1, #name do
12,762✔
1160
        local char_number = string.byte(name:sub(i,i))
24,060✔
1161
        if not lj_char_isident(char_number) then
24,060✔
1162
            return false
1✔
1163
        end
1164
    end
1165

1166
    return true
732✔
1167
end
1168

1169
function utils.update_storage_call_error_description(err, func_name, replicaset_id)
791✔
1170
    if err == nil then
841✔
1171
        return nil
×
1172
    end
1173

1174
    if (err.type == 'ClientError' or err.type == 'AccessDeniedError' or err.type == 'LuajitError')
1,548✔
1175
        and type(err.message) == 'string' then
1,328✔
1176
        local not_defined_str = string.format("Procedure '%s' is not defined", func_name)
667✔
1177
        local not_registered_str = string.format("Function '%s' is not registered", func_name)
667✔
1178
        local access_denied_str = string.format("Execute access to function '%s' is denied", func_name)
667✔
1179
        if err.message == not_defined_str or err.message:startswith(access_denied_str)
1,989✔
1180
                or err.message:find(not_registered_str)
1,328✔
1181
                or err.message == "Procedure '_crud.call_on_storage' is not defined"
1,324✔
1182
                or err.message:startswith("Execute access to function '_crud.call_on_storage' is denied") then
1,975✔
1183
            if func_name:startswith('_crud.') then
20✔
1184
                err = NotInitializedError:new("Function '%s' is not registered: " ..
12✔
1185
                    "crud isn't initialized on replicaset %q or crud module versions mismatch " ..
6✔
1186
                    "between router and storage",
6✔
1187
                    func_name, replicaset_id or "Unknown")
12✔
1188
            else
1189
                err = NotInitializedError:new("Function '%s' is not registered", func_name)
8✔
1190
            end
1191
        end
1192
    end
1193
    return err
841✔
1194
end
1195

1196
--- Insert each value from values to list
1197
--
1198
-- @function list_extend
1199
--
1200
-- @param table list
1201
--  List to be extended
1202
--
1203
-- @param table values
1204
--  Values to be inserted to list
1205
--
1206
-- @return[1] list
1207
--  List with old values and inserted values
1208
function utils.list_extend(list, values)
791✔
1209
    dev_checks('table', 'table')
4,913✔
1210

1211
    for _, value in ipairs(values) do
120,568✔
1212
        table.insert(list, value)
115,655✔
1213
    end
1214

1215
    return list
4,913✔
1216
end
1217

1218
function utils.list_slice(list, start_index, end_index)
791✔
1219
    dev_checks('table', 'number', '?number')
48✔
1220

1221
    if end_index == nil then
48✔
1222
        end_index = table.maxn(list)
48✔
1223
    end
1224

1225
    local slice = {}
48✔
1226
    for i = start_index, end_index do
120✔
1227
        table.insert(slice, list[i])
72✔
1228
    end
1229

1230
    return slice
48✔
1231
end
1232

1233
local expected_vshard_api = {
791✔
1234
    'routeall', 'route', 'bucket_id_strcrc32',
1235
    'callrw', 'callro', 'callbro', 'callre',
1236
    'callbre', 'map_callrw'
1237
}
1238

1239
--- Verifies that a table has expected vshard
1240
--  router handles.
1241
local function verify_vshard_router(router)
1242
    dev_checks("table")
106✔
1243

1244
    for _, func_name in ipairs(expected_vshard_api) do
664✔
1245
        if type(router[func_name]) ~= 'function' then
602✔
1246
            return false
44✔
1247
        end
1248
    end
1249

1250
    return true
62✔
1251
end
1252

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

1270
    local router_instance
1271

1272
    if type(router) == 'string' then
143,565✔
1273
        if not is_cartridge then
70✔
1274
            return nil, VshardRouterError:new("Vshard groups are supported only in Tarantool Cartridge")
×
1275
        end
1276

1277
        local router_service = cartridge.service_get('vshard-router')
70✔
1278
        assert(router_service ~= nil)
70✔
1279

1280
        router_instance = router_service.get(router)
140✔
1281
        if router_instance == nil then
70✔
1282
            return nil, VshardRouterError:new("Vshard group %s is not found", router)
×
1283
        end
1284
    elseif type(router) == 'table' then
143,495✔
1285
        if not verify_vshard_router(router) then
212✔
1286
            return nil, VshardRouterError:new("Invalid opts.vshard_router table value, " ..
88✔
1287
                                              "a vshard router instance has been expected")
88✔
1288
        end
1289

1290
        router_instance = router
62✔
1291
    else
1292
        assert(type(router) == 'nil')
143,389✔
1293
        router_instance = vshard.router.static
143,389✔
1294

1295
        if router_instance == nil then
143,389✔
1296
            return nil, VshardRouterError:new("Default vshard group is not found and custom " ..
88✔
1297
                                              "is not specified with opts.vshard_router")
88✔
1298
        end
1299
    end
1300

1301
    return router_instance
143,477✔
1302
end
1303

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

1316
    return true, cartridge_hotreload
383✔
1317
end
1318

1319
if utils.tarantool_supports_intervals() then
1,582✔
1320
    -- https://github.com/tarantool/tarantool/blob/0510ffa07afd84a70c9c6f1a4c28aacd73a393d6/src/lua/datetime.lua#L175-179
1321
    local interval_t = ffi.typeof('struct interval')
791✔
1322

1323
    utils.is_interval = function(o)
1324
        return ffi.istype(interval_t, o)
20✔
1325
    end
1326
else
1327
    utils.is_interval = function()
1328
        return false
×
1329
    end
1330
end
1331

1332
for k, v in pairs(require('crud.common.vshard_utils')) do
9,492✔
1333
    utils[k] = v
7,119✔
1334
end
1335

1336
function utils.append_array(array_src, array_dst)
791✔
1337
    if not array_dst then
152,773✔
1338
        return array_src
11✔
1339
    end
1340

1341
    table.move(array_dst, 1, #array_dst, #array_src + 1, array_src)
152,762✔
1342

1343
    return array_src
152,762✔
1344
end
1345

1346
function utils.is_uint(value)
791✔
1347
    if type(value) == 'number' then
1,200✔
1348
        return value >= 0 and math.floor(value) == value
1,084✔
1349
    elseif type(value) == 'cdata' then
116✔
1350
        local ok, casted = pcall(tonumber, value)
8✔
1351
        return ok and type(casted) == 'number' and casted >= 0 and math.floor(casted) == casted
8✔
1352
    end
1353

1354
    return false
108✔
1355
end
1356

1357
return utils
791✔
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