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

tarantool / crud / 8541410078

03 Apr 2024 03:28PM UTC coverage: 88.686% (-0.04%) from 88.722%
8541410078

push

github

DifferentialOrange
ci: install newer vshard for reusable test

Tarantool 3 config requires vshard 0.1.25 or newer, while tests install
older versions by default (since crud is able to work with older
vshards).

At the same time, newer vshard can work with old Tarantools.

4852 of 5471 relevant lines covered (88.69%)

6076.59 hits per line

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

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

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

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

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

28
local utils = {}
331✔
29

30
utils.STORAGE_NAMESPACE = '_crud'
331✔
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)
331✔
38
    dev_checks('string')
8,674✔
39

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

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

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

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

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

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

123
    return nil, nil
×
124
end
125

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

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

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

158
        fiber.sleep(iter_sleep)
×
159
    end
160

161
    if replicaset == nil then
77,230✔
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})
154,460✔
168

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

187
function utils.get_space(space_name, vshard_router, timeout, replica_id)
331✔
188
    local spaces, err, schema_version = utils.get_spaces(vshard_router, timeout, replica_id)
77,214✔
189

190
    if spaces == nil then
77,214✔
191
        return nil, err
×
192
    end
193

194
    return spaces[space_name], err, schema_version
77,214✔
195
end
196

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

206
    local space_format = space:format()
5,535✔
207

208
    return space_format
5,535✔
209
end
210

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

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

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

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

258
    return space
10✔
259
end
260

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

298
    return space
8✔
299
end
300

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

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

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

341
function utils.flatten(object, space_format, bucket_id, skip_nullability_check)
331✔
342
    local flatten_func = flatten_functions_cache[space_format]
5,627✔
343
    if flatten_func ~= nil then
5,627✔
344
        local data, err = flatten_func(object, bucket_id, skip_nullability_check)
5,488✔
345
        if err ~= nil then
5,488✔
346
            return nil, FlattenError:new(err)
1,210✔
347
        end
348
        return data
4,883✔
349
    end
350

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

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

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

363
    local fieldmap = {}
139✔
364

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

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

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

404
function utils.unflatten(tuple, space_format)
331✔
405
    if tuple == nil then return nil end
10,128✔
406

407
    local object = {}
10,128✔
408

409
    for fieldno, field_format in ipairs(space_format) do
57,508✔
410
        local value = tuple[fieldno]
47,381✔
411

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

416
        object[field_format.name] = value
47,380✔
417
    end
418

419
    return object
10,127✔
420
end
421

422
function utils.extract_key(tuple, key_parts)
331✔
423
    local key = {}
179,446✔
424
    for i, part in ipairs(key_parts) do
359,603✔
425
        key[i] = tuple[part.fieldno]
180,157✔
426
    end
427
    return key
179,446✔
428
end
429

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

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

439
    for _, pk_part in ipairs(pk_parts) do
11,431✔
440
        if not key_fieldnos[pk_part.fieldno] then
6,176✔
441
            table.insert(merged_parts, pk_part)
2,935✔
442
        end
443
    end
444

445
    return merged_parts
5,255✔
446
end
447

448
function utils.enrich_field_names_with_cmp_key(field_names, key_parts, space_format)
331✔
449
    if field_names == nil then
3,546✔
450
        return nil
3,503✔
451
    end
452

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

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

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

469
    return enriched_field_names
43✔
470
end
471

472

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

478
    if suffix_candidate:find('^entrypoint$')
1,166✔
479
    or suffix_candidate:find('^alpha%d$')
1,166✔
480
    or suffix_candidate:find('^beta%d$')
1,165✔
481
    or suffix_candidate:find('^rc%d$') then
1,163✔
482
        return suffix_candidate
7✔
483
    end
484

485
    return nil
1,159✔
486
end
487

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

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

516
utils.get_version_suffix = get_version_suffix
331✔
517

518

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

525
local function get_version_suffix_weight(suffix)
526
    if suffix == nil then
11,714✔
527
        return 0
10,007✔
528
    end
529

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

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

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

564
    if major > major_to_compare then return true end
5,852✔
565
    if major < major_to_compare then return false end
5,837✔
566

567
    if minor > minor_to_compare then return true end
4,170✔
568
    if minor < minor_to_compare then return false end
431✔
569

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

601
utils.is_version_in_range = is_version_in_range
331✔
602

603

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

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

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

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

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

619
utils.get_tarantool_version = get_tarantool_version
331✔
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()
825✔
625

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

630
utils.tarantool_version_at_least = tarantool_version_at_least
331✔
631

632
function utils.is_enterprise_package()
331✔
633
    return tarantool.package == 'Tarantool Enterprise'
2,990✔
634
end
635

636

637
local enabled_tarantool_features = {}
331✔
638

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

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

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

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

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

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

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

784
determine_enabled_features()
331✔
785

786
for feature_name, feature_enabled in pairs(enabled_tarantool_features) do
4,965✔
787
    local util_name
