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

tarantool / crud / 8354849543

20 Mar 2024 06:41AM UTC coverage: 88.694% (+0.1%) from 88.597%
8354849543

push

github

DifferentialOrange
scan: fix nil filters

Before this patch, some nil conditions failed with internal filter
library build error. This patch fixes this internal error, as well as
normalize filters to behave similar to core indexes.

The logic in core Tarantool select is as follows. `nil` condition in
index select is an absence of condition, thus all data is returned
disregarding the condition (condition may affect the order). `box.NULL`
condition is a condition for the null value -- in case of EQ,
only records with null index value are returned, in case of GT,
all non-null values are returned since nulls are in the beginning of an
index and so on. `nil`s and `box.NULL`s in tuple are both satisfy
`box.NULL` equity.

After this patch, `nil` filter condition is treated as no condition.
This is a breaking change since conditions for `'>'` and `'<'`
operator with `nil` operand had resulted with empty response
before this patch. But since it was inconsistent with scanning index
conditions and wasn't intentional, we change it here.

Closes #422

16 of 16 new or added lines in 1 file covered. (100.0%)

68 existing lines in 1 file now uncovered.

4762 of 5369 relevant lines covered (88.69%)

6853.45 hits per line

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

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

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

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

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

28
local utils = {}
463✔
29

30
--- Returns a full call string for a storage function name.
31
--
32
--  @param string name a base name of the storage function.
33
--
34
--  @return a full string for the call.
35
function utils.get_storage_call(name)
463✔
36
    dev_checks('string')
12,117✔
37

38
    return '_crud.' .. name
12,117✔
39
end
40

41
local CRUD_STORAGE_INFO_FUNC_NAME = utils.get_storage_call('storage_info_on_storage')
463✔
42

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

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

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

107
    return string.format(
650✔
108
        "Failed for %s: %s",
650✔
109
        replicaset_id,
650✔
110
        string.format(msg, ...)
650✔
111
    )
650✔
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

UNCOV
123
    return nil, nil
×
124
end
125

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

129
    timeout = timeout or const.DEFAULT_VSHARD_CALL_TIMEOUT
87,796✔
130
    local deadline = fiber.clock() + timeout
175,592✔
131
    local iter_sleep = math.min(timeout / 100, 0.1)
87,796✔
132
    while (
133
        -- Break if the deadline condition is exceeded.
134
        -- Handling for deadline errors are below in the code.
135
        fiber.clock() < deadline
175,592✔
136
    ) do
87,796✔
137
        -- Try to get master with timeout.
138
        replicasets = vshard_router:routeall()
175,592✔
139
        if replica_id ~= nil then
87,796✔
140
            -- Get the same replica on which the last DML operation was performed.
141
            -- This approach is temporary and is related to [1], [2].
142
            -- [1] https://github.com/tarantool/crud/issues/236
143
            -- [2] https://github.com/tarantool/crud/issues/361
144
            replicaset_id, replicaset = get_replicaset_by_replica_id(replicasets, replica_id)
40✔
145
            break
20✔
146
        else
147
            replicaset_id, replicaset = next(replicasets)
87,776✔
148
        end
149

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

UNCOV
158
        fiber.sleep(iter_sleep)
×
159
    end
160

161
    if replicaset == nil then
87,796✔
UNCOV
162
        return nil, GetSpaceError:new(
×
UNCOV
163
            'The router returned empty replicasets: ' ..
×
164
            'perhaps other instances are unavailable or you have configured only the router')
×
165
    end
166

167
    master = utils.get_replicaset_master(replicaset, {cached = true})
175,592✔
168

169
    if master == nil then
87,796✔
UNCOV
170
        local error_msg = string.format(
×
UNCOV
171
            'The master was not found in replicaset %s, ' ..
×
172
            'check status of the master and repeat the operation later',
173
             replicaset_id)
×
UNCOV
174
        return nil, GetSpaceError:new(error_msg)
×
175
    end
176

177
    if master.conn.error ~= nil then
87,796✔
UNCOV
178
        local error_msg = string.format(
×
179
            'The connection to the master of replicaset %s is not valid: %s',
180
             replicaset_id, master.conn.error)
×
UNCOV
181
        return nil, GetSpaceError:new(error_msg)
×
182
    end
183

184
    return master.conn.space, nil, master.conn.schema_version
87,796✔
185
end
186

187
function utils.get_space(space_name, vshard_router, timeout, replica_id)
463✔
188
    local spaces, err, schema_version = utils.get_spaces(vshard_router, timeout, replica_id)
87,764✔
189

190
    if spaces == nil then
87,764✔
UNCOV
191
        return nil, err
×
192
    end
193

194
    return spaces[space_name], err, schema_version
87,764✔
195
end
196

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

206
    local space_format = space:format()
9,612✔
207

208
    return space_format
9,612✔
209
end
210

211
function utils.fetch_latest_metadata_when_single_storage(space, space_name, netbox_schema_version,
463✔
212
                                                         vshard_router, opts, storage_info)
213
    -- Checking the relevance of the schema version is necessary
214
    -- to prevent the irrelevant metadata of the DML operation.
215
    -- This approach is temporary and is related to [1], [2].
216
    -- [1] https://github.com/tarantool/crud/issues/236
217
    -- [2] https://github.com/tarantool/crud/issues/361
218
    local latest_space, err
219

220
    assert(storage_info.replica_schema_version ~= nil,
40✔
221
           'check the replica_schema_version value from storage ' ..
20✔
222
           'for correct use of the fetch_latest_metadata opt')
20✔
223

224
    local replica_id
225
    if storage_info.replica_id == nil then -- Backward compatibility.
20✔
UNCOV
226
        assert(storage_info.replica_uuid ~= nil,
×
UNCOV
227
               'check the replica_uuid value from storage ' ..
×
228
               'for correct use of the fetch_latest_metadata opt')
×
229
        replica_id = storage_info.replica_uuid
×
230
    else
231
        replica_id = storage_info.replica_id
20✔
232
    end
233

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

