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

tarantool / crud / 29342538535

14 Jul 2026 02:48PM UTC coverage: 88.268% (-0.05%) from 88.313%
29342538535

push

github

p0rtale
fix: allow read-only operations when all masters are down

Read-only operations (get, select, pairs, count, min, max) used to
fail with connection errors if all masters in the cluster were unavailable,
even when healthy replicas were up and failover hadn't processed yet.

To resolve this, the following improvements were made:
- Introduced a `read_only` flag to `utils.get_space[s]` to fetch cluster
  schema from any healthy replica if masters are down.
- Updated `get`, `select`, `pairs`, `count`, `min`, `max` to use this
  new flag.
- Rewrote `call.any` to iterate through all replicasets and utilize
  vshard's `callro` instead of `call` to fetch metadata from replicas.

55 of 57 new or added lines in 16 files covered. (96.49%)

5 existing lines in 1 file now uncovered.

5274 of 5975 relevant lines covered (88.27%)

13209.62 hits per line

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

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

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

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

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

28
local utils = {}
801✔
29

30
utils.STORAGE_NAMESPACE = '_crud'
801✔
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)
801✔
38
    dev_checks('string')
22,820✔
39

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

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

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

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

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

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

123
    return nil, nil
×
124
end
125

126
local function find_any_healthy_replica_conn(replicasets)
127
    for _, replicaset in pairs(replicasets) do
24✔
128
        for _, replica in pairs(replicaset.replicas) do
24✔
129
            if replica:is_connected() then
24✔
130
                return replica.conn
12✔
131
            end
132
        end
133
    end
NEW
134
    return nil
×
135
end
136

137
--- Returns all spaces existing in the schema.
138
--
139
-- @function get_spaces
140
--
141
-- @param table vshard_router
142
--  A vshard router instance to route requests.
143
--
144
-- @tparam ?number opts.timeout
145
--  Function call timeout. If not specified, `const.DEFAULT_VSHARD_CALL_TIMEOUT` is used.
146
--
147
-- @tparam ?boolean opts.read_only
148
--  If true, the function will try to fetch spaces from any available healthy replica.
149
--
150
-- @tparam ?string opts.replica_id
151
--  The exact replica ID to fetch the replicaset from.
152
--
153
-- @return[1] table map of spaces
154
-- @return[1] nil
155
-- @return[1] number schema version
156
-- @treturn[2] nil
157
-- @treturn[2] table Error description
158
function utils.get_spaces(vshard_router, opts)
801✔
159
    local replicasets, replicaset, replicaset_id, master, ro_replica_conn
160

161
    opts = opts or {}
159,927✔
162

163
    local timeout = opts.timeout or const.DEFAULT_VSHARD_CALL_TIMEOUT
159,927✔
164
    local deadline = fiber.clock() + timeout
319,854✔
165
    local iter_sleep = math.min(timeout / 100, 0.1)
159,927✔
166
    while (
167
        -- Break if the deadline condition is exceeded.
168
        -- Handling for deadline errors are below in the code.
169
        fiber.clock() < deadline
319,854✔
170
    ) do
159,927✔
171
        -- Try to get master with timeout.
172
        replicasets = vshard_router:routeall()
319,854✔
173
        if opts.replica_id ~= nil then
159,927✔
174
            -- Get the same replica on which the last DML operation was performed.
175
            -- This approach is temporary and is related to [1], [2].
176
            -- [1] https://github.com/tarantool/crud/issues/236
177
            -- [2] https://github.com/tarantool/crud/issues/361
NEW
178
            replicaset_id, replicaset = get_replicaset_by_replica_id(replicasets, opts.replica_id)
×
179
            break
180
        else
181
            replicaset_id, replicaset = next(replicasets)
159,927✔
182
        end
183

184
        if replicaset ~= nil then
159,927✔
185
            -- Get cached, reload (if required) will be processed in other place.
186
            master = utils.get_replicaset_master(replicaset, {cached = true})
319,854✔
187
            if master ~= nil and master.conn.error == nil then
159,927✔
188
                break
159,915✔
189
            end
190
        end
191

192
        -- If the master check above didn't succeed (or master is dead),
193
        -- and this is a read-only operation, try to find any available healthy replica.
194
        if opts.read_only then
12✔
195
            ro_replica_conn = find_any_healthy_replica_conn(replicasets)
24✔
196
            if ro_replica_conn ~= nil then
12✔
197
                break
12✔
198
            end
199
        end
200

UNCOV
201
        fiber.sleep(iter_sleep)
×
202
    end
203

204
    if opts.read_only and ro_replica_conn ~= nil then
159,927✔
205
        return ro_replica_conn.space, nil, ro_replica_conn.schema_version
12✔
206
    end
207

208
    if replicaset == nil then
159,915✔
209
        return nil, GetSpaceError:new(
×
210
            'The router returned empty replicasets: ' ..
×
211
            'perhaps other instances are unavailable or you have configured only the router')
×
212
    end
213

214
    master = utils.get_replicaset_master(replicaset, {cached = true})
319,830✔
215

216
    if master == nil then
159,915✔
217
        local error_msg = string.format(
×
218
            'The master was not found in replicaset %s, ' ..
×
219
            'check status of the master and repeat the operation later',
220
             replicaset_id)
×
221
        return nil, GetSpaceError:new(error_msg)
×
222
    end
223

224
    if master.conn.error ~= nil then
159,915✔
225
        local error_msg = string.format(
×
226
            'The connection to the master of replicaset %s is not valid: %s',
227
             replicaset_id, master.conn.error)
×
228
        return nil, GetSpaceError:new(error_msg)
×
229
    end
230

231
    return master.conn.space, nil, master.conn.schema_version
159,915✔
232
end
233

234
--- Returns a specific space by its name.
235
--
236
-- @function get_space
237
--
238
-- @param string space_name
239
--  A space name to fetch.
240
--
241
-- @param table vshard_router
242
--  A vshard router instance to route requests.
243
--
244
-- @tparam ?number opts.timeout
245
--  Function call timeout.
246
--
247
-- @tparam ?boolean opts.read_only
248
--  If true, the function will try to fetch the space from any available healthy replica.
249
--
250
-- @tparam ?string opts.replica_id
251
--  The exact replica ID to fetch the replicaset from.
252
--
253
-- @return[1] table space object
254
-- @return[1] nil
255
-- @return[1] number schema version
256
-- @treturn[2] nil
257
-- @treturn[2] table Error description
258
function utils.get_space(space_name, vshard_router, opts)
801✔
259
    local spaces, err, schema_version = utils.get_spaces(vshard_router, opts)
159,895✔
260

261
    if spaces == nil then
159,895✔
262
        return nil, err
×
263
    end
264

265
    return spaces[space_name], err, schema_version
159,895✔
266
end
267

268
function utils.get_space_format(space_name, vshard_router)
801✔
269
    local space, err = utils.get_space(space_name, vshard_router)
10,844✔
270
    if err ~= nil then
10,844✔
271
        return nil, GetSpaceFormatError:new("An error occurred during the operation: %s", err)
×
272
    end
273
    if space == nil then
10,844✔
274
        return nil, GetSpaceFormatError:new("Space %q doesn't exist", space_name)
