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

tarantool / crud / 26413701858

25 May 2026 06:06PM UTC coverage: 88.324% (+0.01%) from 88.313%
26413701858

Pull #497

github

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

Read-only operations (get, select, 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`, `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.
Pull Request #497: fix: allow read-only operations when all masters are down

28 of 29 new or added lines in 6 files covered. (96.55%)

1 existing line in 1 file now uncovered.

5265 of 5961 relevant lines covered (88.32%)

12445.22 hits per line

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

87.59
/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")
760✔
106

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

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

123
    return nil, nil
×
124
end
125

126
local function find_any_healthy_replica_conn(replicasets)
127
    for _, replicaset in pairs(replicasets) do
20✔
128
        for _, replica in pairs(replicaset.replicas) do
20✔
129
            if replica.conn ~= nil and replica.conn.error == nil then
10✔
130
                return replica.conn
10✔
131
            end
132
        end
133
    end
NEW
134
    return nil
×
135
end
136

137
function utils.get_spaces(vshard_router, timeout, replica_id, read_only)
801✔
138
    local replicasets, replicaset, replicaset_id, master, ro_replica_conn
139

140
    timeout = timeout or const.DEFAULT_VSHARD_CALL_TIMEOUT
153,566✔
141
    local deadline = fiber.clock() + timeout
307,132✔
142
    local iter_sleep = math.min(timeout / 100, 0.1)
153,566✔
143
    while (
144
        -- Break if the deadline condition is exceeded.
145
        -- Handling for deadline errors are below in the code.
146
        fiber.clock() < deadline
307,132✔
147
    ) do
153,566✔
148
        -- Try to get master with timeout.
149
        replicasets = vshard_router:routeall()
307,132✔
150
        if replica_id ~= nil then
153,566✔
151
            -- Get the same replica on which the last DML operation was performed.
152
            -- This approach is temporary and is related to [1], [2].
153
            -- [1] https://github.com/tarantool/crud/issues/236
154
            -- [2] https://github.com/tarantool/crud/issues/361
155
            replicaset_id, replicaset = get_replicaset_by_replica_id(replicasets, replica_id)
40✔
156
            break
20✔
157
        else
158
            replicaset_id, replicaset = next(replicasets)
153,546✔
159
        end
160

161
        if replicaset ~= nil then
153,546✔
162
            -- Get cached, reload (if required) will be processed in other place.
163
            master = utils.get_replicaset_master(replicaset, {cached = true})
307,092✔
164
            if master ~= nil and master.conn.error == nil then
153,546✔
165
                break
153,536✔
166
            end
167
        end
168

169
        -- If the master check above didn't succeed (or master is dead),
170
        -- and this is a read-only operation, try to find any available healthy replica.
171
        if read_only then
10✔
172
            ro_replica_conn = find_any_healthy_replica_conn(replicasets)
20✔
173
            if ro_replica_conn ~= nil then
10✔
174
                break
10✔
175
            end
176
        end
177

UNCOV
178
        fiber.sleep(iter_sleep)
×
179
    end
180

181
    if read_only and ro_replica_conn ~= nil then
153,566✔
182
        return ro_replica_conn.space, nil, ro_replica_conn.schema_version
10✔
183
    end
184

185
    if replicaset == nil then
153,556✔
186
        return nil, GetSpaceError:new(
×
187
            'The router returned empty replicasets: ' ..
×
188
            'perhaps other instances are unavailable or you have configured only the router')
×
189
    end
190

191
    master = utils.get_replicaset_master(replicaset, {cached = true})
307,112✔
192

193
    if master == nil then
153,556✔
194
        local error_msg = string.format(
×
195
            'The master was not found in replicaset %s, ' ..
×
196
            'check status of the master and repeat the operation later',
197
             replicaset_id)
×
198
        return nil, GetSpaceError:new(error_msg)
×
199
    end
200

201
    if master.conn.error ~= nil then
153,556✔
202
        local error_msg = string.format(
×
203
            'The connection to the master of replicaset %s is not valid: %s',
204
             replicaset_id, master.conn.error)
×
205
        return nil, GetSpaceError:new(error_msg)
×
206
    end
207

208
    return master.conn.space, nil, master.conn.schema_version
153,556✔
209
end
210

211
function utils.get_space(space_name, vshard_router, timeout, replica_id, read_only)
801✔
212
    local spaces, err, schema_version = utils.get_spaces(vshard_router, timeout, replica_id, read_only)
153,534✔
213

214
    if spaces == nil then
153,534✔
215
        return nil, err
×
216
    end
217

218
    return spaces[space_name], err, schema_version
153,534✔
219
end
220

221
function utils.get_space_format(space_name, vshard_router)
801✔
222
    local space, err = utils.get_space(space_name, vshard_router)
10,850✔
223
    if err ~= nil then
10,850✔
224
        return nil, GetSpaceFormatError:new("An error occurred during the operation: %s", err)
×
225
    end
226
    if space == nil then
10,850✔
227
        return nil, GetSpaceFormatError:new("Space %q doesn't exist", space_name)
344✔
228
    end
229

230
    local space_format = space:format()
10,678✔
231

232
    return space_format
10,678✔
233
end
234

235
function utils.fetch_latest_metadata_when_single_storage(space, space_name, netbox_schema_version,
801✔
236
                                                         vshard_router, opts, storage_info)
237
    -- Checking the relevance of the schema version is necessary
238
    -- to prevent the irrelevant metadata of the DML operation.
239
    -- This approach is temporary and is related to [1], [2].
240
    -- [1] https://github.com/tarantool/crud/issues/236
241
    -- [2] https://github.com/tarantool/crud/issues/361
242
    local latest_space, err
243

244
    assert(storage_info.replica_schema_version ~= nil,
40✔
245
           'check the replica_schema_version value from storage ' ..
20✔
246
           'for correct use of the fetch_latest_metadata opt')
20✔
247

248
    local replica_id
249
    if storage_info.replica_id == nil then -- Backward compatibility.
20✔
250
        assert(storage_info.replica_uuid ~= nil,
×
251
               'check the replica_uuid value from storage ' ..
×
252
               'for correct use of the fetch_latest_metadata opt')
×
253
        replica_id = storage_info.replica_uuid
×
254
    else
255
        replica_id = storage_info.replica_id
20✔
256
    end
257

258
    assert(netbox_schema_version ~= nil,
40✔
259
           'check the netbox_schema_version value from net_box conn on router ' ..
20✔
260
           'for correct use of the fetch_latest_metadata opt')
20✔
261

262
    if storage_info.replica_schema_version ~= netbox_schema_version then
20✔
263
        local ok, reload_schema_err = schema.reload_schema(vshard_router)
20✔
264
        if ok then
20✔
265
            latest_space, err = utils.get_space(space_name, vshard_router,
40✔
266
                                                opts.timeout, replica_id)
40✔
267
            if err ~= nil then
20✔
268
                local warn_msg = "Failed to fetch space for latest schema actualization, metadata may be outdated: %s"
×
269
                log.warn(warn_msg, err)
×
270
            end
271
            if latest_space == nil then
20✔
272
                log.warn("Failed to find space for latest schema actualization, metadata may be outdated")
×
273
            end
274
        else
275
            log.warn("Failed to reload schema, metadata may be outdated: %s", reload_schema_err)
×
276
        end
277
    end
278
    if err == nil and latest_space ~= nil then
20✔
279
        space = latest_space
20✔
280
    end
281

282
    return space
20✔
283
end
284

285
function utils.fetch_latest_metadata_when_map_storages(space, space_name, vshard_router, opts,
801✔
286
                                                       storages_info, netbox_schema_version)
287
    -- Checking the relevance of the schema version is necessary
288
    -- to prevent the irrelevant metadata of the DML operation.
289
    -- This approach is temporary and is related to [1], [2].
290
    -- [1] https://github.com/tarantool/crud/issues/236
291
    -- [2] https://github.com/tarantool/crud/issues/361
292
    local latest_space, err
293
    for _, storage_info in pairs(storages_info) do
32✔
294
        assert(storage_info.replica_schema_version ~= nil,
32✔
295
            'check the replica_schema_version value from storage ' ..
16✔
296
            'for correct use of the fetch_latest_metadata opt')
16✔
297
        assert(netbox_schema_version ~= nil,
32✔
298
               'check the netbox_schema_version value from net_box conn on router ' ..
16✔
299
               'for correct use of the fetch_latest_metadata opt')
16✔
300
        if storage_info.replica_schema_version ~= netbox_schema_version then
16✔
301
            local ok, reload_schema_err = schema.reload_schema(vshard_router)
16✔
302
            if ok then
16✔
303
                latest_space, err = utils.get_space(space_name, vshard_router, opts.timeout)
32✔
304
                if err ~= nil then
16✔
305
                    local warn_msg = "Failed to fetch space for latest schema actualization, " ..
×
306
                                     "metadata may be outdated: %s"
307
                    log.warn(warn_msg, err)
×
308
                end
309
                if latest_space == nil then
16✔
310
                    log.warn("Failed to find space for latest schema actualization, metadata may be outdated")
×
311
                end
312
            else
313
                log.warn("Failed to reload schema, metadata may be outdated: %s", reload_schema_err)
×
314
            end
315
            if err == nil and latest_space ~= nil then
16✔
316
                space = latest_space
16✔
317
            end
318
            break
16✔
319
        end
320
    end
321

322
    return space
16✔
323
end
324

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

356
    return iter
4✔
357
end
358

359
local function append(lines, s, ...)
360
    table.insert(lines, string.format(s, ...))
7,782✔
361
end
362

363
local flatten_functions_cache = setmetatable({}, {__mode = 'k'})
801✔
364

365
function utils.flatten(object, space_format, bucket_id, skip_nullability_check)
801✔
366
    local flatten_func = flatten_functions_cache[space_format]
10,856✔
367
    if flatten_func ~= nil then
10,856✔
368
        local data, err = flatten_func(object, bucket_id, skip_nullability_check)
10,580✔
369
        if err ~= nil then
10,580✔
370
            return nil, FlattenError:new(err)
2,062✔
371
        end
372
        return data
9,549✔
373
    end
374

375
    local lines = {}
276✔
376
    append(lines, 'local object, bucket_id, skip_nullability_check = ...')
276✔
377

378
    append(lines, 'for k in pairs(object) do')
276✔
379
    append(lines, '    if fieldmap[k] == nil then')
276✔
380
    append(lines, '        return nil, format(\'Unknown field %%q is specified\', k)')
276✔
381
    append(lines, '    end')
276✔
382
    append(lines, 'end')
276✔
383

384
    local len = #space_format
276✔
385
    append(lines, 'local result = {%s}', string.rep('NULL,', len))
276✔
386

387
    local fieldmap = {}
276✔
388

389
    for i, field in ipairs(space_format) do
1,436✔
390
        fieldmap[field.name] = true
1,160✔
391
        if field.name ~= 'bucket_id' then
1,160✔
392
            append(lines, 'if object[%q] ~= nil then', field.name)
884✔
393
            append(lines, '    result[%d] = object[%q]', i, field.name)
884✔
394
            if field.is_nullable ~= true then
884✔
395
                append(lines, 'elseif skip_nullability_check ~= true then')
771✔
396
                append(lines, '    return nil, \'Field %q isn\\\'t nullable' ..
1,542✔
397
                              ' (set skip_nullability_check_on_flatten option to true to skip check)\'',
771✔
398
                              field.name)
771✔
399
            end
400
            append(lines, 'end')
1,768✔
401
        else
402
            append(lines, 'if bucket_id ~= nil then')
276✔
403
            append(lines, '    result[%d] = bucket_id', i, field.name)
276✔
404
            append(lines, 'else')
276✔
405
            append(lines, '    result[%d] = object[%q]', i, field.name)
276✔
406
            append(lines, 'end')
276✔
407
        end
408
    end
409
    append(lines, 'return result')
276✔
410

411
    local code = table.concat(lines, '\n')
276✔
412
    local env = {
276✔
413
        pairs = pairs,
276✔
414
        format = string.format,
276✔
415
        fieldmap = fieldmap,
276✔
416
        NULL = box.NULL,
276✔
417
    }
418
    flatten_func = assert(load(code, nil, 't', env))
276✔
419

420
    flatten_functions_cache[space_format] = flatten_func
276✔
421
    local data, err = flatten_func(object, bucket_id, skip_nullability_check)
276✔
422
    if err ~= nil then
276✔
423
        return nil, FlattenError:new(err)
30✔
424
    end
425
    return data
261✔
426
end
427

428
function utils.unflatten(tuple, space_format)
801✔
429
    if tuple == nil then return nil end
20,261✔
430

431
    local object = {}
20,261✔
432

433
    for fieldno, field_format in ipairs(space_format) do
114,758✔
434
        local value = tuple[fieldno]
94,498✔
435

436
        if not field_format.is_nullable and value == nil then
94,498✔
437
            return nil, UnflattenError:new("Field %s isn't nullable", fieldno)
2✔
438
        end
439

440
        object[field_format.name] = value
94,497✔
441
    end
442

443
    return object
20,260✔
444
end
445

446
function utils.extract_key(tuple, key_parts)
801✔
447
    local key = {}
359,786✔
448
    for i, part in ipairs(key_parts) do
720,992✔
449
        key[i] = tuple[part.fieldno]
361,206✔
450
    end
451
    return key
359,786✔
452
end
453

454
function utils.merge_primary_key_parts(key_parts, pk_parts)
801✔
455
    local merged_parts = {}
7,066✔
456
    local key_fieldnos = {}
7,066✔
457

458
    for _, part in ipairs(key_parts) do
14,352✔
459
        table.insert(merged_parts, part)
7,286✔
460
        key_fieldnos[part.fieldno] = true
7,286✔
461
    end
462

463
    for _, pk_part in ipairs(pk_parts) do
15,185✔
464
        if not key_fieldnos[pk_part.fieldno] then
8,119✔
465
            table.insert(merged_parts, pk_part)
3,160✔
466
        end
467
    end
468

469
    return merged_parts
7,066✔
470
end
471

472
function utils.enrich_field_names_with_cmp_key(field_names, key_parts, space_format)
801✔
473
    if field_names == nil then
6,943✔
474
        return nil
6,858✔
475
    end
476

477
    local enriched_field_names = {}
85✔
478
    local key_field_names = {}
85✔
479

480
    for _, field_name in ipairs(field_names) do
251✔
481
        table.insert(enriched_field_names, field_name)
166✔
482
        key_field_names[field_name] = true
166✔
483
    end
484

485
    for _, part in ipairs(key_parts) do
223✔
486
        local field_name = space_format[part.fieldno].name
138✔
487
        if not key_field_names[field_name] then
138✔
488
            table.insert(enriched_field_names, field_name)
108✔
489
            key_field_names[field_name] = true
108✔
490
        end
491
    end
492

493
    return enriched_field_names
85✔
494
end
495

496

497
local function get_version_suffix(suffix_candidate)
498
    if type(suffix_candidate) ~= 'string' then
2,762✔
499
        return nil
×
500
    end
501

502
    if suffix_candidate:find('^entrypoint$')
2,762✔
503
    or suffix_candidate:find('^alpha%d$')
2,762✔
504
    or suffix_candidate:find('^beta%d$')
2,761✔
505
    or suffix_candidate:find('^rc%d$') then
2,759✔
506
        return suffix_candidate
7✔
507
    end
508

509
    return nil
2,755✔
510
end
511

512
local function get_commits_since_from_version_part(commits_since_candidate)
513
    if commits_since_candidate == nil then
2,753✔
514
        return 0
×
515
    end
516

517
    local ok, val = pcall(tonumber, commits_since_candidate)
2,753✔
518
    if ok then
2,753✔
519
        return val
2,753✔
520
    else
521
        -- It may be unknown suffix instead.
522
        -- Since suffix already unknown, there is no way to properly compare versions.
523
        return 0
×
524
    end
525
end
526

527
local function get_commits_since(suffix, commits_since_candidate_1, commits_since_candidate_2)
528
    -- x.x.x.-candidate_1-candidate_2
529

530
    if suffix ~= nil then
2,753✔
531
        -- X.Y.Z-suffix-N
532
        return get_commits_since_from_version_part(commits_since_candidate_2)
×
533
    else
534
        -- X.Y.Z-N
535
        -- Possibly X.Y.Z-suffix-N with unknown suffix
536
        return get_commits_since_from_version_part(commits_since_candidate_1)
2,753✔
537
    end
538
end
539

540
utils.get_version_suffix = get_version_suffix
801✔
541

542

543
local suffix_with_digit_weight = {
801✔
544
    alpha = -3000,
545
    beta  = -2000,
546
    rc    = -1000,
547
}
548

549
local function get_version_suffix_weight(suffix)
550
    if suffix == nil then
29,668✔
551
        return 0
25,611✔
552
    end
553

554
    if suffix:find('^entrypoint$') then
4,057✔
555
        return -math.huge
1,611✔
556
    end
557

558
    for header, weight in pairs(suffix_with_digit_weight) do
8,140✔
559
        local pos, _, digits = suffix:find('^' .. header .. '(%d)$')
5,692✔
560
        if pos ~= nil then
5,692✔
561
            return weight + tonumber(digits)
2,444✔
562
        end
563
    end
564

565
    UtilsInternalError:assert(false,
4✔
566
        'Unexpected suffix %q, parse with "utils.get_version_suffix" first', suffix)
2✔
567
end
568

569
utils.get_version_suffix_weight = get_version_suffix_weight
801✔
570

571

572
local function is_version_ge(major, minor,
573
                             patch, suffix, commits_since,
574
                             major_to_compare, minor_to_compare,
575
                             patch_to_compare, suffix_to_compare, commits_since_to_compare)
576
    major = major or 0
14,829✔
577
    minor = minor or 0
14,829✔
578
    patch = patch or 0
14,829✔
579
    local suffix_weight = get_version_suffix_weight(suffix)
14,829✔
580
    commits_since = commits_since or 0
14,829✔
581

582
    major_to_compare = major_to_compare or 0
14,829✔
583
    minor_to_compare = minor_to_compare or 0
14,829✔
584
    patch_to_compare = patch_to_compare or 0
14,829✔
585
    local suffix_weight_to_compare = get_version_suffix_weight(suffix_to_compare)
14,829✔
586
    commits_since_to_compare = commits_since_to_compare or 0
14,829✔
587

588
    if major > major_to_compare then return true end
14,829✔
589
    if major < major_to_compare then return false end
14,814✔
590

591
    if minor > minor_to_compare then return true end
10,748✔
592
    if minor < minor_to_compare then return false end
950✔
593

594
    if patch > patch_to_compare then return true end
943✔
595
    if patch < patch_to_compare then return false end
20✔
596

597
    if suffix_weight > suffix_weight_to_compare then return true end
18✔
598
    if suffix_weight < suffix_weight_to_compare then return false end
13✔
599

600
    if commits_since > commits_since_to_compare then return true end
8✔
601
    if commits_since < commits_since_to_compare then return false end
7✔
602

603
    return true
6✔
604
end
605

606
utils.is_version_ge = is_version_ge
801✔
607

608

609
local function is_version_in_range(major, minor,
610
                                   patch, suffix, commits_since,
611
                                   major_left_side, minor_left_side,
612
                                   patch_left_side, suffix_left_side, commits_since_left_side,
613
                                   major_right_side, minor_right_side,
614
                                   patch_right_side, suffix_right_side, commits_since_right_side)
615
    return is_version_ge(major, minor,
3,210✔
616
                         patch, suffix, commits_since,
1,605✔
617
                         major_left_side, minor_left_side,
1,605✔
618
                         patch_left_side, suffix_left_side, commits_since_left_side)
1,605✔
619
       and is_version_ge(major_right_side, minor_right_side,
1,608✔
620
                         patch_right_side, suffix_right_side, commits_since_right_side,
3✔
621
                         major, minor,
3✔
622
                         patch, suffix, commits_since)
1,608✔
623
end
624

625
utils.is_version_in_range = is_version_in_range
801✔
626

627

628
local function get_tarantool_version()
629
    local version_parts = rawget(_G, '_TARANTOOL'):split('-', 3)
2,753✔
630

631
    local major_minor_patch_parts = version_parts[1]:split('.', 2)
2,753✔
632
    local major = tonumber(major_minor_patch_parts[1])
2,753✔
633
    local minor = tonumber(major_minor_patch_parts[2])
2,753✔
634
    local patch = tonumber(major_minor_patch_parts[3])
2,753✔
635

636
    local suffix = get_version_suffix(version_parts[2])
2,753✔
637

638
    local commits_since = get_commits_since(suffix, version_parts[2], version_parts[3])
2,753✔
639

640
    return major, minor, patch, suffix, commits_since
2,753✔
641
end
642

643
utils.get_tarantool_version = get_tarantool_version
801✔
644

645

646
local function tarantool_version_at_least(wanted_major, wanted_minor,
647
                                          wanted_patch, wanted_suffix, wanted_commits_since)
648
    local major, minor, patch, suffix, commits_since = get_tarantool_version()
1,951✔
649

650
    return is_version_ge(major, minor, patch, suffix, commits_since,
1,951✔
651
                         wanted_major, wanted_minor, wanted_patch, wanted_suffix, wanted_commits_since)
1,951✔
652
end
653

654
utils.tarantool_version_at_least = tarantool_version_at_least
801✔
655

656
function utils.is_enterprise_package()
801✔
657
    return tarantool.package == 'Tarantool Enterprise'
7,225✔
658
end
659

660

661
local enabled_tarantool_features = {}
801✔
662

663
local function determine_enabled_features()
664
    local major, minor, patch, suffix, commits_since = get_tarantool_version()
801✔
665

666
    -- since Tarantool 2.3.1
667
    enabled_tarantool_features.fieldpaths = is_version_ge(major, minor, patch, suffix, commits_since,
1,602✔
668
                                                          2, 3, 1, nil, nil)
1,602✔
669

670
    -- Full support (Lua type, space format type and indexes) for decimal type
671
    -- is since Tarantool 2.3.1 [1]
672
    --
673
    -- [1] https://github.com/tarantool/tarantool/commit/485439e33196e26d120e622175f88b4edc7a5aa1
674
    enabled_tarantool_features.decimals = is_version_ge(major, minor, patch, suffix, commits_since,
1,602✔
675
                                                        2, 3, 1, nil, nil)
1,602✔
676

677
    -- Full support (Lua type, space format type and indexes) for uuid type
678
    -- is since Tarantool 2.4.1 [1]
679
    --
680
    -- [1] https://github.com/tarantool/tarantool/commit/b238def8065d20070dcdc50b54c2536f1de4c7c7
681
    enabled_tarantool_features.uuids = is_version_ge(major, minor, patch, suffix, commits_since,
1,602✔
682
                                                     2, 4, 1, nil, nil)
1,602✔
683

684
    -- Full support (Lua type, space format type and indexes) for datetime type
685
    -- is since Tarantool 2.10.0-beta2 [1]
686
    --
687
    -- [1] https://github.com/tarantool/tarantool/commit/3bd870261c462416c29226414fe0a2d79aba0c74
688
    enabled_tarantool_features.datetimes = is_version_ge(major, minor, patch, suffix, commits_since,
1,602✔
689
                                                         2, 10, 0, 'beta2', nil)
1,602✔
690

691
    -- Full support (Lua type, space format type and indexes) for datetime type
692
    -- is since Tarantool 2.10.0-rc1 [1]
693
    --
694
    -- [1] https://github.com/tarantool/tarantool/commit/38f0c904af4882756c6dc802f1895117d3deae6a
695
    enabled_tarantool_features.intervals = is_version_ge(major, minor, patch, suffix, commits_since,
1,602✔
696
                                                         2, 10, 0, 'rc1', nil)
1,602✔
697

698
    -- since Tarantool 2.6.3 / 2.7.2 / 2.8.1
699
    enabled_tarantool_features.jsonpath_indexes = is_version_ge(major, minor, patch, suffix, commits_since,
1,602✔
700
                                                                2, 8, 1, nil, nil)
801✔
701
                                               or is_version_in_range(major, minor, patch, suffix, commits_since,
801✔
702
                                                                      2, 7, 2, nil, nil,
703
                                                                      2, 7, math.huge, nil, nil)
×
704
                                               or is_version_in_range(major, minor, patch, suffix, commits_since,
×
705
                                                                      2, 6, 3, nil, nil,
706
                                                                      2, 6, math.huge, nil, nil)
801✔
707

708
    -- The merger module was implemented in 2.2.1, see [1].
709
    -- However it had the critical problem [2], which leads to
710
    -- segfault at attempt to use the module from a fiber serving
711
    -- iproto request. So we don't use it in versions before the
712
    -- fix.
713
    --
714
    -- [1]: https://github.com/tarantool/tarantool/issues/3276
715
    -- [2]: https://github.com/tarantool/tarantool/issues/4954
716
    enabled_tarantool_features.builtin_merger = is_version_ge(major, minor, patch, suffix, commits_since,
1,602✔
717
                                                              2, 6, 0, nil, nil)
801✔
718
                                             or is_version_in_range(major, minor, patch, suffix, commits_since,
801✔
719
                                                                    2, 5, 1, nil, nil,
720
                                                                    2, 5, math.huge, nil, nil)
×
721
                                             or is_version_in_range(major, minor, patch, suffix, commits_since,
×
722
                                                                    2, 4, 2, nil, nil,
723
                                                                    2, 4, math.huge, nil, nil)
×
724
                                             or is_version_in_range(major, minor, patch, suffix, commits_since,
×
725
                                                                    2, 3, 3, nil, nil,
726
                                                                    2, 3, math.huge, nil, nil)
801✔
727

728
    -- The external merger module leans on a set of relatively
729
    -- new APIs in tarantool. So it works only on tarantool
730
    -- versions, which offer those APIs.
731
    --
732
    -- See README of the module:
733
    -- https://github.com/tarantool/tuple-merger
734
    enabled_tarantool_features.external_merger = is_version_ge(major, minor, patch, suffix, commits_since,
1,602✔
735
                                                               2, 7, 0, nil, nil)
801✔
736
                                              or is_version_in_range(major, minor, patch, suffix, commits_since,
801✔
737
                                                                     2, 6, 1, nil, nil,
738
                                                                     2, 6, math.huge, nil, nil)
×
739
                                              or is_version_in_range(major, minor, patch, suffix, commits_since,
×
740
                                                                     2, 5, 2, nil, nil,
741
                                                                     2, 5, math.huge, nil, nil)
×
742
                                              or is_version_in_range(major, minor, patch, suffix, commits_since,
×
743
                                                                     2, 4, 3, nil, nil,
744
                                                                     2, 4, math.huge, nil, nil)
×
745
                                              or is_version_in_range(major, minor, patch, suffix, commits_since,
×
746
                                                                     1, 10, 8, nil, nil,
747
                                                                     1, 10, math.huge, nil, nil)
801✔
748

749
    enabled_tarantool_features.netbox_skip_header_option = is_version_ge(major, minor, patch, suffix, commits_since,
1,602✔
750
                                                                         2, 2, 0, nil, nil)
1,602✔
751

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

756
    -- Native `after` option in index:pairs() and index:select() for O(1) cursor positioning
757
    -- Available since Tarantool 2.10
758
    enabled_tarantool_features.index_pairs_after = is_version_ge(major, minor, patch, suffix, commits_since,
1,602✔
759
                                                                 2, 10, 0, nil, nil)
1,602✔
760

761
    enabled_tarantool_features.tarantool_3 = is_version_ge(major, minor, patch, suffix, commits_since,
1,602✔
762
                                                           3, 0, 0, nil, nil)
1,602✔
763

764
    enabled_tarantool_features.config_get_inside_roles = (
801✔
765
        -- https://github.com/tarantool/tarantool/commit/ebb170cb8cf2b9c4634bcf0178665909f578c335
766
        not utils.is_enterprise_package()
1,602✔
767
        and is_version_ge(major, minor, patch, suffix, commits_since,
1,602✔
768
                          3, 1, 0, 'entrypoint', 77)
801✔
769
    ) or (
801✔
770
        -- https://github.com/tarantool/tarantool/commit/e0e1358cb60d6749c34daf508e05586e0959bf89
771
        not utils.is_enterprise_package()
1,602✔
772
        and is_version_in_range(major, minor, patch, suffix, commits_since,
1,602✔
773
                                3, 0, 1, nil, 10,
801✔
774
                                3, 0, math.huge, nil, nil)
801✔
775
    ) or (
801✔
776
        -- https://github.com/tarantool/tarantool-ee/commit/368cc4007727af30ae3ca3a3cdfc7065f34e02aa
777
        utils.is_enterprise_package()
801✔
778
        and is_version_ge(major, minor, patch, suffix, commits_since,
801✔
779
                          3, 1, 0, 'entrypoint', 44)
×
780
    ) or (
×
781
        -- https://github.com/tarantool/tarantool-ee/commit/1dea81bed4cbe4856a0fc77dcc548849a2dabf45
782
        utils.is_enterprise_package()
801✔
783
        and is_version_in_range(major, minor, patch, suffix, commits_since,
801✔
784
                                3, 0, 1, nil, 10,
785
                                3, 0, math.huge, nil, nil)
×
786
    )
801✔
787

788
    enabled_tarantool_features.role_privileges_not_revoked = (
801✔
789
        -- https://github.com/tarantool/tarantool/commit/b982b46442e62e05ab6340343233aa766ad5e52c
790
        not utils.is_enterprise_package()
1,602✔
791
        and is_version_ge(major, minor, patch, suffix, commits_since,
1,602✔
792
                          3, 1, 0, 'entrypoint', 179)
801✔
793
    ) or (
801✔
794
        -- https://github.com/tarantool/tarantool/commit/ee2faf7c328abc54631233342cb9b88e4ce8cae4
795
        not utils.is_enterprise_package()
1,602✔
796
        and is_version_in_range(major, minor, patch, suffix, commits_since,
1,602✔
797
                                3, 0, 1, nil, 57,
801✔
798
                                3, 0, math.huge, nil, nil)
801✔
799
    ) or (
801✔
800
        -- https://github.com/tarantool/tarantool-ee/commit/5388e9d0f40d86226dc15bb27d85e63b0198e789
801
        utils.is_enterprise_package()
801✔
802
        and is_version_ge(major, minor, patch, suffix, commits_since,
801✔
803
                          3, 1, 0, 'entrypoint', 82)
×
804
    ) or (
×
805
        -- https://github.com/tarantool/tarantool-ee/commit/83d378d01bf2761da8ec684b6afe5683d38faeae
806
        utils.is_enterprise_package()
801✔
807
        and is_version_in_range(major, minor, patch, suffix, commits_since,
801✔
808
                                3, 0, 1, nil, 35,
809
                                3, 0, math.huge, nil, nil)
×
810
    )
801✔
811
end
812

813
determine_enabled_features()
801✔
814

815
for feature_name, feature_enabled in pairs(enabled_tarantool_features) do
12,816✔
816
    local util_name
817
    if feature_name == 'tarantool_3' then
11,214✔
818
        util_name = ('is_%s'):format(feature_name)
801✔
819
    elseif feature_name == 'builtin_merger' then
10,413✔
820
        util_name = ('tarantool_has_%s'):format(feature_name)
801✔
821
    elseif feature_name == 'role_privileges_not_revoked' then
9,612✔
822
        util_name = ('tarantool_%s'):format(feature_name)
801✔
823
    else
824
        util_name = ('tarantool_supports_%s'):format(feature_name)
8,811✔
825
    end
826

827
    local util_func = function() return feature_enabled end
43,789✔
828

829
    utils[util_name] = util_func
11,214✔
830
end
831

832
local function add_nullable_fields_recursive(operations, operations_map, space_format, tuple, id)
833
    if id < 2 or tuple[id - 1] ~= box.NULL then
×
834
        return operations
×
835
    end
836

837
    if space_format[id - 1].is_nullable and not operations_map[id - 1] then
×
838
        table.insert(operations, {'=', id - 1, box.NULL})
×
839
        return add_nullable_fields_recursive(operations, operations_map, space_format, tuple, id - 1)
×
840
    end
841

842
    return operations
×
843
end
844

845
-- Tarantool < 2.1 has no fields `box.error.NO_SUCH_FIELD_NO` and `box.error.NO_SUCH_FIELD_NAME`.
846
if tarantool_version_at_least(2, 1, 0, nil) then
1,602✔
847
    function utils.is_field_not_found(err_code)
801✔
848
        return err_code == box.error.NO_SUCH_FIELD_NO or err_code == box.error.NO_SUCH_FIELD_NAME
81✔
849
    end
850
else
851
    function utils.is_field_not_found(err_code)
×
852
        return err_code == box.error.NO_SUCH_FIELD
×
853
    end
854
end
855

856
local function get_operations_map(operations)
857
    local map = {}
×
858
    for _, operation in ipairs(operations) do
×
859
        map[operation[2]] = true
×
860
    end
861

862
    return map
×
863
end
864

865
function utils.add_intermediate_nullable_fields(operations, space_format, tuple)
801✔
866
    if tuple == nil then
2✔
867
        return operations
×
868
    end
869

870
    -- If tarantool doesn't supports the fieldpaths, we already
871
    -- have converted operations (see this function call in update.lua)
872
    if utils.tarantool_supports_fieldpaths() then
4✔
873
        local formatted_operations, err = utils.convert_operations(operations, space_format)
2✔
874
        if err ~= nil then
2✔
875
            return operations
2✔
876
        end
877

878
        operations = formatted_operations
×
879
    end
880

881
    -- We need this map to check if there is a field update
882
    -- operation with constant complexity
883
    local operations_map = get_operations_map(operations)
×
884
    for _, operation in ipairs(operations) do
×
885
        operations = add_nullable_fields_recursive(
×
886
            operations, operations_map,
887
            space_format, tuple, operation[2]
×
888
        )
889
    end
890

891
    table.sort(operations, function(v1, v2) return v1[2] < v2[2] end)
×
892
    return operations
×
893
end
894

895
function utils.convert_operations(user_operations, space_format)
801✔
896
    local converted_operations = {}
2✔
897

898
    for _, operation in ipairs(user_operations) do
2✔
899
        if type(operation[2]) == 'string' then
2✔
900
            local field_id
901
            for fieldno, field_format in ipairs(space_format) do
10✔
902
                if field_format.name == operation[2] then
8✔
903
                    field_id = fieldno
×
904
                    break
905
                end
906
            end
907

908
            if field_id == nil then
2✔
909
                return nil, ParseOperationsError:new(
4✔
910
                        "Space format doesn't contain field named %q", operation[2])
4✔
911
            end
912

913
            table.insert(converted_operations, {
×
914
                operation[1], field_id, operation[3]
×
915
            })
916
        else
917
            table.insert(converted_operations, operation)
×
918
        end
919
    end
920

921
    return converted_operations
×
922
end
923

924
function utils.unflatten_rows(rows, metadata)
801✔
925
    if metadata == nil then
17,779✔
926
        return nil, UnflattenError:new('Metadata is not provided')
×
927
    end
928

929
    local result = table.new(#rows, 0)
17,779✔
930
    local err
931
    for i, row in ipairs(rows) do
37,006✔
932
        result[i], err = utils.unflatten(row, metadata)
38,454✔
933
        if err ~= nil then
19,227✔
934
            return nil, err
×
935
        end
936
    end
937
    return result
17,779✔
938
end
939

940
local inverted_tarantool_iters = {
801✔
941
    [box.index.EQ] = box.index.REQ,
801✔
942
    [box.index.GT] = box.index.LT,
801✔
943
    [box.index.GE] = box.index.LE,
801✔
944
    [box.index.LT] = box.index.GT,
801✔
945
    [box.index.LE] = box.index.GE,
801✔
946
    [box.index.REQ] = box.index.EQ,
801✔
947
}
948

949
function utils.invert_tarantool_iter(iter)
801✔
950
    local inverted_iter = inverted_tarantool_iters[iter]
49✔
951
    assert(inverted_iter ~= nil, "Unsupported Tarantool iterator: " .. tostring(iter))
49✔
952
    return inverted_iter
49✔
953
end
954

955
function utils.reverse_inplace(t)
801✔
956
    for i = 1,math.floor(#t / 2) do
91✔
957
        t[i], t[#t - i + 1] = t[#t - i + 1], t[i]
43✔
958
    end
959
    return t
48✔
960
end
961

962
function utils.get_bucket_id_fieldno(space, shard_index_name)
801✔
963
    shard_index_name = shard_index_name or 'bucket_id'
720,621✔
964
    local bucket_id_index = space.index[shard_index_name]
720,621✔
965
    if bucket_id_index == nil then
720,621✔
966
        return nil, ShardingError:new('%q index is not found', shard_index_name)
24✔
967
    end
968

969
    return bucket_id_index.parts[1].fieldno
720,609✔
970
end
971

972
-- Build a map with field number as a keys and part number
973
-- as a values using index parts as a source.
974
function utils.get_index_fieldno_map(index_parts)
801✔
975
    dev_checks('table')
159✔
976

977
    local fieldno_map = {}
159✔
978
    for i, part in ipairs(index_parts) do
428✔
979
        local fieldno = part.fieldno
269✔
980
        fieldno_map[fieldno] = i
269✔
981
    end
982

983
    return fieldno_map
159✔
984
end
985

986
-- Build a map with field names as a keys and fieldno's
987
-- as a values using space format as a source.
988
function utils.get_format_fieldno_map(space_format)
801✔
989
    dev_checks('table')
7,720✔
990

991
    local fieldno_map = {}
7,720✔
992
    for fieldno, field_format in ipairs(space_format) do
39,203✔
993
        fieldno_map[field_format.name] = fieldno
31,483✔
994
    end
995

996
    return fieldno_map
7,720✔
997
end
998

999
local uuid_t = ffi.typeof('struct tt_uuid')
801✔
1000
function utils.is_uuid(value)
801✔
1001
    return ffi.istype(uuid_t, value)
780✔
1002
end
1003

1004
local function get_field_format(space_format, field_name)
1005
    dev_checks('table', 'string')
466✔
1006

1007
    local metadata = space_format_cache[space_format]
466✔
1008
    if metadata ~= nil then
466✔
1009
        return metadata[field_name]
446✔
1010
    end
1011

1012
    space_format_cache[space_format] = {}
20✔
1013
    for _, field in ipairs(space_format) do
134✔
1014
        space_format_cache[space_format][field.name] = field
114✔
1015
    end
1016

1017
    return space_format_cache[space_format][field_name]
20✔
1018
end
1019

1020
local function filter_format_fields(space_format, field_names)
1021
    dev_checks('table', 'table')
182✔
1022

1023
    local filtered_space_format = {}
182✔
1024

1025
    for i, field_name in ipairs(field_names) do
618✔
1026
        filtered_space_format[i] = get_field_format(space_format, field_name)
932✔
1027
        if filtered_space_format[i] == nil then
466✔
1028
            return nil, FilterFieldsError:new(
60✔
1029
                    'Space format doesn\'t contain field named %q', field_name
30✔
1030
            )
60✔
1031
        end
1032
    end
1033

1034
    return filtered_space_format
152✔
1035
end
1036

1037
function utils.get_fields_format(space_format, field_names)
801✔
1038
    dev_checks('table', '?table')
5,636✔
1039

1040
    if field_names == nil then
5,636✔
1041
        return table.copy(space_format)
5,572✔
1042
    end
1043

1044
    local filtered_space_format, err = filter_format_fields(space_format, field_names)
64✔
1045

1046
    if err ~= nil then
64✔
1047
        return nil, err
2✔
1048
    end
1049

1050
    return filtered_space_format
62✔
1051
end
1052

1053
function utils.format_result(rows, space, field_names)
801✔
1054
    local result = {}
131,155✔
1055
    local err
1056
    local space_format = space:format()
131,155✔
1057
    result.rows = rows
131,155✔
1058

1059
    if field_names == nil then
131,155✔
1060
        result.metadata = table.copy(space_format)
262,074✔
1061
        return result
131,037✔
1062
    end
1063

1064
    result.metadata, err = filter_format_fields(space_format, field_names)
236✔
1065

1066
    if err ~= nil then
118✔
1067
        return nil, err
28✔
1068
    end
1069

1070
    return result
90✔
1071
end
1072

1073
local function truncate_tuple_metadata(tuple_metadata, field_names)
1074
    dev_checks('?table', 'table')
31✔
1075

1076
    if tuple_metadata == nil then
31✔
1077
        return nil
3✔
1078
    end
1079

1080
    local truncated_metadata = {}
28✔
1081

1082
    if #tuple_metadata < #field_names then
28✔
1083
        return nil, FilterFieldsError:new(
×
1084
                'Field names don\'t match to tuple metadata'
1085
        )
1086
    end
1087

1088
    for i, name in ipairs(field_names) do
79✔
1089
        if tuple_metadata[i].name ~= name then
53✔
1090
            return nil, FilterFieldsError:new(
4✔
1091
                    'Field names don\'t match to tuple metadata'
1092
            )
4✔
1093
        end
1094

1095
        table.insert(truncated_metadata, tuple_metadata[i])
51✔
1096
    end
1097

1098
    return truncated_metadata
26✔
1099
end
1100

1101
function utils.cut_objects(objs, field_names)
801✔
1102
    dev_checks('table', 'table')
5✔
1103

1104
    for i, obj in ipairs(objs) do
20✔
1105
        objs[i] = schema.filter_obj_fields(obj, field_names)
30✔
1106
    end
1107

1108
    return objs
5✔
1109
end
1110

1111
function utils.cut_rows(rows, metadata, field_names)
801✔
1112
    dev_checks('table', '?table', 'table')
31✔
1113

1114
    local truncated_metadata, err = truncate_tuple_metadata(metadata, field_names)
31✔
1115

1116
    if err ~= nil then
31✔
1117
        return nil, err
2✔
1118
    end
1119

1120
    for i, row in ipairs(rows) do
72✔
1121
        rows[i] = schema.truncate_row_trailing_fields(row, field_names)
86✔
1122
    end
1123

1124
    return {
29✔
1125
        metadata = truncated_metadata,
29✔
1126
        rows = rows,
29✔
1127
    }
29✔
1128
end
1129

1130
local function flatten_obj(vshard_router, space_name, obj, skip_nullability_check)
1131
    local space_format, err = utils.get_space_format(space_name, vshard_router)
10,850✔
1132
    if err ~= nil then
10,850✔
1133
        return nil, FlattenError:new("Failed to get space format: %s", err), const.NEED_SCHEMA_RELOAD
344✔
1134
    end
1135

1136
    local tuple, err = utils.flatten(obj, space_format, nil, skip_nullability_check)
10,678✔
1137
    if err ~= nil then
10,678✔
1138
        return nil, FlattenError:new("Object is specified in bad format: %s", err), const.NEED_SCHEMA_RELOAD
2,088✔
1139
    end
1140

1141
    return tuple
9,634✔
1142
end
1143

1144
function utils.flatten_obj_reload(vshard_router, space_name, obj, skip_nullability_check)
801✔
1145
    return schema.wrap_func_reload(vshard_router, flatten_obj, space_name, obj, skip_nullability_check)
10,206✔
1146
end
1147

1148
-- Merge two options map.
1149
--
1150
-- `opts_a` and/or `opts_b` can be `nil`.
1151
--
1152
-- If `opts_a.foo` and `opts_b.foo` exists, prefer `opts_b.foo`.
1153
function utils.merge_options(opts_a, opts_b)
801✔
1154
    return fun.chain(opts_a or {}, opts_b or {}):tomap()
19,114✔
1155
end
1156

1157
local function lj_char_isident(n)
1158
    return bit.band(lj_char_bits[n + 2], LJ_CHAR_IDENT) == LJ_CHAR_IDENT
12,030✔
1159
end
1160

1161
local function lj_char_isdigit(n)
1162
    return bit.band(lj_char_bits[n + 2], LJ_CHAR_DIGIT) == LJ_CHAR_DIGIT
734✔
1163
end
1164

1165
function utils.check_name_isident(name)
801✔
1166
    dev_checks('string')
735✔
1167

1168
    -- sharding function name cannot
1169
    -- be equal to lua keyword
1170
    if LUA_KEYWORDS[name] then
735✔
1171
        return false
1✔
1172
    end
1173

1174
    -- sharding function name cannot
1175
    -- begin with a digit
1176
    local char_number = string.byte(name:sub(1,1))
1,468✔
1177
    if lj_char_isdigit(char_number) then
1,468✔
1178
        return false
1✔
1179
    end
1180

1181
    -- sharding func name must be sequence
1182
    -- of letters, digits, or underscore symbols
1183
    for i = 1, #name do
12,762✔
1184
        local char_number = string.byte(name:sub(i,i))
24,060✔
1185
        if not lj_char_isident(char_number) then
24,060✔
1186
            return false
1✔
1187
        end
1188
    end
1189

1190
    return true
732✔
1191
end
1192

1193
function utils.update_storage_call_error_description(err, func_name, replicaset_id)
801✔
1194
    if err == nil then
851✔
1195
        return nil
×
1196
    end
1197

1198
    if (err.type == 'ClientError' or err.type == 'AccessDeniedError' or err.type == 'LuajitError')
1,594✔
1199
        and type(err.message) == 'string' then
1,328✔
1200
        local not_defined_str = string.format("Procedure '%s' is not defined", func_name)
667✔
1201
        local not_registered_str = string.format("Function '%s' is not registered", func_name)
667✔
1202
        local access_denied_str = string.format("Execute access to function '%s' is denied", func_name)
667✔
1203
        if err.message == not_defined_str or err.message:startswith(access_denied_str)
1,989✔
1204
                or err.message:find(not_registered_str)
1,328✔
1205
                or err.message == "Procedure '_crud.call_on_storage' is not defined"
1,324✔
1206
                or err.message:startswith("Execute access to function '_crud.call_on_storage' is denied") then
1,975✔
1207
            if func_name:startswith('_crud.') then
20✔
1208
                err = NotInitializedError:new("Function '%s' is not registered: " ..
12✔
1209
                    "crud isn't initialized on replicaset %q or crud module versions mismatch " ..
6✔
1210
                    "between router and storage",
6✔
1211
                    func_name, replicaset_id or "Unknown")
12✔
1212
            else
1213
                err = NotInitializedError:new("Function '%s' is not registered", func_name)
8✔
1214
            end
1215
        end
1216
    end
1217
    return err
851✔
1218
end
1219

1220
--- Insert each value from values to list
1221
--
1222
-- @function list_extend
1223
--
1224
-- @param table list
1225
--  List to be extended
1226
--
1227
-- @param table values
1228
--  Values to be inserted to list
1229
--
1230
-- @return[1] list
1231
--  List with old values and inserted values
1232
function utils.list_extend(list, values)
801✔
1233
    dev_checks('table', 'table')
4,611✔
1234

1235
    for _, value in ipairs(values) do
79,674✔
1236
        table.insert(list, value)
75,063✔
1237
    end
1238

1239
    return list
4,611✔
1240
end
1241

1242
function utils.list_slice(list, start_index, end_index)
801✔
1243
    dev_checks('table', 'number', '?number')
48✔
1244

1245
    if end_index == nil then
48✔
1246
        end_index = table.maxn(list)
48✔
1247
    end
1248

1249
    local slice = {}
48✔
1250
    for i = start_index, end_index do
120✔
1251
        table.insert(slice, list[i])
72✔
1252
    end
1253

1254
    return slice
48✔
1255
end
1256

1257
local expected_vshard_api = {
801✔
1258
    'routeall', 'route', 'bucket_id_strcrc32',
1259
    'callrw', 'callro', 'callbro', 'callre',
1260
    'callbre', 'map_callrw'
1261
}
1262

1263
--- Verifies that a table has expected vshard
1264
--  router handles.
1265
local function verify_vshard_router(router)
1266
    dev_checks("table")
106✔
1267

1268
    for _, func_name in ipairs(expected_vshard_api) do
664✔
1269
        if type(router[func_name]) ~= 'function' then
602✔
1270
            return false
44✔
1271
        end
1272
    end
1273

1274
    return true
62✔
1275
end
1276

1277
--- Get a vshard router instance from a parameter.
1278
--
1279
--  If a string passed, extract router instance from
1280
--  Cartridge vshard groups. If table passed, verifies
1281
--  that a table is a vshard router instance.
1282
--
1283
-- @function get_vshard_router_instance
1284
--
1285
-- @param[opt] router name of a vshard group or a vshard router
1286
--  instance
1287
--
1288
-- @return[1] table vshard router instance
1289
-- @treturn[2] nil
1290
-- @treturn[2] table Error description
1291
function utils.get_vshard_router_instance(router)
801✔
1292
    dev_checks('?string|table')
142,962✔
1293

1294
    local router_instance
1295

1296
    if type(router) == 'string' then
142,962✔
1297
        if not is_cartridge then
70✔
1298
            return nil, VshardRouterError:new("Vshard groups are supported only in Tarantool Cartridge")
×
1299
        end
1300

1301
        local router_service = cartridge.service_get('vshard-router')
70✔
1302
        assert(router_service ~= nil)
70✔
1303

1304
        router_instance = router_service.get(router)
140✔
1305
        if router_instance == nil then
70✔
1306
            return nil, VshardRouterError:new("Vshard group %s is not found", router)
×
1307
        end
1308
    elseif type(router) == 'table' then
142,892✔
1309
        if not verify_vshard_router(router) then
212✔
1310
            return nil, VshardRouterError:new("Invalid opts.vshard_router table value, " ..
88✔
1311
                                              "a vshard router instance has been expected")
88✔
1312
        end
1313

1314
        router_instance = router
62✔
1315
    else
1316
        assert(type(router) == 'nil')
142,786✔
1317
        router_instance = vshard.router.static
142,786✔
1318

1319
        if router_instance == nil then
142,786✔
1320
            return nil, VshardRouterError:new("Default vshard group is not found and custom " ..
88✔
1321
                                              "is not specified with opts.vshard_router")
88✔
1322
        end
1323
    end
1324

1325
    return router_instance
142,874✔
1326
end
1327

1328
--- Check if Tarantool Cartridge hotreload supported
1329
--  and get its implementaion.
1330
--
1331
-- @function is_cartridge_hotreload_supported
1332
--
1333
-- @return[1] true or false
1334
-- @return[1] module table, if supported
1335
function utils.is_cartridge_hotreload_supported()
801✔
1336
    if not is_cartridge_hotreload then
383✔
1337
        return false
×
1338
    end
1339

1340
    return true, cartridge_hotreload
383✔
1341
end
1342

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

1347
    utils.is_interval = function(o)
1348
        return ffi.istype(interval_t, o)
20✔
1349
    end
1350
else
1351
    utils.is_interval = function()
1352
        return false
×
1353
    end
1354
end
1355

1356
for k, v in pairs(require('crud.common.vshard_utils')) do
9,612✔
1357
    utils[k] = v
7,209✔
1358
end
1359

1360
function utils.append_array(array_src, array_dst)
801✔
1361
    if not array_dst then
151,922✔
1362
        return array_src
11✔
1363
    end
1364

1365
    table.move(array_dst, 1, #array_dst, #array_src + 1, array_src)
151,911✔
1366

1367
    return array_src
151,911✔
1368
end
1369

1370
function utils.is_uint(value)
801✔
1371
    if type(value) == 'number' then
1,183✔
1372
        return value >= 0 and math.floor(value) == value
1,067✔
1373
    elseif type(value) == 'cdata' then
116✔
1374
        local ok, casted = pcall(tonumber, value)
8✔
1375
        return ok and type(casted) == 'number' and casted >= 0 and math.floor(casted) == casted
8✔
1376
    end
1377

1378
    return false
108✔
1379
end
1380

1381
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