diff --git a/changelogs/unreleased/gh-8946-foreign-key-disables-truncate.md b/changelogs/unreleased/gh-8946-foreign-key-disables-truncate.md new file mode 100644 index 0000000000000000000000000000000000000000..92dbfc5f4add8897fe2e82407940b57c647dfd6a --- /dev/null +++ b/changelogs/unreleased/gh-8946-foreign-key-disables-truncate.md @@ -0,0 +1,4 @@ +## bugfix/core + +* Fixed a bug when a space that is referenced by a foreign key could not + be truncated even if the referring space was empty (gh-8946). diff --git a/src/box/alter.cc b/src/box/alter.cc index 9aa186fc439a1127bd9b963480d4e054001c99b1..f8784da6ed7b71a37b32507cacc8914cc5bb57ab 100644 --- a/src/box/alter.cc +++ b/src/box/alter.cc @@ -40,6 +40,7 @@ #include "coll_id_def.h" #include "txn.h" #include "tuple.h" +#include "tuple_constraint.h" #include "fiber.h" /* for gc_pool */ #include "scoped_guard.h" #include <base64.h> @@ -1951,6 +1952,42 @@ space_check_pinned(struct space *space) return 0; } +/** + * Check whether @a space holders prohibit truncate of the space. + * For example truncation in not allowed if another non-empty space refers + * to this space via foreign key link. + * Return 0 if allowed, or -1 if not allowed (diag is set). + */ +static int +space_check_truncate(struct space *space) +{ + /* Check for foreign keys that refers to this space. */ + struct space_cache_holder *h; + rlist_foreach_entry(h, &space->space_cache_pin_list, link) { + if (h->selfpin) + continue; + if (h->type != SPACE_HOLDER_FOREIGN_KEY) + continue; + struct tuple_constraint *constr = + container_of(h, struct tuple_constraint, + space_cache_holder); + struct space *other_space = constr->space; + /* + * If the referring space is empty then the truncate can't + * break foreign key consistency. + */ + if (space_bsize(other_space) == 0) + continue; + const char *type_str = + space_cache_holder_type_strs[h->type]; + diag_set(ClientError, ER_ALTER_SPACE, + space_name(space), + tt_sprintf("space is referenced by %s", type_str)); + return -1; + } + return 0; +} + /** * A trigger which is invoked on replace in a data dictionary * space _space. @@ -2404,11 +2441,10 @@ on_replace_dd_index(struct trigger * /* trigger */, void *event) "space sequence exists"); return -1; } - /* - * Must not truncate pinned space. + * Check space's holders. */ - if (space_check_pinned(old_space) != 0) + if (space_check_truncate(old_space) != 0) return -1; } @@ -2667,7 +2703,8 @@ on_replace_dd_truncate(struct trigger * /* trigger */, void *event) return -1; } - if (space_check_pinned(old_space) != 0) + /* Check space's holders. */ + if (space_check_truncate(old_space) != 0) return -1; struct alter_space *alter = alter_space_new(old_space); diff --git a/src/box/space_cache.h b/src/box/space_cache.h index 344f8359ec01f770ffdcf8279aebe6dc1bf46a22..97d64b52b1bf2294777699449d5c464659b0b258 100644 --- a/src/box/space_cache.h +++ b/src/box/space_cache.h @@ -176,7 +176,7 @@ space_cache_unpin(struct space_cache_holder *holder); /** * Check whether the @a space has holders or not. * If it has, @a type argument is set to the first holder's type. - * The function must be in cache (asserted). + * The space must be in cache (asserted). * If a space has holders, it must not be deleted (asserted). */ bool diff --git a/test/engine-luatest/gh_6436_complex_foreign_key_test.lua b/test/engine-luatest/gh_6436_complex_foreign_key_test.lua index e024a0882aa9281d4fd287451d1db1d7091ed30f..9a26e28516a5cb8694a964a1788a65a83db9af42 100644 --- a/test/engine-luatest/gh_6436_complex_foreign_key_test.lua +++ b/test/engine-luatest/gh_6436_complex_foreign_key_test.lua @@ -109,7 +109,7 @@ g.test_complex_foreign_key_primary = function(cg) t.assert_equals(country:select{}, {{1, 11, 'Russia'}, {1, 12, 'France'}}) t.assert_error_msg_content_equals( "Can't modify space 'country': space is referenced by foreign key", - function() country:drop() end + box.atomic, country.drop, country ) t.assert_error_msg_content_equals( "Foreign key 'country' integrity check failed: wrong foreign field name", @@ -198,7 +198,7 @@ g.test_complex_foreign_key_secondary = function(cg) {101, 1, 'earth', 'rf', 'France'}}) t.assert_error_msg_content_equals( "Can't modify space 'country': space is referenced by foreign key", - function() country:drop() end + box.atomic, country.drop, country ) t.assert_equals(country:select{}, {{100, 1, 'earth', 'ru', 'Russia'}, {101, 1, 'earth', 'rf', 'France'}}) @@ -293,7 +293,7 @@ g.test_complex_foreign_key_numeric = function(cg) {101, 1, 'earth', 'rf', 'France'}}) t.assert_error_msg_content_equals( "Can't modify space 'country': space is referenced by foreign key", - function() country:drop() end + box.atomic, country.drop, country ) t.assert_equals(country:select{}, {{100, 1, 'earth', 'ru', 'Russia'}, {101, 1, 'earth', 'rf', 'France'}}) diff --git a/test/engine-luatest/gh_6436_field_foreign_key_test.lua b/test/engine-luatest/gh_6436_field_foreign_key_test.lua index 133d2ea0c39126fc6020dedf125a148432eb3ed5..bdb04acde40606513d0fce15761061827655948e 100644 --- a/test/engine-luatest/gh_6436_field_foreign_key_test.lua +++ b/test/engine-luatest/gh_6436_field_foreign_key_test.lua @@ -137,7 +137,7 @@ g.test_foreign_key_primary = function(cg) t.assert_equals(country:select{}, {{1, 'ru', 'Russia'}, {2, 'fr', 'France'}}) t.assert_error_msg_content_equals( "Can't modify space 'country': space is referenced by foreign key", - function() country:drop() end + box.atomic, country.drop, country ) t.assert_equals(country:select{}, {{1, 'ru', 'Russia'}, {2, 'fr', 'France'}}) t.assert_error_msg_content_equals( @@ -215,7 +215,7 @@ g.test_foreign_key_secondary = function(cg) t.assert_equals(country:select{}, {{1, 'ru', 'Russia'}, {2, 'fr', 'France'}}) t.assert_error_msg_content_equals( "Can't modify space 'country': space is referenced by foreign key", - function() country:drop() end + box.atomic, country.drop, country ) t.assert_equals(country:select{}, {{1, 'ru', 'Russia'}, {2, 'fr', 'France'}}) t.assert_error_msg_content_equals( @@ -297,7 +297,7 @@ g.test_foreign_key_numeric = function(cg) t.assert_equals(country:select{}, {{1, 'ru', 'Russia'}, {2, 'fr', 'France'}}) t.assert_error_msg_content_equals( "Can't modify space 'country': space is referenced by foreign key", - function() country:drop() end + box.atomic, country.drop, country ) t.assert_equals(country:select{}, {{1, 'ru', 'Russia'}, {2, 'fr', 'France'}}) t.assert_error_msg_content_equals( diff --git a/test/engine-luatest/gh_8946_foreign_key_disables_truncate_test.lua b/test/engine-luatest/gh_8946_foreign_key_disables_truncate_test.lua new file mode 100644 index 0000000000000000000000000000000000000000..17c6d3f2c0b5c207a01b3b4833b5faf17cdea21f --- /dev/null +++ b/test/engine-luatest/gh_8946_foreign_key_disables_truncate_test.lua @@ -0,0 +1,131 @@ +-- https://github.com/tarantool/tarantool/issues/8946 +-- Test that a space that another empty space refers to can be truncated. +local server = require('luatest.server') +local t = require('luatest') + +local engines = {{engine = 'memtx'}, {engine = 'vinyl'}} +local g = t.group('gh-8946-foreign-key-truncate-test', engines) + +g.before_all(function(cg) + cg.server = server:new({alias = 'master'}) + cg.server:start() +end) + +g.after_all(function(cg) + cg.server:stop() + cg.server = nil +end) + +g.after_each(function(cg) + cg.server:exec(function() + if box.space.client_phones then + box.space.client_phones:drop() + end + if box.space.client then + box.space.client:drop() + end + end) +end) + +-- Field foreign key must not disable truncate if referring space is empty. +g.test_field_foreign_key_truncate = function(cg) + local engine = cg.params.engine + + cg.server:exec(function(engine) + box.schema.space.create('client', {engine = engine}) + box.space.client:format({ + {name = 'customer_id', type = 'string', is_nullable = false}, + {name = 'esia_id', type = 'string', is_nullable = false}, + }) + + box.space.client:create_index('pk_client_customer_id', { + parts = {{field = 'customer_id', collation = 'unicode'}}, + type = 'tree', unique = true + }) + box.space.client:create_index('idx_client_esia_id', { + parts = {{field = 'esia_id', collation = 'unicode'}}, + type = 'tree', unique = false + }) + + box.schema.space.create('client_phones', {engine = engine}) + box.space.client_phones:format({ + {name = 'phone', type = 'string', is_nullable = false}, + {name = 'customer_id', + foreign_key = {space = 'client', field = 'customer_id'}}, + }) + + box.space.client_phones:create_index('idx_client_phones_phone', { + parts = {{field = 'phone', collation = 'unicode'}}, + type = 'tree', unique = true + }) + + box.space.client:insert{'01','esia-01'} + box.space.client:insert{'02','esia-02'} + + box.space.client_phones:insert{'9121234','01'} + box.space.client_phones:insert{'3222222','02'} + + -- Now truncate is prohibited. + t.assert_error_msg_content_equals( + "Can't modify space 'client': space is referenced by foreign key", + box.space.client.truncate, box.space.client) + + box.space.client_phones:truncate() + + -- Now truncate is allowed. + box.space.client:truncate() + end, {engine}) +end + +-- Tuple foreign key must not disable truncate if referring space is empty. +g.test_tuple_foreign_key_truncate = function(cg) + local engine = cg.params.engine + + cg.server:exec(function(engine) + box.schema.space.create('client', {engine = engine}) + box.space.client:format({ + {name = 'customer_id', type = 'string', is_nullable = false}, + {name = 'esia_id', type = 'string', is_nullable = false}, + }) + + box.space.client:create_index('pk_client_customer_id', { + parts = {{field = 'customer_id', collation = 'unicode'}}, + type = 'tree', unique = true + }) + box.space.client:create_index('idx_client_esia_id', { + parts = {{field = 'esia_id', collation = 'unicode'}}, + type = 'tree', unique = false + }) + + box.schema.space.create('client_phones', { + engine = engine, + foreign_key = {space = 'client', + field = {customer_id = 'customer_id'}} + }) + box.space.client_phones:format({ + {name = 'phone', type = 'string', is_nullable = false}, + {name = 'customer_id'}, + }) + + box.space.client_phones:create_index('idx_client_phones_phone', { + parts = {{field = 'phone', collation = 'unicode'}}, + type = 'tree', unique = true + }) + + box.space.client:insert{'01','esia-01'} + box.space.client:insert{'02','esia-02'} + + box.space.client_phones:insert{'9121234','01'} + box.space.client_phones:insert{'3222222','02'} + + -- Now truncate is prohibited. + t.assert_error_msg_content_equals( + "Can't modify space 'client': space is referenced by foreign key", + box.space.client.truncate, box.space.client) + + box.space.client_phones:truncate() + + -- Now truncate is allowed. + box.space.client:truncate() + end, {engine}) +end diff --git a/test/sql/delete.result b/test/sql/delete.result index 3c2ed1e2e90027d1a983509a6a385d7deb94147a..3f3b8e5ef08a43ba87ea4c2413e870b5d6b0ca1d 100644 --- a/test/sql/delete.result +++ b/test/sql/delete.result @@ -141,6 +141,10 @@ box.execute("CREATE TABLE t2(x INT PRIMARY KEY REFERENCES t1(id));") --- - row_count: 1 ... +box.execute("INSERT INTO t2 VALUES(1);") +--- +- row_count: 1 +... box.execute("TRUNCATE TABLE t1;") --- - null diff --git a/test/sql/delete.test.lua b/test/sql/delete.test.lua index 82cf3322320b93abde9ce7d8eedcc204af66ed40..8eab0b0aead93a05306e1ed090b4354836d505b7 100644 --- a/test/sql/delete.test.lua +++ b/test/sql/delete.test.lua @@ -60,6 +60,7 @@ box.execute("TRUNCATE TABLE v1;") -- Can't truncate table with FK. box.execute("CREATE TABLE t2(x INT PRIMARY KEY REFERENCES t1(id));") +box.execute("INSERT INTO t2 VALUES(1);") box.execute("TRUNCATE TABLE t1;") -- Table triggers should be ignored.