332✔
275
    end
276

277
    local space_format = space:format()
10,678✔
278

279
    return space_format
10,678✔
280
end
281

282
function utils.fetch_latest_metadata_when_single_storage(space, space_name, netbox_schema_version,
801✔
283
                                                         vshard_router, opts, storage_info)
284
    -- Checking the relevance of the schema version is necessary
285
    -- to prevent the irrelevant metadata of the DML operation.
286
    -- This approach is temporary and is related to [1], [2].
287
    -- [1] https://github.com/tarantool/crud/issues/236
288
    -- [2] https://github.com/tarantool/crud/issues/361
289
    local latest_space, err
290

291
    assert(storage_info.replica_schema_version ~= nil,
40✔
292
           'check the replica_schema_version value from storage ' ..
20✔
293
           'for correct use of the fetch_latest_metadata opt')
20✔
294

295
    local replica_id
296
    if storage_info.replica_id == nil then -- Backward compatibility.
20✔
297
        assert(storage_info.replica_uuid ~= nil,
×
298
               'check the replica_uuid value from storage ' ..
×
299
               'for correct use of the fetch_latest_metadata opt')
×
300
        replica_id = storage_info.replica_uuid
×
301
    else
302
        replica_id = storage_info.replica_id
20✔
303
    end
304

305
    assert(netbox_schema_version ~= nil,
40✔
306
           'check the netbox_schema_version value from net_box conn on router ' ..
20✔
307
           'for correct use of the fetch_latest_metadata opt')
20✔
308

309
    if storage_info.replica_schema_version ~= netbox_schema_version then
20✔
310
        local ok, reload_schema_err = schema.reload_schema(vshard_router)
20✔
311
        if ok then
20✔
312
            latest_space, err = utils.get_space(space_name, vshard_router,
40✔
313
                                                opts.timeout, replica_id)
40✔
314
            if err ~= nil then
20✔
315
                local warn_msg = "Failed to fetch space for latest schema actualization, metadata may be outdated: %s"
×
316
                log.warn(warn_msg, err)
×
317
            end
318
            if latest_space == nil then
20✔
319
                log.warn("Failed to find space for latest schema actualization, metadata may be outdated")
×
320
            end
321
        else
322
            log.warn("Failed to reload schema, metadata may be outdated: %s", reload_schema_err)
×
323
        end
324
    end
325
    if err == nil and latest_space ~= nil then
20✔
326
        space = latest_space
20✔
327
    end
328

329
    return space
20✔
330
end
331

332
function utils.fetch_latest_metadata_when_map_storages(space, space_name, vshard_router, opts,
801✔
333
                                                       storages_info, netbox_schema_version)
334
    -- Checking the relevance of the schema version is necessary
335
    -- to prevent the irrelevant metadata of the DML operation.
336
    -- This approach is temporary and is related to [1], [2].
337
    -- [1] https://github.com/tarantool/crud/issues/236
338
    -- [2] https://github.com/tarantool/crud/issues/361
339
    local latest_space, err
340
    for _, storage_info in pairs(storages_info) do
32✔
341
        assert(storage_info.replica_schema_version ~= nil,
32✔
342
            'check the replica_schema_version value from storage ' ..
16✔
343
            'for correct use of the fetch_latest_metadata opt')
16✔
344
        assert(netbox_schema_version ~= nil,
32✔
345
               'check the netbox_schema_version value from net_box conn on router ' ..
16✔
346
               'for correct use of the fetch_latest_metadata opt')
16✔
347
        if storage_info.replica_schema_version ~= netbox_schema_version then
16✔
348
            local ok, reload_schema_err = schema.reload_schema(vshard_router)
16✔
349
            if ok then
16✔
350
                latest_space, err = utils.get_space(space_name, vshard_router, opts.timeout)
32✔
351
                if err ~= nil then
16✔
352
                    local warn_msg = "Failed to fetch space for latest schema actualization, " ..
×
353
                                     "metadata may be outdated: %s"
354
                    log.warn(warn_msg, err)
×
355
                end
356
                if latest_space == nil then
16✔
357
                    log.warn("Failed to find space for latest schema actualization, metadata may be outdated")
×
358
                end
359
            else
360
                log.warn("Failed to reload schema, metadata may be outdated: %s", reload_schema_err)
×
361
            end
362
            if err == nil and latest_space ~= nil then
16✔
363
                space = latest_space
16✔
364
            end
365
            break
16✔
366
        end
367
    end
368

369
    return space
16✔
370
end
371

372
function utils.fetch_latest_metadata_for_select(space_name, vshard_router, opts,
801✔
373
                                                storages_info, iter)
374
    -- Checking the relevance of the schema version is necessary
375
    -- to prevent the irrelevant metadata of the DML operation.
376
    -- This approach is temporary and is related to [1], [2].
377
    -- [1] https://github.com/tarantool/crud/issues/236
378
    -- [2] https://github.com/tarantool/crud/issues/361
379
    for _, storage_info in pairs(storages_info) do
8✔
380
        assert(storage_info.replica_schema_version ~= nil,
8✔
381
               'check the replica_schema_version value from storage ' ..
4✔
382
               'for correct use of the fetch_latest_metadata opt')
4✔
383
        assert(iter.netbox_schema_version ~= nil,
8✔
384
               'check the netbox_schema_version value from net_box conn on router ' ..
4✔
385
               'for correct use of the fetch_latest_metadata opt')
4✔
386
        if storage_info.replica_schema_version ~= iter.netbox_schema_version then
4✔
387
            local ok, reload_schema_err = schema.reload_schema(vshard_router)
4✔
388
            if ok then
4✔
389
                local err
390
                iter.space, err = utils.get_space(space_name, vshard_router, opts.timeout)
8✔
391
                if err ~= nil then
4✔
392
                    local warn_msg = "Failed to fetch space for latest schema actualization, " ..
×
393
                                     "metadata may be outdated: %s"
394
                    log.warn(warn_msg, err)
×
395
                end
396
            else
397
                log.warn("Failed to reload schema, metadata may be outdated: %s", reload_schema_err)
×
398
            end
399
            break
400
        end
401
    end
402

403
    return iter
4✔
404
end
405

406
local function append(lines, s, ...)
407
    table.insert(lines, string.format(s, ...))
7,802✔
408
end
409

410
local flatten_functions_cache = setmetatable({}, {__mode = 'k'})
801✔
411

412
function utils.flatten(object, space_format, bucket_id, skip_nullability_check)
801✔
413
    local flatten_func = flatten_functions_cache[space_format]
10,856✔
414
    if flatten_func ~= nil then
10,856✔
415
        local data, err = flatten_func(object, bucket_id, skip_nullability_check)
10,580✔
416
        if err ~= nil then
10,580✔
417
            return nil, FlattenError:new(err)
2,062✔
418
        end
419
        return data
9,549✔
420
    end
421

422
    local lines = {}
276✔
423
    append(lines, 'local object, bucket_id, skip_nullability_check = ...')
276✔
424

425
    append(lines, 'for k in pairs(object) do')
276✔
426
    append(lines, '    if fieldmap[k] == nil then')
276✔
427
    append(lines, '        return nil, format(\'Unknown field %%q is specified\', k)')
