Skip to content

Commit c0c70d9

Browse files
Gumixlocker
authored andcommitted
box: drop space_id for FK referring to same space
It is inconvenient to create self-referencing FK constraints, as the space ID will only be generated during space creation. This is especially useful for SQL, since the format for the space is created at compile time, and the space ID is only obtained at run time. Closes #7200 @TarantoolBot document Title: Describe foreign keys referring to the same space Since: 2.11 Root document: https://www.tarantool.io/en/doc/latest/book/box/data_model/#foreign-keys It is possible to create a foreign key that refers to the same space (a child space equals to the parent space). To do that, omit `space` in the `foreign_key` parameter, or set it to the id or to the name of the current space. (cherry picked from commit f21f8e9)
1 parent bb6e1a4 commit c0c70d9

7 files changed

+149
-38
lines changed
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
## feature/core
2+
3+
* Now it is possible to skip space id in the space format for the
4+
foreign key referring to the same space (gh-7200).

src/box/lua/schema.lua

Lines changed: 19 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -538,17 +538,15 @@ end
538538
-- foreign field.
539539
-- If is_complex, field is expected to be a table with local field ->
540540
-- foreign field mapping.
541-
local function normalize_foreign_key_one(space_id, space_name, def,
542-
error_prefix, is_complex)
543-
if def.space == nil then
544-
box.error(box.error.ILLEGAL_PARAMS,
545-
error_prefix .. "foreign key: space must be specified")
546-
end
541+
-- If fkey_same_space, the foreign key refers to the same space.
542+
local function normalize_foreign_key_one(def, error_prefix, is_complex,
543+
fkey_same_space)
547544
if def.field == nil then
548545
box.error(box.error.ILLEGAL_PARAMS,
549546
error_prefix .. "foreign key: field must be specified")
550547
end
551-
if type(def.space) ~= 'string' and type(def.space) ~= 'number' then
548+
if def.space ~= nil and
549+
type(def.space) ~= 'string' and type(def.space) ~= 'number' then
552550
box.error(box.error.ILLEGAL_PARAMS,
553551
error_prefix .. "foreign key: space must be string or number")
554552
end
@@ -598,7 +596,6 @@ local function normalize_foreign_key_one(space_id, space_name, def,
598596
end
599597
def.field = setmap(converted)
600598
end
601-
local fkey_same_space = (def.space == space_id or def.space == space_name)
602599
if not box.space[def.space] and not fkey_same_space then
603600
box.error(box.error.ILLEGAL_PARAMS,
604601
error_prefix .. "foreign key: space " .. tostring(def.space)
@@ -612,7 +609,7 @@ local function normalize_foreign_key_one(space_id, space_name, def,
612609
end
613610
end
614611
if fkey_same_space then
615-
return {space = space_id, field = def.field}
612+
return {field = def.field}
616613
else
617614
return {space = box.space[def.space].id, field = def.field}
618615
end
@@ -640,18 +637,17 @@ local function normalize_foreign_key(space_id, space_name, fkey, error_prefix,
640637
box.error(box.error.ILLEGAL_PARAMS,
641638
error_prefix .. "foreign key must be a table")
642639
end
643-
if fkey.space ~= nil and fkey.field ~= nil and
640+
if fkey.field ~= nil and
644641
(type(fkey.space) ~= 'table' or type(fkey.field) ~= 'table') then
645642
-- the first, short form.
646-
fkey = normalize_foreign_key_one(space_id, space_name, fkey,
647-
error_prefix, is_complex)
648-
local fkey_same_space = (fkey.space == space_id or
643+
local fkey_same_space = (fkey.space == nil or
644+
fkey.space == space_id or
649645
fkey.space == space_name)
650-
if fkey_same_space then
651-
return {[space_name] = fkey}
652-
else
653-
return {[box.space[fkey.space].name] = fkey}
654-
end
646+
fkey = normalize_foreign_key_one(fkey, error_prefix, is_complex,
647+
fkey_same_space)
648+
local fkey_name = fkey_same_space and space_name or
649+
box.space[fkey.space].name
650+
return {[fkey_name] = fkey}
655651
end
656652
-- the second, detailed form.
657653
local result = {}
@@ -666,8 +662,11 @@ local function normalize_foreign_key(space_id, space_name, fkey, error_prefix,
666662
error_prefix .. "foreign key definition must be a table "
667663
.. "with 'space' and 'field' members")
668664
end
669-
v = normalize_foreign_key_one(space_id, space_name, v, error_prefix,
670-
is_complex)
665+
local fkey_same_space = (v.space == nil or
666+
v.space == space_id or
667+
v.space == space_name)
668+
v = normalize_foreign_key_one(v, error_prefix, is_complex,
669+
fkey_same_space)
671670
result[k] = v
672671
end
673672
return result

src/box/lua/space.cc

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -309,7 +309,12 @@ lbox_push_space_foreign_key(struct lua_State *L, struct space *space, int i)
309309
continue;
310310

311311
lua_newtable(L);
312-
lua_pushnumber(L, c->def.fkey.space_id);
312+
if (c->def.fkey.space_id == 0) {
313+
/* No space id - no field. */
314+
lua_pushnil(L);
315+
} else {
316+
lua_pushnumber(L, c->def.fkey.space_id);
317+
}
313318
lua_setfield(L, -2, "space");
314319
lua_newtable(L);
315320
for (uint32_t j = 0; j < c->def.fkey.field_mapping_size; j++) {

src/box/tuple_constraint_def.c

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -239,7 +239,7 @@ tuple_constraint_def_decode_fkey(const char **data,
239239
{
240240
/*
241241
* Expected normal form of foreign keys: {name1=data1, name2=data2..},
242-
* where dataX has form: {space=id, field=id/name}
242+
* where dataX has form: {field=id/name} or {space=id, field=id/name}
243243
*/
244244
if (mp_typeof(**data) != MP_MAP) {
245245
diag_set(ClientError, errcode,
@@ -293,6 +293,7 @@ tuple_constraint_def_decode_fkey(const char **data,
293293
new_def[i].fkey.field_mapping_size = 0;
294294
uint32_t def_size = mp_decode_map(data);
295295
bool has_space = false, has_field = false;
296+
struct tuple_constraint_fkey_def *fk = &new_def[i].fkey;
296297
for (size_t j = 0; j < def_size; j++) {
297298
if (mp_typeof(**data) != MP_STR) {
298299
diag_set(ClientError, errcode,
@@ -314,11 +315,10 @@ tuple_constraint_def_decode_fkey(const char **data,
314315
} else {
315316
diag_set(ClientError, errcode,
316317
"foreign key definition is expected "
317-
"to be {space=.., field=..}");
318+
"to be {[space=..,] field=..}");
318319
return -1;
319320
}
320321
int rc;
321-
struct tuple_constraint_fkey_def *fk = &new_def[i].fkey;
322322
if (is_space)
323323
rc = space_id_decode(data, &fk->space_id,
324324
errcode);
@@ -331,10 +331,12 @@ tuple_constraint_def_decode_fkey(const char **data,
331331
if (rc != 0)
332332
return rc;
333333
}
334-
if (!has_space || !has_field) {
334+
if (!has_space)
335+
fk->space_id = 0;
336+
if (!has_field) {
335337
diag_set(ClientError, errcode,
336338
"foreign key definition is expected "
337-
"to be {space=.., field=..}");
339+
"to be {[space=..,] field=..}");
338340
return -1;
339341
}
340342
}

src/box/tuple_constraint_fkey.c

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -588,8 +588,11 @@ tuple_constraint_fkey_init(struct tuple_constraint *constr,
588588
constr->space = space;
589589
tuple_constraint_fkey_update_local(constr, field_no);
590590

591-
bool fkey_same_space = constr->def.fkey.space_id == space->def->id;
592-
struct space *foreign_space = space_by_id(constr->def.fkey.space_id);
591+
bool fkey_same_space = constr->def.fkey.space_id == 0 ||
592+
constr->def.fkey.space_id == space->def->id;
593+
uint32_t space_id = fkey_same_space ? space->def->id :
594+
constr->def.fkey.space_id;
595+
struct space *foreign_space = space_by_id(space_id);
593596
if (fkey_same_space && foreign_space == NULL)
594597
foreign_space = space;
595598
enum space_cache_holder_type type = SPACE_HOLDER_FOREIGN_KEY;

test/engine-luatest/gh_6436_field_foreign_key_test.lua

Lines changed: 0 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -53,21 +53,11 @@ g.test_bad_foreign_key = function(cg)
5353
"Illegal parameters, format[2]: foreign key definition must be a table with 'space' and 'field' members",
5454
function() box.schema.create_space('city', {engine=engine, format=fmt}) end
5555
)
56-
fmt = gen_format({field = 'id'})
57-
t.assert_error_msg_content_equals(
58-
"Illegal parameters, format[2]: foreign key definition must be a table with 'space' and 'field' members",
59-
function() box.schema.create_space('city', {engine=engine, format=fmt}) end
60-
)
6156
fmt = gen_format({fkey={space = 'country'}})
6257
t.assert_error_msg_content_equals(
6358
"Illegal parameters, format[2]: foreign key: field must be specified",
6459
function() box.schema.create_space('city', {engine=engine, format=fmt}) end
6560
)
66-
fmt = gen_format({fkey={field = 'id'}})
67-
t.assert_error_msg_content_equals(
68-
"Illegal parameters, format[2]: foreign key: space must be specified",
69-
function() box.schema.create_space('city', {engine=engine, format=fmt}) end
70-
)
7161
fmt = gen_format({space = 'planet', field = 'id'})
7262
t.assert_error_msg_content_equals(
7363
"Illegal parameters, format[2]: foreign key: space planet was not found",
Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
local server = require('test.luatest_helpers.server')
2+
local t = require('luatest')
3+
local g = t.group('gh-7200-fkey-without-space-id',
4+
{{engine = 'memtx'}, {engine = 'vinyl'}})
5+
6+
g.before_all(function(cg)
7+
cg.server = server:new({alias = 'master'})
8+
cg.server:start()
9+
end)
10+
11+
g.after_all(function(cg)
12+
cg.server:stop()
13+
cg.server = nil
14+
end)
15+
16+
g.after_each(function(cg)
17+
cg.server:exec(function()
18+
if box.space.space then box.space.space:drop() end
19+
if box.space.complex then box.space.complex:drop() end
20+
if box.space.filesystem then box.space.filesystem:drop() end
21+
end)
22+
end)
23+
24+
-- Similar to test_foreign_key_primary in gh_6961_fkey_same_space_test.lua,
25+
-- but without specifying space id in the format.
26+
g.test_foreign_key_primary = function(cg)
27+
local engine = cg.params.engine
28+
29+
cg.server:exec(function(engine)
30+
local t = require('luatest')
31+
32+
local fmt = {{name='id', type='unsigned'},
33+
{name='parent_id', type='unsigned',
34+
foreign_key={field='id'}}}
35+
local fs = box.schema.create_space('filesystem', {engine=engine,
36+
format=fmt})
37+
t.assert_equals(fs:format(fs:format()), nil)
38+
fs:drop()
39+
40+
local fmt = {{name='id', type='unsigned'},
41+
{name='parent_id', type='unsigned',
42+
foreign_key={fkey={field='id'}}}}
43+
local fs = box.schema.create_space('filesystem', {engine=engine})
44+
fs:format(fmt)
45+
t.assert_equals(fs:format(), fmt)
46+
fs:create_index('pk')
47+
fs:create_index('sk', {unique=false, parts={'parent_id'}})
48+
t.assert_error_msg_content_equals(
49+
"Foreign key constraint 'fkey' failed for field '2 (parent_id)': foreign tuple was not found",
50+
function() fs:insert{1, 0} end
51+
)
52+
end, {engine})
53+
end
54+
55+
-- Test complex foreign key without specifying space id.
56+
g.test_complex_foreign_key1 = function(cg)
57+
local engine = cg.params.engine
58+
59+
cg.server:exec(function(engine)
60+
local t = require('luatest')
61+
62+
local fmt = {{name='id1', type='unsigned'},
63+
{name='id2', type='unsigned'}}
64+
local fkey = {field={id1='id1', id2='id2'}}
65+
local opts = {engine=engine, format=fmt, foreign_key=fkey}
66+
local space = box.schema.create_space('complex', opts)
67+
68+
-- Note that 'space' field is not present in the table
69+
t.assert_equals(space.foreign_key,
70+
{complex={field={id1='id1', id2='id2'}}})
71+
end, {engine})
72+
end
73+
74+
g.test_complex_foreign_key2 = function(cg)
75+
local engine = cg.params.engine
76+
77+
cg.server:exec(function(engine)
78+
local t = require('luatest')
79+
80+
local fmt = {{name='id1', type='unsigned'},
81+
{name='id2', type='unsigned'}}
82+
local fkey = {name={field={id1='id2', id2='id1'}}}
83+
local opts = {engine=engine, format=fmt, foreign_key=fkey}
84+
local space = box.schema.create_space('complex', opts)
85+
86+
-- Note that 'space' field is not present in the table
87+
t.assert_equals(space.foreign_key,
88+
{name={field={id1='id2', id2='id1'}}})
89+
end, {engine})
90+
end
91+
92+
-- Test foreign key creation by inserting directly into box.space._space
93+
g.test_foreign_key_direct_insert = function(cg)
94+
local engine = cg.params.engine
95+
96+
cg.server:exec(function(engine)
97+
local t = require('luatest')
98+
99+
local fmt = {{name = 'i', type = 'integer'},
100+
{name = 'j', type = 'integer'},
101+
{name = 'k', type = 'integer',
102+
foreign_key = {k1 = {field = 'i'}}}}
103+
local opts = {foreign_key = {k2 = {field = {i = 'i', j = 'j'}}}}
104+
box.space._space:insert{512, 1, 'space', engine, 0, opts, fmt}
105+
t.assert_equals(box.space._space:get(512).flags, opts)
106+
t.assert_equals(box.space._space:get(512).format, fmt)
107+
end, {engine})
108+
end

0 commit comments

Comments
 (0)