238
    if storage_info.replica_schema_version ~= netbox_schema_version then
20✔
239
        local ok, reload_schema_err = schema.reload_schema(vshard_router)
20✔
240
        if ok then
20✔
241
            latest_space, err = utils.get_space(space_name, vshard_router,
40✔
242
                                                opts.timeout, replica_id)
40✔
243
            if err ~= nil then
20✔
UNCOV
244
                local warn_msg = "Failed to fetch space for latest schema actualization, metadata may be outdated: %s"
×
UNCOV
245
                log.warn(warn_msg, err)
×
246
            end
247
            if latest_space == nil then
20✔
UNCOV
248
                log.warn("Failed to find space for latest schema actualization, metadata may be outdated")
×
249
            end
250
        else
UNCOV
251
            log.warn("Failed to reload schema, metadata may be outdated: %s", reload_schema_err)
×
252
        end
253
    end
254
    if err == nil and latest_space ~= nil then
20✔
255
        space = latest_space
20✔
256
    end
257

258
    return space
20✔
259
end
260

261
function utils.fetch_latest_metadata_when_map_storages(space, space_name, vshard_router, opts,
463✔
262
                                                       storages_info, netbox_schema_version)
263
    -- Checking the relevance of the schema version is necessary
264
    -- to prevent the irrelevant metadata of the DML operation.
265
    -- This approach is temporary and is related to [1], [2].
266
    -- [1] https://github.com/tarantool/crud/issues/236
267
    -- [2] https://github.com/tarantool/crud/issues/361
268
    local latest_space, err
269
    for _, storage_info in pairs(storages_info) do
32✔
270
        assert(storage_info.replica_schema_version ~= nil,
32✔
271
            'check the replica_schema_version value from storage ' ..
16✔
272
            'for correct use of the fetch_latest_metadata opt')
16✔
273
        assert(netbox_schema_version ~= nil,
32✔
274
               'check the netbox_schema_version value from net_box conn on router ' ..
16✔
275
               'for correct use of the fetch_latest_metadata opt')
16✔
276
        if storage_info.replica_schema_version ~= netbox_schema_version then
16✔
277
            local ok, reload_schema_err = schema.reload_schema(vshard_router)
16✔
278
            if ok then
16✔
279
                latest_space, err = utils.get_space(space_name, vshard_router, opts.timeout)
32✔
280
                if err ~= nil then
16✔
UNCOV
281
                    local warn_msg = "Failed to fetch space for latest schema actualization, " ..
×
282
                                     "metadata may be outdated: %s"
283
                    log.warn(warn_msg, err)
×
284
                end
285
                if latest_space == nil then
16✔
UNCOV
286
                    log.warn("Failed to find space for latest schema actualization, metadata may be outdated")
×
287
                end
288
            else
UNCOV
289
                log.warn("Failed to reload schema, metadata may be outdated: %s", reload_schema_err)
×
290
            end
291
            if err == nil and latest_space ~= nil then
16✔
292
                space = latest_space
16✔
293
            end
294
            break
16✔
295
        end
296
    end
297

298
    return space
16✔
299
end
300

301
function utils.fetch_latest_metadata_for_select(space_name, vshard_router, opts,
463✔
302
                                                storages_info, iter)
303
    -- Checking the relevance of the schema version is necessary
304
    -- to prevent the irrelevant metadata of the DML operation.
305
    -- This approach is temporary and is related to [1], [2].
306
    -- [1] https://github.com/tarantool/crud/issues/236
307
    -- [2] https://github.com/tarantool/crud/issues/361
308
    for _, storage_info in pairs(storages_info) do
8✔
309
        assert(storage_info.replica_schema_version ~= nil,
8✔
310
               'check the replica_schema_version value from storage ' ..
4✔
311
               'for correct use of the fetch_latest_metadata opt')
4✔
312
        assert(iter.netbox_schema_version ~= nil,
8✔
313
               'check the netbox_schema_version value from net_box conn on router ' ..
4✔
314
               'for correct use of the fetch_latest_metadata opt')
4✔
315
        if storage_info.replica_schema_version ~= iter.netbox_schema_version then
4✔
316
            local ok, reload_schema_err = schema.reload_schema(vshard_router)
4✔
317
            if ok then
4✔
318
                local err
319
                iter.space, err = utils.get_space(space_name, vshard_router, opts.timeout)
8✔
320
                if err ~= nil then
4✔
UNCOV
321
                    local warn_msg = "Failed to fetch space for latest schema actualization, " ..
×
322
                                     "metadata may be outdated: %s"
323
                    log.warn(warn_msg, err)
×
324
                end
325
            else
UNCOV
326
                log.warn("Failed to reload schema, metadata may be outdated: %s", reload_schema_err)
×
327
            end
328
            break
329
        end
330
    end
331

332
    return iter
4✔
333
end
334

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

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

341
function utils.flatten(object, space_format, bucket_id, skip_nullability_check)
463✔
342
    local flatten_func = flatten_functions_cache[space_format]
9,790✔
343
    if flatten_func ~= nil then
9,790✔
344
        local data, err = flatten_func(object, bucket_id, skip_nullability_check)
9,535✔
345
        if err ~= nil then
9,535✔
346
            return nil, FlattenError:new(err)
1,354✔
347
        end
348
        return data
8,858✔
349
    end
350

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

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

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

363
    local fieldmap = {}
255✔
364

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

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

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

404
function utils.unflatten(tuple, space_format)
463✔
405
    if tuple == nil then return nil end
19,579✔
406

407
    local object = {}
19,579✔
408

409
    for fieldno, field_format in ipairs(space_format) do
110,380✔
410
        local value = tuple[fieldno]
90,802✔
411

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

416
        object[field_format.name] = value
90,801✔
417
    end
418

419
    return object
19,578✔
420
end
421

422
function utils.extract_key(tuple, key_parts)
463✔
423
    local key = {}
185,464✔
424
    for i, part in ipairs(key_parts) do
371,648✔
425
        key[i] = tuple[part.fieldno]
186,184✔
426
    end