276✔
428
    append(lines, '    end')
276✔
429
    append(lines, 'end')
276✔
430

431
    local len = #space_format
276✔
432
    append(lines, 'local result = {%s}', string.rep('NULL,', len))
276✔
433

434
    local fieldmap = {}
276✔
435

436
    for i, field in ipairs(space_format) do
1,444✔
437
        fieldmap[field.name] = true
1,168✔
438
        if field.name ~= 'bucket_id' then
1,168✔
439
            append(lines, 'if object[%q] ~= nil then', field.name)
892✔
440
            append(lines, '    result[%d] = object[%q]', i, field.name)
892✔
441
            if field.is_nullable ~= true then
892✔
442
                append(lines, 'elseif skip_nullability_check ~= true then')
769✔
443
                append(lines, '    return nil, \'Field %q isn\\\'t nullable' ..
1,538✔
444
                              ' (set skip_nullability_check_on_flatten option to true to skip check)\'',
769✔
445
                              field.name)
769✔
446
            end
447
            append(lines, 'end')
1,784✔
448
        else
449
            append(lines, 'if bucket_id ~= nil then')
276✔
450
            append(lines, '    result[%d] = bucket_id', i, field.name)
276✔
451
            append(lines, 'else')
276✔
452
            append(lines, '    result[%d] = object[%q]', i, field.name)
276✔
453
            append(lines, 'end')
276✔
454
        end
455
    end
456
    append(lines, 'return result')
276✔
457

458
    local code = table.concat(lines, '\n')
276✔
459
    local env = {
276✔
460
        pairs = pairs,
276✔
461
        format = string.format,
276✔
462
        fieldmap = fieldmap,
276✔
463
        NULL = box.NULL,
276✔
464
    }
465
    flatten_func = assert(load(code, nil, 't', env))
276✔
466

467
    flatten_functions_cache[space_format] = flatten_func
276✔
468
    local data, err = flatten_func(object, bucket_id, skip_nullability_check)
276✔
469
    if err ~= nil then
276✔
470
        return nil, FlattenError:new(err)
30✔
471
    end
472
    return data
261✔
473
end
474

475
function utils.unflatten(tuple, space_format)
801✔
476
    if tuple == nil then return nil end
20,261✔
477

478
    local object = {}
20,261✔
479

480
    for fieldno, field_format in ipairs(space_format) do
114,758✔
481
        local value = tuple[fieldno]
94,498✔
482

483
        if not field_format.is_nullable and value == nil then
94,498✔
484
            return nil, UnflattenError:new("Field %s isn't nullable", fieldno)
2✔
485
        end
486

487
        object[field_format.name] = value
94,497✔
488
    end
489

490
    return object
20,260✔
491
end
492

493
function utils.extract_key(tuple, key_parts)
801✔
494
    local key = {}
366,634✔
495
    for i, part in ipairs(key_parts) do
734,688✔
496
        key[i] = tuple[part.fieldno]
368,054✔
497
    end
498
    return key
366,634✔
499
end
500

501
function utils.merge_primary_key_parts(key_parts, pk_parts)
801✔
502
    local merged_parts = {}
9,160✔
503
    local key_fieldnos = {}
9,160✔
504

505
    for _, part in ipairs(key_parts) do
18,540✔
506
        table.insert(merged_parts, part)
9,380✔
507
        key_fieldnos[part.fieldno] = true
9,380✔
508
    end
509

510
    for _, pk_part in ipairs(pk_parts) do
20,008✔
511
        if not key_fieldnos[pk_part.fieldno] then
10,848✔
512
            table.insert(merged_parts, pk_part)
4,633✔
513
        end
514
    end
515

516
    return merged_parts
9,160✔
517
end
518

519
function utils.enrich_field_names_with_cmp_key(field_names, key_parts, space_format)
801✔
520
    if field_names == nil then
9,037✔
521
        return nil
8,952✔
522
    end
523

524
    local enriched_field_names = {}
85✔
525
    local key_field_names = {}
85✔
526

527
    for _, field_name in ipairs(field_names) do
251✔
528
        table.insert(enriched_field_names, field_name)
166✔
529
        key_field_names[field_name] = true
166✔
530
    end
531

532
    for _, part in ipairs(key_parts) do
223✔
533
        local field_name = space_format[part.fieldno].name
138✔
534
        if not key_field_names[field_name] then
138✔
535
            table.insert(enriched_field_names, field_name)
108✔
536
            key_field_names[field_name] = true
108✔
537
        end
538
    end
539

540
    return enriched_field_names
85✔
541
end
542

543

544
local function get_version_suffix(suffix_candidate)
545
    if type(suffix_candidate) ~= 'string' then
2,762✔
546
        return nil
×
547
    end
548

549
    if suffix_candidate:find('^entrypoint$')
2,762✔
550
    or suffix_candidate:find('^alpha%d$')
2,762✔
551
    or suffix_candidate:find('^beta%d$')
2,761✔
552
    or suffix_candidate:find('^rc%d$') then
2,759✔
553
        return suffix_candidate
7✔
554
    end
555

556
    return nil
2,755✔
557
end
558

559
local function get_commits_since_from_version_part(commits_since_candidate)
560
    if commits_since_candidate == nil then
2,753✔
561
        return 0
×
562
    end
563

564
    local ok, val = pcall(tonumber, commits_since_candidate)
2,753✔
565
    if ok then
2,753✔
566
        return val
2,753✔
567
    else
568
        -- It may be unknown suffix instead.
569
        -- Since suffix already unknown, there is no way to properly compare versions.
570
        return 0
×
571
    end
572
end
573

574
local function get_commits_since(suffix, commits_since_candidate_1, commits_since_candidate_2)
575
    -- x.x.x.-candidate_1-candidate_2
576

577
    if suffix ~= nil then
2,753✔
578
        -- X.Y.Z-suffix-N
579
        return get_commits_since_from_version_part(commits_since_candidate_2)
×
580
    else
581
        -- X.Y.Z-N
582
        -- Possibly X.Y.Z-suffix-N with unknown suffix
583
        return get_commits_since_from_version_part(commits_since_candidate_1)
2,753✔
584
    end
585
end
586

587
utils.get_version_suffix = get_version_suffix
801✔
588

589

590
local suffix_with_digit_weight = {
801✔
591
    alpha = -3000,
592
    beta  = -2000,
593
    rc    = -1000,
594
}
595

596
local function get_version_suffix_weight(suffix)
597
    if suffix == nil then
29,668✔
598
        return 0
25,611✔
599
    end
600

601
    if suffix:find('^entrypoint$') then
4,057✔
602
        return -math.huge
1,611✔
603
    end
604

605
    for header, weight in pairs(suffix_with_digit_weight) do
8,140✔
606
        local pos, _, digits = suffix:find('^' .. header .. '(%d)$')
5,692✔
607
        if pos ~= nil then
5,692✔
608
            return weight + tonumber(digits)
2,444✔
609
        end
610
    end
611

612
    UtilsInternalError:assert(false,
4✔
613
        'Unexpected suffix %q, parse with "utils.get_version_suffix" first', suffix)
2✔
614
end
615

616
utils.get_version_suffix_weight = get_version_suffix_weight
801✔
617

