Skip to content

Commit 6c30483

Browse files
authored
Update function now adds intermediate nullable fields (if they needed) (#121)
Fixed not fining field in tuple on ``crud.update`` if there are ``is_nullable`` fields in front of it that were added when the schema was changed. Closes #113
1 parent d370ac3 commit 6c30483

File tree

6 files changed

+214
-10
lines changed

6 files changed

+214
-10
lines changed

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,9 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
99

1010
### Fixed
1111

12+
* Fixed not finding field in tuple on ``crud.update`` if
13+
there are ``is_nullable`` fields in front of it that were added
14+
when the schema was changed.
1215
* Fixed select crash when dropping indexes
1316
* Using outdated schema on router-side
1417
* Sparsed tuples generation that led to "Tuple/Key must be MsgPack array" error

crud/common/utils.lua

Lines changed: 61 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -199,11 +199,70 @@ function utils.tarantool_supports_uuids()
199199
return enabled_tarantool_features.uuids
200200
end
201201

202-
function utils.convert_operations(user_operations, space_format)
202+
local function add_nullable_fields_recursive(operations, operations_map, space_format, tuple, id)
203+
if id < 2 or tuple[id - 1] ~= box.NULL then
204+
return operations
205+
end
206+
207+
if space_format[id - 1].is_nullable and not operations_map[id - 1] then
208+
table.insert(operations, {'=', id - 1, box.NULL})
209+
return add_nullable_fields_recursive(operations, operations_map, space_format, tuple, id - 1)
210+
end
211+
212+
return operations
213+
end
214+
215+
-- Tarantool < 2.3 has no fields `box.error.NO_SUCH_FIELD_NO` and `box.error.NO_SUCH_FIELD_NAME`.
216+
if _TARANTOOL >= "2.3" then
217+
function utils.is_field_not_found(err_code)
218+
return err_code == box.error.NO_SUCH_FIELD_NO or err_code == box.error.NO_SUCH_FIELD_NAME
219+
end
220+
else
221+
function utils.is_field_not_found(err_code)
222+
return err_code == box.error.NO_SUCH_FIELD
223+
end
224+
end
225+
226+
local function get_operations_map(operations)
227+
local map = {}
228+
for _, operation in ipairs(operations) do
229+
map[operation[2]] = true
230+
end
231+
232+
return map
233+
end
234+
235+
function utils.add_intermediate_nullable_fields(operations, space_format, tuple)
236+
if tuple == nil then
237+
return operations
238+
end
239+
240+
-- If tarantool doesn't supports the fieldpaths, we already
241+
-- have converted operations (see this function call in update.lua)
203242
if utils.tarantool_supports_fieldpaths() then
204-
return user_operations
243+
local formatted_operations, err = utils.convert_operations(operations, space_format)
244+
if err ~= nil then
245+
return operations
246+
end
247+
248+
operations = formatted_operations
249+
end
250+
251+
-- We need this map to check if there is a field update
252+
-- operation with constant complexity
253+
local operations_map = get_operations_map(operations)
254+
for _, operation in ipairs(operations) do
255+
operations = add_nullable_fields_recursive(
256+
operations, operations_map,
257+
space_format, tuple, operation[2]
258+
)
205259
end
206260

261+
table.sort(operations, function(v1, v2) return v1[2] < v2[2] end)
262+
return operations
263+
end
264+
265+
function utils.convert_operations(user_operations, space_format)
207266
local converted_operations = {}
208267

209268
for _, operation in ipairs(user_operations) do

crud/update.lua

Lines changed: 34 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -24,10 +24,32 @@ local function update_on_storage(space_name, key, operations, field_names)
2424

2525
-- add_space_schema_hash is false because
2626
-- reloading space format on router can't avoid update error on storage
27-
return schema.wrap_box_space_func_result(space, 'update', {key, operations}, {
27+
local res, err = schema.wrap_box_space_func_result(space, 'update', {key, operations}, {
2828
add_space_schema_hash = false,
2929
field_names = field_names,
3030
})
31+
32+
if err ~= nil then
33+
return nil, err
34+
end
35+
36+
if res.err == nil then
37+
return res, nil
38+
end
39+
40+
-- We can only add fields to end of the tuple.
41+
-- If schema is updated and nullable fields are added, then we will get error.
42+
-- Therefore, we need to add filling of intermediate nullable fields.
43+
-- More details: https://github.com/tarantool/tarantool/issues/3378
44+
if utils.is_field_not_found(res.err.code) then
45+
operations = utils.add_intermediate_nullable_fields(operations, space:format(), space:get(key))
46+
res, err = schema.wrap_box_space_func_result(space, 'update', {key, operations}, {
47+
add_space_schema_hash = false,
48+
field_names = field_names,
49+
})
50+
end
51+
52+
return res, err
3153
end
3254

3355
function update.init()
@@ -46,7 +68,11 @@ local function call_update_on_router(space_name, key, user_operations, opts)
4668

4769
opts = opts or {}
4870

49-
local space = utils.get_space(space_name, vshard.router.routeall())
71+
local space, err = utils.get_space(space_name, vshard.router.routeall())
72+
if err ~= nil then
73+
return nil, UpdateError:new("Failed to get space %q: %s", space_name, err), true
74+
end
75+
5076
if space == nil then
5177
return nil, UpdateError:new("Space %q doesn't exist", space_name), true
5278
end
@@ -57,9 +83,12 @@ local function call_update_on_router(space_name, key, user_operations, opts)
5783
key = key:totable()
5884
end
5985

60-
local operations, err = utils.convert_operations(user_operations, space_format)
61-
if err ~= nil then
62-
return nil, UpdateError:new("Wrong operations are specified: %s", err), true
86+
local operations = user_operations
87+
if not utils.tarantool_supports_fieldpaths() then
88+
operations, err = utils.convert_operations(user_operations, space_format)
89+
if err ~= nil then
90+
return nil, UpdateError:new("Wrong operations are specified: %s", err), true
91+
end
6392
end
6493

6594
local bucket_id = sharding.key_get_bucket_id(key, opts.bucket_id)

crud/upsert.lua

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -61,9 +61,12 @@ local function call_upsert_on_router(space_name, tuple, user_operations, opts)
6161
end
6262

6363
local space_format = space:format()
64-
local operations, err = utils.convert_operations(user_operations, space_format)
65-
if err ~= nil then
66-
return nil, UpsertError:new("Wrong operations are specified: %s", err), true
64+
local operations = user_operations
65+
if not utils.tarantool_supports_fieldpaths() then
66+
operations, err = utils.convert_operations(user_operations, space_format)
67+
if err ~= nil then
68+
return nil, UpsertError:new("Wrong operations are specified: %s", err), true
69+
end
6770
end
6871

6972
local bucket_id, err = sharding.tuple_set_and_return_bucket_id(tuple, space, opts.bucket_id)

test/entrypoint/srv_simple_operations.lua

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,30 @@ package.preload['customers-storage'] = function()
3232
if_not_exists = true,
3333
})
3434

35+
local developers_space = box.schema.space.create('developers', {
36+
format = {
37+
{name = 'id', type = 'unsigned'},
38+
{name = 'bucket_id', type = 'unsigned'},
39+
},
40+
if_not_exists = true,
41+
engine = engine,
42+
})
43+
developers_space:create_index('id', {
44+
parts = { {field = 'id'} },
45+
if_not_exists = true,
46+
})
47+
developers_space:create_index('bucket_id', {
48+
parts = { {field = 'bucket_id'} },
49+
unique = false,
50+
if_not_exists = true,
51+
})
52+
53+
rawset(_G, 'add_extra_field', function(name)
54+
local new_format = box.space.developers:format()
55+
table.insert(new_format, {name = name, type = 'any', is_nullable = true})
56+
box.space.developers:format(new_format)
57+
end)
58+
3559
-- Space with huge amount of nullable fields
3660
-- an object that inserted in such space should get
3761
-- explicit nulls in absence fields otherwise

test/integration/simple_operations_test.lua

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -392,6 +392,92 @@ pgroup:add('test_upsert', function(g)
392392
t.assert_equals(result.rows, {{67, 1143, 'Mikhail Saltykov-Shchedrin', 63}})
393393
end)
394394

395+
pgroup:add('test_intermediate_nullable_fields_update', function(g)
396+
local result, err = g.cluster.main_server.net_box:call(
397+
'crud.insert', {'developers', {1, box.NULL}})
398+
t.assert_equals(err, nil)
399+
400+
local objects = crud.unflatten_rows(result.rows, result.metadata)
401+
t.assert_equals(objects, {
402+
{
403+
id = 1,
404+
bucket_id = 477
405+
}
406+
})
407+
408+
helpers.call_on_servers(g.cluster, {'s1-master', 's2-master'}, function(server)
409+
for i = 1, 12 do
410+
server.net_box:call('add_extra_field', {'extra_' .. tostring(i)})
411+
end
412+
end)
413+
414+
local result, err = g.cluster.main_server.net_box:call('crud.update',
415+
{'developers', 1, {{'=', 'extra_3', { a = { b = {} } } }}})
416+
t.assert_equals(err, nil)
417+
objects = crud.unflatten_rows(result.rows, result.metadata)
418+
t.assert_equals(objects, {
419+
{
420+
id = 1,
421+
bucket_id = 477,
422+
extra_1 = box.NULL,
423+
extra_2 = box.NULL,
424+
extra_3 = {a = {b = {}}},
425+
}
426+
})
427+
428+
-- This tests use jsonpath updates.
429+
if _TARANTOOL >= "2.3" then
430+
local _, err = g.cluster.main_server.net_box:call('crud.update',
431+
{'developers', 1, {{'=', '[5].a.b[1]', 3}, {'=', 'extra_5', 'extra_value_5'}}})
432+
t.assert_equals(err.err, "Failed to update: Field ''extra_5'' was not found in the tuple")
433+
end
434+
435+
result, err = g.cluster.main_server.net_box:call('crud.update',
436+
{'developers', 1, {{'=', 5, 'extra_value_3'}, {'=', 7, 'extra_value_5'}}})
437+
t.assert_equals(err, nil)
438+
objects = crud.unflatten_rows(result.rows, result.metadata)
439+
t.assert_equals(objects, {
440+
{
441+
id = 1,
442+
bucket_id = 477,
443+
extra_1 = box.NULL,
444+
extra_2 = box.NULL,
445+
extra_3 = 'extra_value_3',
446+
extra_4 = box.NULL,
447+
extra_5 = 'extra_value_5',
448+
}
449+
})
450+
451+
result, err = g.cluster.main_server.net_box:call('crud.update',
452+
{'developers', 1, {
453+
{'=', 14, 'extra_value_12'},
454+
{'=', 'extra_9', 'extra_value_9'},
455+
{'=', 'extra_3', 'updated_extra_value_3'}
456+
}
457+
})
458+
459+
t.assert_equals(err, nil)
460+
objects = crud.unflatten_rows(result.rows, result.metadata)
461+
t.assert_equals(objects, {
462+
{
463+
id = 1,
464+
bucket_id = 477,
465+
extra_1 = box.NULL,
466+
extra_2 = box.NULL,
467+
extra_3 = 'updated_extra_value_3',
468+
extra_4 = box.NULL,
469+
extra_5 = 'extra_value_5',
470+
extra_6 = box.NULL,
471+
extra_7 = box.NULL,
472+
extra_8 = box.NULL,
473+
extra_9 = 'extra_value_9',
474+
extra_10 = box.NULL,
475+
extra_11 = box.NULL,
476+
extra_12 = 'extra_value_12'
477+
}
478+
})
479+
end)
480+
395481
pgroup:add('test_object_with_nullable_fields', function(g)
396482
-- Insert
397483
local result, err = g.cluster.main_server.net_box:call(

0 commit comments

Comments
 (0)