427
    return key
185,464✔
428
end
429

430
function utils.merge_primary_key_parts(key_parts, pk_parts)
463✔
431
    local merged_parts = {}
6,828✔
432
    local key_fieldnos = {}
6,828✔
433

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

439
    for _, pk_part in ipairs(pk_parts) do
14,666✔
440
        if not key_fieldnos[pk_part.fieldno] then
7,838✔
441
            table.insert(merged_parts, pk_part)
3,345✔
442
        end
443
    end
444

445
    return merged_parts
6,828✔
446
end
447

448
function utils.enrich_field_names_with_cmp_key(field_names, key_parts, space_format)
463✔
449
    if field_names == nil then
4,881✔
450
        return nil
4,796✔
451
    end
452

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

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

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

469
    return enriched_field_names
85✔
470
end
471

472

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

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

485
    return nil
1,648✔
486
end
487

488
utils.get_version_suffix = get_version_suffix
463✔
489

490

491
local suffix_with_digit_weight = {
463✔
492
    alpha = -3000,
493
    beta  = -2000,
494
    rc    = -1000,
495
}
496

497
local function get_version_suffix_weight(suffix)
498
    if suffix == nil then
10,800✔
499
        return 0
9,831✔
500
    end
501

502
    if suffix:find('^entrypoint$') then
969✔
503
        return -math.huge
9✔
504
    end
505

506
    for header, weight in pairs(suffix_with_digit_weight) do
2,878✔
507
        local pos, _, digits = suffix:find('^' .. header .. '(%d)$')
1,916✔
508
        if pos ~= nil then
1,916✔
509
            return weight + tonumber(digits)
958✔
510
        end
511
    end
512

513
    UtilsInternalError:assert(false,
4✔
514
        'Unexpected suffix %q, parse with "utils.get_version_suffix" first', suffix)
2✔
515
end
516

517
utils.get_version_suffix_weight = get_version_suffix_weight
463✔
518

519

520
local function is_version_ge(major, minor,
521
                             patch, suffix,
522
                             major_to_compare, minor_to_compare,
523
                             patch_to_compare, suffix_to_compare)
524
    major = major or 0
5,395✔
525
    minor = minor or 0
5,395✔
526
    patch = patch or 0
5,395✔
527
    local suffix_weight = get_version_suffix_weight(suffix)
5,395✔
528

529
    major_to_compare = major_to_compare or 0
5,395✔
530
    minor_to_compare = minor_to_compare or 0
5,395✔
531
    patch_to_compare = patch_to_compare or 0
5,395✔
532
    local suffix_weight_to_compare = get_version_suffix_weight(suffix_to_compare)
5,395✔
533

534
    if major > major_to_compare then return true end
5,395✔
535
    if major < major_to_compare then return false end
5,382✔
536

537
    if minor > minor_to_compare then return true end
5,371✔
538
    if minor < minor_to_compare then return false end
552✔
539

540
    if patch > patch_to_compare then return true end
547✔
541
    if patch < patch_to_compare then return false end
11✔
542

543
    if suffix_weight > suffix_weight_to_compare then return true end
10✔
544
    if suffix_weight < suffix_weight_to_compare then return false end
7✔
545

546
    return true
4✔
547
end
548

549
utils.is_version_ge = is_version_ge
463✔
550

551

552
local function is_version_in_range(major, minor,
553
                                   patch, suffix,
554
                                   major_left_side, minor_left_side,
555
                                   patch_left_side, suffix_left_side,
556
                                   major_right_side, minor_right_side,
557
                                   patch_right_side, suffix_right_side)
558
    return is_version_ge(major, minor,
4✔
559
                         patch, suffix,
2✔
560
                         major_left_side, minor_left_side,
2✔
561
                         patch_left_side, suffix_left_side)
2✔
562
       and is_version_ge(major_right_side, minor_right_side,
4✔
563
                         patch_right_side, suffix_right_side,
2✔
564
                         major, minor,
2✔
565
                         patch, suffix)
4✔
566
end
567

568
utils.is_version_in_range = is_version_in_range
463✔
569

570

571
local function get_tarantool_version()
572
    local version_parts = rawget(_G, '_TARANTOOL'):split('-', 1)
1,646✔
573

574
    local major_minor_patch_parts = version_parts[1]:split('.', 2)
1,646✔
575
    local major = tonumber(major_minor_patch_parts[1])
1,646✔
576
    local minor = tonumber(major_minor_patch_parts[2])
1,646✔
577
    local patch = tonumber(major_minor_patch_parts[3])
1,646✔
578

579
    local suffix = get_version_suffix(version_parts[2])
1,646✔
580

581
    return major, minor, patch, suffix
1,646✔
582
end
583

584
utils.get_tarantool_version = get_tarantool_version
463✔
585

586

587
local function tarantool_version_at_least(wanted_major, wanted_minor, wanted_patch)
588
    local major, minor, patch, suffix = get_tarantool_version()
1,182✔
589

590
    return is_version_ge(major, minor, patch, suffix,
1,182✔
591
                         wanted_major, wanted_minor, wanted_patch, nil)
1,182✔
592
end
593

594
utils.tarantool_version_at_least = tarantool_version_at_least
463✔
595

596

597
local enabled_tarantool_features = {}
463✔
598

599
local function determine_enabled_features()
600
    local major, minor, patch, suffix = get_tarantool_version()
463✔
601

602
    -- since Tarantool 2.3.1
603
    enabled_tarantool_features.fieldpaths = is_version_ge(major, minor, patch, suffix,
926✔
604
                                                          2, 3, 1, nil)
926✔
605

606
    -- Full support (Lua type, space format type and indexes) for decimal type
607
    -- is since Tarantool 2.3.1 [1]
608
    --
609
    -- [1] https://github.com/tarantool/tarantool/commit/485439e33196e26d120e622175f88b4edc7a5aa1
610
    enabled_tarantool_features.decimals = is_version_ge(major, minor, patch, suffix,
926✔
611
                                                        2, 3, 1, nil)
926✔
612