788
    if feature_name == 'tarantool_3' then
4,303✔
789
        util_name = ('is_%s'):format(feature_name)
331✔
790
    elseif feature_name == 'builtin_merger' then
3,972✔
791
        util_name = ('tarantool_has_%s'):format(feature_name)
331✔
792
    elseif feature_name == 'role_privileges_not_revoked' then
3,641✔
793
        util_name = ('tarantool_%s'):format(feature_name)
331✔
794
    else
795
        util_name = ('tarantool_supports_%s'):format(feature_name)
3,310✔
796
    end
797

798
    local util_func = function() return feature_enabled end
19,335✔
799

800
    utils[util_name] = util_func
4,303✔
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
662✔
818
    function utils.is_field_not_found(err_code)
331✔
819
        return err_code == box.error.NO_SUCH_FIELD_NO or err_code == box.error.NO_SUCH_FIELD_NAME
48✔
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)
331✔
837
    if tuple == nil then
1✔
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
2✔
844
        local formatted_operations, err = utils.convert_operations(operations, space_format)
1✔
845
        if err ~= nil then
1✔
846
            return operations
1✔
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)
331✔
867
    local converted_operations = {}
1✔
868

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

879
            if field_id == nil then
1✔
880
                return nil, ParseOperationsError:new(
2✔
881
                        "Space format doesn't contain field named %q", operation[2])
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)
331✔
896
    if metadata == nil then
8,885✔
897
        return nil, UnflattenError:new('Metadata is not provided')
×
898
    end
899

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

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

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

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

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

940
    return bucket_id_index.parts[1].fieldno
179,692✔
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)
331✔
946
    dev_checks('table')
59✔
947

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

954
    return fieldno_map
59✔
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)
331✔
960
    dev_checks('table')
3,927✔
961

962
    local fieldno_map = {}
3,927✔
963
    for fieldno, field_format in ipairs(space_format) do
20,179✔
964
        fieldno_map[field_format.name] = fieldno
16,252✔
965
    end
966

967
    return fieldno_map
3,927✔
968
end
969

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

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

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

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

988
    return space_format_cache[space_format][field_name]
10✔
989
end
990

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

994
    local filtered_space_format = {}
91✔
995

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

1005
    return filtered_space_format
76✔
1006
end
1007

1008
function utils.get_fields_format(space_format, field_names)
331✔
1009
    dev_checks('table', '?table')
2,834✔
1010

1011
    if field_names == nil then
2,834✔
1012
        return table.copy(space_format)
2,802✔
1013
    end
1014

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

1017
    if err ~= nil then
32✔
1018
        return nil, err
1✔
1019
    end
1020

1021
    return filtered_space_format
31✔
1022
end
1023

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

1030
    if field_names == nil then
65,727✔
1031
        result.metadata = table.copy(space_format)
131,336✔
1032
        return result
65,668✔
1033
    end
1034

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

1037
    if err ~= nil then
59✔
1038
        return nil, err
14✔
1039
    end
1040

1041
    return result
45✔
1042
end
1043

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

1047
    if tuple_metadata == nil then
18✔
1048
        return nil
2✔
1049
    end
1050

1051
    local truncated_metadata = {}
16✔
1052

1053
    if #tuple_metadata < #field_names then
16✔
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
44✔
1060
        if tuple_metadata[i].name ~= name then
30✔
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])
28✔
1067
    end
1068

1069
    return truncated_metadata
14✔
1070
end
1071

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

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

1079
    return objs
4✔
1080
end
1081

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

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

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

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

1095
    return {
16✔
1096
        metadata = truncated_metadata,
16✔
1097
        rows = rows,
16✔
1098
    }
16✔
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)
5,636✔
1103
    if err ~= nil then
5,636✔
1104
        return nil, FlattenError:new("Failed to get space format: %s", err), const.NEED_SCHEMA_RELOAD
202✔
1105
    end
1106

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

1112
    return tuple
4,923✔
1113
end
1114

1115
function utils.flatten_obj_reload(vshard_router, space_name, obj, skip_nullability_check)
331✔
1116
    return schema.wrap_func_reload(vshard_router, flatten_obj, space_name, obj, skip_nullability_check)
5,263✔
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)
331✔
1125
    return fun.chain(opts_a or {}, opts_b or {}):tomap()
9,838✔
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
6,044✔
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
370✔
1134
end
1135

1136
function utils.check_name_isident(name)
331✔
1137
    dev_checks('string')
371✔
1138

1139
    -- sharding function name cannot
1140
    -- be equal to lua keyword
1141
    if LUA_KEYWORDS[name] then
371✔
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))
740✔
1148
    if lj_char_isdigit(char_number) then
740✔
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
6,412✔
1155
        local char_number = string.byte(name:sub(i,i))
12,088✔
1156
        if not lj_char_isident(char_number) then
12,088✔
1157
            return false
1✔
1158
        end
1159
    end