618

619
local function is_version_ge(major, minor,
620
                             patch, suffix, commits_since,
621
                             major_to_compare, minor_to_compare,
622
                             patch_to_compare, suffix_to_compare, commits_since_to_compare)
623
    major = major or 0
14,829✔
624
    minor = minor or 0
14,829✔
625
    patch = patch or 0
14,829✔
626
    local suffix_weight = get_version_suffix_weight(suffix)
14,829✔
627
    commits_since = commits_since or 0
14,829✔
628

629
    major_to_compare = major_to_compare or 0
14,829✔
630
    minor_to_compare = minor_to_compare or 0
14,829✔
631
    patch_to_compare = patch_to_compare or 0
14,829✔
632
    local suffix_weight_to_compare = get_version_suffix_weight(suffix_to_compare)
14,829✔
633
    commits_since_to_compare = commits_since_to_compare or 0
14,829✔
634

635
    if major > major_to_compare then return true end
14,829✔
636
    if major < major_to_compare then return false end
14,814✔
637

638
    if minor > minor_to_compare then return true end
10,748✔
639
    if minor < minor_to_compare then return false end
950✔
640

641
    if patch > patch_to_compare then return true end
943✔
642
    if patch < patch_to_compare then return false end
20✔
643

644
    if suffix_weight > suffix_weight_to_compare then return true end
18✔
645
    if suffix_weight < suffix_weight_to_compare then return false end
13✔
646

647
    if commits_since > commits_since_to_compare then return true end
8✔
648
    if commits_since < commits_since_to_compare then return false end
7✔
649

650
    return true
6✔
651
end
652

653
utils.is_version_ge = is_version_ge
801✔
654

655

656
local function is_version_in_range(major, minor,
657
                                   patch, suffix, commits_since,
658
                                   major_left_side, minor_left_side,
659
                                   patch_left_side, suffix_left_side, commits_since_left_side,
660
                                   major_right_side, minor_right_side,
661
                                   patch_right_side, suffix_right_side, commits_since_right_side)
662
    return is_version_ge(major, minor,
3,210✔
663
                         patch, suffix, commits_since,
1,605✔
664
                         major_left_side, minor_left_side,
1,605✔
665
                         patch_left_side, suffix_left_side, commits_since_left_side)
1,605✔
666
       and is_version_ge(major_right_side, minor_right_side,
1,608✔
667
                         patch_right_side, suffix_right_side, commits_since_right_side,
3✔
668
                         major, minor,
3✔
669
                         patch, suffix, commits_since)
1,608✔
670
end
671

672
utils.is_version_in_range = is_version_in_range
801✔
673

674

675
local function get_tarantool_version()
676
    local version_parts = rawget(_G, '_TARANTOOL'):split('-', 3)
2,753✔
677

678
    local major_minor_patch_parts = version_parts[1]:split('.', 2)
2,753✔
679
    local major = tonumber(major_minor_patch_parts[1])
2,753✔
680
    local minor = tonumber(major_minor_patch_parts[2])
2,753✔
681
    local patch = tonumber(major_minor_patch_parts[3])
2,753✔
682

683
    local suffix = get_version_suffix(version_parts[2])
2,753✔
684

685
    local commits_since = get_commits_since(suffix, version_parts[2], version_parts[3])
2,753✔
686

687
    return major, minor, patch, suffix, commits_since
2,753✔
688
end
689

690
utils.get_tarantool_version = get_tarantool_version
801✔
691

692

693
local function tarantool_version_at_least(wanted_major, wanted_minor,
694
                                          wanted_patch, wanted_suffix, wanted_commits_since)
695
    local major, minor, patch, suffix, commits_since = get_tarantool_version()
1,951✔
696

697
    return is_version_ge(major, minor, patch, suffix, commits_since,
1,951✔
698
                         wanted_major, wanted_minor, wanted_patch, wanted_suffix, wanted_commits_since)
1,951✔
699
end
700

701
utils.tarantool_version_at_least = tarantool_version_at_least
801✔
702

703
function utils.is_enterprise_package()
801✔
704
    return tarantool.package == 'Tarantool Enterprise'
7,225✔
705
end
706

707

708
local enabled_tarantool_features = {}
801✔
709

710
local function determine_enabled_features()
711
    local major, minor, patch, suffix, commits_since = get_tarantool_version()
801✔
712

713
    -- since Tarantool 2.3.1
714
    enabled_tarantool_features.fieldpaths = is_version_ge(major, minor, patch, suffix, commits_since,
1,602✔
715
                                                          2, 3, 1, nil, nil)
1,602✔
716

717
    -- Full support (Lua type, space format type and indexes) for decimal type
718
    -- is since Tarantool 2.3.1 [1]
719
    --
720
    -- [1] https://github.com/tarantool/tarantool/commit/485439e33196e26d120e622175f88b4edc7a5aa1
721
    enabled_tarantool_features.decimals = is_version_ge(major, minor, patch, suffix, commits_since,
1,602✔
722
                                                        2, 3, 1, nil, nil)
1,602✔
723

724
    -- Full support (Lua type, space format type and indexes) for uuid type
725
    -- is since Tarantool 2.4.1 [1]
726
    --
727
    -- [1] https://github.com/tarantool/tarantool/commit/b238def8065d20070dcdc50b54c2536f1de4c7c7
728
    enabled_tarantool_features.uuids = is_version_ge(major, minor, patch, suffix, commits_since,
1,602✔
729
                                                     2, 4, 1, nil, nil)
1,602✔
730

731
    -- Full support (Lua type, space format type and indexes) for datetime type
732
    -- is since Tarantool 2.10.0-beta2 [1]
733
    --
734
    -- [1] https://github.com/tarantool/tarantool/commit/3bd870261c462416c29226414fe0a2d79aba0c74
735
    enabled_tarantool_features.datetimes = is_version_ge(major, minor, patch, suffix, commits_since,
1,602✔
736
                                                         2, 10, 0, 'beta2', nil)
1,602✔
737

738
    -- Full support (Lua type, space format type and indexes) for datetime type
739
    -- is since Tarantool 2.10.0-rc1 [1]
740
    --
741
    -- [1] https://github.com/tarantool/tarantool/commit/38f0c904af4882756c6dc802f1895117d3deae6a
742
    enabled_tarantool_features.intervals = is_version_ge(major, minor, patch, suffix, commits_since,
1,602✔
743
                                                         2, 10, 0, 'rc1', nil)
1,602✔
744

745
    -- since Tarantool 2.6.3 / 2.7.2 / 2.8.1
746
    enabled_tarantool_features.jsonpath_indexes = is_version_ge(major, minor, patch, suffix, commits_since,
1,602✔
747
                                                                2, 8, 1, nil, nil)
801✔
748
                                               or is_version_in_range(major, minor, patch, suffix, commits_since,
801✔
749
                                                                      2, 7, 2, nil, nil,
750
                                                                      2, 7, math.huge, nil, nil)
×
751
                                               or is_version_in_range(major, minor, patch, suffix, commits_since,
×
752
                                                                      2, 6, 3, nil, nil,
753
                                                                      2, 6, math.huge, nil, nil)
801✔
754