613
    -- Full support (Lua type, space format type and indexes) for uuid type
614
    -- is since Tarantool 2.4.1 [1]
615
    --
616
    -- [1] https://github.com/tarantool/tarantool/commit/b238def8065d20070dcdc50b54c2536f1de4c7c7
617
    enabled_tarantool_features.uuids = is_version_ge(major, minor, patch, suffix,
926✔
618
                                                     2, 4, 1, nil)
926✔
619

620
    -- Full support (Lua type, space format type and indexes) for datetime type
621
    -- is since Tarantool 2.10.0-beta2 [1]
622
    --
623
    -- [1] https://github.com/tarantool/tarantool/commit/3bd870261c462416c29226414fe0a2d79aba0c74
624
    enabled_tarantool_features.datetimes = is_version_ge(major, minor, patch, suffix,
926✔
625
                                                         2, 10, 0, 'beta2')
926✔
626

627
    -- Full support (Lua type, space format type and indexes) for datetime type
628
    -- is since Tarantool 2.10.0-rc1 [1]
629
    --
630
    -- [1] https://github.com/tarantool/tarantool/commit/38f0c904af4882756c6dc802f1895117d3deae6a
631
    enabled_tarantool_features.intervals = is_version_ge(major, minor, patch, suffix,
926✔
632
                                                         2, 10, 0, 'rc1')
926✔
633

634
    -- since Tarantool 2.6.3 / 2.7.2 / 2.8.1
635
    enabled_tarantool_features.jsonpath_indexes = is_version_ge(major, minor, patch, suffix,
926✔
636
                                                                2, 8, 1, nil)
463✔
637
                                               or is_version_in_range(major, minor, patch, suffix,
463✔
638
                                                                      2, 7, 2, nil,
UNCOV
639
                                                                      2, 7, math.huge, nil)
×
UNCOV
640
                                               or is_version_in_range(major, minor, patch, suffix,
×
641
                                                                      2, 6, 3, nil,
642
                                                                      2, 6, math.huge, nil)
463✔
643

644
    -- The merger module was implemented in 2.2.1, see [1].
645
    -- However it had the critical problem [2], which leads to
646
    -- segfault at attempt to use the module from a fiber serving
647
    -- iproto request. So we don't use it in versions before the
648
    -- fix.
649
    --
650
    -- [1]: https://github.com/tarantool/tarantool/issues/3276
651
    -- [2]: https://github.com/tarantool/tarantool/issues/4954
652
    enabled_tarantool_features.builtin_merger = is_version_ge(major, minor, patch, suffix,
926✔
653
                                                              2, 6, 0, nil)
463✔
654
                                             or is_version_in_range(major, minor, patch, suffix,
463✔
655
                                                                    2, 5, 1, nil,
656
                                                                    2, 5, math.huge, nil)
×
UNCOV
657
                                             or is_version_in_range(major, minor, patch, suffix,
×
658
                                                                    2, 4, 2, nil,
659
                                                                    2, 4, math.huge, nil)
×
UNCOV
660
                                             or is_version_in_range(major, minor, patch, suffix,
×
661
                                                                    2, 3, 3, nil,
662
                                                                    2, 3, math.huge, nil)
463✔
663

664
    -- The external merger module leans on a set of relatively
665
    -- new APIs in tarantool. So it works only on tarantool
666
    -- versions, which offer those APIs.
667
    --
668
    -- See README of the module:
669
    -- https://github.com/tarantool/tuple-merger
670
    enabled_tarantool_features.external_merger = is_version_ge(major, minor, patch, suffix,
926✔
671
                                                               2, 7, 0, nil)
463✔
672
                                              or is_version_in_range(major, minor, patch, suffix,
463✔
673
                                                                     2, 6, 1, nil,
UNCOV
674
                                                                     2, 6, math.huge, nil)
×
UNCOV
675
                                              or is_version_in_range(major, minor, patch, suffix,
×
676
                                                                     2, 5, 2, nil,
UNCOV
677
                                                                     2, 5, math.huge, nil)
×
UNCOV
678
                                              or is_version_in_range(major, minor, patch, suffix,
×
679
                                                                     2, 4, 3, nil,
UNCOV
680
                                                                     2, 4, math.huge, nil)
×
UNCOV
681
                                              or is_version_in_range(major, minor, patch, suffix,
×
682
                                                                     1, 10, 8, nil,
683
                                                                     1, 10, math.huge, nil)
463✔
684

685
    enabled_tarantool_features.netbox_skip_header_option = is_version_ge(major, minor, patch, suffix,
926✔
686
                                                                         2, 2, 0, nil)
926✔
687
end
688

689
determine_enabled_features()
463✔
690

691
for feature_name, feature_enabled in pairs(enabled_tarantool_features) do
5,093✔
692
    local util_name
693
    if feature_name == 'builtin_merger' then
4,167✔
694
        util_name = ('tarantool_has_%s'):format(feature_name)
463✔
695
    else
696
        util_name = ('tarantool_supports_%s'):format(feature_name)
3,704✔
697
    end
698

699
    local util_func = function() return feature_enabled end
25,396✔
700

701
    utils[util_name] = util_func
4,167✔
702
end
703

704
local function add_nullable_fields_recursive(operations, operations_map, space_format, tuple, id)
UNCOV
705
    if id < 2 or tuple[id - 1] ~= box.NULL then
×
UNCOV
706
        return operations
×
707
    end
708

709
    if space_format[id - 1].is_nullable and not operations_map[id - 1] then
×
UNCOV
710
        table.insert(operations, {'=', id - 1, box.NULL})
×
UNCOV
711
        return add_nullable_fields_recursive(operations, operations_map, space_format, tuple, id - 1)
×
712
    end
713

UNCOV
714
    return operations
×
715
end
716

717
-- Tarantool < 2.1 has no fields `box.error.NO_SUCH_FIELD_NO` and `box.error.NO_SUCH_FIELD_NAME`.
718
if tarantool_version_at_least(2, 1, 0, nil) then
926✔
719
    function utils.is_field_not_found(err_code)