1160

1161
    return true
368✔
1162
end
1163

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

1169
    if (err.type == 'ClientError' or err.type == 'AccessDeniedError')
856✔
1170
        and type(err.message) == 'string' then
724✔
1171
        local not_defined_str = string.format("Procedure '%s' is not defined", func_name)
364✔
1172
        local access_denied_str = string.format("Execute access to function '%s' is denied", func_name)
364✔
1173
        if err.message == not_defined_str or err.message:startswith(access_denied_str) then
1,442✔
1174
            if func_name:startswith('_crud.') then
12✔
1175
                err = NotInitializedError:new("Function %s is not registered: " ..
8✔
1176
                    "crud isn't initialized on replicaset %q or crud module versions mismatch " ..
4✔
1177
                    "between router and storage",
4✔
1178
                    func_name, replicaset_id or "Unknown")
8✔
1179
            else
1180
                err = NotInitializedError:new("Function %s is not registered", func_name)
4✔
1181
            end
1182
        end
1183
    end
1184
    return err
466✔
1185
end
1186

1187
--- Insert each value from values to list
1188
--
1189
-- @function list_extend
1190
--
1191
-- @param table list
1192
--  List to be extended
1193
--
1194
-- @param table values
1195
--  Values to be inserted to list
1196
--
1197
-- @return[1] list
1198
--  List with old values and inserted values
1199
function utils.list_extend(list, values)
331✔
1200
    dev_checks('table', 'table')
2,238✔
1201

1202
    for _, value in ipairs(values) do
49,406✔
1203
        table.insert(list, value)
47,168✔
1204
    end
1205

1206
    return list
2,238✔
1207
end
1208

1209
function utils.list_slice(list, start_index, end_index)
331✔
1210
    dev_checks('table', 'number', '?number')
24✔
1211

1212
    if end_index == nil then
24✔
1213
        end_index = table.maxn(list)
24✔
1214
    end
1215

1216
    local slice = {}
24✔
1217
    for i = start_index, end_index do
60✔
1218
        table.insert(slice, list[i])
36✔
1219
    end
1220

1221
    return slice
24✔
1222
end
1223

1224
local expected_vshard_api = {
331✔
1225
    'routeall', 'route', 'bucket_id_strcrc32',
1226
    'callrw', 'callro', 'callbro', 'callre',
1227
    'callbre', 'map_callrw'
1228
}
1229

1230
--- Verifies that a table has expected vshard
1231
--  router handles.
1232
local function verify_vshard_router(router)
1233
    dev_checks("table")
106✔
1234

1235
    for _, func_name in ipairs(expected_vshard_api) do
664✔
1236
        if type(router[func_name]) ~= 'function' then
602✔
1237
            return false
44✔
1238
        end
1239
    end
1240

1241
    return true
62✔
1242
end
1243

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

1261
    local router_instance
1262

1263
    if type(router) == 'string' then
71,798✔
1264
        if not is_cartridge then
70✔
1265
            return nil, VshardRouterError:new("Vshard groups are supported only in Tarantool Cartridge")
×
1266
        end
1267

1268
        local router_service = cartridge.service_get('vshard-router')
70✔
1269
        assert(router_service ~= nil)
70✔
1270

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

1281
        router_instance = router
62✔
1282
    else
1283
        assert(type(router) == 'nil')
71,622✔
1284
        router_instance = vshard.router.static
71,622✔
1285

1286
        if router_instance == nil then
71,622✔
1287
            return nil, VshardRouterError:new("Default vshard group is not found and custom " ..
88✔
1288
                                              "is not specified with opts.vshard_router")
88✔
1289
        end
1290
    end
1291

1292
    return router_instance
71,710✔
1293
end
1294

1295
--- Check if Tarantool Cartridge hotreload supported
1296
--  and get its implementaion.
1297
--
1298
-- @function is_cartridge_hotreload_supported
1299
--
1300
-- @return[1] true or false
1301
-- @return[1] module table, if supported
1302
function utils.is_cartridge_hotreload_supported()
331✔
1303
    if not is_cartridge_hotreload then
218✔
1304
        return false
×
1305
    end
1306

1307
    return true, cartridge_hotreload
218✔
1308
end
1309

1310
if utils.tarantool_supports_intervals() then
662✔
1311
    -- https://github.com/tarantool/tarantool/blob/0510ffa07afd84a70c9c6f1a4c28aacd73a393d6/src/lua/datetime.lua#L175-179
1312
    local interval_t = ffi.typeof('struct interval')
331✔
1313

1314
    utils.is_interval = function(o)
1315
        return ffi.istype(interval_t, o)
12✔
1316
    end
1317
else
1318
    utils.is_interval = function()
1319
        return false
×
1320
    end
1321
end
1322

1323
for k, v in pairs(require('crud.common.vshard_utils')) do
2,979✔
1324
    utils[k] = v
1,986✔
1325
end
1326

1327
return utils
331✔
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