755
    -- The merger module was implemented in 2.2.1, see [1].
756
    -- However it had the critical problem [2], which leads to
757
    -- segfault at attempt to use the module from a fiber serving
758
    -- iproto request. So we don't use it in versions before the
759
    -- fix.
760
    --
761
    -- [1]: https://github.com/tarantool/tarantool/issues/3276
762
    -- [2]: https://github.com/tarantool/tarantool/issues/4954
763
    enabled_tarantool_features.builtin_merger = is_version_ge(major, minor, patch, suffix, commits_since,
1,602✔
764
                                                              2, 6, 0, nil, nil)
801✔
765
                                             or is_version_in_range(major, minor, patch, suffix, commits_since,
801✔
766
                                                                    2, 5, 1, nil, nil,
767
                                                                    2, 5, math.huge, nil, nil)
×
768
                                             or is_version_in_range(major, minor, patch, suffix, commits_since,
×
769
                                                                    2, 4, 2, nil, nil,
770
                                                                    2, 4, math.huge, nil, nil)
×
771
                                             or is_version_in_range(major, minor, patch, suffix, commits_since,
×
772
                                                                    2, 3, 3, nil, nil,
773
                                                                    2, 3, math.huge, nil, nil)
801✔
774

775
    -- The external merger module leans on a set of relatively
776
    -- new APIs in tarantool. So it works only on tarantool
777
    -- versions, which offer those APIs.
778
    --
779
    -- See README of the module:
780
    -- https://github.com/tarantool/tuple-merger
781
    enabled_tarantool_features.external_merger = is_version_ge(major, minor, patch, suffix, commits_since,
1,602✔
782
                                                               2, 7, 0, nil, nil)
801✔
783
                                              or is_version_in_range(major, minor, patch, suffix, commits_since,
801✔
784
                                                                     2, 6, 1, nil, nil,
785
                                                                     2, 6, math.huge, nil, nil)
×
786
                                              or is_version_in_range(major, minor, patch, suffix, commits_since,
×
787
                                                                     2, 5, 2, nil, nil,
788
                                                                     2, 5, math.huge, nil, nil)
×
789
                                              or is_version_in_range(major, minor, patch, suffix, commits_since,
×
790
                                                                     2, 4, 3, nil, nil,
791
                                                                     2, 4, math.huge, nil, nil)
×
792
                                              or is_version_in_range(major, minor, patch, suffix, commits_since,
×
793
                                                                     1, 10, 8, nil, nil,
794
                                                                     1, 10, math.huge, nil, nil)
801✔
795

796
    enabled_tarantool_features.netbox_skip_header_option = is_version_ge(major, minor, patch, suffix, commits_since,
1,602✔
797
                                                                         2, 2, 0, nil, nil)
1,602✔
798

799
    -- https://github.com/tarantool/tarantool/commit/11f2d999a92e45ee41b8c8d0014d8a09290fef7b
800
    enabled_tarantool_features.box_watch = is_version_ge(major, minor, patch, suffix, commits_since,
1,602✔
801
                                                         2, 10, 0, 'beta2', nil)
1,602✔
802

803
    -- Native `after` option in index:pairs() and index:select() for O(1) cursor positioning
804
    -- Available since Tarantool 2.10
805
    enabled_tarantool_features.index_pairs_after = is_version_ge(major, minor, patch, suffix, commits_since,
1,602✔
806
                                                                 2, 10, 0, nil, nil)
1,602✔
807

808
    enabled_tarantool_features.tarantool_3 = is_version_ge(major, minor, patch, suffix, commits_since,
1,602✔
809
                                                           3, 0, 0, nil, nil)
1,602✔
810

811
    enabled_tarantool_features.config_get_inside_roles = (
801✔
812
        -- https://github.com/tarantool/tarantool/commit/ebb170cb8cf2b9c4634bcf0178665909f578c335
813
        not utils.is_enterprise_package()
1,602✔
814
        and is_version_ge(major, minor, patch, suffix, commits_since,
1,602✔
815
                          3, 1, 0, 'entrypoint', 77)
801✔
816
    ) or (
801✔
817
        -- https://github.com/tarantool/tarantool/commit/e0e1358cb60d6749c34daf508e05586e0959bf89
818
        not utils.is_enterprise_package()
1,602✔
819
        and is_version_in_range(major, minor, patch, suffix, commits_since,
1,602✔
820
                                3, 0, 1, nil, 10,
801✔
821
                                3, 0, math.huge, nil, nil)
801✔
822
    ) or (
801✔
823
        -- https://github.com/tarantool/tarantool-ee/commit/368cc4007727af30ae3ca3a3cdfc7065f34e02aa
824
        utils.is_enterprise_package()
801✔
825
        and is_version_ge(major, minor, patch, suffix, commits_since,
801✔
826
                          3, 1, 0, 'entrypoint', 44)
×
827
    ) or (
×
828
        -- https://github.com/tarantool/tarantool-ee/commit/1dea81bed4cbe4856a0fc77dcc548849a2dabf45
829
        utils.is_enterprise_package()
801✔
830
        and is_version_in_range(major, minor, patch, suffix, commits_since,
801✔
831
                                3, 0, 1, nil, 10,
832
                                3, 0, math.huge, nil, nil)
×
833
    )
801✔
834

835
    enabled_tarantool_features.role_privileges_not_revoked = (
801✔
836
        -- https://github.com/tarantool/tarantool/commit/b982b46442e62e05ab6340343233aa766ad5e52c
837
        not utils.is_enterprise_package()
1,602✔
838
        and is_version_ge(major, minor, patch, suffix, commits_since,
1,602✔
839
                          3, 1, 0, 'entrypoint', 179)
801✔
840
    ) or (
801✔
841
        -- https://github.com/tarantool/tarantool/commit/ee2faf7c328abc54631233342cb9b88e4ce8cae4
842
        not utils.is_enterprise_package()
1,602✔
843
        and is_version_in_range(major, minor, patch, suffix, commits_since,
1,602✔
844
                                3, 0, 1, nil, 57,
801✔
845
                                3, 0, math.huge, nil, nil)
801✔
846
    ) or (
801✔
847
        -- https://github.com/tarantool/tarantool-ee/commit/5388e9d0f40d86226dc15bb27d85e63b0198e789
848
        utils.is_enterprise_package()
801✔
849
        and is_version_ge(major, minor, patch, suffix, commits_since,
801✔
850
                          3, 1, 0, 'entrypoint', 82)
×
851
    ) or (
×
852
        -- https://github.com/tarantool/tarantool-ee/commit/83d378d01bf2761da8ec684b6afe5683d38faeae
853
        utils.is_enterprise_package()
801✔
854
        and is_version_in_range(major, minor, patch, suffix, commits_since,
801✔
855
                                3, 0, 1, nil, 35,
856
                                3, 0, math.huge, nil, nil)
×
857
    )
801✔
858
end
859

860
determine_enabled_features()
801✔
861

862
for feature_name, feature_enabled in pairs(enabled_tarantool_features) do
12,816✔
863
    local util_name
864
    if feature_name == 'tarantool_3' then
11,214✔
865
        util_name = ('is_%s'):format(feature_name)
801✔
866
    elseif feature_name == 'builtin_merger' then