463✔
720
        return err_code == box.error.NO_SUCH_FIELD_NO or err_code == box.error.NO_SUCH_FIELD_NAME
51✔
721
    end
722
else
UNCOV
723
    function utils.is_field_not_found(err_code)
×
UNCOV
724
        return err_code == box.error.NO_SUCH_FIELD
×
725
    end
726
end
727

728
local function get_operations_map(operations)
UNCOV
729
    local map = {}
×
UNCOV
730
    for _, operation in ipairs(operations) do
×
UNCOV
731
        map[operation[2]] = true
×
732
    end
733

734
    return map
×
735
end
736

737
function utils.add_intermediate_nullable_fields(operations, space_format, tuple)
463✔
738
    if tuple == nil then
2✔
UNCOV
739
        return operations
×
740
    end
741

742
    -- If tarantool doesn't supports the fieldpaths, we already
743
    -- have converted operations (see this function call in update.lua)
744
    if utils.tarantool_supports_fieldpaths() then
4✔
745
        local formatted_operations, err = utils.convert_operations(operations, space_format)
2✔
746
        if err ~= nil then
2✔
747
            return operations
2✔
748
        end
749

750
        operations = formatted_operations
×
751
    end
752

753
    -- We need this map to check if there is a field update
754
    -- operation with constant complexity
UNCOV
755
    local operations_map = get_operations_map(operations)
×
UNCOV
756
    for _, operation in ipairs(operations) do
×
UNCOV
757
        operations = add_nullable_fields_recursive(
×
758
            operations, operations_map,
UNCOV
759
            space_format, tuple, operation[2]
×
760
        )
761
    end
762

UNCOV
763
    table.sort(operations, function(v1, v2) return v1[2] < v2[2] end)
×
UNCOV
764
    return operations
×
765
end
766

767
function utils.convert_operations(user_operations, space_format)
463✔
768
    local converted_operations = {}
2✔
769

770
    for _, operation in ipairs(user_operations) do
2✔
771
        if type(operation[2]) == 'string' then
2✔
772
            local field_id
773
            for fieldno, field_format in ipairs(space_format) do
10✔
774
                if field_format.name == operation[2] then
8✔
775
                    field_id = fieldno
×
776
                    break
777
                end
778
            end
779

780
            if field_id == nil then
2✔
781
                return nil, ParseOperationsError:new(
4✔
782
                        "Space format doesn't contain field named %q", operation[2])
4✔
783
            end
784

UNCOV
785
            table.insert(converted_operations, {
×
786
                operation[1], field_id, operation[3]
×
787
            })
788
        else
UNCOV
789
            table.insert(converted_operations, operation)
×
790
        end
791
    end
792

UNCOV
793
    return converted_operations
×
794
end
795

796
function utils.unflatten_rows(rows, metadata)
463✔
797
    if metadata == nil then
17,133✔
UNCOV
798
        return nil, UnflattenError:new('Metadata is not provided')
×
799
    end
800

