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

tarantool / crud / 8537556735

03 Apr 2024 11:01AM UTC coverage: 88.686% (-0.07%) from 88.753%
8537556735

push

github

DifferentialOrange
roles: introduce router configuration

This patch introduces `roles.crud-router` role configuration through
`roles_cfg`, similar to existing Cartridge clusterwide configuration
support. For now, storages don't have any configuration, so they remain
unchanged.

After this patch, Tarantool 3 roles have all features supported in
Cartridge roles.

Closes #415

4852 of 5471 relevant lines covered (88.69%)

6110.42 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")
414✔
106

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

114
local function get_replicaset_by_replica_id(replicasets, id)
115
    for replicaset_id, replicaset in pairs(replicasets) do
26✔
116
        for replica_id, _ in pairs(replicaset.replicas) do
59✔
117
            if replica_id == id then
34✔
118
                return replicaset_id, replicaset
9✔
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,511✔
130
    local deadline = fiber.clock() + timeout
155,022✔
131
    local iter_sleep = math.min(timeout / 100, 0.1)
77,511✔
132
    while (
133
        -- Break if the deadline condition is exceeded.
134
        -- Handling for deadline errors are below in the code.
135
        fiber.clock() < deadline
155,022✔
136
    ) do
77,511✔
137
        -- Try to get master with timeout.
138
        replicasets = vshard_router:routeall()
155,022✔
139
        if replica_id ~= nil then
77,511✔
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)
18✔
145
            break
9✔
146
        else
147
            replicaset_id, replicaset = next(replicasets)
77,502✔
148
        end
149

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

158
        fiber.sleep(iter_sleep)
×
159
    end
160

161
    if replicaset == nil then
77,511✔
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})
155,022✔
168

169
    if master == nil then
77,511✔
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,511✔
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,511✔
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,495✔
189

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

194
    return spaces[space_name], err, schema_version
77,495✔
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)
9✔
240
        if ok then
9✔
241
            latest_space, err = utils.get_space(space_name, vshard_router,
18✔
242
                                                opts.timeout, replica_id)
18✔
243
            if err ~= nil then
9✔
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
9✔
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
9✔
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,965✔
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
734✔
366
        fieldmap[field.name] = true
595✔
367
        if field.name ~= 'bucket_id' then
595✔
368
            append(lines, 'if object[%q] ~= nil then', field.name)
456✔
369
            append(lines, '    result[%d] = object[%q]', i, field.name)
456✔
370
            if field.is_nullable ~= true then
456✔
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')
912✔
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 = {}
180,107✔
424
    for i, part in ipairs(key_parts) do
360,925✔
425
        key[i] = tuple[part.fieldno]
180,818✔
426
    end
427
    return key
180,107✔
428
end
429

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

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

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

445
    return merged_parts
5,349✔
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,612✔
450
        return nil
3,569✔
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,512✔
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'
180,358✔
935
    local bucket_id_index = space.index[shard_index_name]
180,358✔
936
    if bucket_id_index == nil then
180,358✔
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
180,352✔
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,993✔
961

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

967
    return fieldno_map
3,993✔
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,900✔
1010

1011
    if field_names == nil then
2,900✔
1012
        return table.copy(space_format)
2,868✔
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,857✔
1026
    local err
1027
    local space_format = space:format()
65,857✔
1028
    result.rows = rows
65,857✔
1029

1030
    if field_names == nil then
65,857✔
1031
        result.metadata = table.copy(space_format)
131,596✔
1032
        return result
65,798✔
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
467✔
1166
        return nil
×
1167
    end
1168

1169
    if (err.type == 'ClientError' or err.type == 'AccessDeniedError')
857✔
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
467✔
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,433✔
1201

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

1206
    return list
2,433✔
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')
72,082✔
1260

1261
    local router_instance
1262

1263
    if type(router) == 'string' then
72,082✔
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
72,012✔
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,906✔
1284
        router_instance = vshard.router.static
71,906✔
1285

1286
        if router_instance == nil then
71,906✔
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,994✔
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