10,413✔
867
        util_name = ('tarantool_has_%s'):format(feature_name)
801✔
868
    elseif feature_name == 'role_privileges_not_revoked' then
9,612✔
869
        util_name = ('tarantool_%s'):format(feature_name)
801✔
870
    else
871
        util_name = ('tarantool_supports_%s'):format(feature_name)
8,811✔
872
    end
873

874
    local util_func = function() return feature_enabled end
54,135✔
875

876
    utils[util_name] = util_func
11,214✔
877
end
878

879
local function add_nullable_fields_recursive(operations, operations_map, space_format, tuple, id)
880
    if id < 2 or tuple[id - 1] ~= box.NULL then
×
881
        return operations
×
882
    end
883

884
    if space_format[id - 1].is_nullable and not operations_map[id - 1] then
×
885
        table.insert(operations, {'=', id - 1, box.NULL})
×
886
        return add_nullable_fields_recursive(operations, operations_map, space_format, tuple, id - 1)
×
887
    end
888

889
    return operations
×
890
end
891

892
-- Tarantool < 2.1 has no fields `box.error.NO_SUCH_FIELD_NO` and `box.error.NO_SUCH_FIELD_NAME`.
893
if tarantool_version_at_least(2, 1, 0, nil) then
1,602✔
894
    function utils.is_field_not_found(err_code)
801✔
895
        return err_code == box.error.NO_SUCH_FIELD_NO or err_code == box.error.NO_SUCH_FIELD_NAME
81✔
896
    end
897
else
898
    function utils.is_field_not_found(err_code)
×
899
        return err_code == box.error.NO_SUCH_FIELD
×
900
    end
901
end
902

903
local function get_operations_map(operations)
904
    local map = {}
×
905
    for _, operation in ipairs(operations) do
×
906
        map[operation[2]] = true
×
907
    end
908

909
    return map
×
910
end
911

912
function utils.add_intermediate_nullable_fields(operations, space_format, tuple)
801✔
913
    if tuple == nil then
2✔
914
        return operations
×
915
    end
916

917
    -- If tarantool doesn't supports the fieldpaths, we already
918
    -- have converted operations (see this function call in update.lua)
919
    if utils.tarantool_supports_fieldpaths() then
4✔
920
        local formatted_operations, err = utils.convert_operations(operations, space_format)
2✔
921
        if err ~= nil then
2✔
922
            return operations
2✔
923
        end
924

925
        operations = formatted_operations
×
926
    end
927

928
    -- We need this map to check if there is a field update
929
    -- operation with constant complexity
930
    local operations_map = get_operations_map(operations)
×
931
    for _, operation in ipairs(operations) do
×
932
        operations = add_nullable_fields_recursive(
×
933
            operations, operations_map,
934
            space_format, tuple, operation[2]
×
935
        )
936
    end
937

938
    table.sort(operations, function(v1, v2) return v1[2] < v2[2] end)
×
939
    return operations
×
940
end
941

942
function utils.convert_operations(user_operations, space_format)
801✔
943
    local converted_operations = {}
2✔
944

945
    for _, operation in ipairs(user_operations) do
2✔
946
        if type(operation[2]) == 'string' then
2✔
947
            local field_id
948
            for fieldno, field_format in ipairs(space_format) do
10✔
949
                if field_format.name == operation[2] then
8✔
950
                    field_id = fieldno
×
951
                    break
952
                end
953
            end
954

955
            if field_id == nil then
2✔
956
                return nil, ParseOperationsError:new(
4✔
957
                        "Space format doesn't contain field named %q", operation[2])
4✔
958
            end
959

960
            table.insert(converted_operations, {
×
961
                operation[1], field_id, operation[3]
×
962
            })
963
        else
964
            table.insert(converted_operations, operation)
×
965
        end
966
    end
967

968
    return converted_operations
×
969
end
970

971
function utils.unflatten_rows(rows, metadata)
801✔
972
    if metadata == nil then
17,779✔
973
        return nil, UnflattenError:new('Metadata is not provided')
×
974
    end
975