801
    local result = table.new(#rows, 0)
17,133✔
802
    local err
803
    for i, row in ipairs(rows) do
35,678✔
804
        result[i], err = utils.unflatten(row, metadata)
37,090✔
805
        if err ~= nil then
18,545✔
UNCOV
806
            return nil, err
×
807
        end
808
    end
809
    return result
17,133✔
810
end
811

812
local inverted_tarantool_iters = {
463✔
813
    [box.index.EQ] = box.index.REQ,
463✔
814
    [box.index.GT] = box.index.LT,
463✔
815
    [box.index.GE] = box.index.LE,
463✔
816
    [box.index.LT] = box.index.GT,
463✔
817
    [box.index.LE] = box.index.GE,
463✔
818
    [box.index.REQ] = box.index.EQ,
463✔
819
}
820

821
function utils.invert_tarantool_iter(iter)
463✔
822
    local inverted_iter = inverted_tarantool_iters[iter]
49✔
823
    assert(inverted_iter ~= nil, "Unsupported Tarantool iterator: " .. tostring(iter))
49✔
824
    return inverted_iter
49✔
825
end
826

827
function utils.reverse_inplace(t)
463✔
828
    for i = 1,math.floor(#t / 2) do
91✔
829
        t[i], t[#t - i + 1] = t[#t - i + 1], t[i]
43✔
830
    end
831
    return t
48✔
832
end
833

834
function utils.get_bucket_id_fieldno(space, shard_index_name)
463✔
835
    shard_index_name = shard_index_name or 'bucket_id'
185,795✔
836
    local bucket_id_index = space.index[shard_index_name]
185,795✔
837
    if bucket_id_index == nil then
185,795✔
838
        return nil, ShardingError:new('%q index is not found', shard_index_name)
24✔
839
    end
840

841
    return bucket_id_index.parts[1].fieldno
185,783✔
842
end
843

844
-- Build a map with field number as a keys and part number
845
-- as a values using index parts as a source.
846
function utils.get_index_fieldno_map(index_parts)
463✔
847
    dev_checks('table')
111✔
848

849
    local fieldno_map = {}
111✔
850
    for i, part in ipairs(index_parts) do
284✔
851
        local fieldno = part.fieldno
173✔
852
        fieldno_map[fieldno] = i
173✔
853
    end
854

855
    return fieldno_map
111✔
856
end
857

858
-- Build a map with field names as a keys and fieldno's
859
-- as a values using space format as a source.
860
function utils.get_format_fieldno_map(space_format)
463✔
861
    dev_checks('table')
5,699✔
862

863
    local fieldno_map = {}
5,699✔
864
    for fieldno, field_format in ipairs(space_format) do
28,395✔
865
        fieldno_map[field_format.name] = fieldno
22,696✔
866
    end
867

868
    return fieldno_map
5,699✔
869
end
870

871
local uuid_t = ffi.typeof('struct tt_uuid')
463✔
872
function utils.is_uuid(value)
463✔
873
    return ffi.istype(uuid_t, value)
784✔
874
end
875

876
local function get_field_format(space_format, field_name)
877
    dev_checks('table', 'string')
466✔
878

879
    local metadata = space_format_cache[space_format]
466✔
880
    if metadata ~= nil then
466✔
881
        return metadata[field_name]
446✔
882
    end
883

884
    space_format_cache[space_format] = {}
20✔
885
    for _, field in ipairs(space_format) do
134✔
886
        space_format_cache[space_format][field.name] = field
114✔
887
    end
888

889
    return space_format_cache[space_format][field_name]
20✔
890
end
891

892
local function filter_format_fields(space_format, field_names)
893
    dev_checks('table', 'table')
182✔
894

895
    local filtered_space_format = {}
182✔
896

897
    for i, field_name in ipairs(field_names) do
618✔
898
        filtered_space_format[i] = get_field_format(space_format, field_name)
932✔
899
        if filtered_space_format[i] == nil then
466✔
900
            return nil, FilterFieldsError:new(
60✔
901
                    'Space format doesn\'t contain field named %q', field_name
30✔
902
            )
60✔
903
        end
904
    end
905

906
    return filtered_space_format
152✔
907
end
908

909
function utils.get_fields_format(space_format, field_names)
463✔
910
    dev_checks('table', '?table')
3,751✔
911

912
    if field_names == nil then
3,751✔
913
        return table.copy(space_format)
3,687✔
914
    end
915

916
    local filtered_space_format, err = filter_format_fields(space_format, field_names)
64✔
917

918
    if err ~= nil then
64✔
919
        return nil, err
2✔
920
    end
921

922
    return filtered_space_format
62✔
923
end
924

925
function utils.format_result(rows, space, field_names)
463✔
926
    local result = {}
70,237✔
927
    local err
928
    local space_format = space:format()
70,237✔
929
    result.rows = rows
70,237✔
930

931
    if field_names == nil then
70,237✔
932
        result.metadata = table.copy(space_format)
140,238✔
933
        return result
70,119✔
934
    end
935

936
    result.metadata, err = filter_format_fields(space_format, field_names)
236✔
937

938
    if err ~= nil then
118✔
939
        return nil, err
28✔
940
    end
941

942
    return result
90✔
943
end
944

945
local function truncate_tuple_metadata(tuple_metadata, field_names)
946
    dev_checks('?table', 'table')
31✔
947

948
    if tuple_metadata == nil then
31✔
949
        return nil
3✔
950
    end
951

952
    local truncated_metadata = {}
28✔
953

954
    if #tuple_metadata < #field_names then
28✔
UNCOV
955
        return nil, FilterFieldsError:new(
×
956
                'Field names don\'t match to tuple metadata'
957
        )
958
    end
959

960
    for i, name in ipairs(field_names) do
79✔
961
        if tuple_metadata[i].name ~= name then
53✔
962
            return nil, FilterFieldsError:new(
4✔
963
                    'Field names don\'t match to tuple metadata'
964
            )
4✔
965
        end
966

967
        table.insert(truncated_metadata, tuple_metadata[i])
51✔
968
    end
969

970
    return truncated_metadata
26✔
971
end
972

973
function utils.cut_objects(objs, field_names)
463✔
974
    dev_checks('table', 'table')
5✔
975

976
    for i, obj in ipairs(objs) do
20✔
977
        objs[i] = schema.filter_obj_fields(obj, field_names)
30✔
978
    end
979

980
    return objs
5✔
981
end
982

983
function utils.cut_rows(rows, metadata, field_names)
463✔
984
    dev_checks('table', '?table', 'table')
31✔
985

986
    local truncated_metadata, err = truncate_tuple_metadata(metadata, field_names)
31✔
987

988
    if err ~= nil then
31✔
989
        return nil, err
2✔
990
    end
991

992
    for i, row in ipairs(rows) do
72✔
993
        rows[i] = schema.truncate_row_trailing_fields(row, field_names)
86✔
994
    end
995

996
    return {
29✔
997
        metadata = truncated_metadata,
29✔
998
        rows = rows,
29✔
999
    }
29✔
1000
end
1001

1002
local function flatten_obj(vshard_router, space_name, obj, skip_nullability_check)
1003
    local space_format, err = utils.get_space_format(space_name, vshard_router)
9,817✔
1004
    if err ~= nil then
9,817✔
1005
        return nil, FlattenError:new("Failed to get space format: %s", err), const.NEED_SCHEMA_RELOAD
410✔
1006
    end
1007

1008
    local tuple, err = utils.flatten(obj, space_format, nil, skip_nullability_check)
9,612✔
1009
    if err ~= nil then
9,612✔
1010
        return nil, FlattenError:new("Object is specified in bad format: %s", err), const.NEED_SCHEMA_RELOAD
1,368✔
1011
    end
1012

1013
    return tuple
8,928✔
1014
end
1015

1016
function utils.flatten_obj_reload(vshard_router, space_name, obj, skip_nullability_check)
463✔
1017
    return schema.wrap_func_reload(vshard_router, flatten_obj, space_name, obj, skip_nullability_check)
9,338✔
1018
end
1019

1020
-- Merge two options map.
1021
--
1022
-- `opts_a` and/or `opts_b` can be `nil`.
1023
--
1024
-- If `opts_a.foo` and `opts_b.foo` exists, prefer `opts_b.foo`.
1025
function utils.merge_options(opts_a, opts_b)
463✔
1026
    return fun.chain(opts_a or {}, opts_b or {}):tomap()
17,570✔
1027
end
1028

1029
local function lj_char_isident(n)
1030
    return bit.band(lj_char_bits[n + 2], LJ_CHAR_IDENT) == LJ_CHAR_IDENT
11,959✔
1031
end
1032

1033
local function lj_char_isdigit(n)
1034
    return bit.band(lj_char_bits[n + 2], LJ_CHAR_DIGIT) == LJ_CHAR_DIGIT
728✔
1035
end
1036

1037
function utils.check_name_isident(name)
463✔
1038
    dev_checks('string')
729✔
1039

1040
    -- sharding function name cannot
1041
    -- be equal to lua keyword
1042
    if LUA_KEYWORDS[name] then
729✔
1043
        return false
1✔
1044
    end
1045

1046
    -- sharding function name cannot
1047
    -- begin with a digit
1048
    local char_number = string.byte(name:sub(1,1))
1,456✔
1049
    if lj_char_isdigit(char_number) then
1,456✔
1050
        return false
1✔
1051
    end
1052

1053
    -- sharding func name must be sequence
1054
    -- of letters, digits, or underscore symbols
1055
    for i = 1, #name do
12,685✔
1056
        local char_number = string.byte(name:sub(i,i))
23,918✔
1057
        if not lj_char_isident(char_number) then
23,918✔
1058
            return false
1✔
1059
        end
1060
    end
1061

1062
    return true
726✔
1063
end
1064

1065
function utils.update_storage_call_error_description(err, func_name, replicaset_id)
463✔
1066
    if err == nil then
711✔
UNCOV
1067
        return nil
×
1068
    end
1069

1070
    if (err.type == 'ClientError' or err.type == 'AccessDeniedError')
1,287✔
1071
        and type(err.message) == 'string' then
993✔
1072
        local not_defined_str = string.format("Procedure '%s' is not defined", func_name)
499✔
1073
        local access_denied_str = string.format("Execute access to function '%s' is denied", func_name)
499✔
1074
        if err.message == not_defined_str or err.message:startswith(access_denied_str) then
1,977✔
1075
            if func_name:startswith('_crud.') then
16✔
1076
                err = NotInitializedError:new("Function %s is not registered: " ..
12✔
1077
                    "crud isn't initialized on replicaset %q or crud module versions mismatch " ..
6✔
1078
                    "between router and storage",
6✔
1079
                    func_name, replicaset_id or "Unknown")
12✔
1080
            else
1081
                err = NotInitializedError:new("Function %s is not registered", func_name)
4✔
1082
            end
1083
        end
1084
    end
1085
    return err
711✔
1086
end
1087

1088
--- Insert each value from values to list
1089
--
1090
-- @function list_extend
1091
--
1092
-- @param table list
1093
--  List to be extended
1094
--
1095
-- @param table values
1096
--  Values to be inserted to list
1097
--
1098
-- @return[1] list
1099
--  List with old values and inserted values
1100
function utils.list_extend(list, values)
463✔
1101
    dev_checks('table', 'table')
2,778✔
1102

1103
    for _, value in ipairs(values) do
20,935✔
1104
        table.insert(list, value)
18,157✔
1105
    end
1106

1107
    return list
2,778✔
1108
end
1109

1110
function utils.list_slice(list, start_index, end_index)
463✔
1111
    dev_checks('table', 'number', '?number')
48✔
1112

1113
    if end_index == nil then
48✔
1114
        end_index = table.maxn(list)
48✔
1115
    end
1116

1117
    local slice = {}
48✔
1118
    for i = start_index, end_index do
120✔
1119
        table.insert(slice, list[i])
72✔
1120
    end
1121

1122
    return slice
48✔
1123
end
1124

1125
--- Polls replicas for storage state
1126
--
1127
-- @function storage_info
1128
--
1129
-- @tparam ?number opts.timeout
1130
--  Function call timeout
1131
--
1132
-- @tparam ?string|table opts.vshard_router
1133
--  Cartridge vshard group name or vshard router instance.
1134
--
1135
-- @return a table of storage states by replica id.
1136
function utils.storage_info(opts)
463✔
1137
    opts = opts or {}
5✔
1138

1139
    local vshard_router, err = utils.get_vshard_router_instance(opts.vshard_router)
5✔
1140
    if err ~= nil then
5✔
UNCOV
1141
        return nil, StorageInfoError:new(err)
×
1142
    end
1143

1144
    local replicasets, err = vshard_router:routeall()
5✔
1145
    if replicasets == nil then
5✔
UNCOV
1146
        return nil, StorageInfoError:new("Failed to get router replicasets: %s", err.err)
×
1147
    end
1148

1149
    local futures_by_replicas = {}
5✔
1150
    local replica_state_by_id = {}
5✔
1151
    local async_opts = {is_async = true}
5✔
1152
    local timeout = opts.timeout or const.DEFAULT_VSHARD_CALL_TIMEOUT
5✔
1153

1154
    for _, replicaset in pairs(replicasets) do
20✔
1155
        for replica_id, replica in pairs(replicaset.replicas) do
40✔
1156
            local master = utils.get_replicaset_master(replicaset, {cached = false})
20✔
1157

1158
            replica_state_by_id[replica_id] = {
20✔
1159
                status = "error",
1160
                is_master = master == replica
20✔
1161
            }
20✔
1162

1163
            local ok, res = pcall(replica.conn.call, replica.conn, CRUD_STORAGE_INFO_FUNC_NAME,
40✔
1164
                                  {}, async_opts)
20✔
1165
            if ok then
20✔
1166
                futures_by_replicas[replica_id] = res
19✔
1167
            else
1168
                local err_msg = string.format("Error getting storage info for %s", replica_id)
1✔
1169
                if res ~= nil then
1✔
1170
                    log.error("%s: %s", err_msg, res)
1✔
1171
                    replica_state_by_id[replica_id].message = tostring(res)
2✔
1172
                else
UNCOV
1173
                    log.error(err_msg)
×
UNCOV
1174
                    replica_state_by_id[replica_id].message = err_msg
×
1175
                end
1176
            end
1177
        end
1178
    end
1179

1180
    local deadline = fiber.clock() + timeout
10✔
1181
    for replica_id, future in pairs(futures_by_replicas) do
29✔
1182
        local wait_timeout = deadline - fiber.clock()
38✔
1183
        if wait_timeout < 0 then
19✔
1184
            wait_timeout = 0
×
1185
        end
1186

1187
        local result, err = future:wait_result(wait_timeout)
19✔
1188
        if result == nil then
19✔
1189
            future:discard()
1✔
1190
            local err_msg = string.format("Error getting storage info for %s", replica_id)
1✔
1191
            if err ~= nil then
1✔
1192
                if err.type == 'ClientError' and err.code == box.error.NO_SUCH_PROC then
2✔
UNCOV
1193
                    replica_state_by_id[replica_id].status = "uninitialized"
×
1194
                else
1195
                    log.error("%s: %s", err_msg, err)
1✔
1196
                    replica_state_by_id[replica_id].message = tostring(err)
2✔
1197
                end
1198
            else
UNCOV
1199
                log.error(err_msg)
×
UNCOV
1200
                replica_state_by_id[replica_id].message = err_msg
×
1201
            end
1202
        else
1203
            replica_state_by_id[replica_id].status = result[1].status or "uninitialized"
18✔
1204
        end
1205
    end
1206

1207
    return replica_state_by_id
5✔
1208
end
1209

1210
--- Storage status information.
1211
--
1212
-- @function storage_info_on_storage
1213
--
1214
-- @return a table with storage status.
1215
function utils.storage_info_on_storage()
463✔
1216
    return {status = "running"}
18✔
1217
end
1218

1219
--- Initializes a storage function by its name.
1220
--
1221
--  It adds the function into the global scope by its name and required
1222
--  access to a vshard storage user.
1223
--
1224
--  @function init_storage_call
1225
--
1226
--  @param string name of a user or nil if there is no need to setup access.
1227
--  @param string name a name of the function.
1228
--  @param function func the function.
1229
--
1230
--  @return nil
1231
function utils.init_storage_call(user, name, func)
463✔
1232
    dev_checks('?string', 'string', 'function')
5,904✔
1233

1234
    rawset(_G['_crud'], name, func)
5,904✔
1235

1236
    if user ~= nil then
5,904✔
1237
        name = utils.get_storage_call(name)
6,208✔
1238
        box.schema.func.create(name, {setuid = true, if_not_exists = true})
3,104✔
1239
        box.schema.user.grant(user, 'execute', 'function', name, {if_not_exists=true})
3,104✔
1240
    end
1241
end
1242

1243
local expected_vshard_api = {
463✔
1244
    'routeall', 'route', 'bucket_id_strcrc32',
1245
    'callrw', 'callro', 'callbro', 'callre',
1246
    'callbre', 'map_callrw'
1247
}
1248

1249
--- Verifies that a table has expected vshard
1250
--  router handles.
1251
local function verify_vshard_router(router)
1252
    dev_checks("table")
212✔
1253

1254
    for _, func_name in ipairs(expected_vshard_api) do
1,328✔
1255
        if type(router[func_name]) ~= 'function' then
1,204✔
1256
            return false
88✔
1257
        end
1258
    end
1259

1260
    return true
124✔
1261
end
1262

1263
--- Get a vshard router instance from a parameter.
1264
--
1265
--  If a string passed, extract router instance from
1266
--  Cartridge vshard groups. If table passed, verifies
1267
--  that a table is a vshard router instance.
1268
--
1269
-- @function get_vshard_router_instance
1270
--
1271
-- @param[opt] router name of a vshard group or a vshard router
1272
--  instance
1273
--
1274
-- @return[1] table vshard router instance
1275
-- @treturn[2] nil
1276
-- @treturn[2] table Error description
1277
function utils.get_vshard_router_instance(router)
463✔
1278
    dev_checks('?string|table')
78,005✔
1279

1280
    local router_instance
1281

1282
    if type(router) == 'string' then
78,005✔
1283
        if not is_cartridge then
132✔
UNCOV
1284
            return nil, VshardRouterError:new("Vshard groups are supported only in Tarantool Cartridge")
×
1285
        end
1286

1287
        local router_service = cartridge.service_get('vshard-router')
132✔
1288
        assert(router_service ~= nil)
132✔
1289

1290
        router_instance = router_service.get(router)
264✔
1291
        if router_instance == nil then
132✔
UNCOV
1292
            return nil, VshardRouterError:new("Vshard group %s is not found", router)
×
1293
        end
1294
    elseif type(router) == 'table' then
77,873✔
1295
        if not verify_vshard_router(router) then
424✔
1296
            return nil, VshardRouterError:new("Invalid opts.vshard_router table value, " ..
176✔
1297
                                              "a vshard router instance has been expected")
176✔
1298
        end
1299

1300
        router_instance = router
124✔
1301
    else
1302
        assert(type(router) == 'nil')
77,661✔
1303
        router_instance = vshard.router.static
77,661✔
1304

1305
        if router_instance == nil then
77,661✔
1306
            return nil, VshardRouterError:new("Default vshard group is not found and custom " ..
176✔
1307
                                              "is not specified with opts.vshard_router")
176✔
1308
        end
1309
    end
1310

1311
    return router_instance
77,829✔
1312
end
1313

1314
--- Check if Tarantool Cartridge hotreload supported
1315
--  and get its implementaion.
1316
--
1317
-- @function is_cartridge_hotreload_supported
1318
--
1319
-- @return[1] true or false
1320
-- @return[1] module table, if supported
1321
function utils.is_cartridge_hotreload_supported()
463✔
1322
    if not is_cartridge_hotreload then
273✔
UNCOV
1323
        return false
×
1324
    end
1325

1326
    return true, cartridge_hotreload
273✔
1327
end
1328

1329
if utils.tarantool_supports_intervals() then
926✔
1330
    -- https://github.com/tarantool/tarantool/blob/0510ffa07afd84a70c9c6f1a4c28aacd73a393d6/src/lua/datetime.lua#L175-179
1331
    local interval_t = ffi.typeof('struct interval')
463✔
1332

1333
    utils.is_interval = function(o)
1334
        return ffi.istype(interval_t, o)
24✔
1335
    end
1336
else
1337
    utils.is_interval = function()
UNCOV
1338
        return false
×
1339
    end
1340
end
1341

1342
for k, v in pairs(require('crud.common.vshard_utils')) do
4,167✔
1343
    utils[k] = v
2,778✔
1344
end
1345

1346
return utils
463✔
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