976
    local result = table.new(#rows, 0)
17,779✔
977
    local err
978
    for i, row in ipairs(rows) do
37,006✔
979
        result[i], err = utils.unflatten(row, metadata)
38,454✔
980
        if err ~= nil then
19,227✔
981
            return nil, err
×
982
        end
983
    end
984
    return result
17,779✔
985
end
986

987
local inverted_tarantool_iters = {
801✔
988
    [box.index.EQ] = box.index.REQ,
801✔
989
    [box.index.GT] = box.index.LT,
801✔
990
    [box.index.GE] = box.index.LE,
801✔
991
    [box.index.LT] = box.index.GT,
801✔
992
    [box.index.LE] = box.index.GE,
801✔
993
    [box.index.REQ] = box.index.EQ,
801✔
994
}
995

996
function utils.invert_tarantool_iter(iter)
801✔
997
    local inverted_iter = inverted_tarantool_iters[iter]
49✔
998
    assert(inverted_iter ~= nil, "Unsupported Tarantool iterator: " .. tostring(iter))
49✔
999
    return inverted_iter
49✔
1000
end
1001

1002
function utils.reverse_inplace(t)
801✔
1003
    for i = 1,math.floor(#t / 2) do
91✔
1004
        t[i], t[#t - i + 1] = t[#t - i + 1], t[i]
43✔
1005
    end
1006
    return t
48✔
1007
end
1008

1009
function utils.get_bucket_id_fieldno(space, shard_index_name)
801✔
1010
    shard_index_name = shard_index_name or 'bucket_id'
734,353✔
1011
    local bucket_id_index = space.index[shard_index_name]
734,353✔
1012
    if bucket_id_index == nil then
734,353✔
1013
        return nil, ShardingError:new('%q index is not found', shard_index_name)
24✔
1014
    end
1015

1016
    return bucket_id_index.parts[1].fieldno
734,341✔
1017
end
1018

1019
-- Build a map with field number as a keys and part number
1020
-- as a values using index parts as a source.
1021
function utils.get_index_fieldno_map(index_parts)
801✔
1022
    dev_checks('table')
159✔
1023

1024
    local fieldno_map = {}
159✔
1025
    for i, part in ipairs(index_parts) do
428✔
1026
        local fieldno = part.fieldno
269✔
1027
        fieldno_map[fieldno] = i
269✔
1028
    end
1029

1030
    return fieldno_map
159✔
1031
end
1032

1033
-- Build a map with field names as a keys and fieldno's
1034
-- as a values using space format as a source.
1035
function utils.get_format_fieldno_map(space_format)
801✔
1036
    dev_checks('table')
9,814✔
1037

1038
    local fieldno_map = {}
9,814✔
1039
    for fieldno, field_format in ipairs(space_format) do
49,673✔
1040
        fieldno_map[field_format.name] = fieldno
39,859✔
1041
    end
1042

1043
    return fieldno_map
9,814✔
1044
end
1045

1046
local uuid_t = ffi.typeof('struct tt_uuid')
801✔
1047
function utils.is_uuid(value)
801✔
1048
    return ffi.istype(uuid_t, value)
780✔
1049
end
1050

1051
local function get_field_format(space_format, field_name)
1052
    dev_checks('table', 'string')
466✔
1053

1054
    local metadata = space_format_cache[space_format]
466✔
1055
    if metadata ~= nil then
466✔
1056
        return metadata[field_name]
446✔
1057
    end
1058

1059
    space_format_cache[space_format] = {}
20✔
1060
    for _, field in ipairs(space_format) do
134✔
1061
        space_format_cache[space_format][field.name] = field
114✔
1062
    end
1063

1064
    return space_format_cache[space_format][field_name]
20✔
1065
end
1066

1067
local function filter_format_fields(space_format, field_names)
1068
    dev_checks('table', 'table')
182✔
1069

1070
    local filtered_space_format = {}
182✔
1071

1072
    for i, field_name in ipairs(field_names) do
618✔
1073
        filtered_space_format[i] = get_field_format(space_format, field_name)
932✔
1074
        if filtered_space_format[i] == nil then
466✔
1075
            return nil, FilterFieldsError:new(
60✔
1076
                    'Space format doesn\'t contain field named %q', field_name
30✔
1077
            )
60✔
1078
        end
1079
    end
1080

1081
    return filtered_space_format
152✔
1082
end
1083

1084
function utils.get_fields_format(space_format, field_names)
801✔
1085
    dev_checks('table', '?table')
7,730✔
1086

1087
    if field_names == nil then
7,730✔
1088
        return table.copy(space_format)
7,666✔
1089
    end
1090

1091
    local filtered_space_format, err = filter_format_fields(space_format, field_names)
64✔
1092

1093
    if err ~= nil then
64✔
1094
        return nil, err
2✔
1095
    end
1096

1097
    return filtered_space_format
62✔
1098
end
1099

1100
function utils.format_result(rows, space, field_names)
801✔
1101
    local result = {}
134,173✔
1102
    local err
1103
    local space_format = space:format()
134,173✔
1104
    result.rows = rows
134,173✔
1105

1106
    if field_names == nil then
134,173✔
1107
        result.metadata = table.copy(space_format)
268,110✔
1108
        return result
134,055✔
1109
    end
1110

1111
    result.metadata, err = filter_format_fields(space_format, field_names)
236✔
1112

1113
    if err ~= nil then
118✔
1114
        return nil, err
28✔
1115
    end
1116

1117
    return result
90✔
1118
end
1119

1120
local function truncate_tuple_metadata(tuple_metadata, field_names)
1121
    dev_checks('?table', 'table')
31✔
1122

1123
    if tuple_metadata == nil then
31✔
1124
        return nil
3✔
1125
    end
1126

1127
    local truncated_metadata = {}
28✔
1128

1129
    if #tuple_metadata < #field_names then
28✔
1130
        return nil, FilterFieldsError:new(
×
1131
                'Field names don\'t match to tuple metadata'
1132
        )
1133
    end
1134

1135
    for i, name in ipairs(field_names) do
79✔
1136
        if tuple_metadata[i].name ~= name then
53✔
1137
            return nil, FilterFieldsError:new(
4✔
1138
                    'Field names don\'t match to tuple metadata'
1139
            )
4✔
1140
        end
1141

1142
        table.insert(truncated_metadata, tuple_metadata[i])
51✔
1143
    end
1144

1145
    return truncated_metadata
26✔
1146
end
1147

1148
function utils.cut_objects(objs, field_names)
801✔
1149
    dev_checks('table', 'table')
5✔
1150

1151
    for i, obj in ipairs(objs) do
20✔
1152
        objs[i] = schema.filter_obj_fields(obj, field_names)
30✔
1153
    end
1154

1155
    return objs
5✔
1156
end
1157

1158
function utils.cut_rows(rows, metadata, field_names)
801✔
1159
    dev_checks('table', '?table', 'table')
31✔
1160

1161
    local truncated_metadata, err = truncate_tuple_metadata(metadata, field_names)
31✔
1162

1163
    if err ~= nil then
31✔
1164
        return nil, err
2✔
1165
    end
1166

1167
    for i, row in ipairs(rows) do
72✔
1168
        rows[i] = schema.truncate_row_trailing_fields(row, field_names)
86✔
1169
    end
1170

1171
    return {
29✔
1172
        metadata = truncated_metadata,
29✔
1173
        rows = rows,
29✔
1174
    }
29✔
1175
end
1176

1177
local function flatten_obj(vshard_router, space_name, obj, skip_nullability_check)
1178
    local space_format, err = utils.get_space_format(space_name, vshard_router)
10,844✔
1179
    if err ~= nil then
10,844✔
1180
        return nil, FlattenError:new("Failed to get space format: %s", err), const.NEED_SCHEMA_RELOAD
332✔
1181
    end
1182

1183
    local tuple, err = utils.flatten(obj, space_format, nil, skip_nullability_check)
10,678✔
1184
    if err ~= nil then
10,678✔
1185
        return nil, FlattenError:new("Object is specified in bad format: %s", err), const.NEED_SCHEMA_RELOAD
2,088✔
1186
    end
1187

1188
    return tuple
9,634✔
1189
end
1190

1191
function utils.flatten_obj_reload(vshard_router, space_name, obj, skip_nullability_check)
801✔
1192
    return schema.wrap_func_reload(vshard_router, flatten_obj, space_name, obj, skip_nullability_check)
10,206✔
1193
end
1194

1195
-- Merge two options map.
1196
--
1197
-- `opts_a` and/or `opts_b` can be `nil`.
1198
--
1199
-- If `opts_a.foo` and `opts_b.foo` exists, prefer `opts_b.foo`.
1200
function utils.merge_options(opts_a, opts_b)
801✔
1201
    return fun.chain(opts_a or {}, opts_b or {}):tomap()
19,114✔
1202
end
1203

1204
local function lj_char_isident(n)
1205
    return bit.band(lj_char_bits[n + 2], LJ_CHAR_IDENT) == LJ_CHAR_IDENT
12,030✔
1206
end
1207

1208
local function lj_char_isdigit(n)
1209
    return bit.band(lj_char_bits[n + 2], LJ_CHAR_DIGIT) == LJ_CHAR_DIGIT
734✔
1210
end
1211

1212
function utils.check_name_isident(name)
801✔
1213
    dev_checks('string')
735✔
1214

1215
    -- sharding function name cannot
1216
    -- be equal to lua keyword
1217
    if LUA_KEYWORDS[name] then
735✔
1218
        return false
1✔
1219
    end
1220

1221
    -- sharding function name cannot
1222
    -- begin with a digit
1223
    local char_number = string.byte(name:sub(1,1))
1,468✔
1224
    if lj_char_isdigit(char_number) then
1,468✔
1225
        return false
1✔
1226
    end
1227

1228
    -- sharding func name must be sequence
1229
    -- of letters, digits, or underscore symbols
1230
    for i = 1, #name do
12,762✔
1231
        local char_number = string.byte(name:sub(i,i))
24,060✔
1232
        if not lj_char_isident(char_number) then
24,060✔
1233
            return false
1✔
1234
        end
1235
    end
1236

1237
    return true
732✔
1238
end
1239

1240
function utils.update_storage_call_error_description(err, func_name, replicaset_id)
801✔
1241
    if err == nil then
891✔
1242
        return nil
×
1243
    end
1244

1245
    if (err.type == 'ClientError' or err.type == 'AccessDeniedError' or err.type == 'LuajitError')
1,742✔
1246
        and type(err.message) == 'string' then
1,330✔
1247
        local not_defined_str = string.format("Procedure '%s' is not defined", func_name)
669✔
1248
        local not_registered_str = string.format("Function '%s' is not registered", func_name)
669✔
1249
        local access_denied_str = string.format("Execute access to function '%s' is denied", func_name)
669✔
1250
        if err.message == not_defined_str or err.message:startswith(access_denied_str)
1,991✔
1251
                or err.message:find(not_registered_str)
1,330✔
1252
                or err.message == "Procedure '_crud.call_on_storage' is not defined"
1,326✔
1253
                or err.message:startswith("Execute access to function '_crud.call_on_storage' is denied") then
1,979✔
1254
            if func_name:startswith('_crud.') then
20✔
1255
                err = NotInitializedError:new("Function '%s' is not registered: " ..
12✔
1256
                    "crud isn't initialized on replicaset %q or crud module versions mismatch " ..
6✔
1257
                    "between router and storage",
6✔
1258
                    func_name, replicaset_id or "Unknown")
12✔
1259
            else
1260
                err = NotInitializedError:new("Function '%s' is not registered", func_name)
8✔
1261
            end
1262
        end
1263
    end
1264
    return err
891✔
1265
end
1266

1267
--- Insert each value from values to list
1268
--
1269
-- @function list_extend
1270
--
1271
-- @param table list
1272
--  List to be extended
1273
--
1274
-- @param table values
1275
--  Values to be inserted to list
1276
--
1277
-- @return[1] list
1278
--  List with old values and inserted values
1279
function utils.list_extend(list, values)
801✔
1280
    dev_checks('table', 'table')
7,262✔
1281

1282
    for _, value in ipairs(values) do
44,826✔
1283
        table.insert(list, value)
37,564✔
1284
    end
1285

1286
    return list
7,262✔
1287
end
1288

1289
function utils.list_slice(list, start_index, end_index)
801✔
1290
    dev_checks('table', 'number', '?number')
48✔
1291

1292
    if end_index == nil then
48✔
1293
        end_index = table.maxn(list)
48✔
1294
    end
1295

1296
    local slice = {}
48✔
1297
    for i = start_index, end_index do
120✔
1298
        table.insert(slice, list[i])
72✔
1299
    end
1300

1301
    return slice
48✔
1302
end
1303

1304
local expected_vshard_api = {
801✔
1305
    'routeall', 'route', 'bucket_id_strcrc32',
1306
    'callrw', 'callro', 'callbro', 'callre',
1307
    'callbre', 'map_callrw'
1308
}
1309

1310
--- Verifies that a table has expected vshard
1311
--  router handles.
1312
local function verify_vshard_router(router)
1313
    dev_checks("table")
106✔
1314

1315
    for _, func_name in ipairs(expected_vshard_api) do
664✔
1316
        if type(router[func_name]) ~= 'function' then
602✔
1317
            return false
44✔
1318
        end
1319
    end
1320

1321
    return true
62✔
1322
end
1323

1324
--- Get a vshard router instance from a parameter.
1325
--
1326
--  If a string passed, extract router instance from
1327
--  Cartridge vshard groups. If table passed, verifies
1328
--  that a table is a vshard router instance.
1329
--
1330
-- @function get_vshard_router_instance
1331
--
1332
-- @param[opt] router name of a vshard group or a vshard router
1333
--  instance
1334
--
1335
-- @return[1] table vshard router instance
1336
-- @treturn[2] nil
1337
-- @treturn[2] table Error description
1338
function utils.get_vshard_router_instance(router)
801✔
1339
    dev_checks('?string|table')
149,338✔
1340

1341
    local router_instance
1342

1343
    if type(router) == 'string' then
149,338✔
1344
        if not is_cartridge then
70✔
1345
            return nil, VshardRouterError:new("Vshard groups are supported only in Tarantool Cartridge")
×
1346
        end
1347

1348
        local router_service = cartridge.service_get('vshard-router')
70✔
1349
        assert(router_service ~= nil)
70✔
1350

1351
        router_instance = router_service.get(router)
140✔
1352
        if router_instance == nil then
70✔
1353
            return nil, VshardRouterError:new("Vshard group %s is not found", router)
×
1354
        end
1355
    elseif type(router) == 'table' then
149,268✔
1356
        if not verify_vshard_router(router) then
212✔
1357
            return nil, VshardRouterError:new("Invalid opts.vshard_router table value, " ..
88✔
1358
                                              "a vshard router instance has been expected")
88✔
1359
        end
1360

1361
        router_instance = router
62✔
1362
    else
1363
        assert(type(router) == 'nil')
149,162✔
1364
        router_instance = vshard.router.static
149,162✔
1365

1366
        if router_instance == nil then
149,162✔
1367
            return nil, VshardRouterError:new("Default vshard group is not found and custom " ..
88✔
1368
                                              "is not specified with opts.vshard_router")
88✔
1369
        end
1370
    end
1371

1372
    return router_instance
149,250✔
1373
end
1374

1375
--- Check if Tarantool Cartridge hotreload supported
1376
--  and get its implementaion.
1377
--
1378
-- @function is_cartridge_hotreload_supported
1379
--
1380
-- @return[1] true or false
1381
-- @return[1] module table, if supported
1382
function utils.is_cartridge_hotreload_supported()
801✔
1383
    if not is_cartridge_hotreload then
383✔
1384
        return false
×
1385
    end
1386

1387
    return true, cartridge_hotreload
383✔
1388
end
1389

1390
if utils.tarantool_supports_intervals() then
1,602✔
1391
    -- https://github.com/tarantool/tarantool/blob/0510ffa07afd84a70c9c6f1a4c28aacd73a393d6/src/lua/datetime.lua#L175-179
1392
    local interval_t = ffi.typeof('struct interval')
801✔
1393

1394
    utils.is_interval = function(o)
1395
        return ffi.istype(interval_t, o)
20✔
1396
    end
1397
else
1398
    utils.is_interval = function()
1399
        return false
×
1400
    end
1401
end
1402

1403
for k, v in pairs(require('crud.common.vshard_utils')) do
9,612✔
1404
    utils[k] = v
7,209✔
1405
end
1406

1407
function utils.append_array(array_src, array_dst)
801✔
1408
    if not array_dst then
161,413✔
1409
        return array_src
11✔
1410
    end
1411

1412
    table.move(array_dst, 1, #array_dst, #array_src + 1, array_src)
161,402✔
1413

1414
    return array_src
161,402✔
1415
end
1416

1417
function utils.is_uint(value)
801✔
1418
    if type(value) == 'number' then
1,446✔
1419
        return value >= 0 and math.floor(value) == value
1,330✔
1420
    elseif type(value) == 'cdata' then
116✔
1421
        local ok, casted = pcall(tonumber, value)
8✔
1422
        return ok and type(casted) == 'number' and casted >= 0 and math.floor(casted) == casted
8✔
1423
    end
1424

1425
    return false
108✔
1426
end
1427

1428
return utils
801✔
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