* [tarantool-patches] [PATCH v3 0/3] box: run checks on insertions in LUA spaces @ 2019-04-16 13:51 Kirill Shcherbatov 2019-04-16 13:51 ` [tarantool-patches] [PATCH v3 1/3] schema: add new system space for CHECK constraints Kirill Shcherbatov ` (2 more replies) 0 siblings, 3 replies; 25+ messages in thread From: Kirill Shcherbatov @ 2019-04-16 13:51 UTC (permalink / raw) To: tarantool-patches, korablev; +Cc: Kirill Shcherbatov Fire CK constraints for LUA spaces. To achieve this goal, we reworked data dictionary, to store ck constraints in separate space _ck_constraints and updated data migration script to migrate existent data there. This also would be useful in future to implement ALTER SPACE ADD CONSTRAINT operation. Now we do not support CK constraint creation on non-empty space. Each CK has own precompiled VDBE machine that performs this check with tuple fields mapped to it's memory with sql_bind() api. In case of ck constraint conflict detected by this VM we abort the transaction and return error to user. Finally, we introduced a LUA-wrapper that provide a user-friendly way to manage space ck constraints. Changes in version 2: - some path parts has been already merged in master - dropped commits "box: fix _trigger and _ck_constraint access check" and "sql: disallow use of TYPEOF in Check" because they are not required and simply wrong - code rebased on actual master - reworked ck_constrain_def and ck_constraint structures and methods to provide a little more consistent API - reworked structures are used on SQL parse - new user-friendly API - use on_replace trigger instead before_replace trigger to deal with already-validated tuple - many minor review fixes - many amazing new test v2: https://www.freelists.org/post/tarantool-patches/PATCH-v2-09-sql-Checks-on-server-side v1: https://www.freelists.org/post/tarantool-patches/PATCH-v1-04-sql-Checks-on-server-side Branch: http://github.com/tarantool/tarantool/tree/kshch/gh-3691-checks-on-server-side Issue: https://github.com/tarantool/tarantool/issues/3691 Kirill Shcherbatov (3): schema: add new system space for CHECK constraints box: run check constraint tests on space alter box: user-friendly interface to manage ck constraints src/box/CMakeLists.txt | 1 + src/box/alter.cc | 267 ++++++++++++++++- src/box/alter.h | 1 + src/box/bootstrap.snap | Bin 4374 -> 4415 bytes src/box/ck_constraint.c | 271 +++++++++++++++++ src/box/ck_constraint.h | 184 ++++++++++++ src/box/errcode.h | 5 +- src/box/lua/schema.lua | 23 ++ src/box/lua/space.cc | 2 + src/box/lua/upgrade.lua | 39 +++ src/box/schema.cc | 8 + src/box/schema_def.h | 9 + src/box/space.c | 2 + src/box/space.h | 5 + src/box/space_def.c | 98 +----- src/box/space_def.h | 2 - src/box/sql.c | 86 +----- src/box/sql.h | 36 --- src/box/sql/build.c | 173 +++++++++-- src/box/sql/insert.c | 125 +++----- src/box/sql/parse_def.h | 22 ++ src/box/sql/select.c | 11 +- src/box/sql/sqlInt.h | 26 ++ src/box/sql/tokenize.c | 1 - src/box/sql/vdbeapi.c | 8 - test/app-tap/tarantoolctl.test.lua | 6 +- test/box-py/bootstrap.result | 4 + test/box/access.result | 3 + test/box/access.test.lua | 1 + test/box/access_misc.result | 2 + test/box/access_sysview.result | 6 +- test/box/alter.result | 6 +- test/box/misc.result | 2 + test/sql-tap/check.test.lua | 42 +-- test/sql-tap/fkey2.test.lua | 4 +- test/sql-tap/sql-errors.test.lua | 2 +- test/sql-tap/table.test.lua | 8 +- test/sql/checks.result | 417 +++++++++++++++++++++++--- test/sql/checks.test.lua | 163 ++++++++-- test/sql/errinj.result | 140 +++++++++ test/sql/errinj.test.lua | 45 +++ test/sql/gh-2981-check-autoinc.result | 12 +- test/sql/upgrade.result | 19 ++ test/sql/upgrade.test.lua | 5 + test/wal_off/alter.result | 2 +- 45 files changed, 1814 insertions(+), 480 deletions(-) create mode 100644 src/box/ck_constraint.c create mode 100644 src/box/ck_constraint.h -- 2.21.0 ^ permalink raw reply [flat|nested] 25+ messages in thread
* [tarantool-patches] [PATCH v3 1/3] schema: add new system space for CHECK constraints 2019-04-16 13:51 [tarantool-patches] [PATCH v3 0/3] box: run checks on insertions in LUA spaces Kirill Shcherbatov @ 2019-04-16 13:51 ` Kirill Shcherbatov 2019-04-25 20:38 ` [tarantool-patches] " n.pettik 2019-04-16 13:51 ` [tarantool-patches] [PATCH v3 2/3] box: run check constraint tests on space alter Kirill Shcherbatov 2019-04-16 13:51 ` [tarantool-patches] [PATCH v3 3/3] box: user-friendly interface to manage ck constraints Kirill Shcherbatov 2 siblings, 1 reply; 25+ messages in thread From: Kirill Shcherbatov @ 2019-04-16 13:51 UTC (permalink / raw) To: tarantool-patches, korablev; +Cc: Kirill Shcherbatov This patch introduces a new system space to persist check constraints. Format of the space: _ck_constraint (space id = 357) [<constraint name> STR, <space id> UINT, <expression string>STR] CK constraint is local to space, so every pair <CK name, space id> is unique (and it is PK in _ck_constraint space). After insertion into this space, a new instance describing check constraint is created. Check constraint holds Expr AST tree. While space features check constraints, it isn't allowed to be dropped. The :drop() space method firstly deletes all check constraints and then removes entry from _space. Because space alter, index alter and space truncate operations case space recreation, introduced RebuildCkConstrains object that compile new ck constraint objects, replace and remove existent instances atomically(when some compilation fails, nothing changed). In fact, in scope of this patch we don't really need to recreate ck_constraint object in such situations (patch space_def pointer in AST like we did it before is enough now, but we would recompile VDBE that represents ck constraint in future that can fail). The main motivation for these changes is the ability to support ADD CHECK CONSTRAINT operation in the future. Needed for #3691 --- src/box/CMakeLists.txt | 1 + src/box/alter.cc | 244 ++++++++++++++++++++++-- src/box/alter.h | 1 + src/box/bootstrap.snap | Bin 4374 -> 4415 bytes src/box/ck_constraint.c | 115 ++++++++++++ src/box/ck_constraint.h | 163 ++++++++++++++++ src/box/errcode.h | 4 +- src/box/lua/schema.lua | 4 + src/box/lua/space.cc | 2 + src/box/lua/upgrade.lua | 39 ++++ src/box/schema.cc | 8 + src/box/schema_def.h | 9 + src/box/space.c | 2 + src/box/space.h | 5 + src/box/space_def.c | 98 +--------- src/box/space_def.h | 2 - src/box/sql.c | 86 +-------- src/box/sql.h | 36 ---- src/box/sql/build.c | 173 +++++++++++++---- src/box/sql/insert.c | 64 ++++--- src/box/sql/parse_def.h | 22 +++ src/box/sql/select.c | 11 +- src/box/sql/tokenize.c | 1 - test/app-tap/tarantoolctl.test.lua | 6 +- test/box-py/bootstrap.result | 4 + test/box/access.result | 3 + test/box/access.test.lua | 1 + test/box/access_misc.result | 2 + test/box/access_sysview.result | 6 +- test/box/alter.result | 6 +- test/box/misc.result | 1 + test/sql-tap/check.test.lua | 32 ++-- test/sql-tap/fkey2.test.lua | 4 +- test/sql-tap/sql-errors.test.lua | 2 +- test/sql-tap/table.test.lua | 4 +- test/sql/checks.result | 255 +++++++++++++++++++++----- test/sql/checks.test.lua | 107 +++++++---- test/sql/errinj.result | 134 ++++++++++++++ test/sql/errinj.test.lua | 45 +++++ test/sql/gh-2981-check-autoinc.result | 8 +- test/sql/upgrade.result | 19 ++ test/sql/upgrade.test.lua | 5 + test/wal_off/alter.result | 2 +- 43 files changed, 1321 insertions(+), 415 deletions(-) create mode 100644 src/box/ck_constraint.c create mode 100644 src/box/ck_constraint.h diff --git a/src/box/CMakeLists.txt b/src/box/CMakeLists.txt index 31600745a..104a68551 100644 --- a/src/box/CMakeLists.txt +++ b/src/box/CMakeLists.txt @@ -94,6 +94,7 @@ add_library(box STATIC space.c space_def.c sequence.c + ck_constraint.c fk_constraint.c func.c func_def.c diff --git a/src/box/alter.cc b/src/box/alter.cc index 9279426d2..e96d502c9 100644 --- a/src/box/alter.cc +++ b/src/box/alter.cc @@ -29,6 +29,7 @@ * SUCH DAMAGE. */ #include "alter.h" +#include "ck_constraint.h" #include "column_mask.h" #include "schema.h" #include "user.h" @@ -529,17 +530,6 @@ space_def_new_from_tuple(struct tuple *tuple, uint32_t errcode, engine_name, engine_name_len, &opts, fields, field_count); auto def_guard = make_scoped_guard([=] { space_def_delete(def); }); - if (def->opts.checks != NULL && - sql_checks_resolve_space_def_reference(def->opts.checks, - def) != 0) { - box_error_t *err = box_error_last(); - if (box_error_code(err) != ENOMEM) { - tnt_raise(ClientError, errcode, def->name, - box_error_message(err)); - } else { - diag_raise(); - } - } struct engine *engine = engine_find_xc(def->engine_name); engine_check_space_def_xc(engine, def); def_guard.is_active = false; @@ -1380,6 +1370,71 @@ UpdateSchemaVersion::alter(struct alter_space *alter) ++schema_version; } +/** + * As ck_constraint object depends on space_def we must rebuild + * all ck constraints on space alter. + * + * To perform it transactionally, we create a list of a new ck + * constraints objects in ::prepare method that is fault-tolerant. + * Finally in ::alter or ::rollback methods we only swap thouse + * lists securely. + */ +class RebuildCkConstraints: public AlterSpaceOp +{ +public: + RebuildCkConstraints(struct alter_space *alter) : AlterSpaceOp(alter), + ck_constraint(RLIST_HEAD_INITIALIZER(ck_constraint)) {} + struct rlist ck_constraint; + virtual void prepare(struct alter_space *alter); + virtual void alter(struct alter_space *alter); + virtual void rollback(struct alter_space *alter); + virtual ~RebuildCkConstraints(); +}; + +void +RebuildCkConstraints::prepare(struct alter_space *alter) +{ + struct ck_constraint *old_ck_constraint; + rlist_foreach_entry(old_ck_constraint, &alter->old_space->ck_constraint, + link) { + struct ck_constraint *new_ck_constraint = + ck_constraint_new(old_ck_constraint->def, + alter->new_space->def); + if (new_ck_constraint == NULL) + diag_raise(); + rlist_add_entry(&ck_constraint, new_ck_constraint, link); + } +} + +void +RebuildCkConstraints::alter(struct alter_space *alter) +{ + rlist_swap(&alter->new_space->ck_constraint, &ck_constraint); + rlist_swap(&ck_constraint, &alter->old_space->ck_constraint); +} + +void +RebuildCkConstraints::rollback(struct alter_space *alter) +{ + rlist_swap(&alter->old_space->ck_constraint, &ck_constraint); + rlist_swap(&ck_constraint, &alter->new_space->ck_constraint); +} + +RebuildCkConstraints::~RebuildCkConstraints() +{ + struct ck_constraint *old_ck_constraint, *tmp; + rlist_foreach_entry_safe(old_ck_constraint, &ck_constraint, link, tmp) { + /** + * Ck constraint definition is now managed by + * other Ck constraint object. Prevent it's + * destruction as a part of ck_constraint_delete + * call. + */ + old_ck_constraint->def = NULL; + ck_constraint_delete(old_ck_constraint); + } +} + /* }}} */ /** @@ -1745,6 +1800,11 @@ on_replace_dd_space(struct trigger * /* trigger */, void *event) space_name(old_space), "the space has foreign key constraints"); } + if (!rlist_empty(&old_space->ck_constraint)) { + tnt_raise(ClientError, ER_DROP_SPACE, + space_name(old_space), + "the space has check constraints"); + } /** * The space must be deleted from the space * cache right away to achieve linearisable @@ -1842,6 +1902,7 @@ on_replace_dd_space(struct trigger * /* trigger */, void *event) def->field_count); (void) new CheckSpaceFormat(alter); (void) new ModifySpace(alter, def); + (void) new RebuildCkConstraints(alter); def_guard.is_active = false; /* Create MoveIndex ops for all space indexes. */ alter_space_move_indexes(alter, 0, old_space->index_id_max + 1); @@ -2085,6 +2146,7 @@ on_replace_dd_index(struct trigger * /* trigger */, void *event) */ alter_space_move_indexes(alter, iid + 1, old_space->index_id_max + 1); /* Add an op to update schema_version on commit. */ + (void) new RebuildCkConstraints(alter); (void) new UpdateSchemaVersion(alter); alter_space_do(txn, alter); scoped_guard.is_active = false; @@ -2152,6 +2214,7 @@ on_replace_dd_truncate(struct trigger * /* trigger */, void *event) (void) new TruncateIndex(alter, old_index->def->iid); } + (void) new RebuildCkConstraints(alter); alter_space_do(txn, alter); scoped_guard.is_active = false; } @@ -4035,6 +4098,161 @@ on_replace_dd_fk_constraint(struct trigger * /* trigger*/, void *event) } } +/** Create an instance of check constraint definition by tuple. */ +static struct ck_constraint_def * +ck_constraint_def_new_from_tuple(struct tuple *tuple) +{ + uint32_t name_len; + const char *name = + tuple_field_str_xc(tuple, BOX_CK_CONSTRAINT_FIELD_NAME, + &name_len); + if (name_len > BOX_NAME_MAX) { + tnt_raise(ClientError, ER_CREATE_CK_CONSTRAINT, + tt_cstr(name, BOX_INVALID_NAME_MAX), + "check constraint name is too long"); + } + identifier_check_xc(name, name_len); + uint32_t expr_str_len; + const char *expr_str = + tuple_field_str_xc(tuple, BOX_CK_CONSTRAINT_FIELD_EXPR_STR, + &expr_str_len); + + uint32_t name_offset, expr_str_offset; + size_t ck_def_sz = + ck_constraint_def_sizeof(name_len, expr_str_len, &name_offset, + &expr_str_offset); + struct ck_constraint_def *ck_def = + (struct ck_constraint_def *)malloc(ck_def_sz); + if (ck_def == NULL) + tnt_raise(OutOfMemory, ck_def_sz, "malloc", "ck_def"); + + ck_def->name = (char *)ck_def + name_offset; + ck_def->expr_str = (char *)ck_def + expr_str_offset; + memcpy(ck_def->expr_str, expr_str, expr_str_len); + ck_def->expr_str[expr_str_len] = '\0'; + memcpy(ck_def->name, name, name_len); + ck_def->name[name_len] = '\0'; + + return ck_def; +} + +/** Trigger invoked on rollback in the _ck_constraint space. */ +static void +on_replace_ck_constraint_rollback(struct trigger *trigger, void *event) +{ + struct txn_stmt *stmt = txn_last_stmt((struct txn *) event); + struct ck_constraint *ck = (struct ck_constraint *)trigger->data; + struct space *space = NULL; + if (ck != NULL) + space = space_by_id(ck->space_id); + if (stmt->old_tuple != NULL && stmt->new_tuple == NULL) { + /* Rollback DELETE check constraint. */ + assert(ck != NULL); + assert(space != NULL); + assert(space_ck_constraint_by_name(space, + ck->def->name, strlen(ck->def->name)) == NULL); + rlist_add_entry(&space->ck_constraint, ck, link); + } else if (stmt->new_tuple != NULL && stmt->old_tuple == NULL) { + /* Rollback INSERT check constraint. */ + assert(space != NULL); + assert(space_ck_constraint_by_name(space, + ck->def->name, strlen(ck->def->name)) != NULL); + rlist_del_entry(ck, link); + ck_constraint_delete(ck); + } else { + /* Rollback REPLACE check constraint. */ + assert(space != NULL); + const char *name = ck->def->name; + struct ck_constraint *new_ck = + space_ck_constraint_by_name(space, name, strlen(name)); + assert(new_ck != NULL); + rlist_del_entry(new_ck, link); + rlist_add_entry(&space->ck_constraint, ck, link); + ck_constraint_delete(new_ck); + } +} + +/** + * Trigger invoked on commit in the _ck_constraint space. + * Drop useless old check constraint object if exists. + */ +static void +on_replace_ck_constraint_commit(struct trigger *trigger, void * /* event */) +{ + struct ck_constraint *old_ck_constraint = + (struct ck_constraint *)trigger->data; + if (old_ck_constraint != NULL) + ck_constraint_delete(old_ck_constraint); +} + +/** A trigger invoked on replace in the _ck_constraint space. */ +static void +on_replace_dd_ck_constraint(struct trigger * /* trigger*/, void *event) +{ + struct txn *txn = (struct txn *) event; + txn_check_singlestatement_xc(txn, "Space _ck_constraint"); + struct txn_stmt *stmt = txn_current_stmt(txn); + struct tuple *old_tuple = stmt->old_tuple; + struct tuple *new_tuple = stmt->new_tuple; + uint32_t space_id = + tuple_field_u32_xc(old_tuple != NULL ? old_tuple : new_tuple, + BOX_CK_CONSTRAINT_FIELD_SPACE_ID); + struct space *space = space_cache_find_xc(space_id); + struct trigger *on_rollback = + txn_alter_trigger_new(on_replace_ck_constraint_rollback, NULL); + struct trigger *on_commit = + txn_alter_trigger_new(on_replace_ck_constraint_commit, NULL); + + if (new_tuple != NULL) { + /* Create or replace check constraint. */ + struct ck_constraint_def *ck_def = + ck_constraint_def_new_from_tuple(new_tuple); + auto ck_guard = make_scoped_guard([=] { free(ck_def); }); + /* + * FIXME: Ck constraint creation on non-empty + * space must be implemented as preparatory + * step for ALTER SPACE ADD CONSTRAINT feature. + */ + struct index *pk = space_index(space, 0); + if (pk != NULL && index_size(pk) > 0) { + tnt_raise(ClientError, ER_CREATE_CK_CONSTRAINT, + ck_def->name, + "referencing space must be empty"); + } + struct ck_constraint *new_ck_constraint = + ck_constraint_new(ck_def, space->def); + if (new_ck_constraint == NULL) + diag_raise(); + ck_guard.is_active = false; + const char *name = new_ck_constraint->def->name; + struct ck_constraint *old_ck_constraint = + space_ck_constraint_by_name(space, name, strlen(name)); + if (old_ck_constraint != NULL) + rlist_del_entry(old_ck_constraint, link); + rlist_add_entry(&space->ck_constraint, new_ck_constraint, link); + on_commit->data = old_ck_constraint; + on_rollback->data = old_tuple == NULL ? new_ck_constraint : + old_ck_constraint; + } else { + assert(new_tuple == NULL && old_tuple != NULL); + /* Drop check constraint. */ + uint32_t name_len; + const char *name = + tuple_field_str_xc(old_tuple, + BOX_CK_CONSTRAINT_FIELD_NAME, + &name_len); + struct ck_constraint *old_ck_constraint = + space_ck_constraint_by_name(space, name, name_len); + assert(old_ck_constraint != NULL); + rlist_del_entry(old_ck_constraint, link); + on_commit->data = old_ck_constraint; + on_rollback->data = old_ck_constraint; + } + + txn_on_rollback(txn, on_rollback); + txn_on_commit(txn, on_commit); +} + struct trigger alter_space_on_replace_space = { RLIST_LINK_INITIALIZER, on_replace_dd_space, NULL, NULL }; @@ -4103,4 +4321,8 @@ struct trigger on_replace_fk_constraint = { RLIST_LINK_INITIALIZER, on_replace_dd_fk_constraint, NULL, NULL }; +struct trigger on_replace_ck_constraint = { + RLIST_LINK_INITIALIZER, on_replace_dd_ck_constraint, NULL, NULL +}; + /* vim: set foldmethod=marker */ diff --git a/src/box/alter.h b/src/box/alter.h index 4108fa47c..b9ba7b846 100644 --- a/src/box/alter.h +++ b/src/box/alter.h @@ -46,6 +46,7 @@ extern struct trigger on_replace_sequence_data; extern struct trigger on_replace_space_sequence; extern struct trigger on_replace_trigger; extern struct trigger on_replace_fk_constraint; +extern struct trigger on_replace_ck_constraint; extern struct trigger on_stmt_begin_space; extern struct trigger on_stmt_begin_index; extern struct trigger on_stmt_begin_truncate; diff --git a/src/box/bootstrap.snap b/src/box/bootstrap.snap index 871a93f9856f97636ad55b7f5260e5c6957fef36..fcca39c32db55034aea83a68e245fb4f4fd104da 100644 GIT binary patch literal 4415 diff --git a/src/box/ck_constraint.c b/src/box/ck_constraint.c new file mode 100644 index 000000000..daeebb78b --- /dev/null +++ b/src/box/ck_constraint.c @@ -0,0 +1,115 @@ +/* + * Copyright 2010-2019, Tarantool AUTHORS, please see AUTHORS file. + * + * Redistribution and use in source and binary forms, with or + * without modification, are permitted provided that the following + * conditions are met: + * + * 1. Redistributions of source code must retain the above + * copyright notice, this list of conditions and the + * following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following + * disclaimer in the documentation and/or other materials + * provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY <COPYRIGHT HOLDER> ``AS IS'' AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED + * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL + * <COPYRIGHT HOLDER> OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, + * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR + * BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF + * THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF + * SUCH DAMAGE. + */ +#include "ck_constraint.h" +#include "errcode.h" +#include "small/rlist.h" +#include "sql.h" +#include "sql/sqlInt.h" + +/** + * Resolve space_def references for check constraint via AST + * tree traversal. + * @param ck_constraint Check constraint object to update. + * @param space_def Space definition to use. + * @retval 0 on success. + * @retval -1 on error. + */ +static int +ck_constraint_resolve_space_def(struct Expr *expr, + struct space_def *space_def) +{ + struct Parse parser; + sql_parser_create(&parser, sql_get()); + parser.parse_only = true; + sql_resolve_self_reference(&parser, space_def, NC_IsCheck, expr, NULL); + int rc = parser.is_aborted ? -1 : 0; + sql_parser_destroy(&parser); + return rc; +} + +struct ck_constraint * +ck_constraint_new(struct ck_constraint_def *ck_constraint_def, + struct space_def *space_def) +{ + if (space_def->field_count == 0) { + diag_set(ClientError, ER_UNSUPPORTED, "Tarantool", + "CK constraint for space without format"); + return NULL; + } + struct ck_constraint *ck_constraint = malloc(sizeof(*ck_constraint)); + if (ck_constraint == NULL) { + diag_set(OutOfMemory, sizeof(*ck_constraint), "malloc", + "ck_constraint"); + return NULL; + } + ck_constraint->def = NULL; + ck_constraint->space_id = space_def->id; + rlist_create(&ck_constraint->link); + ck_constraint->expr = + sql_expr_compile(sql_get(), ck_constraint_def->expr_str, + strlen(ck_constraint_def->expr_str)); + if (ck_constraint->expr == NULL || + ck_constraint_resolve_space_def(ck_constraint->expr, + space_def) != 0) { + diag_set(ClientError, ER_CREATE_CK_CONSTRAINT, + ck_constraint_def->name, + box_error_message(box_error_last())); + goto error; + } + + ck_constraint->def = ck_constraint_def; + return ck_constraint; +error: + ck_constraint_delete(ck_constraint); + return NULL; +} + +void +ck_constraint_delete(struct ck_constraint *ck_constraint) +{ + sql_expr_delete(sql_get(), ck_constraint->expr, false); + free(ck_constraint->def); + TRASH(ck_constraint); + free(ck_constraint); +} + +struct ck_constraint * +space_ck_constraint_by_name(struct space *space, const char *name, + uint32_t name_len) +{ + struct ck_constraint *ck_constraint = NULL; + rlist_foreach_entry(ck_constraint, &space->ck_constraint, link) { + if (strlen(ck_constraint->def->name) == name_len && + memcmp(ck_constraint->def->name, name, name_len) == 0) + return ck_constraint; + } + return NULL; +} diff --git a/src/box/ck_constraint.h b/src/box/ck_constraint.h new file mode 100644 index 000000000..615612605 --- /dev/null +++ b/src/box/ck_constraint.h @@ -0,0 +1,163 @@ +#ifndef INCLUDES_BOX_CK_CONSTRAINT_H +#define INCLUDES_BOX_CK_CONSTRAINT_H +/* + * Copyright 2010-2019, Tarantool AUTHORS, please see AUTHORS file. + * + * Redistribution and use in source and binary forms, with or + * without modification, are permitted provided that the following + * conditions are met: + * + * 1. Redistributions of source code must retain the above + * copyright notice, this list of conditions and the + * following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following + * disclaimer in the documentation and/or other materials + * provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY <COPYRIGHT HOLDER> ``AS IS'' AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED + * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL + * <COPYRIGHT HOLDER> OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, + * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR + * BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF + * THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF + * SUCH DAMAGE. + */ + +#include <stdint.h> +#include "small/rlist.h" + +#if defined(__cplusplus) +extern "C" { +#endif + +struct space; +struct space_def; +struct Expr; + +/** + * Check constraint definition. + * See ck_constraint_def_sizeof() definition for implementation + * details and memory layout. + */ +struct ck_constraint_def { + /** + * The 0-terminated string, a name of the check + * constraint. Must be unique for a given space. + */ + char *name; + /** + * The 0-terminated string that defines check constraint + * expression. + * + * For instance: "field1 + field2 > 2 * 3". + */ + char *expr_str; +}; + +/** + * Structure representing check constraint. + * See ck_constraint_new() definition. + */ +struct ck_constraint { + /** The check constraint definition. */ + struct ck_constraint_def *def; + /** + * The check constraint expression AST is built for + * ck_constraint::def::expr_str with sql_expr_compile + * and resolved with sql_resolve_self_reference for + * space with space[ck_constraint::space_id] definition. + */ + struct Expr *expr; + /** + * The id of the space this check constraint is + * built for. + */ + uint32_t space_id; + /** + * Organize check constraint structs into linked list + * with space::ck_constraint. + */ + struct rlist link; +}; + +/** + * Calculate check constraint definition memory size and fields + * offsets for given arguments. + * + * Alongside with struct ck_constraint_def itself, we reserve + * memory for string containing its name and expression string. + * + * Memory layout: + * +-----------------------------+ <- Allocated memory starts here + * | struct ck_constraint_def | + * |-----------------------------| + * | name + \0 | + * |-----------------------------| + * | expr_str + \0 | + * +-----------------------------+ + * + * @param name_len The length of the name. + * @param expr_str_len The length of the expr_str. + * @param[out] name_offset The offset of the name string. + * @param[out] expr_str_offset The offset of the expr_str string. + */ +static inline uint32_t +ck_constraint_def_sizeof(uint32_t name_len, uint32_t expr_str_len, + uint32_t *name_offset, uint32_t *expr_str_offset) +{ + *name_offset = sizeof(struct ck_constraint_def); + *expr_str_offset = *name_offset + name_len + 1; + return *expr_str_offset + expr_str_len + 1; +} + +/** + * Create a new check constraint object by given check constraint + * definition and definition of the space this constraint is + * related to. + * + * @param ck_constraint_def The check constraint definition object + * to use. Expected to be allocated with + * malloc. Ck constraint object manages + * this allocation in case of successful + * creation. + * @param space_def The space definition of the space this check + * constraint must be constructed for. + * @retval not NULL Check constraint object pointer on success. + * @retval NULL Otherwise. +*/ +struct ck_constraint * +ck_constraint_new(struct ck_constraint_def *ck_constraint_def, + struct space_def *space_def); + +/** + * Destroy check constraint memory, release acquired resources. + * @param ck_constraint The check constraint object to destroy. + */ +void +ck_constraint_delete(struct ck_constraint *ck_constraint); + +/** + * Find check constraint object in space by given name and + * name_len. + * @param space The space to lookup check constraint. + * @param name The check constraint name. + * @param name_len The length of the name. + * @retval not NULL Check constrain if exists, NULL otherwise. + */ +struct ck_constraint * +space_ck_constraint_by_name(struct space *space, const char *name, + uint32_t name_len); + +#if defined(__cplusplus) +} /* extern "C" { */ +#endif + +#endif /* INCLUDES_BOX_CK_CONSTRAINT_H */ diff --git a/src/box/errcode.h b/src/box/errcode.h index 3f8cb8e0e..7878bd66b 100644 --- a/src/box/errcode.h +++ b/src/box/errcode.h @@ -245,8 +245,8 @@ struct errcode_record { /*190 */_(ER_INT_LITERAL_MAX, "Integer literal %s%s exceeds the supported range %lld - %lld") \ /*191 */_(ER_SQL_PARSER_LIMIT, "%s %d exceeds the limit (%d)") \ /*192 */_(ER_INDEX_DEF_UNSUPPORTED, "%s are prohibited in an index definition") \ - /*193 */_(ER_CK_DEF_UNSUPPORTED, "%s are prohibited in a CHECK constraint definition") \ - + /*193 */_(ER_CK_DEF_UNSUPPORTED, "%s are prohibited in a check constraint definition") \ + /*194 */_(ER_CREATE_CK_CONSTRAINT, "Failed to create check constraint '%s': %s") \ /* * !IMPORTANT! Please follow instructions at start of the file * when adding new errors. diff --git a/src/box/lua/schema.lua b/src/box/lua/schema.lua index f31cf7f2c..e01f500e6 100644 --- a/src/box/lua/schema.lua +++ b/src/box/lua/schema.lua @@ -513,6 +513,7 @@ box.schema.space.drop = function(space_id, space_name, opts) local _truncate = box.space[box.schema.TRUNCATE_ID] local _space_sequence = box.space[box.schema.SPACE_SEQUENCE_ID] local _fk_constraint = box.space[box.schema.FK_CONSTRAINT_ID] + local _ck_constraint = box.space[box.schema.CK_CONSTRAINT_ID] local sequence_tuple = _space_sequence:delete{space_id} if sequence_tuple ~= nil and sequence_tuple[3] == true then -- Delete automatically generated sequence. @@ -524,6 +525,9 @@ box.schema.space.drop = function(space_id, space_name, opts) for _, t in _fk_constraint.index.child_id:pairs({space_id}) do _fk_constraint:delete({t.name, space_id}) end + for _, t in _ck_constraint.index.space_id:pairs({space_id}) do + _ck_constraint:delete({t.name, space_id}) + end local keys = _vindex:select(space_id) for i = #keys, 1, -1 do local v = keys[i] diff --git a/src/box/lua/space.cc b/src/box/lua/space.cc index 93269b72b..f06f2ad2e 100644 --- a/src/box/lua/space.cc +++ b/src/box/lua/space.cc @@ -567,6 +567,8 @@ box_lua_space_init(struct lua_State *L) lua_setfield(L, -2, "TRIGGER_ID"); lua_pushnumber(L, BOX_FK_CONSTRAINT_ID); lua_setfield(L, -2, "FK_CONSTRAINT_ID"); + lua_pushnumber(L, BOX_CK_CONSTRAINT_ID); + lua_setfield(L, -2, "CK_CONSTRAINT_ID"); lua_pushnumber(L, BOX_TRUNCATE_ID); lua_setfield(L, -2, "TRUNCATE_ID"); lua_pushnumber(L, BOX_SEQUENCE_ID); diff --git a/src/box/lua/upgrade.lua b/src/box/lua/upgrade.lua index 89d6e3d52..517976aae 100644 --- a/src/box/lua/upgrade.lua +++ b/src/box/lua/upgrade.lua @@ -74,6 +74,7 @@ local function set_system_triggers(val) box.space._schema:run_triggers(val) box.space._cluster:run_triggers(val) box.space._fk_constraint:run_triggers(val) + box.space._ck_constraint:run_triggers(val) end -------------------------------------------------------------------------------- @@ -94,6 +95,7 @@ local function erase() truncate(box.space._schema) truncate(box.space._cluster) truncate(box.space._fk_constraint) + truncate(box.space._ck_constraint) end local function create_sysview(source_id, target_id) @@ -735,6 +737,43 @@ local function upgrade_to_2_1_3() id = id + 1 end end + + -- In previous Tarantool releases check constraints were + -- stored in space opts. Now we use separate space + -- _ck_constraint for this purpose. Perform legacy data + -- migration. + local MAP = setmap({}) + local _space = box.space[box.schema.SPACE_ID] + local _index = box.space[box.schema.INDEX_ID] + local _ck_constraint = box.space[box.schema.CK_CONSTRAINT_ID] + log.info("create space _ck_constraint") + local format = {{name='name', type='string'}, + {name='space_id', type='unsigned'}, + {name='expr_str', type='str'}} + _space:insert{_ck_constraint.id, ADMIN, '_ck_constraint', 'memtx', 0, MAP, format} + + log.info("create index primary on _ck_constraint") + _index:insert{_ck_constraint.id, 0, 'primary', 'tree', + {unique = true}, {{0, 'string'}, {1, 'unsigned'}}} + + log.info("create secondary index child_id on _ck_constraint") + _index:insert{_ck_constraint.id, 1, 'space_id', 'tree', + {unique = false}, {{1, 'unsigned'}}} + for _, space in _space:pairs() do + local flags = space.flags + if flags['checks'] ~= nil then + for i, check in pairs(flags['checks']) do + local expr_str = check.expr + local check_name = check.name or + "CK_CONSTRAINT_"..i.."_"..space.name + _ck_constraint:insert({check_name, space.id, expr_str}) + end + flags['checks'] = nil + _space:replace(box.tuple.new({space.id, space.owner, space.name, + space.engine, space.field_count, + flags, space.format})) + end + end end local function get_version() diff --git a/src/box/schema.cc b/src/box/schema.cc index 9a55c2f14..4b20f0032 100644 --- a/src/box/schema.cc +++ b/src/box/schema.cc @@ -460,6 +460,14 @@ schema_init() sc_space_new(BOX_FK_CONSTRAINT_ID, "_fk_constraint", key_parts, 2, &on_replace_fk_constraint, NULL); + /* _ck_сonstraint - check constraints. */ + key_parts[0].fieldno = 0; /* constraint name */ + key_parts[0].type = FIELD_TYPE_STRING; + key_parts[1].fieldno = 1; /* space id */ + key_parts[1].type = FIELD_TYPE_UNSIGNED; + sc_space_new(BOX_CK_CONSTRAINT_ID, "_ck_constraint", key_parts, 2, + &on_replace_ck_constraint, NULL); + /* * _vinyl_deferred_delete - blackhole that is needed * for writing deferred DELETE statements generated by diff --git a/src/box/schema_def.h b/src/box/schema_def.h index eeeeb950b..83af47ce3 100644 --- a/src/box/schema_def.h +++ b/src/box/schema_def.h @@ -108,6 +108,8 @@ enum { BOX_SPACE_SEQUENCE_ID = 340, /** Space id of _fk_constraint. */ BOX_FK_CONSTRAINT_ID = 356, + /** Space id of _ck_contraint. */ + BOX_CK_CONSTRAINT_ID = 357, /** End of the reserved range of system spaces. */ BOX_SYSTEM_ID_MAX = 511, BOX_ID_NIL = 2147483647 @@ -238,6 +240,13 @@ enum { BOX_FK_CONSTRAINT_FIELD_PARENT_COLS = 8, }; +/** _ck_constraint fields. */ +enum { + BOX_CK_CONSTRAINT_FIELD_NAME = 0, + BOX_CK_CONSTRAINT_FIELD_SPACE_ID = 1, + BOX_CK_CONSTRAINT_FIELD_EXPR_STR = 2, +}; + /* * Different objects which can be subject to access * control. diff --git a/src/box/space.c b/src/box/space.c index 54ba97fba..e4e0a2264 100644 --- a/src/box/space.c +++ b/src/box/space.c @@ -165,6 +165,7 @@ space_create(struct space *space, struct engine *engine, space_fill_index_map(space); rlist_create(&space->parent_fk_constraint); rlist_create(&space->child_fk_constraint); + rlist_create(&space->ck_constraint); return 0; fail_free_indexes: @@ -225,6 +226,7 @@ space_delete(struct space *space) assert(space->sql_triggers == NULL); assert(rlist_empty(&space->parent_fk_constraint)); assert(rlist_empty(&space->child_fk_constraint)); + assert(rlist_empty(&space->ck_constraint)); space->vtab->destroy(space); } diff --git a/src/box/space.h b/src/box/space.h index 13a220d13..e8529beb3 100644 --- a/src/box/space.h +++ b/src/box/space.h @@ -193,6 +193,11 @@ struct space { * of index id. */ struct index **index; + /** + * List of check constraints linked with + * ck_constraint::link. + */ + struct rlist ck_constraint; /** * Lists of foreign key constraints. In SQL terms child * space is the "from" table i.e. the table that contains diff --git a/src/box/space_def.c b/src/box/space_def.c index d0864cc72..cb68cea1c 100644 --- a/src/box/space_def.c +++ b/src/box/space_def.c @@ -35,28 +35,12 @@ #include "sql.h" #include "msgpuck.h" -/** - * Make checks from msgpack. - * @param str pointer to array of maps - * e.g. [{"expr": "x < y", "name": "ONE"}, ..]. - * @param len array items count. - * @param[out] opt pointer to store parsing result. - * @param errcode Code of error to set if something is wrong. - * @param field_no Field number of an option in a parent element. - * @retval 0 on success. - * @retval not 0 on error. Also set diag message. - */ -static int -checks_array_decode(const char **str, uint32_t len, char *opt, uint32_t errcode, - uint32_t field_no); - const struct space_opts space_opts_default = { /* .group_id = */ 0, /* .is_temporary = */ false, /* .is_ephemeral = */ false, /* .view = */ false, /* .sql = */ NULL, - /* .checks = */ NULL, }; const struct opt_def space_opts_reg[] = { @@ -64,8 +48,7 @@ const struct opt_def space_opts_reg[] = { OPT_DEF("temporary", OPT_BOOL, struct space_opts, is_temporary), OPT_DEF("view", OPT_BOOL, struct space_opts, is_view), OPT_DEF("sql", OPT_STRPTR, struct space_opts, sql), - OPT_DEF_ARRAY("checks", struct space_opts, checks, - checks_array_decode), + OPT_DEF_LEGACY("checks"), OPT_END, }; @@ -113,16 +96,6 @@ space_def_dup_opts(struct space_def *def, const struct space_opts *opts) return -1; } } - if (opts->checks != NULL) { - def->opts.checks = sql_expr_list_dup(sql_get(), opts->checks, 0); - if (def->opts.checks == NULL) { - free(def->opts.sql); - diag_set(OutOfMemory, 0, "sql_expr_list_dup", - "def->opts.checks"); - return -1; - } - sql_checks_update_space_def_reference(def->opts.checks, def); - } return 0; } @@ -300,74 +273,5 @@ void space_opts_destroy(struct space_opts *opts) { free(opts->sql); - sql_expr_list_delete(sql_get(), opts->checks); TRASH(opts); } - -static int -checks_array_decode(const char **str, uint32_t len, char *opt, uint32_t errcode, - uint32_t field_no) -{ - char *errmsg = tt_static_buf(); - struct ExprList *checks = NULL; - const char **map = str; - struct sql *db = sql_get(); - for (uint32_t i = 0; i < len; i++) { - checks = sql_expr_list_append(db, checks, NULL); - if (checks == NULL) { - diag_set(OutOfMemory, 0, "sql_expr_list_append", - "checks"); - goto error; - } - const char *expr_name = NULL; - const char *expr_str = NULL; - uint32_t expr_name_len = 0; - uint32_t expr_str_len = 0; - uint32_t map_size = mp_decode_map(map); - for (uint32_t j = 0; j < map_size; j++) { - if (mp_typeof(**map) != MP_STR) { - diag_set(ClientError, errcode, field_no, - "key must be a string"); - goto error; - } - uint32_t key_len; - const char *key = mp_decode_str(map, &key_len); - if (mp_typeof(**map) != MP_STR) { - snprintf(errmsg, TT_STATIC_BUF_LEN, - "invalid MsgPack map field '%.*s' type", - key_len, key); - diag_set(ClientError, errcode, field_no, errmsg); - goto error; - } - if (key_len == 4 && memcmp(key, "expr", key_len) == 0) { - expr_str = mp_decode_str(map, &expr_str_len); - } else if (key_len == 4 && - memcmp(key, "name", key_len) == 0) { - expr_name = mp_decode_str(map, &expr_name_len); - } else { - snprintf(errmsg, TT_STATIC_BUF_LEN, - "invalid MsgPack map field '%.*s'", - key_len, key); - diag_set(ClientError, errcode, field_no, errmsg); - goto error; - } - } - if (sql_check_list_item_init(checks, i, expr_name, expr_name_len, - expr_str, expr_str_len) != 0) { - box_error_t *err = box_error_last(); - if (box_error_code(err) != ENOMEM) { - snprintf(errmsg, TT_STATIC_BUF_LEN, - "invalid expression specified (%s)", - box_error_message(err)); - diag_set(ClientError, errcode, field_no, - errmsg); - } - goto error; - } - } - *(struct ExprList **)opt = checks; - return 0; -error: - sql_expr_list_delete(db, checks); - return -1; -} diff --git a/src/box/space_def.h b/src/box/space_def.h index 52ff56764..71d168342 100644 --- a/src/box/space_def.h +++ b/src/box/space_def.h @@ -71,8 +71,6 @@ struct space_opts { bool is_view; /** SQL statement that produced this space. */ char *sql; - /** SQL Checks expressions list. */ - struct ExprList *checks; }; extern const struct space_opts space_opts_default; diff --git a/src/box/sql.c b/src/box/sql.c index 1fb93e106..7c55224d8 100644 --- a/src/box/sql.c +++ b/src/box/sql.c @@ -1031,15 +1031,8 @@ sql_encode_table_opts(struct region *region, struct space_def *def, bool is_error = false; mpstream_init(&stream, region, region_reserve_cb, region_alloc_cb, set_encode_error, &is_error); - int checks_cnt = 0; - struct ExprList_item *a; bool is_view = def->opts.is_view; - struct ExprList *checks = def->opts.checks; - if (checks != NULL) { - checks_cnt = checks->nExpr; - a = checks->a; - } - mpstream_encode_map(&stream, 2 * is_view + (checks_cnt > 0)); + mpstream_encode_map(&stream, 2 * is_view); if (is_view) { assert(def->opts.sql != NULL); @@ -1048,23 +1041,6 @@ sql_encode_table_opts(struct region *region, struct space_def *def, mpstream_encode_str(&stream, "view"); mpstream_encode_bool(&stream, true); } - if (checks_cnt > 0) { - mpstream_encode_str(&stream, "checks"); - mpstream_encode_array(&stream, checks_cnt); - } - for (int i = 0; i < checks_cnt && !is_error; ++i, ++a) { - int items = (a->pExpr != NULL) + (a->zName != NULL); - mpstream_encode_map(&stream, items); - assert(a->pExpr != NULL); - struct Expr *pExpr = a->pExpr; - assert(pExpr->u.zToken != NULL); - mpstream_encode_str(&stream, "expr"); - mpstream_encode_str(&stream, pExpr->u.zToken); - if (a->zName != NULL) { - mpstream_encode_str(&stream, "name"); - mpstream_encode_str(&stream, a->zName); - } - } mpstream_flush(&stream); if (is_error) { diag_set(OutOfMemory, stream.pos - stream.buf, @@ -1298,63 +1274,3 @@ sql_ephemeral_space_new(Parse *parser, const char *name) return space; } - -int -sql_check_list_item_init(struct ExprList *expr_list, int column, - const char *expr_name, uint32_t expr_name_len, - const char *expr_str, uint32_t expr_str_len) -{ - assert(column < expr_list->nExpr); - struct ExprList_item *item = &expr_list->a[column]; - memset(item, 0, sizeof(*item)); - if (expr_name != NULL) { - item->zName = sqlDbStrNDup(db, expr_name, expr_name_len); - if (item->zName == NULL) { - diag_set(OutOfMemory, expr_name_len, "sqlDbStrNDup", - "item->zName"); - return -1; - } - } - if (expr_str != NULL) { - item->pExpr = sql_expr_compile(db, expr_str, expr_str_len); - /* The item->zName would be released later. */ - if (item->pExpr == NULL) - return -1; - } - return 0; -} - -static int -update_space_def_callback(Walker *walker, Expr *expr) -{ - if (expr->op == TK_COLUMN && ExprHasProperty(expr, EP_Resolved)) - expr->space_def = walker->u.space_def; - return WRC_Continue; -} - -void -sql_checks_update_space_def_reference(ExprList *expr_list, - struct space_def *def) -{ - assert(expr_list != NULL); - Walker w; - memset(&w, 0, sizeof(w)); - w.xExprCallback = update_space_def_callback; - w.u.space_def = def; - for (int i = 0; i < expr_list->nExpr; i++) - sqlWalkExpr(&w, expr_list->a[i].pExpr); -} - -int -sql_checks_resolve_space_def_reference(ExprList *expr_list, - struct space_def *def) -{ - Parse parser; - sql_parser_create(&parser, sql_get()); - parser.parse_only = true; - - sql_resolve_self_reference(&parser, def, NC_IsCheck, NULL, expr_list); - int rc = parser.is_aborted ? -1 : 0; - sql_parser_destroy(&parser); - return rc; -} diff --git a/src/box/sql.h b/src/box/sql.h index 262a48bcb..4bb27da8e 100644 --- a/src/box/sql.h +++ b/src/box/sql.h @@ -285,42 +285,6 @@ sql_resolve_self_reference(struct Parse *parser, struct space_def *def, int type, struct Expr *expr, struct ExprList *expr_list); -/** - * Initialize check_list_item. - * @param expr_list ExprList with item. - * @param column index. - * @param expr_name expression name (optional). - * @param expr_name_len expresson name length (optional). - * @param expr_str expression to build string. - * @param expr_str_len expression to build string length. - * @retval 0 on success. - * @retval -1 on error. - */ -int -sql_check_list_item_init(struct ExprList *expr_list, int column, - const char *expr_name, uint32_t expr_name_len, - const char *expr_str, uint32_t expr_str_len); - -/** - * Resolve space_def references checks for expr_list. - * @param expr_list to modify. - * @param def to refer to. - * @retval 0 on success. - * @retval -1 on error. - */ -int -sql_checks_resolve_space_def_reference(struct ExprList *expr_list, - struct space_def *def); - -/** - * Update space_def references for expr_list. - * @param expr_list to modify. - * @param def to refer to. - */ -void -sql_checks_update_space_def_reference(struct ExprList *expr_list, - struct space_def *def); - /** * Initialize a new parser object. * A number of service allocations are performed on the region, diff --git a/src/box/sql/build.c b/src/box/sql/build.c index b1ed64a01..e55bbe43b 100644 --- a/src/box/sql/build.c +++ b/src/box/sql/build.c @@ -47,6 +47,7 @@ #include "vdbeInt.h" #include "tarantoolInt.h" #include "box/box.h" +#include "box/ck_constraint.h" #include "box/fk_constraint.h" #include "box/sequence.h" #include "box/session.h" @@ -643,37 +644,74 @@ primary_key_exit: void sql_add_check_constraint(struct Parse *parser) { - struct create_ck_def *ck_def = &parser->create_ck_def; + struct create_ck_def *create_ck_def = &parser->create_ck_def; + struct ExprSpan *expr_span = create_ck_def->expr; + sql_expr_delete(parser->db, expr_span->pExpr, false); + struct alter_entity_def *alter_def = (struct alter_entity_def *) &parser->create_ck_def; assert(alter_def->entity_type == ENTITY_TYPE_CK); (void) alter_def; - struct Expr *expr = ck_def->expr->pExpr; struct space *space = parser->create_table_def.new_space; - if (space != NULL) { - expr->u.zToken = - sqlDbStrNDup(parser->db, - (char *) ck_def->expr->zStart, - (int) (ck_def->expr->zEnd - - ck_def->expr->zStart)); - if (expr->u.zToken == NULL) - goto release_expr; - space->def->opts.checks = - sql_expr_list_append(parser->db, - space->def->opts.checks, expr); - if (space->def->opts.checks == NULL) { - sqlDbFree(parser->db, expr->u.zToken); - goto release_expr; - } - struct create_entity_def *entity_def = &ck_def->base.base; - if (entity_def->name.n > 0) { - sqlExprListSetName(parser, space->def->opts.checks, - &entity_def->name, 1); + assert(space != NULL); + + /* Prepare payload for ck constraint definition. */ + struct region *region = &parser->region; + struct Token *name_token = &create_ck_def->base.base.name; + const char *name; + if (name_token->n > 0) { + name = sql_normalized_name_region_new(region, name_token->z, + name_token->n); + if (name == NULL) { + parser->is_aborted = true; + return; } } else { -release_expr: - sql_expr_delete(parser->db, expr, false); + uint32_t ck_idx = ++parser->create_table_def.check_count; + name = tt_sprintf("CK_CONSTRAINT_%d_%s", ck_idx, + space->def->name); } + uint32_t name_len = strlen(name); + + uint32_t expr_str_len = (uint32_t)(create_ck_def->expr->zEnd - + create_ck_def->expr->zStart); + const char *expr_str = create_ck_def->expr->zStart; + + /* + * Allocate memory for ck constraint parse structure and + * ck constraint definition as a single memory chunk on + * region: + * + * [ck_parse][ck_def[name][expr_str]] + * |_____^ |___^ ^ + * |_________| + */ + uint32_t name_offset, expr_str_offset; + size_t ck_def_sz = + ck_constraint_def_sizeof(name_len, expr_str_len, &name_offset, + &expr_str_offset); + struct ck_constraint_parse *ck_parse = + region_alloc(region, sizeof(*ck_parse) + ck_def_sz); + if (ck_parse == NULL) { + diag_set(OutOfMemory, sizeof(*ck_parse) + ck_def_sz, "region", + "ck_parse"); + parser->is_aborted = true; + return; + } + struct ck_constraint_def *ck_def = + (struct ck_constraint_def *)((char *)ck_parse + + sizeof(*ck_parse)); + ck_parse->ck_def = ck_def; + rlist_create(&ck_parse->link); + + ck_def->name = (char *)ck_def + name_offset; + ck_def->expr_str = (char *)ck_def + expr_str_offset; + memcpy(ck_def->expr_str, expr_str, expr_str_len); + ck_def->expr_str[expr_str_len] = '\0'; + memcpy(ck_def->name, name, name_len); + ck_def->name[name_len] = '\0'; + + rlist_add_entry(&parser->create_table_def.new_check, ck_parse, link); } /* @@ -943,6 +981,37 @@ emitNewSysSequenceRecord(Parse *pParse, int reg_seq_id, const char *seq_name) return first_col; } +/** + * Generate opcodes to serialize check constraint definition into + * MsgPack and insert produced tuple into _ck_constraint space. + * @param parser Parsing context. + * @param ck_def Check constraint definition to be serialized. + * @param reg_space_id The VDBE register containing space id. +*/ +static void +vdbe_emit_ck_constraint_create(struct Parse *parser, + const struct ck_constraint_def *ck_def, + uint32_t reg_space_id) +{ + struct sql *db = parser->db; + struct Vdbe *v = sqlGetVdbe(parser); + assert(v != NULL); + int ck_constraint_reg = sqlGetTempRange(parser, 4); + sqlVdbeAddOp4(v, OP_String8, 0, ck_constraint_reg, 0, + sqlDbStrDup(db, ck_def->name), P4_DYNAMIC); + sqlVdbeAddOp2(v, OP_SCopy, reg_space_id, ck_constraint_reg + 1); + sqlVdbeAddOp4(v, OP_String8, 0, ck_constraint_reg + 2, 0, + sqlDbStrDup(db, ck_def->expr_str), P4_DYNAMIC); + sqlVdbeAddOp3(v, OP_MakeRecord, ck_constraint_reg, 3, + ck_constraint_reg + 3); + sqlVdbeAddOp3(v, OP_SInsert, BOX_CK_CONSTRAINT_ID, 0, + ck_constraint_reg + 3); + save_record(parser, BOX_CK_CONSTRAINT_ID, ck_constraint_reg, 2, + v->nOp - 1); + VdbeComment((v, "Create CK constraint %s", ck_def->name)); + sqlReleaseTempRange(parser, ck_constraint_reg, 4); +} + int emitNewSysSpaceSequenceRecord(Parse *pParse, int space_id, const char reg_seq_id) { @@ -1118,20 +1187,18 @@ resolve_link(struct Parse *parse_context, const struct space_def *def, void sqlEndTable(struct Parse *pParse) { - sql *db = pParse->db; /* The database connection */ - - assert(!db->mallocFailed); + assert(!pParse->db->mallocFailed); struct space *new_space = pParse->create_table_def.new_space; if (new_space == NULL) return; - assert(!db->init.busy); + assert(!pParse->db->init.busy); assert(!new_space->def->opts.is_view); if (sql_space_primary_key(new_space) == NULL) { diag_set(ClientError, ER_CREATE_SPACE, new_space->def->name, "PRIMARY KEY missing"); pParse->is_aborted = true; - goto cleanup; + return; } /* @@ -1221,9 +1288,12 @@ sqlEndTable(struct Parse *pParse) fk_def->child_id = reg_space_id; vdbe_emit_fk_constraint_create(pParse, fk_def); } -cleanup: - sql_expr_list_delete(db, new_space->def->opts.checks); - new_space->def->opts.checks = NULL; + struct ck_constraint_parse *ck_parse; + rlist_foreach_entry(ck_parse, &pParse->create_table_def.new_check, + link) { + vdbe_emit_ck_constraint_create(pParse, ck_parse->ck_def, + reg_space_id); + } } void @@ -1421,6 +1491,37 @@ vdbe_emit_fk_constraint_drop(struct Parse *parse_context, char *constraint_name, sqlReleaseTempRange(parse_context, key_reg, 3); } +/** + * Generate VDBE program to remove entry from _ck_constraint space. + * + * @param parser Parsing context. + * @param ck_name Name of CK constraint to be dropped. + * @param child_id Id of table which constraint belongs to. + */ +static void +vdbe_emit_ck_constraint_drop(struct Parse *parser, const char *ck_name, + uint32_t space_id) +{ + struct Vdbe *v = sqlGetVdbe(parser); + struct sql *db = v->db; + assert(v != NULL); + int key_reg = sqlGetTempRange(parser, 3); + sqlVdbeAddOp4(v, OP_String8, 0, key_reg, 0, sqlDbStrDup(db, ck_name), + P4_DYNAMIC); + sqlVdbeAddOp2(v, OP_Integer, space_id, key_reg + 1); + const char *error_msg = + tt_sprintf(tnt_errcode_desc(ER_NO_SUCH_CONSTRAINT), ck_name); + if (vdbe_emit_halt_with_presence_test(parser, BOX_CK_CONSTRAINT_ID, 0, + key_reg, 2, ER_NO_SUCH_CONSTRAINT, + error_msg, false, + OP_Found) != 0) + return; + sqlVdbeAddOp3(v, OP_MakeRecord, key_reg, 2, key_reg + 2); + sqlVdbeAddOp2(v, OP_SDelete, BOX_CK_CONSTRAINT_ID, key_reg + 2); + VdbeComment((v, "Delete CK constraint %s", ck_name)); + sqlReleaseTempRange(parser, key_reg, 3); +} + /** * Generate code to drop a table. * This routine includes dropping triggers, sequences, @@ -1494,6 +1595,13 @@ sql_code_drop_table(struct Parse *parse_context, struct space *space, return; vdbe_emit_fk_constraint_drop(parse_context, fk_name_dup, space_id); } + /* Delete all CK constraints. */ + struct ck_constraint *ck_constraint; + rlist_foreach_entry(ck_constraint, &space->ck_constraint, link) { + vdbe_emit_ck_constraint_drop(parse_context, + ck_constraint->def->name, + space_id); + } /* * Drop all _space and _index entries that refer to the * table. @@ -2780,8 +2888,7 @@ sqlSrcListDelete(sql * db, SrcList * pList) */ assert(pItem->space == NULL || !pItem->space->def->opts.is_temporary || - (pItem->space->index == NULL && - pItem->space->def->opts.checks == NULL)); + pItem->space->index == NULL); sql_select_delete(db, pItem->pSelect); sql_expr_delete(db, pItem->pOn, false); sqlIdListDelete(db, pItem->pUsing); diff --git a/src/box/sql/insert.c b/src/box/sql/insert.c index c2aac553f..a075d4dc3 100644 --- a/src/box/sql/insert.c +++ b/src/box/sql/insert.c @@ -36,6 +36,7 @@ #include "sqlInt.h" #include "tarantoolInt.h" #include "vdbeInt.h" +#include "box/ck_constraint.h" #include "box/session.h" #include "box/schema.h" #include "bit/bit.h" @@ -933,34 +934,27 @@ vdbe_emit_constraint_checks(struct Parse *parse_context, struct space *space, if (on_conflict == ON_CONFLICT_ACTION_DEFAULT) on_conflict = ON_CONFLICT_ACTION_ABORT; /* Test all CHECK constraints. */ - struct ExprList *checks = def->opts.checks; enum on_conflict_action on_conflict_check = on_conflict; if (on_conflict == ON_CONFLICT_ACTION_REPLACE) on_conflict_check = ON_CONFLICT_ACTION_ABORT; - if (checks != NULL) { + if (!rlist_empty(&space->ck_constraint)) parse_context->ckBase = new_tuple_reg; - for (int i = 0; i < checks->nExpr; i++) { - struct Expr *expr = checks->a[i].pExpr; - if (is_update && - checkConstraintUnchanged(expr, upd_cols)) - continue; - int all_ok = sqlVdbeMakeLabel(v); - sqlExprIfTrue(parse_context, expr, all_ok, - SQL_JUMPIFNULL); - if (on_conflict == ON_CONFLICT_ACTION_IGNORE) { - sqlVdbeGoto(v, ignore_label); - } else { - char *name = checks->a[i].zName; - if (name == NULL) - name = def->name; - sqlHaltConstraint(parse_context, - SQL_CONSTRAINT_CHECK, - on_conflict_check, name, - P4_TRANSIENT, - P5_ConstraintCheck); - } - sqlVdbeResolveLabel(v, all_ok); + struct ck_constraint *ck_constraint; + rlist_foreach_entry(ck_constraint, &space->ck_constraint, link) { + struct Expr *expr = ck_constraint->expr; + if (is_update && checkConstraintUnchanged(expr, upd_cols) != 0) + continue; + int all_ok = sqlVdbeMakeLabel(v); + sqlExprIfTrue(parse_context, expr, all_ok, SQL_JUMPIFNULL); + if (on_conflict == ON_CONFLICT_ACTION_IGNORE) { + sqlVdbeGoto(v, ignore_label); + } else { + char *name = ck_constraint->def->name; + sqlHaltConstraint(parse_context, SQL_CONSTRAINT_CHECK, + on_conflict_check, name, P4_TRANSIENT, + P5_ConstraintCheck); } + sqlVdbeResolveLabel(v, all_ok); } sql_emit_table_types(v, space->def, new_tuple_reg); /* @@ -1245,14 +1239,26 @@ xferOptimization(Parse * pParse, /* Parser context */ if (pSrcIdx == NULL) return 0; } - /* Get server checks. */ - ExprList *pCheck_src = src->def->opts.checks; - ExprList *pCheck_dest = dest->def->opts.checks; - if (pCheck_dest != NULL && - sqlExprListCompare(pCheck_src, pCheck_dest, -1) != 0) { - /* Tables have different CHECK constraints. Ticket #2252 */ + /* + * Dissallow the transfer optimization if the check + * constraints are differ. + */ + if (rlist_empty(&dest->ck_constraint) != + rlist_empty(&src->ck_constraint)) return 0; + struct rlist *dst_ck_link = &dest->ck_constraint; + struct ck_constraint *src_ck; + rlist_foreach_entry(src_ck, &src->ck_constraint, link) { + dst_ck_link = rlist_next(dst_ck_link); + if (dst_ck_link == &dest->ck_constraint) + return 0; + struct ck_constraint *dest_ck = + rlist_entry(dst_ck_link, struct ck_constraint, link); + if (sqlExprCompare(src_ck->expr, dest_ck->expr, -1) != 0) + return 0; } + if (rlist_next(dst_ck_link) != &dest->ck_constraint) + return 0; /* Disallow the transfer optimization if the destination table constains * any foreign key constraints. This is more restrictive than necessary. * So the extra complication to make this rule less restrictive is probably diff --git a/src/box/sql/parse_def.h b/src/box/sql/parse_def.h index a1af2bacd..ec434420d 100644 --- a/src/box/sql/parse_def.h +++ b/src/box/sql/parse_def.h @@ -123,6 +123,20 @@ struct fk_constraint_parse { struct rlist link; }; +/** + * Structure representing check constraint appeared within + * CREATE TABLE statement. Used only during parsing. + */ +struct ck_constraint_parse { + /** + * Check constraint declared in <CREATE TABLE ...> + * statement. Must be coded after space creation. + */ + struct ck_constraint_def *ck_def; + /** Organize these structs into linked list. */ + struct rlist link; +}; + /** * Possible SQL index types. Note that PK and UNIQUE constraints * are implemented as indexes and have their own types: @@ -189,6 +203,13 @@ struct create_table_def { * Foreign key constraint appeared in CREATE TABLE stmt. */ struct rlist new_fkey; + /** + * Number of CK constraints declared within + * CREATE TABLE statement. + */ + uint32_t check_count; + /** Check constraint appeared in CREATE TABLE stmt. */ + struct rlist new_check; /** True, if table to be created has AUTOINCREMENT PK. */ bool has_autoinc; }; @@ -437,6 +458,7 @@ create_table_def_init(struct create_table_def *table_def, struct Token *name, create_entity_def_init(&table_def->base, ENTITY_TYPE_TABLE, NULL, name, if_not_exists); rlist_create(&table_def->new_fkey); + rlist_create(&table_def->new_check); } static inline void diff --git a/src/box/sql/select.c b/src/box/sql/select.c index b1ec8c758..531e29ed5 100644 --- a/src/box/sql/select.c +++ b/src/box/sql/select.c @@ -6445,7 +6445,12 @@ sql_expr_extract_select(struct Parse *parser, struct Select *select) struct ExprList *expr_list = select->pEList; assert(expr_list->nExpr == 1); parser->parsed_ast_type = AST_TYPE_EXPR; - parser->parsed_ast.expr = sqlExprDup(parser->db, - expr_list->a->pExpr, - EXPRDUP_REDUCE); + /* + * Extract a copy of parsed expression. + * We cannot use EXPRDUP_REDUCE flag in sqlExprDup call + * because some compiled Expr (like Checks expressions) + * may require further resolve with sqlResolveExprNames. + */ + parser->parsed_ast.expr = + sqlExprDup(parser->db, expr_list->a->pExpr, 0); } diff --git a/src/box/sql/tokenize.c b/src/box/sql/tokenize.c index 8cc35323c..3791be567 100644 --- a/src/box/sql/tokenize.c +++ b/src/box/sql/tokenize.c @@ -434,7 +434,6 @@ parser_space_delete(struct sql *db, struct space *space) assert(space->def->opts.is_temporary); for (uint32_t i = 0; i < space->index_count; ++i) index_def_delete(space->index[i]->def); - sql_expr_list_delete(db, space->def->opts.checks); } /** diff --git a/test/app-tap/tarantoolctl.test.lua b/test/app-tap/tarantoolctl.test.lua index 62a78d6bf..9ac0b5c69 100755 --- a/test/app-tap/tarantoolctl.test.lua +++ b/test/app-tap/tarantoolctl.test.lua @@ -352,7 +352,7 @@ do local filler_code = [[ box.cfg{memtx_memory = 104857600, background=false} - local space = box.schema.create_space("test") + space = box.schema.create_space("test") space:create_index("primary") space:insert({[1] = 1, [2] = 2, [3] = 3, [4] = 4}) space:replace({[1] = 2, [2] = 2, [3] = 3, [4] = 4}) @@ -403,8 +403,8 @@ do check_ctlcat_xlog(test_i, dir, "--from=3 --to=6 --format=json --show-system --replica 1", "\n", 3) check_ctlcat_xlog(test_i, dir, "--from=3 --to=6 --format=json --show-system --replica 1 --replica 2", "\n", 3) check_ctlcat_xlog(test_i, dir, "--from=3 --to=6 --format=json --show-system --replica 2", "\n", 0) - check_ctlcat_snap(test_i, dir, "--space=280", "---\n", 21) - check_ctlcat_snap(test_i, dir, "--space=288", "---\n", 47) + check_ctlcat_snap(test_i, dir, "--space=280", "---\n", 22) + check_ctlcat_snap(test_i, dir, "--space=288", "---\n", 49) end) end) diff --git a/test/box-py/bootstrap.result b/test/box-py/bootstrap.result index 379f6c51f..82a8ff39e 100644 --- a/test/box-py/bootstrap.result +++ b/test/box-py/bootstrap.result @@ -78,6 +78,8 @@ box.space._space:select{} {'name': 'is_deferred', 'type': 'boolean'}, {'name': 'match', 'type': 'string'}, {'name': 'on_delete', 'type': 'string'}, {'name': 'on_update', 'type': 'string'}, {'name': 'child_cols', 'type': 'array'}, {'name': 'parent_cols', 'type': 'array'}]] + - [357, 1, '_ck_constraint', 'memtx', 0, {}, [{'name': 'name', 'type': 'string'}, + {'name': 'space_id', 'type': 'unsigned'}, {'name': 'expr_str', 'type': 'str'}]] ... box.space._index:select{} --- @@ -130,6 +132,8 @@ box.space._index:select{} - [340, 1, 'sequence', 'tree', {'unique': false}, [[1, 'unsigned']]] - [356, 0, 'primary', 'tree', {'unique': true}, [[0, 'string'], [1, 'unsigned']]] - [356, 1, 'child_id', 'tree', {'unique': false}, [[1, 'unsigned']]] + - [357, 0, 'primary', 'tree', {'unique': true}, [[0, 'string'], [1, 'unsigned']]] + - [357, 1, 'space_id', 'tree', {'unique': false}, [[1, 'unsigned']]] ... box.space._user:select{} --- diff --git a/test/box/access.result b/test/box/access.result index 9c190240f..801112799 100644 --- a/test/box/access.result +++ b/test/box/access.result @@ -1487,6 +1487,9 @@ box.schema.user.grant('tester', 'read', 'space', '_trigger') box.schema.user.grant('tester', 'read', 'space', '_fk_constraint') --- ... +box.schema.user.grant('tester', 'read', 'space', '_ck_constraint') +--- +... box.session.su("tester") --- ... diff --git a/test/box/access.test.lua b/test/box/access.test.lua index 4baeb2ef6..2f62d6f53 100644 --- a/test/box/access.test.lua +++ b/test/box/access.test.lua @@ -554,6 +554,7 @@ box.schema.user.grant('tester', 'create' , 'sequence') box.schema.user.grant('tester', 'read', 'space', '_sequence') box.schema.user.grant('tester', 'read', 'space', '_trigger') box.schema.user.grant('tester', 'read', 'space', '_fk_constraint') +box.schema.user.grant('tester', 'read', 'space', '_ck_constraint') box.session.su("tester") -- successful create s1 = box.schema.space.create("test_space") diff --git a/test/box/access_misc.result b/test/box/access_misc.result index 36ebfae09..d34b95983 100644 --- a/test/box/access_misc.result +++ b/test/box/access_misc.result @@ -818,6 +818,8 @@ box.space._space:select() {'name': 'is_deferred', 'type': 'boolean'}, {'name': 'match', 'type': 'string'}, {'name': 'on_delete', 'type': 'string'}, {'name': 'on_update', 'type': 'string'}, {'name': 'child_cols', 'type': 'array'}, {'name': 'parent_cols', 'type': 'array'}]] + - [357, 1, '_ck_constraint', 'memtx', 0, {}, [{'name': 'name', 'type': 'string'}, + {'name': 'space_id', 'type': 'unsigned'}, {'name': 'expr_str', 'type': 'str'}]] ... box.space._func:select() --- diff --git a/test/box/access_sysview.result b/test/box/access_sysview.result index ae042664a..77a24b425 100644 --- a/test/box/access_sysview.result +++ b/test/box/access_sysview.result @@ -230,11 +230,11 @@ box.session.su('guest') ... #box.space._vspace:select{} --- -- 22 +- 23 ... #box.space._vindex:select{} --- -- 48 +- 50 ... #box.space._vuser:select{} --- @@ -262,7 +262,7 @@ box.session.su('guest') ... #box.space._vindex:select{} --- -- 48 +- 50 ... #box.space._vuser:select{} --- diff --git a/test/box/alter.result b/test/box/alter.result index c1b1de135..44630557c 100644 --- a/test/box/alter.result +++ b/test/box/alter.result @@ -107,7 +107,7 @@ space = box.space[t[1]] ... space.id --- -- 357 +- 358 ... space.field_count --- @@ -152,7 +152,7 @@ space_deleted ... space:replace{0} --- -- error: Space '357' does not exist +- error: Space '358' does not exist ... _index:insert{_space.id, 0, 'primary', 'tree', {unique=true}, {{0, 'unsigned'}}} --- @@ -230,6 +230,8 @@ _index:select{} - [340, 1, 'sequence', 'tree', {'unique': false}, [[1, 'unsigned']]] - [356, 0, 'primary', 'tree', {'unique': true}, [[0, 'string'], [1, 'unsigned']]] - [356, 1, 'child_id', 'tree', {'unique': false}, [[1, 'unsigned']]] + - [357, 0, 'primary', 'tree', {'unique': true}, [[0, 'string'], [1, 'unsigned']]] + - [357, 1, 'space_id', 'tree', {'unique': false}, [[1, 'unsigned']]] ... -- modify indexes of a system space _index:delete{_index.id, 0} diff --git a/test/box/misc.result b/test/box/misc.result index a1f7a0990..eeb63366e 100644 --- a/test/box/misc.result +++ b/test/box/misc.result @@ -522,6 +522,7 @@ t; 191: box.error.SQL_PARSER_LIMIT 192: box.error.INDEX_DEF_UNSUPPORTED 193: box.error.CK_DEF_UNSUPPORTED + 194: box.error.CREATE_CK_CONSTRAINT ... test_run:cmd("setopt delimiter ''"); --- diff --git a/test/sql-tap/check.test.lua b/test/sql-tap/check.test.lua index b01afca7c..0dda3fac8 100755 --- a/test/sql-tap/check.test.lua +++ b/test/sql-tap/check.test.lua @@ -55,7 +55,7 @@ test:do_catchsql_test( INSERT INTO t1 VALUES(6,7, 2); ]], { -- <check-1.3> - 1, "Failed to execute SQL statement: CHECK constraint failed: T1" + 1, "Failed to execute SQL statement: CHECK constraint failed: CK_CONSTRAINT_1_T1" -- </check-1.3> }) @@ -75,7 +75,7 @@ test:do_catchsql_test( INSERT INTO t1 VALUES(4,3, 2); ]], { -- <check-1.5> - 1, "Failed to execute SQL statement: CHECK constraint failed: T1" + 1, "Failed to execute SQL statement: CHECK constraint failed: CK_CONSTRAINT_2_T1" -- </check-1.5> }) @@ -147,7 +147,7 @@ test:do_catchsql_test( UPDATE t1 SET x=7 WHERE x==2 ]], { -- <check-1.12> - 1, "Failed to execute SQL statement: CHECK constraint failed: T1" + 1, "Failed to execute SQL statement: CHECK constraint failed: CK_CONSTRAINT_1_T1" -- </check-1.12> }) @@ -167,7 +167,7 @@ test:do_catchsql_test( UPDATE t1 SET x=5 WHERE x==2 ]], { -- <check-1.14> - 1, "Failed to execute SQL statement: CHECK constraint failed: T1" + 1, "Failed to execute SQL statement: CHECK constraint failed: CK_CONSTRAINT_1_T1" -- </check-1.14> }) @@ -319,7 +319,7 @@ test:do_catchsql_test( ); ]], { -- <check-3.1> - 1, "Failed to create space 'T3': Subqueries are prohibited in a CHECK constraint definition" + 1, "Failed to create check constraint 'CK_CONSTRAINT_1_T3': Subqueries are prohibited in a check constraint definition" -- </check-3.1> }) @@ -344,7 +344,7 @@ test:do_catchsql_test( ); ]], { -- <check-3.3> - 1, "Failed to create space 'T3': Can’t resolve field 'Q'" + 1, "Failed to create check constraint 'CK_CONSTRAINT_1_T3': Can’t resolve field 'Q'" -- </check-3.3> }) @@ -368,7 +368,7 @@ test:do_catchsql_test( ); ]], { -- <check-3.5> - 1, "Failed to create space 'T3': Field 'X' was not found in the space 'T2' format" + 1, "Failed to create check constraint 'CK_CONSTRAINT_1_T3': Field 'X' was not found in the space 'T2' format" -- </check-3.5> }) @@ -413,7 +413,7 @@ test:do_catchsql_test( INSERT INTO t3 VALUES(111,222,333); ]], { -- <check-3.9> - 1, "Failed to execute SQL statement: CHECK constraint failed: T3" + 1, "Failed to execute SQL statement: CHECK constraint failed: CK_CONSTRAINT_1_T3" -- </check-3.9> }) @@ -484,7 +484,7 @@ test:do_catchsql_test( UPDATE t4 SET x=0, y=1; ]], { -- <check-4.6> - 1, "Failed to execute SQL statement: CHECK constraint failed: T4" + 1, "Failed to execute SQL statement: CHECK constraint failed: CK_CONSTRAINT_1_T4" -- </check-4.6> }) @@ -504,7 +504,7 @@ test:do_catchsql_test( UPDATE t4 SET x=0, y=2; ]], { -- <check-4.9> - 1, "Failed to execute SQL statement: CHECK constraint failed: T4" + 1, "Failed to execute SQL statement: CHECK constraint failed: CK_CONSTRAINT_1_T4" -- </check-4.9> }) @@ -516,7 +516,7 @@ test:do_catchsql_test( ); ]], { -- <check-5.1> - 1, "Wrong space options (field 5): invalid expression specified (bindings are not allowed in DDL)" + 1, "Failed to create check constraint 'CK_CONSTRAINT_1_T5': bindings are not allowed in DDL" -- </check-5.1> }) @@ -528,7 +528,7 @@ test:do_catchsql_test( ); ]], { -- <check-5.2> - 1, "Wrong space options (field 5): invalid expression specified (bindings are not allowed in DDL)" + 1, "Failed to create check constraint 'CK_CONSTRAINT_1_T5': bindings are not allowed in DDL" -- </check-5.2> }) @@ -581,7 +581,7 @@ test:do_catchsql_test( UPDATE OR FAIL t1 SET x=7-x, y=y+1; ]], { -- <check-6.5> - 1, "Failed to execute SQL statement: CHECK constraint failed: T1" + 1, "Failed to execute SQL statement: CHECK constraint failed: CK_CONSTRAINT_1_T1" -- </check-6.5> }) @@ -603,7 +603,7 @@ test:do_catchsql_test( INSERT OR ROLLBACK INTO t1 VALUES(8,40.0, 10); ]], { -- <check-6.7> - 1, "Failed to execute SQL statement: CHECK constraint failed: T1" + 1, "Failed to execute SQL statement: CHECK constraint failed: CK_CONSTRAINT_1_T1" -- </check-6.7> }) @@ -636,7 +636,7 @@ test:do_catchsql_test( REPLACE INTO t1 VALUES(6,7, 11); ]], { -- <check-6.12> - 1, "Failed to execute SQL statement: CHECK constraint failed: T1" + 1, "Failed to execute SQL statement: CHECK constraint failed: CK_CONSTRAINT_1_T1" -- </check-6.12> }) @@ -700,7 +700,7 @@ test:do_catchsql_test( 7.3, " INSERT INTO t6 VALUES(11) ", { -- <7.3> - 1, "Failed to execute SQL statement: CHECK constraint failed: T6" + 1, "Failed to execute SQL statement: CHECK constraint failed: CK_CONSTRAINT_1_T6" -- </7.3> }) diff --git a/test/sql-tap/fkey2.test.lua b/test/sql-tap/fkey2.test.lua index 54e5059b3..695a379a6 100755 --- a/test/sql-tap/fkey2.test.lua +++ b/test/sql-tap/fkey2.test.lua @@ -362,7 +362,7 @@ test:do_catchsql_test( UPDATE ab SET a = 5; ]], { -- <fkey2-3.2> - 1, "Failed to execute SQL statement: CHECK constraint failed: EF" + 1, "Failed to execute SQL statement: CHECK constraint failed: CK_CONSTRAINT_1_EF" -- </fkey2-3.2> }) @@ -382,7 +382,7 @@ test:do_catchsql_test( UPDATE ab SET a = 5; ]], { -- <fkey2-3.4> - 1, "Failed to execute SQL statement: CHECK constraint failed: EF" + 1, "Failed to execute SQL statement: CHECK constraint failed: CK_CONSTRAINT_1_EF" -- </fkey2-3.4> }) diff --git a/test/sql-tap/sql-errors.test.lua b/test/sql-tap/sql-errors.test.lua index 4e173b692..d742cf6cf 100755 --- a/test/sql-tap/sql-errors.test.lua +++ b/test/sql-tap/sql-errors.test.lua @@ -313,7 +313,7 @@ test:do_catchsql_test( CREATE TABLE t27 (i INT PRIMARY KEY, CHECK(i < (SELECT * FROM t0))); ]], { -- <sql-errors-1.27> - 1,"Failed to create space 'T27': Subqueries are prohibited in a CHECK constraint definition" + 1,"Failed to create check constraint 'CK_CONSTRAINT_1_T27': Subqueries are prohibited in a check constraint definition" -- </sql-errors-1.27> }) diff --git a/test/sql-tap/table.test.lua b/test/sql-tap/table.test.lua index 5b793c0fc..066662f33 100755 --- a/test/sql-tap/table.test.lua +++ b/test/sql-tap/table.test.lua @@ -1221,7 +1221,7 @@ test:do_catchsql_test( INSERT INTO T21 VALUES(1, -1, 1); ]], { -- <table-21.3> - 1, "Failed to execute SQL statement: CHECK constraint failed: T21" + 1, "Failed to execute SQL statement: CHECK constraint failed: CK_CONSTRAINT_1_T21" -- </table-21.3> }) @@ -1231,7 +1231,7 @@ test:do_catchsql_test( INSERT INTO T21 VALUES(1, 1, -1); ]], { -- <table-21.4> - 1, "Failed to execute SQL statement: CHECK constraint failed: T21" + 1, "Failed to execute SQL statement: CHECK constraint failed: CK_CONSTRAINT_2_T21" -- </table-21.4> }) diff --git a/test/sql/checks.result b/test/sql/checks.result index f7cddec43..0042243ea 100644 --- a/test/sql/checks.result +++ b/test/sql/checks.result @@ -18,8 +18,10 @@ box.execute('pragma sql_default_engine=\''..engine..'\'') -- -- gh-3272: Move SQL CHECK into server -- --- invalid expression -opts = {checks = {{expr = 'X><5'}}} +-- Until Tarantool version 2.2 check constraints were stored in +-- space opts. +-- Make sure that now this legacy option is ignored. +opts = {checks = {{expr = 'X>5'}}} --- ... format = {{name = 'X', type = 'unsigned'}} @@ -30,89 +32,256 @@ t = {513, 1, 'test', 'memtx', 0, opts, format} ... s = box.space._space:insert(t) --- -- error: 'Wrong space options (field 5): invalid expression specified (Syntax error - near ''<'')' ... -opts = {checks = {{expr = 'X>5'}}} +_ = box.space.test:create_index('pk') --- ... -format = {{name = 'X', type = 'unsigned'}} +-- Invalid expression test. +box.space._ck_constraint:insert({'CK_CONSTRAINT_01', 513, 'X><5'}) --- +- error: 'Failed to create check constraint ''CK_CONSTRAINT_01'': Syntax error near + ''<''' ... -t = {513, 1, 'test', 'memtx', 0, opts, format} +-- Unexistent space test. +box.space._ck_constraint:insert({'CK_CONSTRAINT_01', 550, 'X<5'}) --- +- error: Space '550' does not exist ... -s = box.space._space:insert(t) +-- Pass integer instead of expression. +box.space._ck_constraint:insert({'CK_CONSTRAINT_01', 550, 666}) +--- +- error: 'Tuple field 3 type does not match one required by operation: expected string' +... +-- Check constraints LUA creation test. +box.space._ck_constraint:insert({'CK_CONSTRAINT_01', 513, 'X<5'}) --- +- ['CK_CONSTRAINT_01', 513, 'X<5'] ... -box.space._space:delete(513) +box.space._ck_constraint:count({}) +--- +- 1 +... +box.execute("INSERT INTO \"test\" VALUES(5);") +--- +- error: 'Failed to execute SQL statement: CHECK constraint failed: CK_CONSTRAINT_01' +... +box.space._ck_constraint:replace({'CK_CONSTRAINT_01', 513, 'X<=5'}) +--- +- ['CK_CONSTRAINT_01', 513, 'X<=5'] +... +box.execute("INSERT INTO \"test\" VALUES(5);") +--- +- row_count: 1 +... +box.execute("INSERT INTO \"test\" VALUES(6);") +--- +- error: 'Failed to execute SQL statement: CHECK constraint failed: CK_CONSTRAINT_01' +... +-- Can't drop table with check constraints. +box.space.test:delete({5}) +--- +- [5] +... +box.space.test.index.pk:drop() +--- +... +box.space._space:delete({513}) +--- +- error: 'Can''t drop space ''test'': the space has check constraints' +... +box.space._ck_constraint:delete({'CK_CONSTRAINT_01', 513}) +--- +- ['CK_CONSTRAINT_01', 513, 'X<=5'] +... +box.space._space:delete({513}) --- - [513, 1, 'test', 'memtx', 0, {'checks': [{'expr': 'X>5'}]}, [{'name': 'X', 'type': 'unsigned'}]] ... -opts = {checks = {{expr = 'X>5', name = 'ONE'}}} +-- Create table with checks in sql. +box.execute("CREATE TABLE t1(x INTEGER CONSTRAINT ONE CHECK( x<5 ), y REAL CONSTRAINT TWO CHECK( y>x ), z INTEGER PRIMARY KEY);") --- +- row_count: 1 ... -format = {{name = 'X', type = 'unsigned'}} +box.space._ck_constraint:count() --- +- 2 ... -t = {513, 1, 'test', 'memtx', 0, opts, format} +box.execute("INSERT INTO t1 VALUES (7, 1, 1)") --- +- error: 'Failed to execute SQL statement: CHECK constraint failed: ONE' ... -s = box.space._space:insert(t) +box.execute("INSERT INTO t1 VALUES (2, 1, 1)") --- +- error: 'Failed to execute SQL statement: CHECK constraint failed: TWO' ... -box.space._space:delete(513) +box.execute("INSERT INTO t1 VALUES (2, 4, 1)") --- -- [513, 1, 'test', 'memtx', 0, {'checks': [{'name': 'ONE', 'expr': 'X>5'}]}, [{'name': 'X', - 'type': 'unsigned'}]] +- row_count: 1 ... --- extra invlalid field name -opts = {checks = {{expr = 'X>5', name = 'ONE', extra = 'TWO'}}} +box.execute("DROP TABLE t1") --- +- row_count: 1 ... -format = {{name = 'X', type = 'unsigned'}} +-- Test xferOptimization for space that have CK constraints. +box.execute("CREATE TABLE first (id FLOAT PRIMARY KEY CHECK(id < 5), a INT CONSTRAINT ONE CHECK(a < 5));") --- +- row_count: 1 ... -t = {513, 1, 'test', 'memtx', 0, opts, format} +box.execute("CREATE TABLE second (id FLOAT PRIMARY KEY CHECK(id < 5), a INT CONSTRAINT ONE CHECK(a < 5));") --- +- row_count: 1 ... -s = box.space._space:insert(t) +box.execute("CREATE TABLE third (id FLOAT PRIMARY KEY, a INT CONSTRAINT ONE CHECK(a < 5));") --- -- error: 'Wrong space options (field 5): invalid MsgPack map field ''extra''' +- row_count: 1 ... -opts = {checks = {{expr_invalid_label = 'X>5'}}} +box.execute("INSERT INTO first VALUES(1, 1);") --- +- row_count: 1 ... -format = {{name = 'X', type = 'unsigned'}} +box.execute("INSERT INTO first VALUES(2, 2);") --- +- row_count: 1 ... -t = {513, 1, 'test', 'memtx', 0, opts, format} +box.execute("INSERT INTO first VALUES(3, 3);") --- +- row_count: 1 ... -s = box.space._space:insert(t) +box.execute("INSERT OR IGNORE INTO second SELECT * FROM first;") --- -- error: 'Wrong space options (field 5): invalid MsgPack map field ''expr_invalid_label''' +- row_count: 3 ... --- invalid field type -opts = {checks = {{name = 123}}} +box.execute("INSERT OR IGNORE INTO third SELECT * FROM first;") --- +- row_count: 3 ... -format = {{name = 'X', type = 'unsigned'}} +box.execute("DELETE FROM second;") --- +- row_count: 3 ... -t = {513, 1, 'test', 'memtx', 0, opts, format} +box.execute("INSERT OR IGNORE INTO second SELECT * FROM third;") --- +- row_count: 3 ... -s = box.space._space:insert(t) +box.execute("DROP TABLE first;") +--- +- row_count: 1 +... +box.execute("DROP TABLE second;") +--- +- row_count: 1 +... +box.execute("DROP TABLE third;") +--- +- row_count: 1 +... +-- Test space creation rollback on spell error in ck constraint. +box.execute("CREATE TABLE first (id FLOAT PRIMARY KEY CHECK(id < 5), a INT CONSTRAINT ONE CHECK(a >< 5));") +--- +- error: Syntax error near '<' +... +box.space.FIRST == nil +--- +- true +... +box.space._ck_constraint:count() == 0 +--- +- true +... +box.space.FIRST:drop() +--- +- error: '[string "return box.space.FIRST:drop() "]:1: attempt to index field ''FIRST'' + (a nil value)' +... +-- Ck constraints are disallowed for spaces having no format. +s = box.schema.create_space('test') +--- +... +_ = s:create_index('pk') +--- +... +_ = box.space._ck_constraint:insert({'physics', s.id, 'X<Y'}) +--- +- error: Tarantool does not support CK constraint for space without format +... +s:format({{name='X', type='integer'}, {name='Y', type='integer'}}) +--- +... +_ = box.space._ck_constraint:insert({'physics', s.id, 'X<Y'}) +--- +... +box.execute("INSERT INTO \"test\" VALUES(2, 1);") +--- +- error: 'Failed to execute SQL statement: CHECK constraint failed: physics' +... +s:format({{name='Y', type='integer'}, {name='X', type='integer'}}) +--- +... +box.execute("INSERT INTO \"test\" VALUES(1, 2);") +--- +- error: 'Failed to execute SQL statement: CHECK constraint failed: physics' +... +box.execute("INSERT INTO \"test\" VALUES(2, 1);") +--- +- row_count: 1 +... +s:truncate() +--- +... +box.execute("INSERT INTO \"test\" VALUES(1, 2);") +--- +- error: 'Failed to execute SQL statement: CHECK constraint failed: physics' +... +s:format({}) +--- +- error: Tarantool does not support CK constraint for space without format +... +s:format() +--- +- [{'name': 'Y', 'type': 'integer'}, {'name': 'X', 'type': 'integer'}] +... +s:format({{name='Y1', type='integer'}, {name='X1', type='integer'}}) +--- +- error: 'Failed to create check constraint ''physics'': Can’t resolve field ''X''' +... +-- Ck constraint creation is forbidden for non-empty space +s:insert({2, 1}) +--- +- [2, 1] +... +_ = box.space._ck_constraint:insert({'conflict', s.id, 'X>10'}) +--- +- error: 'Failed to create check constraint ''conflict'': referencing space must be + empty' +... +s:truncate() +--- +... +_ = box.space._ck_constraint:insert({'conflict', s.id, 'X>10'}) +--- +... +box.execute("INSERT INTO \"test\" VALUES(1, 2);") +--- +- error: 'Failed to execute SQL statement: CHECK constraint failed: conflict' +... +box.execute("INSERT INTO \"test\" VALUES(11, 11);") +--- +- error: 'Failed to execute SQL statement: CHECK constraint failed: physics' +... +box.execute("INSERT INTO \"test\" VALUES(12, 11);") +--- +- row_count: 1 +... +s:drop() --- -- error: 'Wrong space options (field 5): invalid MsgPack map field ''name'' type' ... -- -- gh-3611: Segfault on table creation with check referencing this table -- box.execute("CREATE TABLE w2 (s1 INT PRIMARY KEY, CHECK ((SELECT COUNT(*) FROM w2) = 0));") --- -- error: 'Failed to create space ''W2'': Space ''W2'' does not exist' +- error: 'Failed to create check constraint ''CK_CONSTRAINT_1_W2'': Subqueries are + prohibited in a check constraint definition' ... box.execute("DROP TABLE w2;") --- @@ -123,22 +292,8 @@ box.execute("DROP TABLE w2;") -- box.execute("CREATE TABLE t5(x INT PRIMARY KEY, y INT, CHECK( x*y < ? ));") --- -- error: 'Wrong space options (field 5): invalid expression specified (bindings are - not allowed in DDL)' -... -opts = {checks = {{expr = '?>5', name = 'ONE'}}} ---- -... -format = {{name = 'X', type = 'unsigned'}} ---- -... -t = {513, 1, 'test', 'memtx', 0, opts, format} ---- -... -s = box.space._space:insert(t) ---- -- error: 'Wrong space options (field 5): invalid expression specified (bindings are - not allowed in DDL)' +- error: 'Failed to create check constraint ''CK_CONSTRAINT_1_T5'': bindings are not + allowed in DDL' ... test_run:cmd("clear filter") --- diff --git a/test/sql/checks.test.lua b/test/sql/checks.test.lua index 5bfcf12f8..0cdfde2e9 100644 --- a/test/sql/checks.test.lua +++ b/test/sql/checks.test.lua @@ -8,41 +8,90 @@ box.execute('pragma sql_default_engine=\''..engine..'\'') -- gh-3272: Move SQL CHECK into server -- --- invalid expression -opts = {checks = {{expr = 'X><5'}}} -format = {{name = 'X', type = 'unsigned'}} -t = {513, 1, 'test', 'memtx', 0, opts, format} -s = box.space._space:insert(t) - +-- Until Tarantool version 2.2 check constraints were stored in +-- space opts. +-- Make sure that now this legacy option is ignored. opts = {checks = {{expr = 'X>5'}}} format = {{name = 'X', type = 'unsigned'}} t = {513, 1, 'test', 'memtx', 0, opts, format} s = box.space._space:insert(t) -box.space._space:delete(513) +_ = box.space.test:create_index('pk') -opts = {checks = {{expr = 'X>5', name = 'ONE'}}} -format = {{name = 'X', type = 'unsigned'}} -t = {513, 1, 'test', 'memtx', 0, opts, format} -s = box.space._space:insert(t) -box.space._space:delete(513) +-- Invalid expression test. +box.space._ck_constraint:insert({'CK_CONSTRAINT_01', 513, 'X><5'}) +-- Unexistent space test. +box.space._ck_constraint:insert({'CK_CONSTRAINT_01', 550, 'X<5'}) +-- Pass integer instead of expression. +box.space._ck_constraint:insert({'CK_CONSTRAINT_01', 550, 666}) --- extra invlalid field name -opts = {checks = {{expr = 'X>5', name = 'ONE', extra = 'TWO'}}} -format = {{name = 'X', type = 'unsigned'}} -t = {513, 1, 'test', 'memtx', 0, opts, format} -s = box.space._space:insert(t) +-- Check constraints LUA creation test. +box.space._ck_constraint:insert({'CK_CONSTRAINT_01', 513, 'X<5'}) +box.space._ck_constraint:count({}) -opts = {checks = {{expr_invalid_label = 'X>5'}}} -format = {{name = 'X', type = 'unsigned'}} -t = {513, 1, 'test', 'memtx', 0, opts, format} -s = box.space._space:insert(t) +box.execute("INSERT INTO \"test\" VALUES(5);") +box.space._ck_constraint:replace({'CK_CONSTRAINT_01', 513, 'X<=5'}) +box.execute("INSERT INTO \"test\" VALUES(5);") +box.execute("INSERT INTO \"test\" VALUES(6);") +-- Can't drop table with check constraints. +box.space.test:delete({5}) +box.space.test.index.pk:drop() +box.space._space:delete({513}) +box.space._ck_constraint:delete({'CK_CONSTRAINT_01', 513}) +box.space._space:delete({513}) --- invalid field type -opts = {checks = {{name = 123}}} -format = {{name = 'X', type = 'unsigned'}} -t = {513, 1, 'test', 'memtx', 0, opts, format} -s = box.space._space:insert(t) +-- Create table with checks in sql. +box.execute("CREATE TABLE t1(x INTEGER CONSTRAINT ONE CHECK( x<5 ), y REAL CONSTRAINT TWO CHECK( y>x ), z INTEGER PRIMARY KEY);") +box.space._ck_constraint:count() +box.execute("INSERT INTO t1 VALUES (7, 1, 1)") +box.execute("INSERT INTO t1 VALUES (2, 1, 1)") +box.execute("INSERT INTO t1 VALUES (2, 4, 1)") +box.execute("DROP TABLE t1") + +-- Test xferOptimization for space that have CK constraints. +box.execute("CREATE TABLE first (id FLOAT PRIMARY KEY CHECK(id < 5), a INT CONSTRAINT ONE CHECK(a < 5));") +box.execute("CREATE TABLE second (id FLOAT PRIMARY KEY CHECK(id < 5), a INT CONSTRAINT ONE CHECK(a < 5));") +box.execute("CREATE TABLE third (id FLOAT PRIMARY KEY, a INT CONSTRAINT ONE CHECK(a < 5));") +box.execute("INSERT INTO first VALUES(1, 1);") +box.execute("INSERT INTO first VALUES(2, 2);") +box.execute("INSERT INTO first VALUES(3, 3);") +box.execute("INSERT OR IGNORE INTO second SELECT * FROM first;") +box.execute("INSERT OR IGNORE INTO third SELECT * FROM first;") +box.execute("DELETE FROM second;") +box.execute("INSERT OR IGNORE INTO second SELECT * FROM third;") +box.execute("DROP TABLE first;") +box.execute("DROP TABLE second;") +box.execute("DROP TABLE third;") + +-- Test space creation rollback on spell error in ck constraint. +box.execute("CREATE TABLE first (id FLOAT PRIMARY KEY CHECK(id < 5), a INT CONSTRAINT ONE CHECK(a >< 5));") +box.space.FIRST == nil +box.space._ck_constraint:count() == 0 +box.space.FIRST:drop() +-- Ck constraints are disallowed for spaces having no format. +s = box.schema.create_space('test') +_ = s:create_index('pk') +_ = box.space._ck_constraint:insert({'physics', s.id, 'X<Y'}) +s:format({{name='X', type='integer'}, {name='Y', type='integer'}}) +_ = box.space._ck_constraint:insert({'physics', s.id, 'X<Y'}) +box.execute("INSERT INTO \"test\" VALUES(2, 1);") +s:format({{name='Y', type='integer'}, {name='X', type='integer'}}) +box.execute("INSERT INTO \"test\" VALUES(1, 2);") +box.execute("INSERT INTO \"test\" VALUES(2, 1);") +s:truncate() +box.execute("INSERT INTO \"test\" VALUES(1, 2);") +s:format({}) +s:format() +s:format({{name='Y1', type='integer'}, {name='X1', type='integer'}}) +-- Ck constraint creation is forbidden for non-empty space +s:insert({2, 1}) +_ = box.space._ck_constraint:insert({'conflict', s.id, 'X>10'}) +s:truncate() +_ = box.space._ck_constraint:insert({'conflict', s.id, 'X>10'}) +box.execute("INSERT INTO \"test\" VALUES(1, 2);") +box.execute("INSERT INTO \"test\" VALUES(11, 11);") +box.execute("INSERT INTO \"test\" VALUES(12, 11);") +s:drop() -- -- gh-3611: Segfault on table creation with check referencing this table @@ -55,10 +104,4 @@ box.execute("DROP TABLE w2;") -- box.execute("CREATE TABLE t5(x INT PRIMARY KEY, y INT, CHECK( x*y < ? ));") -opts = {checks = {{expr = '?>5', name = 'ONE'}}} -format = {{name = 'X', type = 'unsigned'}} -t = {513, 1, 'test', 'memtx', 0, opts, format} -s = box.space._space:insert(t) - - test_run:cmd("clear filter") diff --git a/test/sql/errinj.result b/test/sql/errinj.result index ee36a387b..453b8485d 100644 --- a/test/sql/errinj.result +++ b/test/sql/errinj.result @@ -463,3 +463,137 @@ errinj.set("ERRINJ_SQL_NAME_NORMALIZATION", false) --- - ok ... +-- +-- Tests which are aimed at verifying work of commit/rollback +-- triggers on _ck_constraint space. +-- +s = box.schema.space.create('test', {format = {{name = 'X', type = 'unsigned'}}}) +--- +... +pk = box.space.test:create_index('pk') +--- +... +errinj.set("ERRINJ_WAL_IO", true) +--- +- ok +... +_ = box.space._ck_constraint:insert({'CK_CONSTRAINT_01', s.id, 'X<5'}) +--- +- error: Failed to write to disk +... +errinj.set("ERRINJ_WAL_IO", false) +--- +- ok +... +_ = box.space._ck_constraint:insert({'CK_CONSTRAINT_01', s.id, 'X<5'}) +--- +... +box.execute("INSERT INTO \"test\" VALUES(5);") +--- +- error: 'Failed to execute SQL statement: CHECK constraint failed: CK_CONSTRAINT_01' +... +errinj.set("ERRINJ_WAL_IO", true) +--- +- ok +... +_ = box.space._ck_constraint:replace({'CK_CONSTRAINT_01', s.id, 'X<=5'}) +--- +- error: Failed to write to disk +... +errinj.set("ERRINJ_WAL_IO", false) +--- +- ok +... +_ = box.space._ck_constraint:replace({'CK_CONSTRAINT_01', s.id, 'X<=5'}) +--- +... +box.execute("INSERT INTO \"test\" VALUES(5);") +--- +- row_count: 1 +... +errinj.set("ERRINJ_WAL_IO", true) +--- +- ok +... +_ = box.space._ck_constraint:delete({'CK_CONSTRAINT_01', s.id}) +--- +- error: Failed to write to disk +... +box.execute("INSERT INTO \"test\" VALUES(6);") +--- +- error: 'Failed to execute SQL statement: CHECK constraint failed: CK_CONSTRAINT_01' +... +errinj.set("ERRINJ_WAL_IO", false) +--- +- ok +... +_ = box.space._ck_constraint:delete({'CK_CONSTRAINT_01', s.id}) +--- +... +box.execute("INSERT INTO \"test\" VALUES(6);") +--- +- row_count: 1 +... +s:drop() +--- +... +-- +-- Test that failed space alter doesn't harm ck constraints +-- +s = box.schema.create_space('test') +--- +... +_ = s:create_index('pk') +--- +... +s:format({{name='X', type='integer'}, {name='Y', type='integer'}}) +--- +... +_ = box.space._ck_constraint:insert({'XlessY', s.id, 'X < Y'}) +--- +... +_ = box.space._ck_constraint:insert({'Xgreater10', s.id, 'X > 10'}) +--- +... +box.execute("INSERT INTO \"test\" VALUES(2, 1);") +--- +- error: 'Failed to execute SQL statement: CHECK constraint failed: Xgreater10' +... +box.execute("INSERT INTO \"test\" VALUES(20, 10);") +--- +- error: 'Failed to execute SQL statement: CHECK constraint failed: XlessY' +... +box.execute("INSERT INTO \"test\" VALUES(20, 100);") +--- +- row_count: 1 +... +s:truncate() +--- +... +errinj.set("ERRINJ_WAL_IO", true) +--- +- ok +... +s:format({{name='Y', type='integer'}, {name='X', type='integer'}}) +--- +- error: Failed to write to disk +... +errinj.set("ERRINJ_WAL_IO", false) +--- +- ok +... +box.execute("INSERT INTO \"test\" VALUES(2, 1);") +--- +- error: 'Failed to execute SQL statement: CHECK constraint failed: XlessY' +... +box.execute("INSERT INTO \"test\" VALUES(20, 10);") +--- +- error: 'Failed to execute SQL statement: CHECK constraint failed: XlessY' +... +box.execute("INSERT INTO \"test\" VALUES(20, 100);") +--- +- row_count: 1 +... +s:drop() +--- +... diff --git a/test/sql/errinj.test.lua b/test/sql/errinj.test.lua index 1aff6d77e..ebe8eabae 100644 --- a/test/sql/errinj.test.lua +++ b/test/sql/errinj.test.lua @@ -149,3 +149,48 @@ box.execute("CREATE TABLE hello (id INT primary key,x INT,y INT);") dummy_f = function(int) return 1 end box.internal.sql_create_function("counter1", "INT", dummy_f, -1, false) errinj.set("ERRINJ_SQL_NAME_NORMALIZATION", false) + +-- +-- Tests which are aimed at verifying work of commit/rollback +-- triggers on _ck_constraint space. +-- +s = box.schema.space.create('test', {format = {{name = 'X', type = 'unsigned'}}}) +pk = box.space.test:create_index('pk') + +errinj.set("ERRINJ_WAL_IO", true) +_ = box.space._ck_constraint:insert({'CK_CONSTRAINT_01', s.id, 'X<5'}) +errinj.set("ERRINJ_WAL_IO", false) +_ = box.space._ck_constraint:insert({'CK_CONSTRAINT_01', s.id, 'X<5'}) +box.execute("INSERT INTO \"test\" VALUES(5);") +errinj.set("ERRINJ_WAL_IO", true) +_ = box.space._ck_constraint:replace({'CK_CONSTRAINT_01', s.id, 'X<=5'}) +errinj.set("ERRINJ_WAL_IO", false) +_ = box.space._ck_constraint:replace({'CK_CONSTRAINT_01', s.id, 'X<=5'}) +box.execute("INSERT INTO \"test\" VALUES(5);") +errinj.set("ERRINJ_WAL_IO", true) +_ = box.space._ck_constraint:delete({'CK_CONSTRAINT_01', s.id}) +box.execute("INSERT INTO \"test\" VALUES(6);") +errinj.set("ERRINJ_WAL_IO", false) +_ = box.space._ck_constraint:delete({'CK_CONSTRAINT_01', s.id}) +box.execute("INSERT INTO \"test\" VALUES(6);") +s:drop() + +-- +-- Test that failed space alter doesn't harm ck constraints +-- +s = box.schema.create_space('test') +_ = s:create_index('pk') +s:format({{name='X', type='integer'}, {name='Y', type='integer'}}) +_ = box.space._ck_constraint:insert({'XlessY', s.id, 'X < Y'}) +_ = box.space._ck_constraint:insert({'Xgreater10', s.id, 'X > 10'}) +box.execute("INSERT INTO \"test\" VALUES(2, 1);") +box.execute("INSERT INTO \"test\" VALUES(20, 10);") +box.execute("INSERT INTO \"test\" VALUES(20, 100);") +s:truncate() +errinj.set("ERRINJ_WAL_IO", true) +s:format({{name='Y', type='integer'}, {name='X', type='integer'}}) +errinj.set("ERRINJ_WAL_IO", false) +box.execute("INSERT INTO \"test\" VALUES(2, 1);") +box.execute("INSERT INTO \"test\" VALUES(20, 10);") +box.execute("INSERT INTO \"test\" VALUES(20, 100);") +s:drop() diff --git a/test/sql/gh-2981-check-autoinc.result b/test/sql/gh-2981-check-autoinc.result index 9e347ca6b..7384c81e8 100644 --- a/test/sql/gh-2981-check-autoinc.result +++ b/test/sql/gh-2981-check-autoinc.result @@ -29,7 +29,7 @@ box.execute("insert into t1 values (18, null);") ... box.execute("insert into t1(s2) values (null);") --- -- error: 'Failed to execute SQL statement: CHECK constraint failed: T1' +- error: 'Failed to execute SQL statement: CHECK constraint failed: CK_CONSTRAINT_1_T1' ... box.execute("insert into t2 values (18, null);") --- @@ -37,7 +37,7 @@ box.execute("insert into t2 values (18, null);") ... box.execute("insert into t2(s2) values (null);") --- -- error: 'Failed to execute SQL statement: CHECK constraint failed: T2' +- error: 'Failed to execute SQL statement: CHECK constraint failed: CK_CONSTRAINT_1_T2' ... box.execute("insert into t2 values (24, null);") --- @@ -45,7 +45,7 @@ box.execute("insert into t2 values (24, null);") ... box.execute("insert into t2(s2) values (null);") --- -- error: 'Failed to execute SQL statement: CHECK constraint failed: T2' +- error: 'Failed to execute SQL statement: CHECK constraint failed: CK_CONSTRAINT_1_T2' ... box.execute("insert into t3 values (9, null)") --- @@ -53,7 +53,7 @@ box.execute("insert into t3 values (9, null)") ... box.execute("insert into t3(s2) values (null)") --- -- error: 'Failed to execute SQL statement: CHECK constraint failed: T3' +- error: 'Failed to execute SQL statement: CHECK constraint failed: CK_CONSTRAINT_1_T3' ... box.execute("DROP TABLE t1") --- diff --git a/test/sql/upgrade.result b/test/sql/upgrade.result index 3a55f7c53..5da110c14 100644 --- a/test/sql/upgrade.result +++ b/test/sql/upgrade.result @@ -188,6 +188,25 @@ i[1].opts.sql == nil --- - true ... +box.space._space:get(s.id).flags.checks == nil +--- +- true +... +check = box.space._ck_constraint:select()[1] +--- +... +check ~= nil +--- +- true +... +check.name +--- +- CK_CONSTRAINT_1_T5 +... +check.expr_str +--- +- x < 2 +... s:drop() --- ... diff --git a/test/sql/upgrade.test.lua b/test/sql/upgrade.test.lua index b76a8f373..cfda74a08 100644 --- a/test/sql/upgrade.test.lua +++ b/test/sql/upgrade.test.lua @@ -62,6 +62,11 @@ s ~= nil i = box.space._index:select(s.id) i ~= nil i[1].opts.sql == nil +box.space._space:get(s.id).flags.checks == nil +check = box.space._ck_constraint:select()[1] +check ~= nil +check.name +check.expr_str s:drop() test_run:switch('default') diff --git a/test/wal_off/alter.result b/test/wal_off/alter.result index becdf1312..ee9551952 100644 --- a/test/wal_off/alter.result +++ b/test/wal_off/alter.result @@ -28,7 +28,7 @@ end; ... #spaces; --- -- 65511 +- 65510 ... -- cleanup for k, v in pairs(spaces) do -- 2.21.0 ^ permalink raw reply [flat|nested] 25+ messages in thread
* [tarantool-patches] Re: [PATCH v3 1/3] schema: add new system space for CHECK constraints 2019-04-16 13:51 ` [tarantool-patches] [PATCH v3 1/3] schema: add new system space for CHECK constraints Kirill Shcherbatov @ 2019-04-25 20:38 ` n.pettik 2019-05-07 9:53 ` Kirill Shcherbatov 0 siblings, 1 reply; 25+ messages in thread From: n.pettik @ 2019-04-25 20:38 UTC (permalink / raw) To: tarantool-patches; +Cc: Kirill Shcherbatov > On 16 Apr 2019, at 16:51, Kirill Shcherbatov <kshcherbatov@tarantool.org> wrote: > > This patch introduces a new system space to persist check > constraints. Format of the space: > > _ck_constraint (space id = 357) > [<constraint name> STR, <space id> UINT, <expression string>STR] > > CK constraint is local to space, so every pair <CK name, space id> > is unique (and it is PK in _ck_constraint space). > > After insertion into this space, a new instance describing check > constraint is created. Check constraint holds Expr AST tree. Nit: AST includes ’tree’ word :) -> ‘holds AST of expression representing it’. > While space features check constraints, it isn't allowed to > be dropped. The :drop() space method firstly deletes all check > constraints and then removes entry from _space. > > Because space alter, index alter and space truncate operations > case -> cause, I guess. > space recreation, introduced RebuildCkConstrains object > that compile new ck constraint objects, replace and remove > existent instances atomically(when some compilation fails, > nothing changed). Still don’t understand necessity of re-creating check constraints in case of truncation or index alteration. Truncate can turn out to be quite expensive in terms of performance since we will have to rebuild and recompile all check, foreign key constraints and SQL triggers. > In fact, in scope of this patch we don't > really need to recreate ck_constraint object in such situations > (patch space_def pointer in AST like we did it before is enough > now, but we would recompile VDBE that represents ck constraint > in future that can fail). > The main motivation for these changes is the ability to support > ADD CHECK CONSTRAINT operation in the future. > Why can’t we implement this statement without separate space? > --- > > /* }}} */ > @@ -4035,6 +4098,161 @@ on_replace_dd_fk_constraint(struct trigger * /* trigger*/, void *event) > } > } > > +/** Create an instance of check constraint definition by tuple. */ > +static struct ck_constraint_def * > +ck_constraint_def_new_from_tuple(struct tuple *tuple) > +{ > + uint32_t name_len; > + const char *name = > + tuple_field_str_xc(tuple, BOX_CK_CONSTRAINT_FIELD_NAME, > + &name_len); > + if (name_len > BOX_NAME_MAX) { > + tnt_raise(ClientError, ER_CREATE_CK_CONSTRAINT, > + tt_cstr(name, BOX_INVALID_NAME_MAX), > + "check constraint name is too long"); > + } > + identifier_check_xc(name, name_len); > + uint32_t expr_str_len; > + const char *expr_str = > + tuple_field_str_xc(tuple, BOX_CK_CONSTRAINT_FIELD_EXPR_STR, > + &expr_str_len); > + > + uint32_t name_offset, expr_str_offset; > + size_t ck_def_sz = > + ck_constraint_def_sizeof(name_len, expr_str_len, &name_offset, > + &expr_str_offset); Nit: function returns uint_32. Please, make types be consistent. > diff --git a/src/box/ck_constraint.c b/src/box/ck_constraint.c > new file mode 100644 > index 000000000..daeebb78b > --- /dev/null > +++ b/src/box/ck_constraint.c > @@ -0,0 +1,115 @@ > > +#include "ck_constraint.h" > +#include "errcode.h" > +#include "small/rlist.h” This header is already included in ck_constraint.h > +#include "sql.h" > +#include "sql/sqlInt.h” It’s awful that we keep on including this header... > + > +/** > + * Resolve space_def references for check constraint via AST > + * tree traversal. > + * @param ck_constraint Check constraint object to update. > + * @param space_def Space definition to use. > + * @retval 0 on success. > + * @retval -1 on error. > + */ > +static int > +ck_constraint_resolve_space_def(struct Expr *expr, > + struct space_def *space_def) > +{ I would call this method like “ck_constraint_resolve_names” or “ck_constraint_resolve_field_names”. IMHO we can’t resolve space definition and it may sound confusing... > diff --git a/src/box/ck_constraint.h b/src/box/ck_constraint.h > new file mode 100644 > index 000000000..615612605 > --- /dev/null > +++ b/src/box/ck_constraint.h > @@ -0,0 +1,163 @@ > > +/** > + * Find check constraint object in space by given name and > + * name_len. > + * @param space The space to lookup check constraint. > + * @param name The check constraint name. > + * @param name_len The length of the name. > + * @retval not NULL Check constrain if exists, NULL otherwise. > + */ > +struct ck_constraint * This function is used only in alter.cc, mb it is worth moving it to alter.cc and make it static? > +space_ck_constraint_by_name(struct space *space, const char *name, > + uint32_t name_len); > + > > diff --git a/src/box/lua/schema.lua b/src/box/lua/schema.lua > index f31cf7f2c..e01f500e6 100644 > --- a/src/box/lua/schema.lua > +++ b/src/box/lua/schema.lua > > local function create_sysview(source_id, target_id) > @@ -735,6 +737,43 @@ local function upgrade_to_2_1_3() > id = id + 1 > end > end > + This upgrade should be part of 2.2.0, I guess. > + -- In previous Tarantool releases check constraints were > + -- stored in space opts. Now we use separate space > + -- _ck_constraint for this purpose. Perform legacy data > + -- migration. > diff --git a/src/box/schema_def.h b/src/box/schema_def.h > +/** _ck_constraint fields. */ > +enum { > + BOX_CK_CONSTRAINT_FIELD_NAME = 0, > + BOX_CK_CONSTRAINT_FIELD_SPACE_ID = 1, > + BOX_CK_CONSTRAINT_FIELD_EXPR_STR = 2, In addition to these properties mb it is worth adding <constraint check time> field: ANSI says that every constraint can be created with this option. I suggest to do it now to avoid changing system spaces in future. Note that _fk_constraints already has this attribute. > diff --git a/src/box/sql/build.c b/src/box/sql/build.c > index b1ed64a01..e55bbe43b 100644 > --- a/src/box/sql/build.c > +++ b/src/box/sql/build.c > @@ -47,6 +47,7 @@ > #include "vdbeInt.h" > #include "tarantoolInt.h" > #include "box/box.h" > +#include "box/ck_constraint.h" > #include "box/fk_constraint.h" > #include "box/sequence.h" > #include "box/session.h" > @@ -643,37 +644,74 @@ primary_key_exit: > void > sql_add_check_constraint(struct Parse *parser) Please, make name of this function consistent with sql_create_foreign_key(). i.e. rename _add_check_...() to sql_create_check_contraint() or vice versa. > { > - struct create_ck_def *ck_def = &parser->create_ck_def; > + struct create_ck_def *create_ck_def = &parser->create_ck_def; > + struct ExprSpan *expr_span = create_ck_def->expr; > + sql_expr_delete(parser->db, expr_span->pExpr, false); Looks inefficient: we delete expr right after its creation.. Could we come up with workaround to avoid this? I mean cut string containing expression without allocating/releasing memory for it and without filling in expr tree? > + uint32_t name_len = strlen(name); Nit: strlen() return size_t type. > + > + uint32_t expr_str_len = (uint32_t)(create_ck_def->expr->zEnd - > + create_ck_def->expr->zStart); > + const char *expr_str = create_ck_def->expr->zStart; > + > + /* > + * Allocate memory for ck constraint parse structure and > + * ck constraint definition as a single memory chunk on > + * region: > + * > + * [ck_parse][ck_def[name][expr_str]] > + * |_____^ |___^ ^ > + * |_________| > + */ > + uint32_t name_offset, expr_str_offset; > + size_t ck_def_sz = > + ck_constraint_def_sizeof(name_len, expr_str_len, &name_offset, > + &expr_str_offset); Nit: ck_constraint_def_sizeof() return uint32_t Also why do we not use vdbe_emit_halt_with_presence_test() during creation of check constraints? tarantool> create table t2(id int primary key, constraint ck1 check(id > 0), constraint ck1 check(id < 0)) --- - error: Duplicate key exists in unique index 'primary' in space '_ck_constraint' ... tarantool> create table t2(id int primary key, constraint fk1 foreign key(id) references t2, constraint fk1 foreign key(id) references t2) --- - error: Constraint FK1 already exists ... > diff --git a/src/box/sql/insert.c b/src/box/sql/insert.c > index c2aac553f..a075d4dc3 100644 > --- a/src/box/sql/insert.c > +++ b/src/box/sql/insert.c > > @@ -1245,14 +1239,26 @@ xferOptimization(Parse * pParse, /* Parser context */ > if (pSrcIdx == NULL) > return 0; > } > - /* Get server checks. */ > - ExprList *pCheck_src = src->def->opts.checks; > - ExprList *pCheck_dest = dest->def->opts.checks; > - if (pCheck_dest != NULL && > - sqlExprListCompare(pCheck_src, pCheck_dest, -1) != 0) { > - /* Tables have different CHECK constraints. Ticket #2252 */ > + /* > + * Dissallow the transfer optimization if the check > + * constraints are differ. > + */ > + if (rlist_empty(&dest->ck_constraint) != > + rlist_empty(&src->ck_constraint)) > return 0; > + struct rlist *dst_ck_link = &dest->ck_constraint; > + struct ck_constraint *src_ck; > + rlist_foreach_entry(src_ck, &src->ck_constraint, link) { > + dst_ck_link = rlist_next(dst_ck_link); > + if (dst_ck_link == &dest->ck_constraint) > + return 0; > + struct ck_constraint *dest_ck = > + rlist_entry(dst_ck_link, struct ck_constraint, link); > + if (sqlExprCompare(src_ck->expr, dest_ck->expr, -1) != 0) > + return 0; > } > + if (rlist_next(dst_ck_link) != &dest->ck_constraint) > + return 0; After next patch this check could be removed: VDBE program for checking consistency of check constraints would be generated automatically. In SQLite xFer allows to avoid generation of this program. On the other hand, if you added way to temporarily disable check constraints, we would able to leave this check and re-enable them after query is executed. So, consider way of disabling/enabling check constraints - it might be useful for other cases as well. > diff --git a/src/box/sql/parse_def.h b/src/box/sql/parse_def.h > index a1af2bacd..ec434420d 100644 > --- a/src/box/sql/parse_def.h > +++ b/src/box/sql/parse_def.h > @@ -123,6 +123,20 @@ struct fk_constraint_parse { > struct rlist link; > }; > > +/** > + * Structure representing check constraint appeared within > + * CREATE TABLE statement. Used only during parsing. > + */ > +struct ck_constraint_parse { > + /** > + * Check constraint declared in <CREATE TABLE ...> > + * statement. Must be coded after space creation. Nit: Definition of check constraints…Mention that they don’t need any cleanups, since they are allocated using region. > diff --git a/src/box/sql/select.c b/src/box/sql/select.c > index b1ec8c758..531e29ed5 100644 > --- a/src/box/sql/select.c > +++ b/src/box/sql/select.c > @@ -6445,7 +6445,12 @@ sql_expr_extract_select(struct Parse *parser, struct Select *select) > struct ExprList *expr_list = select->pEList; > assert(expr_list->nExpr == 1); > parser->parsed_ast_type = AST_TYPE_EXPR; > - parser->parsed_ast.expr = sqlExprDup(parser->db, > - expr_list->a->pExpr, > - EXPRDUP_REDUCE); > + /* > + * Extract a copy of parsed expression. > + * We cannot use EXPRDUP_REDUCE flag in sqlExprDup call > + * because some compiled Expr (like Checks expressions) > + * may require further resolve with sqlResolveExprNames. Agh, still can’t point out what does this flag mean. Could you please briefly explain? > + */ > + parser->parsed_ast.expr = > + sqlExprDup(parser->db, expr_list->a->pExpr, 0); > } > > /** > diff --git a/test/app-tap/tarantoolctl.test.lua b/test/app-tap/tarantoolctl.test.lua > index 62a78d6bf..9ac0b5c69 100755 > --- a/test/app-tap/tarantoolctl.test.lua > +++ b/test/app-tap/tarantoolctl.test.lua > @@ -352,7 +352,7 @@ do > > local filler_code = [[ > box.cfg{memtx_memory = 104857600, background=false} > - local space = box.schema.create_space("test") > + space = box.schema.create_space("test”) Why this diff is needed? > diff --git a/test/sql-tap/check.test.lua b/test/sql-tap/check.test.lua > index b01afca7c..0dda3fac8 100755 > --- a/test/sql-tap/check.test.lua > +++ b/test/sql-tap/check.test.lua > @@ -319,7 +319,7 @@ test:do_catchsql_test( > ); > ]], { > -- <check-3.1> > - 1, "Failed to create space 'T3': Subqueries are prohibited in a CHECK constraint definition" > + 1, "Failed to create check constraint 'CK_CONSTRAINT_1_T3': Subqueries are prohibited in a check constraint definition” In some error messages "check” word is uppercased, in the rest - isn’t. Please, make this rule be consistent. > diff --git a/test/sql/checks.test.lua b/test/sql/checks.test.lua > index 5bfcf12f8..0cdfde2e9 100644 > --- a/test/sql/checks.test.lua > +++ b/test/sql/checks.test.lua > @@ -8,41 +8,90 @@ box.execute('pragma sql_default_engine=\''..engine..'\'') > -- gh-3272: Move SQL CHECK into server > -- > -opts = {checks = {{expr = 'X>5', name = 'ONE'}}} > -format = {{name = 'X', type = 'unsigned'}} > -t = {513, 1, 'test', 'memtx', 0, opts, format} > -s = box.space._space:insert(t) > -box.space._space:delete(513) > +-- Invalid expression test. > +box.space._ck_constraint:insert({'CK_CONSTRAINT_01', 513, 'X><5'}) > +-- Unexistent space test. > +box.space._ck_constraint:insert({'CK_CONSTRAINT_01', 550, 'X<5'}) > +-- Pass integer instead of expression. > +box.space._ck_constraint:insert({'CK_CONSTRAINT_01', 550, 666}) Please, pass valid space id in this particular test (to void confusion). > +-- Test xferOptimization for space that have CK constraints. > +box.execute("CREATE TABLE first (id FLOAT PRIMARY KEY CHECK(id < 5), a INT CONSTRAINT ONE CHECK(a < 5));") > +box.execute("CREATE TABLE second (id FLOAT PRIMARY KEY CHECK(id < 5), a INT CONSTRAINT ONE CHECK(a < 5));") > +box.execute("CREATE TABLE third (id FLOAT PRIMARY KEY, a INT CONSTRAINT ONE CHECK(a < 5));") > +box.execute("INSERT INTO first VALUES(1, 1);") > +box.execute("INSERT INTO first VALUES(2, 2);") > +box.execute("INSERT INTO first VALUES(3, 3);") > +box.execute("INSERT OR IGNORE INTO second SELECT * FROM first;") > +box.execute("INSERT OR IGNORE INTO third SELECT * FROM first;") > +box.execute("DELETE FROM second;") > +box.execute("INSERT OR IGNORE INTO second SELECT * FROM third;”) How does this check that xFer was really fired? > +-- Test space creation rollback on spell error in ck constraint. > +box.execute("CREATE TABLE first (id FLOAT PRIMARY KEY CHECK(id < 5), a INT CONSTRAINT ONE CHECK(a >< 5));") > +box.space.FIRST == nil > +box.space._ck_constraint:count() == 0 > +box.space.FIRST:drop() Isn’t this redundant check? I mean box.space.FIRST == nil already checks that space hasn’t been created. > +-- Ck constraints are disallowed for spaces having no format. > +s = box.schema.create_space('test') > +_ = s:create_index('pk') > +_ = box.space._ck_constraint:insert({'physics', s.id, 'X<Y'}) > +s:format({{name='X', type='integer'}, {name='Y', type='integer'}}) > +_ = box.space._ck_constraint:insert({'physics', s.id, 'X<Y'}) > +box.execute("INSERT INTO \"test\" VALUES(2, 1);") > +s:format({{name='Y', type='integer'}, {name='X', type='integer'}}) > +box.execute("INSERT INTO \"test\" VALUES(1, 2);") > +box.execute("INSERT INTO \"test\" VALUES(2, 1);") > +s:truncate() > +box.execute("INSERT INTO \"test\" VALUES(1, 2);") > +s:format({}) Good test scenario. Also, it would be nice to see a few complex expression. For instance, involving CAST, built-in functions and other allowed in check exprs constructions. > diff --git a/test/sql/upgrade.test.lua b/test/sql/upgrade.test.lua > index b76a8f373..cfda74a08 100644 > --- a/test/sql/upgrade.test.lua > +++ b/test/sql/upgrade.test.lua > @@ -62,6 +62,11 @@ s ~= nil > i = box.space._index:select(s.id) > i ~= nil > i[1].opts.sql == nil > +box.space._space:get(s.id).flags.checks == nil > +check = box.space._ck_constraint:select()[1] > +check ~= nil > +check.name > +check.expr_str Am I right that last time snapshot to be upgraded was changed, it was filled with space featuring check constraint? I see that you didn’t change snapshot, but space has check constraint after upgrade. ^ permalink raw reply [flat|nested] 25+ messages in thread
* [tarantool-patches] Re: [PATCH v3 1/3] schema: add new system space for CHECK constraints 2019-04-25 20:38 ` [tarantool-patches] " n.pettik @ 2019-05-07 9:53 ` Kirill Shcherbatov 2019-05-12 13:45 ` n.pettik 0 siblings, 1 reply; 25+ messages in thread From: Kirill Shcherbatov @ 2019-05-07 9:53 UTC (permalink / raw) To: tarantool-patches, n.pettik > Nit: AST includes ’tree’ word :) -> ‘holds AST of expression representing it’. Fixed. >> In fact, in scope of this patch we don't >> really need to recreate ck_constraint object in such situations >> (patch space_def pointer in AST like we did it before is enough >> now, but we would recompile VDBE that represents ck constraint >> in future that can fail). >> The main motivation for these changes is the ability to support >> ADD CHECK CONSTRAINT operation in the future. >> > > Why can’t we implement this statement without separate space? """ The main motivation for these changes is the ability to support ADD CHECK CONSTRAINT operation in the future. CK constraints are are easier to manage as self-sustained objects: we mustn't the tuple describing target space to do it (unlike the current architecture). """ >> + size_t ck_def_sz = >> + ck_constraint_def_sizeof(name_len, expr_str_len, &name_offset, >> + &expr_str_offset); > > Nit: function returns uint_32. Please, make types be consistent. Fixed. >> +#include "small/rlist.h” > > This header is already included in ck_constraint.h Dropped. >> +static int >> +ck_constraint_resolve_space_def(struct Expr *expr, >> + struct space_def *space_def) >> +{ > > I would call this method like “ck_constraint_resolve_names” or > “ck_constraint_resolve_field_names”. IMHO we can’t resolve > space definition and it may sound confusing... Renamed to ck_constraint_resolve_field_names. >> +struct ck_constraint * > > This function is used only in alter.cc, mb it is worth moving it to alter.cc > and make it static? > >> +space_ck_constraint_by_name(struct space *space, const char *name, >> + uint32_t name_len); >> + I need this function in the last commit also now. > This upgrade should be part of 2.2.0, I guess. Ok. > In addition to these properties mb it is worth adding > <constraint check time> field: ANSI says that every > constraint can be created with this option. I suggest to do > it now to avoid changing system spaces in future. Note that > _fk_constraints already has this attribute. Done. >> void >> sql_add_check_constraint(struct Parse *parser) > > Please, make name of this function consistent with > sql_create_foreign_key(). i.e. rename _add_check_...() > to sql_create_check_contraint() or vice versa. Done. sql_create_check_contraint. >> + sql_expr_delete(parser->db, expr_span->pExpr, false); > > Looks inefficient: we delete expr right after its creation.. > Could we come up with workaround to avoid this? I mean > cut string containing expression without allocating/releasing > memory for it and without filling in expr tree? I don't know, how can we prevent it. Discussed verbally. > Nit: strlen() return size_t type. Done. > Nit: ck_constraint_def_sizeof() return uint32_t > > Also why do we not use vdbe_emit_halt_with_presence_test() > during creation of check constraints? > > tarantool> create table t2(id int primary key, constraint ck1 check(id > 0), constraint ck1 check(id < 0)) > --- > - error: Duplicate key exists in unique index 'primary' in space '_ck_constraint' > ... > tarantool> create table t2(id int primary key, constraint fk1 foreign key(id) references t2, constraint fk1 foreign key(id) references t2) > --- > - error: Constraint FK1 already exists > ... > And this is a bug in FK https://github.com/tarantool/tarantool/issues/4183 >> + if (rlist_next(dst_ck_link) != &dest->ck_constraint) >> + return 0; > > After next patch this check could be removed: VDBE program for checking > consistency of check constraints would be generated automatically. > In SQLite xFer allows to avoid generation of this program. On the other hand, > if you added way to temporarily disable check constraints, we would able > to leave this check and re-enable them after query is executed. So, consider > way of disabling/enabling check constraints - it might be useful for other > cases as well. At first, there is no way to control this state as you propose. Next, disabling/enabling triggers is required for upgrade() functionality and is not a part of public API. As in further patches ck constraint are rely on trigger machinery, we don't need a separate controller. So, I've reject xfer optimization when source or destination space has ck constraints. >> +struct ck_constraint_parse { >> + /** >> + * Check constraint declared in <CREATE TABLE ...> >> + * statement. Must be coded after space creation. > > Nit: Definition of check constraints…Mention that they > don’t need any cleanups, since they are allocated using region. Done. >> + /* >> + * Extract a copy of parsed expression. >> + * We cannot use EXPRDUP_REDUCE flag in sqlExprDup call >> + * because some compiled Expr (like Checks expressions) >> + * may require further resolve with sqlResolveExprNames. > > Agh, still can’t point out what does this flag mean. Could you please briefly explain? Look at the EXPRDUP_REDUCE comment in sqlExprDup: * If the EXPRDUP_REDUCE flag is set, then the structure returned is a * truncated version of the usual Expr structure that will be stored as * part of the in-memory representation of the database schema. Such version doesn't contain space_def pointer by definition and we want to prevent it. >> - local space = box.schema.create_space("test") >> + space = box.schema.create_space("test”) > > Why this diff is needed? It is really redundant. >> - 1, "Failed to create space 'T3': Subqueries are prohibited in a CHECK constraint definition" >> + 1, "Failed to create check constraint 'CK_CONSTRAINT_1_T3': Subqueries are prohibited in a check constraint definition” > > In some error messages "check” word is uppercased, in the rest - isn’t. > Please, make this rule be consistent. In previous review you asked, why do I inconsistently uppercase check keyword in OP_Halt; But now I believe that it is not required: all this errors are changed in the next patch. >> +box.space._ck_constraint:insert({'CK_CONSTRAINT_01', 550, 666}) > > Please, pass valid space id in this particular test (to void confusion). Done. Tnx. > How does this check that xFer was really fired? No we don't have xfer optimization. > +box.space._ck_constraint:count() == 0 >> +box.space.FIRST:drop() > > Isn’t this redundant check? I mean box.space.FIRST == nil already> checks that space hasn’t been created. Dropped. > Good test scenario. > > Also, it would be nice to see a few complex expression. For instance, > involving CAST, built-in functions and other allowed in check exprs > constructions. I've appended a few testcases inspired by sql-tap tests. But, you know, this coverage already exists. >> +check.expr_str > > Am I right that last time snapshot to be upgraded was changed, it > was filled with space featuring check constraint? I see that you didn’t > change snapshot, but space has check constraint after upgrade. Yes, you are. Fix for 2.1.0 migration contained everything what required. =============================================== This patch introduces a new system space to persist check constraints. Format of the space: _ck_constraint (space id = 357) [<constraint name> STR, <space id> UINT, <expression string>STR] CK constraint is local to space, so every pair <CK name, space id> is unique (and it is PK in _ck_constraint space). After insertion into this space, a new instance describing check constraint is created. Check constraint holds AST of expression representing it. While space features check constraints, it isn't allowed to be dropped. The :drop() space method firstly deletes all check constraints and then removes entry from _space. Because space alter, index alter and space truncate operations cause space recreation, introduced RebuildCkConstrains object that compile new ck constraint objects, replace and remove existent instances atomically(when some compilation fails, nothing changed). In fact, in scope of this patch we don't really need to recreate ck_constraint object in such situations (patch space_def pointer in AST like we did it before is enough now, but we would recompile VDBE that represents ck constraint in future that can fail). The main motivation for these changes is the ability to support ADD CHECK CONSTRAINT operation in the future. CK constraints are are easier to manage as self-sustained objects: we mustn't the tuple describing target space to do it(unlike the current architecture). Disabled xfer optimization when some space have ck constraints because in the following patches this xfer optimisation becomes impossible. No reason to rewrite this code. Needed for #3691 --- src/box/CMakeLists.txt | 1 + src/box/alter.cc | 251 ++++++++++++++++++++++++-- src/box/alter.h | 1 + src/box/bootstrap.snap | Bin 4374 -> 4418 bytes src/box/ck_constraint.c | 114 ++++++++++++ src/box/ck_constraint.h | 163 +++++++++++++++++ src/box/errcode.h | 4 +- src/box/lua/schema.lua | 4 + src/box/lua/space.cc | 2 + src/box/lua/upgrade.lua | 43 +++++ src/box/schema.cc | 8 + src/box/schema_def.h | 10 + src/box/space.c | 2 + src/box/space.h | 5 + src/box/space_def.c | 98 +--------- src/box/space_def.h | 2 - src/box/sql.c | 86 +-------- src/box/sql.h | 36 ---- src/box/sql/build.c | 208 +++++++++++++++++---- src/box/sql/insert.c | 53 +++--- src/box/sql/parse.y | 2 +- src/box/sql/parse_def.h | 24 +++ src/box/sql/select.c | 11 +- src/box/sql/sqlInt.h | 2 +- src/box/sql/tokenize.c | 1 - test/app-tap/tarantoolctl.test.lua | 4 +- test/box-py/bootstrap.result | 7 +- test/box/access.result | 3 + test/box/access.test.lua | 1 + test/box/access_misc.result | 3 + test/box/access_sysview.result | 6 +- test/box/alter.result | 6 +- test/box/misc.result | 1 + test/sql-tap/check.test.lua | 32 ++-- test/sql-tap/fkey2.test.lua | 4 +- test/sql-tap/sql-errors.test.lua | 2 +- test/sql-tap/table.test.lua | 4 +- test/sql/checks.result | 210 ++++++++++++++++----- test/sql/checks.test.lua | 96 ++++++---- test/sql/errinj.result | 134 ++++++++++++++ test/sql/errinj.test.lua | 45 +++++ test/sql/gh-2981-check-autoinc.result | 8 +- test/sql/types.result | 2 +- test/sql/upgrade.result | 19 ++ test/sql/upgrade.test.lua | 5 + test/wal_off/alter.result | 2 +- 46 files changed, 1305 insertions(+), 420 deletions(-) create mode 100644 src/box/ck_constraint.c create mode 100644 src/box/ck_constraint.h diff --git a/src/box/CMakeLists.txt b/src/box/CMakeLists.txt index 2be0d1e35..4736a40b4 100644 --- a/src/box/CMakeLists.txt +++ b/src/box/CMakeLists.txt @@ -96,6 +96,7 @@ add_library(box STATIC space.c space_def.c sequence.c + ck_constraint.c fk_constraint.c func.c func_def.c diff --git a/src/box/alter.cc b/src/box/alter.cc index 9279426d2..2126ab369 100644 --- a/src/box/alter.cc +++ b/src/box/alter.cc @@ -29,6 +29,7 @@ * SUCH DAMAGE. */ #include "alter.h" +#include "ck_constraint.h" #include "column_mask.h" #include "schema.h" #include "user.h" @@ -529,17 +530,6 @@ space_def_new_from_tuple(struct tuple *tuple, uint32_t errcode, engine_name, engine_name_len, &opts, fields, field_count); auto def_guard = make_scoped_guard([=] { space_def_delete(def); }); - if (def->opts.checks != NULL && - sql_checks_resolve_space_def_reference(def->opts.checks, - def) != 0) { - box_error_t *err = box_error_last(); - if (box_error_code(err) != ENOMEM) { - tnt_raise(ClientError, errcode, def->name, - box_error_message(err)); - } else { - diag_raise(); - } - } struct engine *engine = engine_find_xc(def->engine_name); engine_check_space_def_xc(engine, def); def_guard.is_active = false; @@ -1380,6 +1370,71 @@ UpdateSchemaVersion::alter(struct alter_space *alter) ++schema_version; } +/** + * As ck_constraint object depends on space_def we must rebuild + * all ck constraints on space alter. + * + * To perform it transactionally, we create a list of a new ck + * constraints objects in ::prepare method that is fault-tolerant. + * Finally in ::alter or ::rollback methods we only swap thouse + * lists securely. + */ +class RebuildCkConstraints: public AlterSpaceOp +{ +public: + RebuildCkConstraints(struct alter_space *alter) : AlterSpaceOp(alter), + ck_constraint(RLIST_HEAD_INITIALIZER(ck_constraint)) {} + struct rlist ck_constraint; + virtual void prepare(struct alter_space *alter); + virtual void alter(struct alter_space *alter); + virtual void rollback(struct alter_space *alter); + virtual ~RebuildCkConstraints(); +}; + +void +RebuildCkConstraints::prepare(struct alter_space *alter) +{ + struct ck_constraint *old_ck_constraint; + rlist_foreach_entry(old_ck_constraint, &alter->old_space->ck_constraint, + link) { + struct ck_constraint *new_ck_constraint = + ck_constraint_new(old_ck_constraint->def, + alter->new_space->def); + if (new_ck_constraint == NULL) + diag_raise(); + rlist_add_entry(&ck_constraint, new_ck_constraint, link); + } +} + +void +RebuildCkConstraints::alter(struct alter_space *alter) +{ + rlist_swap(&alter->new_space->ck_constraint, &ck_constraint); + rlist_swap(&ck_constraint, &alter->old_space->ck_constraint); +} + +void +RebuildCkConstraints::rollback(struct alter_space *alter) +{ + rlist_swap(&alter->old_space->ck_constraint, &ck_constraint); + rlist_swap(&ck_constraint, &alter->new_space->ck_constraint); +} + +RebuildCkConstraints::~RebuildCkConstraints() +{ + struct ck_constraint *old_ck_constraint, *tmp; + rlist_foreach_entry_safe(old_ck_constraint, &ck_constraint, link, tmp) { + /** + * Ck constraint definition is now managed by + * other Ck constraint object. Prevent it's + * destruction as a part of ck_constraint_delete + * call. + */ + old_ck_constraint->def = NULL; + ck_constraint_delete(old_ck_constraint); + } +} + /* }}} */ /** @@ -1745,6 +1800,11 @@ on_replace_dd_space(struct trigger * /* trigger */, void *event) space_name(old_space), "the space has foreign key constraints"); } + if (!rlist_empty(&old_space->ck_constraint)) { + tnt_raise(ClientError, ER_DROP_SPACE, + space_name(old_space), + "the space has check constraints"); + } /** * The space must be deleted from the space * cache right away to achieve linearisable @@ -1842,6 +1902,7 @@ on_replace_dd_space(struct trigger * /* trigger */, void *event) def->field_count); (void) new CheckSpaceFormat(alter); (void) new ModifySpace(alter, def); + (void) new RebuildCkConstraints(alter); def_guard.is_active = false; /* Create MoveIndex ops for all space indexes. */ alter_space_move_indexes(alter, 0, old_space->index_id_max + 1); @@ -2085,6 +2146,7 @@ on_replace_dd_index(struct trigger * /* trigger */, void *event) */ alter_space_move_indexes(alter, iid + 1, old_space->index_id_max + 1); /* Add an op to update schema_version on commit. */ + (void) new RebuildCkConstraints(alter); (void) new UpdateSchemaVersion(alter); alter_space_do(txn, alter); scoped_guard.is_active = false; @@ -2152,6 +2214,7 @@ on_replace_dd_truncate(struct trigger * /* trigger */, void *event) (void) new TruncateIndex(alter, old_index->def->iid); } + (void) new RebuildCkConstraints(alter); alter_space_do(txn, alter); scoped_guard.is_active = false; } @@ -4035,6 +4098,168 @@ on_replace_dd_fk_constraint(struct trigger * /* trigger*/, void *event) } } +/** Create an instance of check constraint definition by tuple. */ +static struct ck_constraint_def * +ck_constraint_def_new_from_tuple(struct tuple *tuple) +{ + uint32_t name_len; + const char *name = + tuple_field_str_xc(tuple, BOX_CK_CONSTRAINT_FIELD_NAME, + &name_len); + if (name_len > BOX_NAME_MAX) { + tnt_raise(ClientError, ER_CREATE_CK_CONSTRAINT, + tt_cstr(name, BOX_INVALID_NAME_MAX), + "check constraint name is too long"); + } + identifier_check_xc(name, name_len); + uint32_t expr_str_len; + const char *expr_str = + tuple_field_str_xc(tuple, BOX_CK_CONSTRAINT_FIELD_EXPR_STR, + &expr_str_len); + + uint32_t name_offset, expr_str_offset; + uint32_t ck_def_sz = + ck_constraint_def_sizeof(name_len, expr_str_len, &name_offset, + &expr_str_offset); + struct ck_constraint_def *ck_def = + (struct ck_constraint_def *)malloc(ck_def_sz); + if (ck_def == NULL) + tnt_raise(OutOfMemory, ck_def_sz, "malloc", "ck_def"); + + ck_def->name = (char *)ck_def + name_offset; + ck_def->expr_str = (char *)ck_def + expr_str_offset; + memcpy(ck_def->expr_str, expr_str, expr_str_len); + ck_def->expr_str[expr_str_len] = '\0'; + memcpy(ck_def->name, name, name_len); + ck_def->name[name_len] = '\0'; + + return ck_def; +} + +/** Trigger invoked on rollback in the _ck_constraint space. */ +static void +on_replace_ck_constraint_rollback(struct trigger *trigger, void *event) +{ + struct txn_stmt *stmt = txn_last_stmt((struct txn *) event); + struct ck_constraint *ck = (struct ck_constraint *)trigger->data; + struct space *space = NULL; + if (ck != NULL) + space = space_by_id(ck->space_id); + if (stmt->old_tuple != NULL && stmt->new_tuple == NULL) { + /* Rollback DELETE check constraint. */ + assert(ck != NULL); + assert(space != NULL); + assert(space_ck_constraint_by_name(space, + ck->def->name, strlen(ck->def->name)) == NULL); + rlist_add_entry(&space->ck_constraint, ck, link); + } else if (stmt->new_tuple != NULL && stmt->old_tuple == NULL) { + /* Rollback INSERT check constraint. */ + assert(space != NULL); + assert(space_ck_constraint_by_name(space, + ck->def->name, strlen(ck->def->name)) != NULL); + rlist_del_entry(ck, link); + ck_constraint_delete(ck); + } else { + /* Rollback REPLACE check constraint. */ + assert(space != NULL); + const char *name = ck->def->name; + struct ck_constraint *new_ck = + space_ck_constraint_by_name(space, name, strlen(name)); + assert(new_ck != NULL); + rlist_del_entry(new_ck, link); + rlist_add_entry(&space->ck_constraint, ck, link); + ck_constraint_delete(new_ck); + } +} + +/** + * Trigger invoked on commit in the _ck_constraint space. + * Drop useless old check constraint object if exists. + */ +static void +on_replace_ck_constraint_commit(struct trigger *trigger, void *event) +{ + struct txn_stmt *stmt = txn_last_stmt((struct txn *) event); + struct ck_constraint *ck = (struct ck_constraint *)trigger->data; + if (stmt->old_tuple != NULL) + ck_constraint_delete(ck); +} + +/** A trigger invoked on replace in the _ck_constraint space. */ +static void +on_replace_dd_ck_constraint(struct trigger * /* trigger*/, void *event) +{ + struct txn *txn = (struct txn *) event; + txn_check_singlestatement_xc(txn, "Space _ck_constraint"); + struct txn_stmt *stmt = txn_current_stmt(txn); + struct tuple *old_tuple = stmt->old_tuple; + struct tuple *new_tuple = stmt->new_tuple; + uint32_t space_id = + tuple_field_u32_xc(old_tuple != NULL ? old_tuple : new_tuple, + BOX_CK_CONSTRAINT_FIELD_SPACE_ID); + struct space *space = space_cache_find_xc(space_id); + struct trigger *on_rollback = + txn_alter_trigger_new(on_replace_ck_constraint_rollback, NULL); + struct trigger *on_commit = + txn_alter_trigger_new(on_replace_ck_constraint_commit, NULL); + + if (new_tuple != NULL) { + bool is_deferred = + tuple_field_bool_xc(new_tuple, + BOX_CK_CONSTRAINT_FIELD_DEFERRED); + if (is_deferred) { + tnt_raise(ClientError, ER_UNSUPPORTED, "Tarantool", + "deferred ck constraints"); + } + /* Create or replace check constraint. */ + struct ck_constraint_def *ck_def = + ck_constraint_def_new_from_tuple(new_tuple); + auto ck_guard = make_scoped_guard([=] { free(ck_def); }); + /* + * FIXME: Ck constraint creation on non-empty + * space must be implemented as preparatory + * step for ALTER SPACE ADD CONSTRAINT feature. + */ + struct index *pk = space_index(space, 0); + if (pk != NULL && index_size(pk) > 0) { + tnt_raise(ClientError, ER_CREATE_CK_CONSTRAINT, + ck_def->name, + "referencing space must be empty"); + } + struct ck_constraint *new_ck_constraint = + ck_constraint_new(ck_def, space->def); + if (new_ck_constraint == NULL) + diag_raise(); + ck_guard.is_active = false; + const char *name = new_ck_constraint->def->name; + struct ck_constraint *old_ck_constraint = + space_ck_constraint_by_name(space, name, strlen(name)); + if (old_ck_constraint != NULL) + rlist_del_entry(old_ck_constraint, link); + rlist_add_entry(&space->ck_constraint, new_ck_constraint, link); + on_commit->data = old_tuple == NULL ? new_ck_constraint : + old_ck_constraint; + on_rollback->data = on_commit->data; + } else { + assert(new_tuple == NULL && old_tuple != NULL); + /* Drop check constraint. */ + uint32_t name_len; + const char *name = + tuple_field_str_xc(old_tuple, + BOX_CK_CONSTRAINT_FIELD_NAME, + &name_len); + struct ck_constraint *old_ck_constraint = + space_ck_constraint_by_name(space, name, name_len); + assert(old_ck_constraint != NULL); + rlist_del_entry(old_ck_constraint, link); + on_commit->data = old_ck_constraint; + on_rollback->data = old_ck_constraint; + } + + txn_on_rollback(txn, on_rollback); + txn_on_commit(txn, on_commit); +} + struct trigger alter_space_on_replace_space = { RLIST_LINK_INITIALIZER, on_replace_dd_space, NULL, NULL }; @@ -4103,4 +4328,8 @@ struct trigger on_replace_fk_constraint = { RLIST_LINK_INITIALIZER, on_replace_dd_fk_constraint, NULL, NULL }; +struct trigger on_replace_ck_constraint = { + RLIST_LINK_INITIALIZER, on_replace_dd_ck_constraint, NULL, NULL +}; + /* vim: set foldmethod=marker */ diff --git a/src/box/alter.h b/src/box/alter.h index 4108fa47c..b9ba7b846 100644 --- a/src/box/alter.h +++ b/src/box/alter.h @@ -46,6 +46,7 @@ extern struct trigger on_replace_sequence_data; extern struct trigger on_replace_space_sequence; extern struct trigger on_replace_trigger; extern struct trigger on_replace_fk_constraint; +extern struct trigger on_replace_ck_constraint; extern struct trigger on_stmt_begin_space; extern struct trigger on_stmt_begin_index; extern struct trigger on_stmt_begin_truncate; diff --git a/src/box/bootstrap.snap b/src/box/bootstrap.snap index 871a93f9856f97636ad55b7f5260e5c6957fef36..d988e27a189a1ec691d652f06e96d5818677d268 100644 GIT binary patch literal 4418 diff --git a/src/box/ck_constraint.c b/src/box/ck_constraint.c new file mode 100644 index 000000000..6d9f8ea44 --- /dev/null +++ b/src/box/ck_constraint.c @@ -0,0 +1,114 @@ +/* + * Copyright 2010-2019, Tarantool AUTHORS, please see AUTHORS file. + * + * Redistribution and use in source and binary forms, with or + * without modification, are permitted provided that the following + * conditions are met: + * + * 1. Redistributions of source code must retain the above + * copyright notice, this list of conditions and the + * following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following + * disclaimer in the documentation and/or other materials + * provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY <COPYRIGHT HOLDER> ``AS IS'' AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED + * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL + * <COPYRIGHT HOLDER> OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, + * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR + * BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF + * THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF + * SUCH DAMAGE. + */ +#include "ck_constraint.h" +#include "errcode.h" +#include "sql.h" +#include "sql/sqlInt.h" + +/** + * Resolve space_def references for check constraint via AST + * tree traversal. + * @param ck_constraint Check constraint object to update. + * @param space_def Space definition to use. + * @retval 0 on success. + * @retval -1 on error. + */ +static int +ck_constraint_resolve_field_names(struct Expr *expr, + struct space_def *space_def) +{ + struct Parse parser; + sql_parser_create(&parser, sql_get()); + parser.parse_only = true; + sql_resolve_self_reference(&parser, space_def, NC_IsCheck, expr, NULL); + int rc = parser.is_aborted ? -1 : 0; + sql_parser_destroy(&parser); + return rc; +} + +struct ck_constraint * +ck_constraint_new(struct ck_constraint_def *ck_constraint_def, + struct space_def *space_def) +{ + if (space_def->field_count == 0) { + diag_set(ClientError, ER_UNSUPPORTED, "Tarantool", + "CK constraint for space without format"); + return NULL; + } + struct ck_constraint *ck_constraint = malloc(sizeof(*ck_constraint)); + if (ck_constraint == NULL) { + diag_set(OutOfMemory, sizeof(*ck_constraint), "malloc", + "ck_constraint"); + return NULL; + } + ck_constraint->def = NULL; + ck_constraint->space_id = space_def->id; + rlist_create(&ck_constraint->link); + ck_constraint->expr = + sql_expr_compile(sql_get(), ck_constraint_def->expr_str, + strlen(ck_constraint_def->expr_str)); + if (ck_constraint->expr == NULL || + ck_constraint_resolve_field_names(ck_constraint->expr, + space_def) != 0) { + diag_set(ClientError, ER_CREATE_CK_CONSTRAINT, + ck_constraint_def->name, + box_error_message(box_error_last())); + goto error; + } + + ck_constraint->def = ck_constraint_def; + return ck_constraint; +error: + ck_constraint_delete(ck_constraint); + return NULL; +} + +void +ck_constraint_delete(struct ck_constraint *ck_constraint) +{ + sql_expr_delete(sql_get(), ck_constraint->expr, false); + free(ck_constraint->def); + TRASH(ck_constraint); + free(ck_constraint); +} + +struct ck_constraint * +space_ck_constraint_by_name(struct space *space, const char *name, + uint32_t name_len) +{ + struct ck_constraint *ck_constraint = NULL; + rlist_foreach_entry(ck_constraint, &space->ck_constraint, link) { + if (strlen(ck_constraint->def->name) == name_len && + memcmp(ck_constraint->def->name, name, name_len) == 0) + return ck_constraint; + } + return NULL; +} diff --git a/src/box/ck_constraint.h b/src/box/ck_constraint.h new file mode 100644 index 000000000..615612605 --- /dev/null +++ b/src/box/ck_constraint.h @@ -0,0 +1,163 @@ +#ifndef INCLUDES_BOX_CK_CONSTRAINT_H +#define INCLUDES_BOX_CK_CONSTRAINT_H +/* + * Copyright 2010-2019, Tarantool AUTHORS, please see AUTHORS file. + * + * Redistribution and use in source and binary forms, with or + * without modification, are permitted provided that the following + * conditions are met: + * + * 1. Redistributions of source code must retain the above + * copyright notice, this list of conditions and the + * following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following + * disclaimer in the documentation and/or other materials + * provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY <COPYRIGHT HOLDER> ``AS IS'' AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED + * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL + * <COPYRIGHT HOLDER> OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, + * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR + * BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF + * THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF + * SUCH DAMAGE. + */ + +#include <stdint.h> +#include "small/rlist.h" + +#if defined(__cplusplus) +extern "C" { +#endif + +struct space; +struct space_def; +struct Expr; + +/** + * Check constraint definition. + * See ck_constraint_def_sizeof() definition for implementation + * details and memory layout. + */ +struct ck_constraint_def { + /** + * The 0-terminated string, a name of the check + * constraint. Must be unique for a given space. + */ + char *name; + /** + * The 0-terminated string that defines check constraint + * expression. + * + * For instance: "field1 + field2 > 2 * 3". + */ + char *expr_str; +}; + +/** + * Structure representing check constraint. + * See ck_constraint_new() definition. + */ +struct ck_constraint { + /** The check constraint definition. */ + struct ck_constraint_def *def; + /** + * The check constraint expression AST is built for + * ck_constraint::def::expr_str with sql_expr_compile + * and resolved with sql_resolve_self_reference for + * space with space[ck_constraint::space_id] definition. + */ + struct Expr *expr; + /** + * The id of the space this check constraint is + * built for. + */ + uint32_t space_id; + /** + * Organize check constraint structs into linked list + * with space::ck_constraint. + */ + struct rlist link; +}; + +/** + * Calculate check constraint definition memory size and fields + * offsets for given arguments. + * + * Alongside with struct ck_constraint_def itself, we reserve + * memory for string containing its name and expression string. + * + * Memory layout: + * +-----------------------------+ <- Allocated memory starts here + * | struct ck_constraint_def | + * |-----------------------------| + * | name + \0 | + * |-----------------------------| + * | expr_str + \0 | + * +-----------------------------+ + * + * @param name_len The length of the name. + * @param expr_str_len The length of the expr_str. + * @param[out] name_offset The offset of the name string. + * @param[out] expr_str_offset The offset of the expr_str string. + */ +static inline uint32_t +ck_constraint_def_sizeof(uint32_t name_len, uint32_t expr_str_len, + uint32_t *name_offset, uint32_t *expr_str_offset) +{ + *name_offset = sizeof(struct ck_constraint_def); + *expr_str_offset = *name_offset + name_len + 1; + return *expr_str_offset + expr_str_len + 1; +} + +/** + * Create a new check constraint object by given check constraint + * definition and definition of the space this constraint is + * related to. + * + * @param ck_constraint_def The check constraint definition object + * to use. Expected to be allocated with + * malloc. Ck constraint object manages + * this allocation in case of successful + * creation. + * @param space_def The space definition of the space this check + * constraint must be constructed for. + * @retval not NULL Check constraint object pointer on success. + * @retval NULL Otherwise. +*/ +struct ck_constraint * +ck_constraint_new(struct ck_constraint_def *ck_constraint_def, + struct space_def *space_def); + +/** + * Destroy check constraint memory, release acquired resources. + * @param ck_constraint The check constraint object to destroy. + */ +void +ck_constraint_delete(struct ck_constraint *ck_constraint); + +/** + * Find check constraint object in space by given name and + * name_len. + * @param space The space to lookup check constraint. + * @param name The check constraint name. + * @param name_len The length of the name. + * @retval not NULL Check constrain if exists, NULL otherwise. + */ +struct ck_constraint * +space_ck_constraint_by_name(struct space *space, const char *name, + uint32_t name_len); + +#if defined(__cplusplus) +} /* extern "C" { */ +#endif + +#endif /* INCLUDES_BOX_CK_CONSTRAINT_H */ diff --git a/src/box/errcode.h b/src/box/errcode.h index 3f8cb8e0e..7878bd66b 100644 --- a/src/box/errcode.h +++ b/src/box/errcode.h @@ -245,8 +245,8 @@ struct errcode_record { /*190 */_(ER_INT_LITERAL_MAX, "Integer literal %s%s exceeds the supported range %lld - %lld") \ /*191 */_(ER_SQL_PARSER_LIMIT, "%s %d exceeds the limit (%d)") \ /*192 */_(ER_INDEX_DEF_UNSUPPORTED, "%s are prohibited in an index definition") \ - /*193 */_(ER_CK_DEF_UNSUPPORTED, "%s are prohibited in a CHECK constraint definition") \ - + /*193 */_(ER_CK_DEF_UNSUPPORTED, "%s are prohibited in a check constraint definition") \ + /*194 */_(ER_CREATE_CK_CONSTRAINT, "Failed to create check constraint '%s': %s") \ /* * !IMPORTANT! Please follow instructions at start of the file * when adding new errors. diff --git a/src/box/lua/schema.lua b/src/box/lua/schema.lua index f31cf7f2c..e01f500e6 100644 --- a/src/box/lua/schema.lua +++ b/src/box/lua/schema.lua @@ -513,6 +513,7 @@ box.schema.space.drop = function(space_id, space_name, opts) local _truncate = box.space[box.schema.TRUNCATE_ID] local _space_sequence = box.space[box.schema.SPACE_SEQUENCE_ID] local _fk_constraint = box.space[box.schema.FK_CONSTRAINT_ID] + local _ck_constraint = box.space[box.schema.CK_CONSTRAINT_ID] local sequence_tuple = _space_sequence:delete{space_id} if sequence_tuple ~= nil and sequence_tuple[3] == true then -- Delete automatically generated sequence. @@ -524,6 +525,9 @@ box.schema.space.drop = function(space_id, space_name, opts) for _, t in _fk_constraint.index.child_id:pairs({space_id}) do _fk_constraint:delete({t.name, space_id}) end + for _, t in _ck_constraint.index.space_id:pairs({space_id}) do + _ck_constraint:delete({t.name, space_id}) + end local keys = _vindex:select(space_id) for i = #keys, 1, -1 do local v = keys[i] diff --git a/src/box/lua/space.cc b/src/box/lua/space.cc index 100da0a79..ce4287bba 100644 --- a/src/box/lua/space.cc +++ b/src/box/lua/space.cc @@ -537,6 +537,8 @@ box_lua_space_init(struct lua_State *L) lua_setfield(L, -2, "TRIGGER_ID"); lua_pushnumber(L, BOX_FK_CONSTRAINT_ID); lua_setfield(L, -2, "FK_CONSTRAINT_ID"); + lua_pushnumber(L, BOX_CK_CONSTRAINT_ID); + lua_setfield(L, -2, "CK_CONSTRAINT_ID"); lua_pushnumber(L, BOX_TRUNCATE_ID); lua_setfield(L, -2, "TRUNCATE_ID"); lua_pushnumber(L, BOX_SEQUENCE_ID); diff --git a/src/box/lua/upgrade.lua b/src/box/lua/upgrade.lua index 89d6e3d52..bbb55b7e1 100644 --- a/src/box/lua/upgrade.lua +++ b/src/box/lua/upgrade.lua @@ -74,6 +74,7 @@ local function set_system_triggers(val) box.space._schema:run_triggers(val) box.space._cluster:run_triggers(val) box.space._fk_constraint:run_triggers(val) + box.space._ck_constraint:run_triggers(val) end -------------------------------------------------------------------------------- @@ -94,6 +95,7 @@ local function erase() truncate(box.space._schema) truncate(box.space._cluster) truncate(box.space._fk_constraint) + truncate(box.space._ck_constraint) end local function create_sysview(source_id, target_id) @@ -737,6 +739,46 @@ local function upgrade_to_2_1_3() end end +local function upgrade_to_2_2_0() + -- In previous Tarantool releases check constraints were + -- stored in space opts. Now we use separate space + -- _ck_constraint for this purpose. Perform legacy data + -- migration. + local MAP = setmap({}) + local _space = box.space[box.schema.SPACE_ID] + local _index = box.space[box.schema.INDEX_ID] + local _ck_constraint = box.space[box.schema.CK_CONSTRAINT_ID] + log.info("create space _ck_constraint") + local format = {{name='name', type='string'}, + {name='space_id', type='unsigned'}, + {name='is_deferred', type='boolean'}, + {name='expr_str', type='str'}} + _space:insert{_ck_constraint.id, ADMIN, '_ck_constraint', 'memtx', 0, MAP, format} + + log.info("create index primary on _ck_constraint") + _index:insert{_ck_constraint.id, 0, 'primary', 'tree', + {unique = true}, {{0, 'string'}, {1, 'unsigned'}}} + + log.info("create secondary index child_id on _ck_constraint") + _index:insert{_ck_constraint.id, 1, 'space_id', 'tree', + {unique = false}, {{1, 'unsigned'}}} + for _, space in _space:pairs() do + local flags = space.flags + if flags['checks'] ~= nil then + for i, check in pairs(flags['checks']) do + local expr_str = check.expr + local check_name = check.name or + "CK_CONSTRAINT_"..i.."_"..space.name + _ck_constraint:insert({check_name, space.id, false, expr_str}) + end + flags['checks'] = nil + _space:replace(box.tuple.new({space.id, space.owner, space.name, + space.engine, space.field_count, + flags, space.format})) + end + end +end + local function get_version() local version = box.space._schema:get{'version'} if version == nil then @@ -768,6 +810,7 @@ local function upgrade(options) {version = mkversion(2, 1, 1), func = upgrade_to_2_1_1, auto = true}, {version = mkversion(2, 1, 2), func = upgrade_to_2_1_2, auto = true}, {version = mkversion(2, 1, 3), func = upgrade_to_2_1_3, auto = true}, + {version = mkversion(2, 2, 0), func = upgrade_to_2_2_0, auto = true}, } for _, handler in ipairs(handlers) do diff --git a/src/box/schema.cc b/src/box/schema.cc index 9a55c2f14..4b20f0032 100644 --- a/src/box/schema.cc +++ b/src/box/schema.cc @@ -460,6 +460,14 @@ schema_init() sc_space_new(BOX_FK_CONSTRAINT_ID, "_fk_constraint", key_parts, 2, &on_replace_fk_constraint, NULL); + /* _ck_сonstraint - check constraints. */ + key_parts[0].fieldno = 0; /* constraint name */ + key_parts[0].type = FIELD_TYPE_STRING; + key_parts[1].fieldno = 1; /* space id */ + key_parts[1].type = FIELD_TYPE_UNSIGNED; + sc_space_new(BOX_CK_CONSTRAINT_ID, "_ck_constraint", key_parts, 2, + &on_replace_ck_constraint, NULL); + /* * _vinyl_deferred_delete - blackhole that is needed * for writing deferred DELETE statements generated by diff --git a/src/box/schema_def.h b/src/box/schema_def.h index eeeeb950b..cdd3f7f63 100644 --- a/src/box/schema_def.h +++ b/src/box/schema_def.h @@ -108,6 +108,8 @@ enum { BOX_SPACE_SEQUENCE_ID = 340, /** Space id of _fk_constraint. */ BOX_FK_CONSTRAINT_ID = 356, + /** Space id of _ck_contraint. */ + BOX_CK_CONSTRAINT_ID = 357, /** End of the reserved range of system spaces. */ BOX_SYSTEM_ID_MAX = 511, BOX_ID_NIL = 2147483647 @@ -238,6 +240,14 @@ enum { BOX_FK_CONSTRAINT_FIELD_PARENT_COLS = 8, }; +/** _ck_constraint fields. */ +enum { + BOX_CK_CONSTRAINT_FIELD_NAME = 0, + BOX_CK_CONSTRAINT_FIELD_SPACE_ID = 1, + BOX_CK_CONSTRAINT_FIELD_DEFERRED = 2, + BOX_CK_CONSTRAINT_FIELD_EXPR_STR = 3, +}; + /* * Different objects which can be subject to access * control. diff --git a/src/box/space.c b/src/box/space.c index 54ba97fba..e4e0a2264 100644 --- a/src/box/space.c +++ b/src/box/space.c @@ -165,6 +165,7 @@ space_create(struct space *space, struct engine *engine, space_fill_index_map(space); rlist_create(&space->parent_fk_constraint); rlist_create(&space->child_fk_constraint); + rlist_create(&space->ck_constraint); return 0; fail_free_indexes: @@ -225,6 +226,7 @@ space_delete(struct space *space) assert(space->sql_triggers == NULL); assert(rlist_empty(&space->parent_fk_constraint)); assert(rlist_empty(&space->child_fk_constraint)); + assert(rlist_empty(&space->ck_constraint)); space->vtab->destroy(space); } diff --git a/src/box/space.h b/src/box/space.h index 13a220d13..e8529beb3 100644 --- a/src/box/space.h +++ b/src/box/space.h @@ -193,6 +193,11 @@ struct space { * of index id. */ struct index **index; + /** + * List of check constraints linked with + * ck_constraint::link. + */ + struct rlist ck_constraint; /** * Lists of foreign key constraints. In SQL terms child * space is the "from" table i.e. the table that contains diff --git a/src/box/space_def.c b/src/box/space_def.c index d0864cc72..cb68cea1c 100644 --- a/src/box/space_def.c +++ b/src/box/space_def.c @@ -35,28 +35,12 @@ #include "sql.h" #include "msgpuck.h" -/** - * Make checks from msgpack. - * @param str pointer to array of maps - * e.g. [{"expr": "x < y", "name": "ONE"}, ..]. - * @param len array items count. - * @param[out] opt pointer to store parsing result. - * @param errcode Code of error to set if something is wrong. - * @param field_no Field number of an option in a parent element. - * @retval 0 on success. - * @retval not 0 on error. Also set diag message. - */ -static int -checks_array_decode(const char **str, uint32_t len, char *opt, uint32_t errcode, - uint32_t field_no); - const struct space_opts space_opts_default = { /* .group_id = */ 0, /* .is_temporary = */ false, /* .is_ephemeral = */ false, /* .view = */ false, /* .sql = */ NULL, - /* .checks = */ NULL, }; const struct opt_def space_opts_reg[] = { @@ -64,8 +48,7 @@ const struct opt_def space_opts_reg[] = { OPT_DEF("temporary", OPT_BOOL, struct space_opts, is_temporary), OPT_DEF("view", OPT_BOOL, struct space_opts, is_view), OPT_DEF("sql", OPT_STRPTR, struct space_opts, sql), - OPT_DEF_ARRAY("checks", struct space_opts, checks, - checks_array_decode), + OPT_DEF_LEGACY("checks"), OPT_END, }; @@ -113,16 +96,6 @@ space_def_dup_opts(struct space_def *def, const struct space_opts *opts) return -1; } } - if (opts->checks != NULL) { - def->opts.checks = sql_expr_list_dup(sql_get(), opts->checks, 0); - if (def->opts.checks == NULL) { - free(def->opts.sql); - diag_set(OutOfMemory, 0, "sql_expr_list_dup", - "def->opts.checks"); - return -1; - } - sql_checks_update_space_def_reference(def->opts.checks, def); - } return 0; } @@ -300,74 +273,5 @@ void space_opts_destroy(struct space_opts *opts) { free(opts->sql); - sql_expr_list_delete(sql_get(), opts->checks); TRASH(opts); } - -static int -checks_array_decode(const char **str, uint32_t len, char *opt, uint32_t errcode, - uint32_t field_no) -{ - char *errmsg = tt_static_buf(); - struct ExprList *checks = NULL; - const char **map = str; - struct sql *db = sql_get(); - for (uint32_t i = 0; i < len; i++) { - checks = sql_expr_list_append(db, checks, NULL); - if (checks == NULL) { - diag_set(OutOfMemory, 0, "sql_expr_list_append", - "checks"); - goto error; - } - const char *expr_name = NULL; - const char *expr_str = NULL; - uint32_t expr_name_len = 0; - uint32_t expr_str_len = 0; - uint32_t map_size = mp_decode_map(map); - for (uint32_t j = 0; j < map_size; j++) { - if (mp_typeof(**map) != MP_STR) { - diag_set(ClientError, errcode, field_no, - "key must be a string"); - goto error; - } - uint32_t key_len; - const char *key = mp_decode_str(map, &key_len); - if (mp_typeof(**map) != MP_STR) { - snprintf(errmsg, TT_STATIC_BUF_LEN, - "invalid MsgPack map field '%.*s' type", - key_len, key); - diag_set(ClientError, errcode, field_no, errmsg); - goto error; - } - if (key_len == 4 && memcmp(key, "expr", key_len) == 0) { - expr_str = mp_decode_str(map, &expr_str_len); - } else if (key_len == 4 && - memcmp(key, "name", key_len) == 0) { - expr_name = mp_decode_str(map, &expr_name_len); - } else { - snprintf(errmsg, TT_STATIC_BUF_LEN, - "invalid MsgPack map field '%.*s'", - key_len, key); - diag_set(ClientError, errcode, field_no, errmsg); - goto error; - } - } - if (sql_check_list_item_init(checks, i, expr_name, expr_name_len, - expr_str, expr_str_len) != 0) { - box_error_t *err = box_error_last(); - if (box_error_code(err) != ENOMEM) { - snprintf(errmsg, TT_STATIC_BUF_LEN, - "invalid expression specified (%s)", - box_error_message(err)); - diag_set(ClientError, errcode, field_no, - errmsg); - } - goto error; - } - } - *(struct ExprList **)opt = checks; - return 0; -error: - sql_expr_list_delete(db, checks); - return -1; -} diff --git a/src/box/space_def.h b/src/box/space_def.h index 52ff56764..71d168342 100644 --- a/src/box/space_def.h +++ b/src/box/space_def.h @@ -71,8 +71,6 @@ struct space_opts { bool is_view; /** SQL statement that produced this space. */ char *sql; - /** SQL Checks expressions list. */ - struct ExprList *checks; }; extern const struct space_opts space_opts_default; diff --git a/src/box/sql.c b/src/box/sql.c index 2310ee5e3..e4dc69c57 100644 --- a/src/box/sql.c +++ b/src/box/sql.c @@ -1034,15 +1034,8 @@ sql_encode_table_opts(struct region *region, struct space_def *def, bool is_error = false; mpstream_init(&stream, region, region_reserve_cb, region_alloc_cb, set_encode_error, &is_error); - int checks_cnt = 0; - struct ExprList_item *a; bool is_view = def->opts.is_view; - struct ExprList *checks = def->opts.checks; - if (checks != NULL) { - checks_cnt = checks->nExpr; - a = checks->a; - } - mpstream_encode_map(&stream, 2 * is_view + (checks_cnt > 0)); + mpstream_encode_map(&stream, 2 * is_view); if (is_view) { assert(def->opts.sql != NULL); @@ -1051,23 +1044,6 @@ sql_encode_table_opts(struct region *region, struct space_def *def, mpstream_encode_str(&stream, "view"); mpstream_encode_bool(&stream, true); } - if (checks_cnt > 0) { - mpstream_encode_str(&stream, "checks"); - mpstream_encode_array(&stream, checks_cnt); - } - for (int i = 0; i < checks_cnt && !is_error; ++i, ++a) { - int items = (a->pExpr != NULL) + (a->zName != NULL); - mpstream_encode_map(&stream, items); - assert(a->pExpr != NULL); - struct Expr *pExpr = a->pExpr; - assert(pExpr->u.zToken != NULL); - mpstream_encode_str(&stream, "expr"); - mpstream_encode_str(&stream, pExpr->u.zToken); - if (a->zName != NULL) { - mpstream_encode_str(&stream, "name"); - mpstream_encode_str(&stream, a->zName); - } - } mpstream_flush(&stream); if (is_error) { diag_set(OutOfMemory, stream.pos - stream.buf, @@ -1301,63 +1277,3 @@ sql_ephemeral_space_new(Parse *parser, const char *name) return space; } - -int -sql_check_list_item_init(struct ExprList *expr_list, int column, - const char *expr_name, uint32_t expr_name_len, - const char *expr_str, uint32_t expr_str_len) -{ - assert(column < expr_list->nExpr); - struct ExprList_item *item = &expr_list->a[column]; - memset(item, 0, sizeof(*item)); - if (expr_name != NULL) { - item->zName = sqlDbStrNDup(db, expr_name, expr_name_len); - if (item->zName == NULL) { - diag_set(OutOfMemory, expr_name_len, "sqlDbStrNDup", - "item->zName"); - return -1; - } - } - if (expr_str != NULL) { - item->pExpr = sql_expr_compile(db, expr_str, expr_str_len); - /* The item->zName would be released later. */ - if (item->pExpr == NULL) - return -1; - } - return 0; -} - -static int -update_space_def_callback(Walker *walker, Expr *expr) -{ - if (expr->op == TK_COLUMN && ExprHasProperty(expr, EP_Resolved)) - expr->space_def = walker->u.space_def; - return WRC_Continue; -} - -void -sql_checks_update_space_def_reference(ExprList *expr_list, - struct space_def *def) -{ - assert(expr_list != NULL); - Walker w; - memset(&w, 0, sizeof(w)); - w.xExprCallback = update_space_def_callback; - w.u.space_def = def; - for (int i = 0; i < expr_list->nExpr; i++) - sqlWalkExpr(&w, expr_list->a[i].pExpr); -} - -int -sql_checks_resolve_space_def_reference(ExprList *expr_list, - struct space_def *def) -{ - Parse parser; - sql_parser_create(&parser, sql_get()); - parser.parse_only = true; - - sql_resolve_self_reference(&parser, def, NC_IsCheck, NULL, expr_list); - int rc = parser.is_aborted ? -1 : 0; - sql_parser_destroy(&parser); - return rc; -} diff --git a/src/box/sql.h b/src/box/sql.h index 262a48bcb..4bb27da8e 100644 --- a/src/box/sql.h +++ b/src/box/sql.h @@ -285,42 +285,6 @@ sql_resolve_self_reference(struct Parse *parser, struct space_def *def, int type, struct Expr *expr, struct ExprList *expr_list); -/** - * Initialize check_list_item. - * @param expr_list ExprList with item. - * @param column index. - * @param expr_name expression name (optional). - * @param expr_name_len expresson name length (optional). - * @param expr_str expression to build string. - * @param expr_str_len expression to build string length. - * @retval 0 on success. - * @retval -1 on error. - */ -int -sql_check_list_item_init(struct ExprList *expr_list, int column, - const char *expr_name, uint32_t expr_name_len, - const char *expr_str, uint32_t expr_str_len); - -/** - * Resolve space_def references checks for expr_list. - * @param expr_list to modify. - * @param def to refer to. - * @retval 0 on success. - * @retval -1 on error. - */ -int -sql_checks_resolve_space_def_reference(struct ExprList *expr_list, - struct space_def *def); - -/** - * Update space_def references for expr_list. - * @param expr_list to modify. - * @param def to refer to. - */ -void -sql_checks_update_space_def_reference(struct ExprList *expr_list, - struct space_def *def); - /** * Initialize a new parser object. * A number of service allocations are performed on the region, diff --git a/src/box/sql/build.c b/src/box/sql/build.c index 6051a2529..3709f6b1d 100644 --- a/src/box/sql/build.c +++ b/src/box/sql/build.c @@ -43,10 +43,12 @@ * COMMIT * ROLLBACK */ +#include <ctype.h> #include "sqlInt.h" #include "vdbeInt.h" #include "tarantoolInt.h" #include "box/box.h" +#include "box/ck_constraint.h" #include "box/fk_constraint.h" #include "box/sequence.h" #include "box/session.h" @@ -638,40 +640,108 @@ primary_key_exit: return; } +/** + * Prepare a 0-terminated string in the wptr memory buffer that + * does not contain a sequence of more than one whatespace + * character. Routine enforces ' ' (space) as whitespace + * delimiter. + * The wptr buffer is expected to have str_len + 1 bytes + * (this is the expected scenario where no extra whitespace + * characters preset in the source string). + * @param wptr The destination memory buffer of size + * @a str_len + 1. + * @param str The source string to be copied. + * @param str_len The source string @a str length. + */ +static void +trim_space_snprintf(char *wptr, const char *str, uint32_t str_len) +{ + const char *str_end = str + str_len; + bool is_prev_chr_space = false; + while (str < str_end) { + if (isspace((unsigned char)*str)) { + if (!is_prev_chr_space) + *wptr++ = ' '; + is_prev_chr_space = true; + str++; + continue; + } + is_prev_chr_space = false; + *wptr++ = *str++; + } + *wptr = '\0'; +} + void -sql_add_check_constraint(struct Parse *parser) +sql_create_check_contraint(struct Parse *parser) { - struct create_ck_def *ck_def = &parser->create_ck_def; + struct create_ck_def *create_ck_def = &parser->create_ck_def; + struct ExprSpan *expr_span = create_ck_def->expr; + sql_expr_delete(parser->db, expr_span->pExpr, false); + struct alter_entity_def *alter_def = (struct alter_entity_def *) &parser->create_ck_def; assert(alter_def->entity_type == ENTITY_TYPE_CK); (void) alter_def; - struct Expr *expr = ck_def->expr->pExpr; struct space *space = parser->create_table_def.new_space; - if (space != NULL) { - expr->u.zToken = - sqlDbStrNDup(parser->db, - (char *) ck_def->expr->zStart, - (int) (ck_def->expr->zEnd - - ck_def->expr->zStart)); - if (expr->u.zToken == NULL) - goto release_expr; - space->def->opts.checks = - sql_expr_list_append(parser->db, - space->def->opts.checks, expr); - if (space->def->opts.checks == NULL) { - sqlDbFree(parser->db, expr->u.zToken); - goto release_expr; - } - struct create_entity_def *entity_def = &ck_def->base.base; - if (entity_def->name.n > 0) { - sqlExprListSetName(parser, space->def->opts.checks, - &entity_def->name, 1); + assert(space != NULL); + + /* Prepare payload for ck constraint definition. */ + struct region *region = &parser->region; + struct Token *name_token = &create_ck_def->base.base.name; + const char *name; + if (name_token->n > 0) { + name = sql_normalized_name_region_new(region, name_token->z, + name_token->n); + if (name == NULL) { + parser->is_aborted = true; + return; } } else { -release_expr: - sql_expr_delete(parser->db, expr, false); + uint32_t ck_idx = ++parser->create_table_def.check_count; + name = tt_sprintf("CK_CONSTRAINT_%d_%s", ck_idx, + space->def->name); + } + size_t name_len = strlen(name); + + uint32_t expr_str_len = (uint32_t)(create_ck_def->expr->zEnd - + create_ck_def->expr->zStart); + const char *expr_str = create_ck_def->expr->zStart; + + /* + * Allocate memory for ck constraint parse structure and + * ck constraint definition as a single memory chunk on + * region: + * + * [ck_parse][ck_def[name][expr_str]] + * |_____^ |___^ ^ + * |_________| + */ + uint32_t name_offset, expr_str_offset; + uint32_t ck_def_sz = + ck_constraint_def_sizeof(name_len, expr_str_len, &name_offset, + &expr_str_offset); + struct ck_constraint_parse *ck_parse = + region_alloc(region, sizeof(*ck_parse) + ck_def_sz); + if (ck_parse == NULL) { + diag_set(OutOfMemory, sizeof(*ck_parse) + ck_def_sz, "region", + "ck_parse"); + parser->is_aborted = true; + return; } + struct ck_constraint_def *ck_def = + (struct ck_constraint_def *)((char *)ck_parse + + sizeof(*ck_parse)); + ck_parse->ck_def = ck_def; + rlist_create(&ck_parse->link); + + ck_def->name = (char *)ck_def + name_offset; + ck_def->expr_str = (char *)ck_def + expr_str_offset; + trim_space_snprintf(ck_def->expr_str, expr_str, expr_str_len); + memcpy(ck_def->name, name, name_len); + ck_def->name[name_len] = '\0'; + + rlist_add_entry(&parser->create_table_def.new_check, ck_parse, link); } /* @@ -939,6 +1009,38 @@ emitNewSysSequenceRecord(Parse *pParse, int reg_seq_id, const char *seq_name) return first_col; } +/** + * Generate opcodes to serialize check constraint definition into + * MsgPack and insert produced tuple into _ck_constraint space. + * @param parser Parsing context. + * @param ck_def Check constraint definition to be serialized. + * @param reg_space_id The VDBE register containing space id. +*/ +static void +vdbe_emit_ck_constraint_create(struct Parse *parser, + const struct ck_constraint_def *ck_def, + uint32_t reg_space_id) +{ + struct sql *db = parser->db; + struct Vdbe *v = sqlGetVdbe(parser); + assert(v != NULL); + int ck_constraint_reg = sqlGetTempRange(parser, 5); + sqlVdbeAddOp4(v, OP_String8, 0, ck_constraint_reg, 0, + sqlDbStrDup(db, ck_def->name), P4_DYNAMIC); + sqlVdbeAddOp2(v, OP_SCopy, reg_space_id, ck_constraint_reg + 1); + sqlVdbeAddOp2(v, OP_Bool, false, ck_constraint_reg + 2); + sqlVdbeAddOp4(v, OP_String8, 0, ck_constraint_reg + 3, 0, + sqlDbStrDup(db, ck_def->expr_str), P4_DYNAMIC); + sqlVdbeAddOp3(v, OP_MakeRecord, ck_constraint_reg, 4, + ck_constraint_reg + 4); + sqlVdbeAddOp3(v, OP_SInsert, BOX_CK_CONSTRAINT_ID, 0, + ck_constraint_reg + 4); + save_record(parser, BOX_CK_CONSTRAINT_ID, ck_constraint_reg, 2, + v->nOp - 1); + VdbeComment((v, "Create CK constraint %s", ck_def->name)); + sqlReleaseTempRange(parser, ck_constraint_reg, 5); +} + int emitNewSysSpaceSequenceRecord(Parse *pParse, int space_id, const char reg_seq_id) { @@ -1108,20 +1210,18 @@ resolve_link(struct Parse *parse_context, const struct space_def *def, void sqlEndTable(struct Parse *pParse) { - sql *db = pParse->db; /* The database connection */ - - assert(!db->mallocFailed); + assert(!pParse->db->mallocFailed); struct space *new_space = pParse->create_table_def.new_space; if (new_space == NULL) return; - assert(!db->init.busy); + assert(!pParse->db->init.busy); assert(!new_space->def->opts.is_view); if (sql_space_primary_key(new_space) == NULL) { diag_set(ClientError, ER_CREATE_SPACE, new_space->def->name, "PRIMARY KEY missing"); pParse->is_aborted = true; - goto cleanup; + return; } /* @@ -1211,9 +1311,12 @@ sqlEndTable(struct Parse *pParse) fk_def->child_id = reg_space_id; vdbe_emit_fk_constraint_create(pParse, fk_def); } -cleanup: - sql_expr_list_delete(db, new_space->def->opts.checks); - new_space->def->opts.checks = NULL; + struct ck_constraint_parse *ck_parse; + rlist_foreach_entry(ck_parse, &pParse->create_table_def.new_check, + link) { + vdbe_emit_ck_constraint_create(pParse, ck_parse->ck_def, + reg_space_id); + } } void @@ -1411,6 +1514,37 @@ vdbe_emit_fk_constraint_drop(struct Parse *parse_context, char *constraint_name, sqlReleaseTempRange(parse_context, key_reg, 3); } +/** + * Generate VDBE program to remove entry from _ck_constraint space. + * + * @param parser Parsing context. + * @param ck_name Name of CK constraint to be dropped. + * @param child_id Id of table which constraint belongs to. + */ +static void +vdbe_emit_ck_constraint_drop(struct Parse *parser, const char *ck_name, + uint32_t space_id) +{ + struct Vdbe *v = sqlGetVdbe(parser); + struct sql *db = v->db; + assert(v != NULL); + int key_reg = sqlGetTempRange(parser, 3); + sqlVdbeAddOp4(v, OP_String8, 0, key_reg, 0, sqlDbStrDup(db, ck_name), + P4_DYNAMIC); + sqlVdbeAddOp2(v, OP_Integer, space_id, key_reg + 1); + const char *error_msg = + tt_sprintf(tnt_errcode_desc(ER_NO_SUCH_CONSTRAINT), ck_name); + if (vdbe_emit_halt_with_presence_test(parser, BOX_CK_CONSTRAINT_ID, 0, + key_reg, 2, ER_NO_SUCH_CONSTRAINT, + error_msg, false, + OP_Found) != 0) + return; + sqlVdbeAddOp3(v, OP_MakeRecord, key_reg, 2, key_reg + 2); + sqlVdbeAddOp2(v, OP_SDelete, BOX_CK_CONSTRAINT_ID, key_reg + 2); + VdbeComment((v, "Delete CK constraint %s", ck_name)); + sqlReleaseTempRange(parser, key_reg, 3); +} + /** * Generate code to drop a table. * This routine includes dropping triggers, sequences, @@ -1484,6 +1618,13 @@ sql_code_drop_table(struct Parse *parse_context, struct space *space, return; vdbe_emit_fk_constraint_drop(parse_context, fk_name_dup, space_id); } + /* Delete all CK constraints. */ + struct ck_constraint *ck_constraint; + rlist_foreach_entry(ck_constraint, &space->ck_constraint, link) { + vdbe_emit_ck_constraint_drop(parse_context, + ck_constraint->def->name, + space_id); + } /* * Drop all _space and _index entries that refer to the * table. @@ -2770,8 +2911,7 @@ sqlSrcListDelete(sql * db, SrcList * pList) */ assert(pItem->space == NULL || !pItem->space->def->opts.is_temporary || - (pItem->space->index == NULL && - pItem->space->def->opts.checks == NULL)); + pItem->space->index == NULL); sql_select_delete(db, pItem->pSelect); sql_expr_delete(db, pItem->pOn, false); sqlIdListDelete(db, pItem->pUsing); diff --git a/src/box/sql/insert.c b/src/box/sql/insert.c index c2aac553f..fa4a94ef7 100644 --- a/src/box/sql/insert.c +++ b/src/box/sql/insert.c @@ -36,6 +36,7 @@ #include "sqlInt.h" #include "tarantoolInt.h" #include "vdbeInt.h" +#include "box/ck_constraint.h" #include "box/session.h" #include "box/schema.h" #include "bit/bit.h" @@ -933,34 +934,27 @@ vdbe_emit_constraint_checks(struct Parse *parse_context, struct space *space, if (on_conflict == ON_CONFLICT_ACTION_DEFAULT) on_conflict = ON_CONFLICT_ACTION_ABORT; /* Test all CHECK constraints. */ - struct ExprList *checks = def->opts.checks; enum on_conflict_action on_conflict_check = on_conflict; if (on_conflict == ON_CONFLICT_ACTION_REPLACE) on_conflict_check = ON_CONFLICT_ACTION_ABORT; - if (checks != NULL) { + if (!rlist_empty(&space->ck_constraint)) parse_context->ckBase = new_tuple_reg; - for (int i = 0; i < checks->nExpr; i++) { - struct Expr *expr = checks->a[i].pExpr; - if (is_update && - checkConstraintUnchanged(expr, upd_cols)) - continue; - int all_ok = sqlVdbeMakeLabel(v); - sqlExprIfTrue(parse_context, expr, all_ok, - SQL_JUMPIFNULL); - if (on_conflict == ON_CONFLICT_ACTION_IGNORE) { - sqlVdbeGoto(v, ignore_label); - } else { - char *name = checks->a[i].zName; - if (name == NULL) - name = def->name; - sqlHaltConstraint(parse_context, - SQL_CONSTRAINT_CHECK, - on_conflict_check, name, - P4_TRANSIENT, - P5_ConstraintCheck); - } - sqlVdbeResolveLabel(v, all_ok); + struct ck_constraint *ck_constraint; + rlist_foreach_entry(ck_constraint, &space->ck_constraint, link) { + struct Expr *expr = ck_constraint->expr; + if (is_update && checkConstraintUnchanged(expr, upd_cols) != 0) + continue; + int all_ok = sqlVdbeMakeLabel(v); + sqlExprIfTrue(parse_context, expr, all_ok, SQL_JUMPIFNULL); + if (on_conflict == ON_CONFLICT_ACTION_IGNORE) { + sqlVdbeGoto(v, ignore_label); + } else { + char *name = ck_constraint->def->name; + sqlHaltConstraint(parse_context, SQL_CONSTRAINT_CHECK, + on_conflict_check, name, P4_TRANSIENT, + P5_ConstraintCheck); } + sqlVdbeResolveLabel(v, all_ok); } sql_emit_table_types(v, space->def, new_tuple_reg); /* @@ -1245,14 +1239,13 @@ xferOptimization(Parse * pParse, /* Parser context */ if (pSrcIdx == NULL) return 0; } - /* Get server checks. */ - ExprList *pCheck_src = src->def->opts.checks; - ExprList *pCheck_dest = dest->def->opts.checks; - if (pCheck_dest != NULL && - sqlExprListCompare(pCheck_src, pCheck_dest, -1) != 0) { - /* Tables have different CHECK constraints. Ticket #2252 */ + /* + * Dissallow the transfer optimization if the are check + * constraints. + */ + if (!rlist_empty(&dest->ck_constraint) || + !rlist_empty(&src->ck_constraint)) return 0; - } /* Disallow the transfer optimization if the destination table constains * any foreign key constraints. This is more restrictive than necessary. * So the extra complication to make this rule less restrictive is probably diff --git a/src/box/sql/parse.y b/src/box/sql/parse.y index 3a443a068..bc9f02599 100644 --- a/src/box/sql/parse.y +++ b/src/box/sql/parse.y @@ -290,7 +290,7 @@ ccons ::= check_constraint_def . check_constraint_def ::= cconsname(N) CHECK LP expr(X) RP. { create_ck_def_init(&pParse->create_ck_def, &N, &X); - sql_add_check_constraint(pParse); + sql_create_check_contraint(pParse); } ccons ::= cconsname(N) REFERENCES nm(T) eidlist_opt(TA) matcharg(M) refargs(R). { diff --git a/src/box/sql/parse_def.h b/src/box/sql/parse_def.h index 5899a7e4e..6c1b6fddd 100644 --- a/src/box/sql/parse_def.h +++ b/src/box/sql/parse_def.h @@ -123,6 +123,22 @@ struct fk_constraint_parse { struct rlist link; }; +/** + * Structure representing check constraint appeared within + * CREATE TABLE statement. Used only during parsing. + * All allocations are performed on region, so no cleanups are + * required. + */ +struct ck_constraint_parse { + /** + * Check constraint declared in <CREATE TABLE ...> + * statement. Must be coded after space creation. + */ + struct ck_constraint_def *ck_def; + /** Organize these structs into linked list. */ + struct rlist link; +}; + /** * Possible SQL index types. Note that PK and UNIQUE constraints * are implemented as indexes and have their own types: @@ -189,6 +205,13 @@ struct create_table_def { * Foreign key constraint appeared in CREATE TABLE stmt. */ struct rlist new_fkey; + /** + * Number of CK constraints declared within + * CREATE TABLE statement. + */ + uint32_t check_count; + /** Check constraint appeared in CREATE TABLE stmt. */ + struct rlist new_check; /** True, if table to be created has AUTOINCREMENT PK. */ bool has_autoinc; }; @@ -437,6 +460,7 @@ create_table_def_init(struct create_table_def *table_def, struct Token *name, create_entity_def_init(&table_def->base, ENTITY_TYPE_TABLE, NULL, name, if_not_exists); rlist_create(&table_def->new_fkey); + rlist_create(&table_def->new_check); } static inline void diff --git a/src/box/sql/select.c b/src/box/sql/select.c index d3472a922..996ac7ccd 100644 --- a/src/box/sql/select.c +++ b/src/box/sql/select.c @@ -6446,7 +6446,12 @@ sql_expr_extract_select(struct Parse *parser, struct Select *select) struct ExprList *expr_list = select->pEList; assert(expr_list->nExpr == 1); parser->parsed_ast_type = AST_TYPE_EXPR; - parser->parsed_ast.expr = sqlExprDup(parser->db, - expr_list->a->pExpr, - EXPRDUP_REDUCE); + /* + * Extract a copy of parsed expression. + * We cannot use EXPRDUP_REDUCE flag in sqlExprDup call + * because some compiled Expr (like Checks expressions) + * may require further resolve with sqlResolveExprNames. + */ + parser->parsed_ast.expr = + sqlExprDup(parser->db, expr_list->a->pExpr, 0); } diff --git a/src/box/sql/sqlInt.h b/src/box/sql/sqlInt.h index 997a46535..2d6936490 100644 --- a/src/box/sql/sqlInt.h +++ b/src/box/sql/sqlInt.h @@ -3347,7 +3347,7 @@ sqlAddPrimaryKey(struct Parse *parse); * @param parser Parsing context. */ void -sql_add_check_constraint(Parse *parser); +sql_create_check_contraint(Parse *parser); void sqlAddDefaultValue(Parse *, ExprSpan *); void sqlAddCollateType(Parse *, Token *); diff --git a/src/box/sql/tokenize.c b/src/box/sql/tokenize.c index 8cc35323c..3791be567 100644 --- a/src/box/sql/tokenize.c +++ b/src/box/sql/tokenize.c @@ -434,7 +434,6 @@ parser_space_delete(struct sql *db, struct space *space) assert(space->def->opts.is_temporary); for (uint32_t i = 0; i < space->index_count; ++i) index_def_delete(space->index[i]->def); - sql_expr_list_delete(db, space->def->opts.checks); } /** diff --git a/test/app-tap/tarantoolctl.test.lua b/test/app-tap/tarantoolctl.test.lua index c1e1490ca..7a72acda2 100755 --- a/test/app-tap/tarantoolctl.test.lua +++ b/test/app-tap/tarantoolctl.test.lua @@ -405,8 +405,8 @@ do check_ctlcat_xlog(test_i, dir, "--from=3 --to=6 --format=json --show-system --replica 1", "\n", 3) check_ctlcat_xlog(test_i, dir, "--from=3 --to=6 --format=json --show-system --replica 1 --replica 2", "\n", 3) check_ctlcat_xlog(test_i, dir, "--from=3 --to=6 --format=json --show-system --replica 2", "\n", 0) - check_ctlcat_snap(test_i, dir, "--space=280", "---\n", 21) - check_ctlcat_snap(test_i, dir, "--space=288", "---\n", 47) + check_ctlcat_snap(test_i, dir, "--space=280", "---\n", 22) + check_ctlcat_snap(test_i, dir, "--space=288", "---\n", 49) end) end) diff --git a/test/box-py/bootstrap.result b/test/box-py/bootstrap.result index 379f6c51f..33bb01f7c 100644 --- a/test/box-py/bootstrap.result +++ b/test/box-py/bootstrap.result @@ -4,7 +4,7 @@ box.internal.bootstrap() box.space._schema:select{} --- - - ['max_id', 511] - - ['version', 2, 1, 3] + - ['version', 2, 2, 0] ... box.space._cluster:select{} --- @@ -78,6 +78,9 @@ box.space._space:select{} {'name': 'is_deferred', 'type': 'boolean'}, {'name': 'match', 'type': 'string'}, {'name': 'on_delete', 'type': 'string'}, {'name': 'on_update', 'type': 'string'}, {'name': 'child_cols', 'type': 'array'}, {'name': 'parent_cols', 'type': 'array'}]] + - [357, 1, '_ck_constraint', 'memtx', 0, {}, [{'name': 'name', 'type': 'string'}, + {'name': 'space_id', 'type': 'unsigned'}, {'name': 'is_deferred', 'type': 'boolean'}, + {'name': 'expr_str', 'type': 'str'}]] ... box.space._index:select{} --- @@ -130,6 +133,8 @@ box.space._index:select{} - [340, 1, 'sequence', 'tree', {'unique': false}, [[1, 'unsigned']]] - [356, 0, 'primary', 'tree', {'unique': true}, [[0, 'string'], [1, 'unsigned']]] - [356, 1, 'child_id', 'tree', {'unique': false}, [[1, 'unsigned']]] + - [357, 0, 'primary', 'tree', {'unique': true}, [[0, 'string'], [1, 'unsigned']]] + - [357, 1, 'space_id', 'tree', {'unique': false}, [[1, 'unsigned']]] ... box.space._user:select{} --- diff --git a/test/box/access.result b/test/box/access.result index 9c190240f..801112799 100644 --- a/test/box/access.result +++ b/test/box/access.result @@ -1487,6 +1487,9 @@ box.schema.user.grant('tester', 'read', 'space', '_trigger') box.schema.user.grant('tester', 'read', 'space', '_fk_constraint') --- ... +box.schema.user.grant('tester', 'read', 'space', '_ck_constraint') +--- +... box.session.su("tester") --- ... diff --git a/test/box/access.test.lua b/test/box/access.test.lua index 4baeb2ef6..2f62d6f53 100644 --- a/test/box/access.test.lua +++ b/test/box/access.test.lua @@ -554,6 +554,7 @@ box.schema.user.grant('tester', 'create' , 'sequence') box.schema.user.grant('tester', 'read', 'space', '_sequence') box.schema.user.grant('tester', 'read', 'space', '_trigger') box.schema.user.grant('tester', 'read', 'space', '_fk_constraint') +box.schema.user.grant('tester', 'read', 'space', '_ck_constraint') box.session.su("tester") -- successful create s1 = box.schema.space.create("test_space") diff --git a/test/box/access_misc.result b/test/box/access_misc.result index 36ebfae09..ca54ac6d4 100644 --- a/test/box/access_misc.result +++ b/test/box/access_misc.result @@ -818,6 +818,9 @@ box.space._space:select() {'name': 'is_deferred', 'type': 'boolean'}, {'name': 'match', 'type': 'string'}, {'name': 'on_delete', 'type': 'string'}, {'name': 'on_update', 'type': 'string'}, {'name': 'child_cols', 'type': 'array'}, {'name': 'parent_cols', 'type': 'array'}]] + - [357, 1, '_ck_constraint', 'memtx', 0, {}, [{'name': 'name', 'type': 'string'}, + {'name': 'space_id', 'type': 'unsigned'}, {'name': 'is_deferred', 'type': 'boolean'}, + {'name': 'expr_str', 'type': 'str'}]] ... box.space._func:select() --- diff --git a/test/box/access_sysview.result b/test/box/access_sysview.result index c6a2b22ed..c3d1e746b 100644 --- a/test/box/access_sysview.result +++ b/test/box/access_sysview.result @@ -230,11 +230,11 @@ box.session.su('guest') ... #box.space._vspace:select{} --- -- 22 +- 23 ... #box.space._vindex:select{} --- -- 48 +- 50 ... #box.space._vuser:select{} --- @@ -262,7 +262,7 @@ box.session.su('guest') ... #box.space._vindex:select{} --- -- 48 +- 50 ... #box.space._vuser:select{} --- diff --git a/test/box/alter.result b/test/box/alter.result index c1b1de135..44630557c 100644 --- a/test/box/alter.result +++ b/test/box/alter.result @@ -107,7 +107,7 @@ space = box.space[t[1]] ... space.id --- -- 357 +- 358 ... space.field_count --- @@ -152,7 +152,7 @@ space_deleted ... space:replace{0} --- -- error: Space '357' does not exist +- error: Space '358' does not exist ... _index:insert{_space.id, 0, 'primary', 'tree', {unique=true}, {{0, 'unsigned'}}} --- @@ -230,6 +230,8 @@ _index:select{} - [340, 1, 'sequence', 'tree', {'unique': false}, [[1, 'unsigned']]] - [356, 0, 'primary', 'tree', {'unique': true}, [[0, 'string'], [1, 'unsigned']]] - [356, 1, 'child_id', 'tree', {'unique': false}, [[1, 'unsigned']]] + - [357, 0, 'primary', 'tree', {'unique': true}, [[0, 'string'], [1, 'unsigned']]] + - [357, 1, 'space_id', 'tree', {'unique': false}, [[1, 'unsigned']]] ... -- modify indexes of a system space _index:delete{_index.id, 0} diff --git a/test/box/misc.result b/test/box/misc.result index a1f7a0990..eeb63366e 100644 --- a/test/box/misc.result +++ b/test/box/misc.result @@ -522,6 +522,7 @@ t; 191: box.error.SQL_PARSER_LIMIT 192: box.error.INDEX_DEF_UNSUPPORTED 193: box.error.CK_DEF_UNSUPPORTED + 194: box.error.CREATE_CK_CONSTRAINT ... test_run:cmd("setopt delimiter ''"); --- diff --git a/test/sql-tap/check.test.lua b/test/sql-tap/check.test.lua index b01afca7c..0dda3fac8 100755 --- a/test/sql-tap/check.test.lua +++ b/test/sql-tap/check.test.lua @@ -55,7 +55,7 @@ test:do_catchsql_test( INSERT INTO t1 VALUES(6,7, 2); ]], { -- <check-1.3> - 1, "Failed to execute SQL statement: CHECK constraint failed: T1" + 1, "Failed to execute SQL statement: CHECK constraint failed: CK_CONSTRAINT_1_T1" -- </check-1.3> }) @@ -75,7 +75,7 @@ test:do_catchsql_test( INSERT INTO t1 VALUES(4,3, 2); ]], { -- <check-1.5> - 1, "Failed to execute SQL statement: CHECK constraint failed: T1" + 1, "Failed to execute SQL statement: CHECK constraint failed: CK_CONSTRAINT_2_T1" -- </check-1.5> }) @@ -147,7 +147,7 @@ test:do_catchsql_test( UPDATE t1 SET x=7 WHERE x==2 ]], { -- <check-1.12> - 1, "Failed to execute SQL statement: CHECK constraint failed: T1" + 1, "Failed to execute SQL statement: CHECK constraint failed: CK_CONSTRAINT_1_T1" -- </check-1.12> }) @@ -167,7 +167,7 @@ test:do_catchsql_test( UPDATE t1 SET x=5 WHERE x==2 ]], { -- <check-1.14> - 1, "Failed to execute SQL statement: CHECK constraint failed: T1" + 1, "Failed to execute SQL statement: CHECK constraint failed: CK_CONSTRAINT_1_T1" -- </check-1.14> }) @@ -319,7 +319,7 @@ test:do_catchsql_test( ); ]], { -- <check-3.1> - 1, "Failed to create space 'T3': Subqueries are prohibited in a CHECK constraint definition" + 1, "Failed to create check constraint 'CK_CONSTRAINT_1_T3': Subqueries are prohibited in a check constraint definition" -- </check-3.1> }) @@ -344,7 +344,7 @@ test:do_catchsql_test( ); ]], { -- <check-3.3> - 1, "Failed to create space 'T3': Can’t resolve field 'Q'" + 1, "Failed to create check constraint 'CK_CONSTRAINT_1_T3': Can’t resolve field 'Q'" -- </check-3.3> }) @@ -368,7 +368,7 @@ test:do_catchsql_test( ); ]], { -- <check-3.5> - 1, "Failed to create space 'T3': Field 'X' was not found in the space 'T2' format" + 1, "Failed to create check constraint 'CK_CONSTRAINT_1_T3': Field 'X' was not found in the space 'T2' format" -- </check-3.5> }) @@ -413,7 +413,7 @@ test:do_catchsql_test( INSERT INTO t3 VALUES(111,222,333); ]], { -- <check-3.9> - 1, "Failed to execute SQL statement: CHECK constraint failed: T3" + 1, "Failed to execute SQL statement: CHECK constraint failed: CK_CONSTRAINT_1_T3" -- </check-3.9> }) @@ -484,7 +484,7 @@ test:do_catchsql_test( UPDATE t4 SET x=0, y=1; ]], { -- <check-4.6> - 1, "Failed to execute SQL statement: CHECK constraint failed: T4" + 1, "Failed to execute SQL statement: CHECK constraint failed: CK_CONSTRAINT_1_T4" -- </check-4.6> }) @@ -504,7 +504,7 @@ test:do_catchsql_test( UPDATE t4 SET x=0, y=2; ]], { -- <check-4.9> - 1, "Failed to execute SQL statement: CHECK constraint failed: T4" + 1, "Failed to execute SQL statement: CHECK constraint failed: CK_CONSTRAINT_1_T4" -- </check-4.9> }) @@ -516,7 +516,7 @@ test:do_catchsql_test( ); ]], { -- <check-5.1> - 1, "Wrong space options (field 5): invalid expression specified (bindings are not allowed in DDL)" + 1, "Failed to create check constraint 'CK_CONSTRAINT_1_T5': bindings are not allowed in DDL" -- </check-5.1> }) @@ -528,7 +528,7 @@ test:do_catchsql_test( ); ]], { -- <check-5.2> - 1, "Wrong space options (field 5): invalid expression specified (bindings are not allowed in DDL)" + 1, "Failed to create check constraint 'CK_CONSTRAINT_1_T5': bindings are not allowed in DDL" -- </check-5.2> }) @@ -581,7 +581,7 @@ test:do_catchsql_test( UPDATE OR FAIL t1 SET x=7-x, y=y+1; ]], { -- <check-6.5> - 1, "Failed to execute SQL statement: CHECK constraint failed: T1" + 1, "Failed to execute SQL statement: CHECK constraint failed: CK_CONSTRAINT_1_T1" -- </check-6.5> }) @@ -603,7 +603,7 @@ test:do_catchsql_test( INSERT OR ROLLBACK INTO t1 VALUES(8,40.0, 10); ]], { -- <check-6.7> - 1, "Failed to execute SQL statement: CHECK constraint failed: T1" + 1, "Failed to execute SQL statement: CHECK constraint failed: CK_CONSTRAINT_1_T1" -- </check-6.7> }) @@ -636,7 +636,7 @@ test:do_catchsql_test( REPLACE INTO t1 VALUES(6,7, 11); ]], { -- <check-6.12> - 1, "Failed to execute SQL statement: CHECK constraint failed: T1" + 1, "Failed to execute SQL statement: CHECK constraint failed: CK_CONSTRAINT_1_T1" -- </check-6.12> }) @@ -700,7 +700,7 @@ test:do_catchsql_test( 7.3, " INSERT INTO t6 VALUES(11) ", { -- <7.3> - 1, "Failed to execute SQL statement: CHECK constraint failed: T6" + 1, "Failed to execute SQL statement: CHECK constraint failed: CK_CONSTRAINT_1_T6" -- </7.3> }) diff --git a/test/sql-tap/fkey2.test.lua b/test/sql-tap/fkey2.test.lua index 54e5059b3..695a379a6 100755 --- a/test/sql-tap/fkey2.test.lua +++ b/test/sql-tap/fkey2.test.lua @@ -362,7 +362,7 @@ test:do_catchsql_test( UPDATE ab SET a = 5; ]], { -- <fkey2-3.2> - 1, "Failed to execute SQL statement: CHECK constraint failed: EF" + 1, "Failed to execute SQL statement: CHECK constraint failed: CK_CONSTRAINT_1_EF" -- </fkey2-3.2> }) @@ -382,7 +382,7 @@ test:do_catchsql_test( UPDATE ab SET a = 5; ]], { -- <fkey2-3.4> - 1, "Failed to execute SQL statement: CHECK constraint failed: EF" + 1, "Failed to execute SQL statement: CHECK constraint failed: CK_CONSTRAINT_1_EF" -- </fkey2-3.4> }) diff --git a/test/sql-tap/sql-errors.test.lua b/test/sql-tap/sql-errors.test.lua index 4e173b692..d742cf6cf 100755 --- a/test/sql-tap/sql-errors.test.lua +++ b/test/sql-tap/sql-errors.test.lua @@ -313,7 +313,7 @@ test:do_catchsql_test( CREATE TABLE t27 (i INT PRIMARY KEY, CHECK(i < (SELECT * FROM t0))); ]], { -- <sql-errors-1.27> - 1,"Failed to create space 'T27': Subqueries are prohibited in a CHECK constraint definition" + 1,"Failed to create check constraint 'CK_CONSTRAINT_1_T27': Subqueries are prohibited in a check constraint definition" -- </sql-errors-1.27> }) diff --git a/test/sql-tap/table.test.lua b/test/sql-tap/table.test.lua index 5b793c0fc..066662f33 100755 --- a/test/sql-tap/table.test.lua +++ b/test/sql-tap/table.test.lua @@ -1221,7 +1221,7 @@ test:do_catchsql_test( INSERT INTO T21 VALUES(1, -1, 1); ]], { -- <table-21.3> - 1, "Failed to execute SQL statement: CHECK constraint failed: T21" + 1, "Failed to execute SQL statement: CHECK constraint failed: CK_CONSTRAINT_1_T21" -- </table-21.3> }) @@ -1231,7 +1231,7 @@ test:do_catchsql_test( INSERT INTO T21 VALUES(1, 1, -1); ]], { -- <table-21.4> - 1, "Failed to execute SQL statement: CHECK constraint failed: T21" + 1, "Failed to execute SQL statement: CHECK constraint failed: CK_CONSTRAINT_2_T21" -- </table-21.4> }) diff --git a/test/sql/checks.result b/test/sql/checks.result index f7cddec43..8da1483f6 100644 --- a/test/sql/checks.result +++ b/test/sql/checks.result @@ -18,8 +18,10 @@ box.execute('pragma sql_default_engine=\''..engine..'\'') -- -- gh-3272: Move SQL CHECK into server -- --- invalid expression -opts = {checks = {{expr = 'X><5'}}} +-- Until Tarantool version 2.2 check constraints were stored in +-- space opts. +-- Make sure that now this legacy option is ignored. +opts = {checks = {{expr = 'X>5'}}} --- ... format = {{name = 'X', type = 'unsigned'}} @@ -30,89 +32,211 @@ t = {513, 1, 'test', 'memtx', 0, opts, format} ... s = box.space._space:insert(t) --- -- error: 'Wrong space options (field 5): invalid expression specified (Syntax error - near ''<'')' ... -opts = {checks = {{expr = 'X>5'}}} +_ = box.space.test:create_index('pk') --- ... -format = {{name = 'X', type = 'unsigned'}} +-- Invalid expression test. +box.space._ck_constraint:insert({'CK_CONSTRAINT_01', 513, false, 'X><5'}) --- +- error: 'Failed to create check constraint ''CK_CONSTRAINT_01'': Syntax error near + ''<''' ... -t = {513, 1, 'test', 'memtx', 0, opts, format} +-- Unexistent space test. +box.space._ck_constraint:insert({'CK_CONSTRAINT_01', 550, false, 'X<5'}) --- +- error: Space '550' does not exist ... -s = box.space._space:insert(t) +-- Pass integer instead of expression. +box.space._ck_constraint:insert({'CK_CONSTRAINT_01', 513, false, 666}) +--- +- error: 'Tuple field 4 type does not match one required by operation: expected string' +... +-- Defered CK constraints are not supported. +box.space._ck_constraint:insert({'CK_CONSTRAINT_01', 513, true, 'X<5'}) +--- +- error: Tarantool does not support deferred ck constraints +... +-- Check constraints LUA creation test. +box.space._ck_constraint:insert({'CK_CONSTRAINT_01', 513, false, 'X<5'}) +--- +- ['CK_CONSTRAINT_01', 513, false, 'X<5'] +... +box.space._ck_constraint:count({}) +--- +- 1 +... +box.execute("INSERT INTO \"test\" VALUES(5);") +--- +- error: 'Failed to execute SQL statement: CHECK constraint failed: CK_CONSTRAINT_01' +... +box.space._ck_constraint:replace({'CK_CONSTRAINT_01', 513, false, 'X<=5'}) +--- +- ['CK_CONSTRAINT_01', 513, false, 'X<=5'] +... +box.execute("INSERT INTO \"test\" VALUES(5);") +--- +- row_count: 1 +... +box.execute("INSERT INTO \"test\" VALUES(6);") +--- +- error: 'Failed to execute SQL statement: CHECK constraint failed: CK_CONSTRAINT_01' +... +-- Can't drop table with check constraints. +box.space.test:delete({5}) --- +- [5] ... -box.space._space:delete(513) +box.space.test.index.pk:drop() +--- +... +box.space._space:delete({513}) +--- +- error: 'Can''t drop space ''test'': the space has check constraints' +... +box.space._ck_constraint:delete({'CK_CONSTRAINT_01', 513}) +--- +- ['CK_CONSTRAINT_01', 513, false, 'X<=5'] +... +box.space._space:delete({513}) --- - [513, 1, 'test', 'memtx', 0, {'checks': [{'expr': 'X>5'}]}, [{'name': 'X', 'type': 'unsigned'}]] ... -opts = {checks = {{expr = 'X>5', name = 'ONE'}}} +-- Create table with checks in sql. +box.execute("CREATE TABLE t1(x INTEGER CONSTRAINT ONE CHECK( x<5 ), y REAL CONSTRAINT TWO CHECK( y>x ), z INTEGER PRIMARY KEY);") --- +- row_count: 1 ... -format = {{name = 'X', type = 'unsigned'}} +box.space._ck_constraint:count() --- +- 2 ... -t = {513, 1, 'test', 'memtx', 0, opts, format} +box.execute("INSERT INTO t1 VALUES (7, 1, 1)") --- +- error: 'Failed to execute SQL statement: CHECK constraint failed: ONE' ... -s = box.space._space:insert(t) +box.execute("INSERT INTO t1 VALUES (2, 1, 1)") --- +- error: 'Failed to execute SQL statement: CHECK constraint failed: TWO' ... -box.space._space:delete(513) +box.execute("INSERT INTO t1 VALUES (2, 4, 1)") --- -- [513, 1, 'test', 'memtx', 0, {'checks': [{'name': 'ONE', 'expr': 'X>5'}]}, [{'name': 'X', - 'type': 'unsigned'}]] +- row_count: 1 ... --- extra invlalid field name -opts = {checks = {{expr = 'X>5', name = 'ONE', extra = 'TWO'}}} +box.execute("DROP TABLE t1") --- +- row_count: 1 ... -format = {{name = 'X', type = 'unsigned'}} +-- Test space creation rollback on spell error in ck constraint. +box.execute("CREATE TABLE first (id FLOAT PRIMARY KEY CHECK(id < 5), a INT CONSTRAINT ONE CHECK(a >< 5));") --- +- error: Syntax error near '<' ... -t = {513, 1, 'test', 'memtx', 0, opts, format} +box.space.FIRST == nil --- +- true ... -s = box.space._space:insert(t) +box.space._ck_constraint:count() == 0 --- -- error: 'Wrong space options (field 5): invalid MsgPack map field ''extra''' +- true ... -opts = {checks = {{expr_invalid_label = 'X>5'}}} +-- Ck constraints are disallowed for spaces having no format. +s = box.schema.create_space('test', {engine = engine}) --- ... -format = {{name = 'X', type = 'unsigned'}} +_ = s:create_index('pk') --- ... -t = {513, 1, 'test', 'memtx', 0, opts, format} +_ = box.space._ck_constraint:insert({'physics', s.id, false, 'X<Y'}) --- +- error: Tarantool does not support CK constraint for space without format ... -s = box.space._space:insert(t) +s:format({{name='X', type='integer'}, {name='Y', type='integer'}}) --- -- error: 'Wrong space options (field 5): invalid MsgPack map field ''expr_invalid_label''' ... --- invalid field type -opts = {checks = {{name = 123}}} +_ = box.space._ck_constraint:insert({'physics', s.id, false, 'X<Y'}) --- ... -format = {{name = 'X', type = 'unsigned'}} +box.execute("INSERT INTO \"test\" VALUES(2, 1);") --- +- error: 'Failed to execute SQL statement: CHECK constraint failed: physics' ... -t = {513, 1, 'test', 'memtx', 0, opts, format} +s:format({{name='Y', type='integer'}, {name='X', type='integer'}}) --- ... -s = box.space._space:insert(t) +box.execute("INSERT INTO \"test\" VALUES(1, 2);") +--- +- error: 'Failed to execute SQL statement: CHECK constraint failed: physics' +... +box.execute("INSERT INTO \"test\" VALUES(2, 1);") +--- +- row_count: 1 +... +s:truncate() +--- +... +box.execute("INSERT INTO \"test\" VALUES(1, 2);") +--- +- error: 'Failed to execute SQL statement: CHECK constraint failed: physics' +... +s:format({}) +--- +- error: Tarantool does not support CK constraint for space without format +... +s:format() +--- +- [{'name': 'Y', 'type': 'integer'}, {'name': 'X', 'type': 'integer'}] +... +s:format({{name='Y1', type='integer'}, {name='X1', type='integer'}}) +--- +- error: 'Failed to create check constraint ''physics'': Can’t resolve field ''X''' +... +-- Ck constraint creation is forbidden for non-empty space +s:insert({2, 1}) +--- +- [2, 1] +... +_ = box.space._ck_constraint:insert({'conflict', s.id, false, 'X>10'}) +--- +- error: 'Failed to create check constraint ''conflict'': referencing space must be + empty' +... +s:truncate() +--- +... +_ = box.space._ck_constraint:insert({'conflict', s.id, false, 'X>10'}) +--- +... +box.execute("INSERT INTO \"test\" VALUES(1, 2);") --- -- error: 'Wrong space options (field 5): invalid MsgPack map field ''name'' type' +- error: 'Failed to execute SQL statement: CHECK constraint failed: conflict' +... +box.execute("INSERT INTO \"test\" VALUES(11, 11);") +--- +- error: 'Failed to execute SQL statement: CHECK constraint failed: physics' +... +box.execute("INSERT INTO \"test\" VALUES(12, 11);") +--- +- row_count: 1 +... +s:drop() +--- +... +box.execute("CREATE TABLE T2(ID INT PRIMARY KEY, CONSTRAINT CK1 CHECK(ID > 0), CONSTRAINT CK1 CHECK(ID < 0))") +--- +- error: Duplicate key exists in unique index 'primary' in space '_ck_constraint' +... +box.space._ck_constraint:select() +--- +- [] ... -- -- gh-3611: Segfault on table creation with check referencing this table -- box.execute("CREATE TABLE w2 (s1 INT PRIMARY KEY, CHECK ((SELECT COUNT(*) FROM w2) = 0));") --- -- error: 'Failed to create space ''W2'': Space ''W2'' does not exist' +- error: 'Failed to create check constraint ''CK_CONSTRAINT_1_W2'': Subqueries are + prohibited in a check constraint definition' ... box.execute("DROP TABLE w2;") --- @@ -123,22 +247,8 @@ box.execute("DROP TABLE w2;") -- box.execute("CREATE TABLE t5(x INT PRIMARY KEY, y INT, CHECK( x*y < ? ));") --- -- error: 'Wrong space options (field 5): invalid expression specified (bindings are - not allowed in DDL)' -... -opts = {checks = {{expr = '?>5', name = 'ONE'}}} ---- -... -format = {{name = 'X', type = 'unsigned'}} ---- -... -t = {513, 1, 'test', 'memtx', 0, opts, format} ---- -... -s = box.space._space:insert(t) ---- -- error: 'Wrong space options (field 5): invalid expression specified (bindings are - not allowed in DDL)' +- error: 'Failed to create check constraint ''CK_CONSTRAINT_1_T5'': bindings are not + allowed in DDL' ... test_run:cmd("clear filter") --- diff --git a/test/sql/checks.test.lua b/test/sql/checks.test.lua index 5bfcf12f8..4029359b1 100644 --- a/test/sql/checks.test.lua +++ b/test/sql/checks.test.lua @@ -8,41 +8,79 @@ box.execute('pragma sql_default_engine=\''..engine..'\'') -- gh-3272: Move SQL CHECK into server -- --- invalid expression -opts = {checks = {{expr = 'X><5'}}} -format = {{name = 'X', type = 'unsigned'}} -t = {513, 1, 'test', 'memtx', 0, opts, format} -s = box.space._space:insert(t) - +-- Until Tarantool version 2.2 check constraints were stored in +-- space opts. +-- Make sure that now this legacy option is ignored. opts = {checks = {{expr = 'X>5'}}} format = {{name = 'X', type = 'unsigned'}} t = {513, 1, 'test', 'memtx', 0, opts, format} s = box.space._space:insert(t) -box.space._space:delete(513) +_ = box.space.test:create_index('pk') -opts = {checks = {{expr = 'X>5', name = 'ONE'}}} -format = {{name = 'X', type = 'unsigned'}} -t = {513, 1, 'test', 'memtx', 0, opts, format} -s = box.space._space:insert(t) -box.space._space:delete(513) +-- Invalid expression test. +box.space._ck_constraint:insert({'CK_CONSTRAINT_01', 513, false, 'X><5'}) +-- Unexistent space test. +box.space._ck_constraint:insert({'CK_CONSTRAINT_01', 550, false, 'X<5'}) +-- Pass integer instead of expression. +box.space._ck_constraint:insert({'CK_CONSTRAINT_01', 513, false, 666}) +-- Defered CK constraints are not supported. +box.space._ck_constraint:insert({'CK_CONSTRAINT_01', 513, true, 'X<5'}) --- extra invlalid field name -opts = {checks = {{expr = 'X>5', name = 'ONE', extra = 'TWO'}}} -format = {{name = 'X', type = 'unsigned'}} -t = {513, 1, 'test', 'memtx', 0, opts, format} -s = box.space._space:insert(t) +-- Check constraints LUA creation test. +box.space._ck_constraint:insert({'CK_CONSTRAINT_01', 513, false, 'X<5'}) +box.space._ck_constraint:count({}) -opts = {checks = {{expr_invalid_label = 'X>5'}}} -format = {{name = 'X', type = 'unsigned'}} -t = {513, 1, 'test', 'memtx', 0, opts, format} -s = box.space._space:insert(t) +box.execute("INSERT INTO \"test\" VALUES(5);") +box.space._ck_constraint:replace({'CK_CONSTRAINT_01', 513, false, 'X<=5'}) +box.execute("INSERT INTO \"test\" VALUES(5);") +box.execute("INSERT INTO \"test\" VALUES(6);") +-- Can't drop table with check constraints. +box.space.test:delete({5}) +box.space.test.index.pk:drop() +box.space._space:delete({513}) +box.space._ck_constraint:delete({'CK_CONSTRAINT_01', 513}) +box.space._space:delete({513}) --- invalid field type -opts = {checks = {{name = 123}}} -format = {{name = 'X', type = 'unsigned'}} -t = {513, 1, 'test', 'memtx', 0, opts, format} -s = box.space._space:insert(t) +-- Create table with checks in sql. +box.execute("CREATE TABLE t1(x INTEGER CONSTRAINT ONE CHECK( x<5 ), y REAL CONSTRAINT TWO CHECK( y>x ), z INTEGER PRIMARY KEY);") +box.space._ck_constraint:count() +box.execute("INSERT INTO t1 VALUES (7, 1, 1)") +box.execute("INSERT INTO t1 VALUES (2, 1, 1)") +box.execute("INSERT INTO t1 VALUES (2, 4, 1)") +box.execute("DROP TABLE t1") + +-- Test space creation rollback on spell error in ck constraint. +box.execute("CREATE TABLE first (id FLOAT PRIMARY KEY CHECK(id < 5), a INT CONSTRAINT ONE CHECK(a >< 5));") +box.space.FIRST == nil +box.space._ck_constraint:count() == 0 + +-- Ck constraints are disallowed for spaces having no format. +s = box.schema.create_space('test', {engine = engine}) +_ = s:create_index('pk') +_ = box.space._ck_constraint:insert({'physics', s.id, false, 'X<Y'}) +s:format({{name='X', type='integer'}, {name='Y', type='integer'}}) +_ = box.space._ck_constraint:insert({'physics', s.id, false, 'X<Y'}) +box.execute("INSERT INTO \"test\" VALUES(2, 1);") +s:format({{name='Y', type='integer'}, {name='X', type='integer'}}) +box.execute("INSERT INTO \"test\" VALUES(1, 2);") +box.execute("INSERT INTO \"test\" VALUES(2, 1);") +s:truncate() +box.execute("INSERT INTO \"test\" VALUES(1, 2);") +s:format({}) +s:format() +s:format({{name='Y1', type='integer'}, {name='X1', type='integer'}}) +-- Ck constraint creation is forbidden for non-empty space +s:insert({2, 1}) +_ = box.space._ck_constraint:insert({'conflict', s.id, false, 'X>10'}) +s:truncate() +_ = box.space._ck_constraint:insert({'conflict', s.id, false, 'X>10'}) +box.execute("INSERT INTO \"test\" VALUES(1, 2);") +box.execute("INSERT INTO \"test\" VALUES(11, 11);") +box.execute("INSERT INTO \"test\" VALUES(12, 11);") +s:drop() +box.execute("CREATE TABLE T2(ID INT PRIMARY KEY, CONSTRAINT CK1 CHECK(ID > 0), CONSTRAINT CK1 CHECK(ID < 0))") +box.space._ck_constraint:select() -- -- gh-3611: Segfault on table creation with check referencing this table @@ -55,10 +93,4 @@ box.execute("DROP TABLE w2;") -- box.execute("CREATE TABLE t5(x INT PRIMARY KEY, y INT, CHECK( x*y < ? ));") -opts = {checks = {{expr = '?>5', name = 'ONE'}}} -format = {{name = 'X', type = 'unsigned'}} -t = {513, 1, 'test', 'memtx', 0, opts, format} -s = box.space._space:insert(t) - - test_run:cmd("clear filter") diff --git a/test/sql/errinj.result b/test/sql/errinj.result index ee36a387b..18d1f3882 100644 --- a/test/sql/errinj.result +++ b/test/sql/errinj.result @@ -463,3 +463,137 @@ errinj.set("ERRINJ_SQL_NAME_NORMALIZATION", false) --- - ok ... +-- +-- Tests which are aimed at verifying work of commit/rollback +-- triggers on _ck_constraint space. +-- +s = box.schema.space.create('test', {format = {{name = 'X', type = 'unsigned'}}}) +--- +... +pk = box.space.test:create_index('pk') +--- +... +errinj.set("ERRINJ_WAL_IO", true) +--- +- ok +... +_ = box.space._ck_constraint:insert({'CK_CONSTRAINT_01', s.id, false, 'X<5'}) +--- +- error: Failed to write to disk +... +errinj.set("ERRINJ_WAL_IO", false) +--- +- ok +... +_ = box.space._ck_constraint:insert({'CK_CONSTRAINT_01', s.id, false, 'X<5'}) +--- +... +box.execute("INSERT INTO \"test\" VALUES(5);") +--- +- error: 'Failed to execute SQL statement: CHECK constraint failed: CK_CONSTRAINT_01' +... +errinj.set("ERRINJ_WAL_IO", true) +--- +- ok +... +_ = box.space._ck_constraint:replace({'CK_CONSTRAINT_01', s.id, false, 'X<=5'}) +--- +- error: Failed to write to disk +... +errinj.set("ERRINJ_WAL_IO", false) +--- +- ok +... +_ = box.space._ck_constraint:replace({'CK_CONSTRAINT_01', s.id, false, 'X<=5'}) +--- +... +box.execute("INSERT INTO \"test\" VALUES(5);") +--- +- row_count: 1 +... +errinj.set("ERRINJ_WAL_IO", true) +--- +- ok +... +_ = box.space._ck_constraint:delete({'CK_CONSTRAINT_01', s.id}) +--- +- error: Failed to write to disk +... +box.execute("INSERT INTO \"test\" VALUES(6);") +--- +- error: 'Failed to execute SQL statement: CHECK constraint failed: CK_CONSTRAINT_01' +... +errinj.set("ERRINJ_WAL_IO", false) +--- +- ok +... +_ = box.space._ck_constraint:delete({'CK_CONSTRAINT_01', s.id}) +--- +... +box.execute("INSERT INTO \"test\" VALUES(6);") +--- +- row_count: 1 +... +s:drop() +--- +... +-- +-- Test that failed space alter doesn't harm ck constraints +-- +s = box.schema.create_space('test') +--- +... +_ = s:create_index('pk') +--- +... +s:format({{name='X', type='integer'}, {name='Y', type='integer'}}) +--- +... +_ = box.space._ck_constraint:insert({'XlessY', s.id, false, 'X < Y'}) +--- +... +_ = box.space._ck_constraint:insert({'Xgreater10', s.id, false, 'X > 10'}) +--- +... +box.execute("INSERT INTO \"test\" VALUES(2, 1);") +--- +- error: 'Failed to execute SQL statement: CHECK constraint failed: Xgreater10' +... +box.execute("INSERT INTO \"test\" VALUES(20, 10);") +--- +- error: 'Failed to execute SQL statement: CHECK constraint failed: XlessY' +... +box.execute("INSERT INTO \"test\" VALUES(20, 100);") +--- +- row_count: 1 +... +s:truncate() +--- +... +errinj.set("ERRINJ_WAL_IO", true) +--- +- ok +... +s:format({{name='Y', type='integer'}, {name='X', type='integer'}}) +--- +- error: Failed to write to disk +... +errinj.set("ERRINJ_WAL_IO", false) +--- +- ok +... +box.execute("INSERT INTO \"test\" VALUES(2, 1);") +--- +- error: 'Failed to execute SQL statement: CHECK constraint failed: XlessY' +... +box.execute("INSERT INTO \"test\" VALUES(20, 10);") +--- +- error: 'Failed to execute SQL statement: CHECK constraint failed: XlessY' +... +box.execute("INSERT INTO \"test\" VALUES(20, 100);") +--- +- row_count: 1 +... +s:drop() +--- +... diff --git a/test/sql/errinj.test.lua b/test/sql/errinj.test.lua index 1aff6d77e..7e5550541 100644 --- a/test/sql/errinj.test.lua +++ b/test/sql/errinj.test.lua @@ -149,3 +149,48 @@ box.execute("CREATE TABLE hello (id INT primary key,x INT,y INT);") dummy_f = function(int) return 1 end box.internal.sql_create_function("counter1", "INT", dummy_f, -1, false) errinj.set("ERRINJ_SQL_NAME_NORMALIZATION", false) + +-- +-- Tests which are aimed at verifying work of commit/rollback +-- triggers on _ck_constraint space. +-- +s = box.schema.space.create('test', {format = {{name = 'X', type = 'unsigned'}}}) +pk = box.space.test:create_index('pk') + +errinj.set("ERRINJ_WAL_IO", true) +_ = box.space._ck_constraint:insert({'CK_CONSTRAINT_01', s.id, false, 'X<5'}) +errinj.set("ERRINJ_WAL_IO", false) +_ = box.space._ck_constraint:insert({'CK_CONSTRAINT_01', s.id, false, 'X<5'}) +box.execute("INSERT INTO \"test\" VALUES(5);") +errinj.set("ERRINJ_WAL_IO", true) +_ = box.space._ck_constraint:replace({'CK_CONSTRAINT_01', s.id, false, 'X<=5'}) +errinj.set("ERRINJ_WAL_IO", false) +_ = box.space._ck_constraint:replace({'CK_CONSTRAINT_01', s.id, false, 'X<=5'}) +box.execute("INSERT INTO \"test\" VALUES(5);") +errinj.set("ERRINJ_WAL_IO", true) +_ = box.space._ck_constraint:delete({'CK_CONSTRAINT_01', s.id}) +box.execute("INSERT INTO \"test\" VALUES(6);") +errinj.set("ERRINJ_WAL_IO", false) +_ = box.space._ck_constraint:delete({'CK_CONSTRAINT_01', s.id}) +box.execute("INSERT INTO \"test\" VALUES(6);") +s:drop() + +-- +-- Test that failed space alter doesn't harm ck constraints +-- +s = box.schema.create_space('test') +_ = s:create_index('pk') +s:format({{name='X', type='integer'}, {name='Y', type='integer'}}) +_ = box.space._ck_constraint:insert({'XlessY', s.id, false, 'X < Y'}) +_ = box.space._ck_constraint:insert({'Xgreater10', s.id, false, 'X > 10'}) +box.execute("INSERT INTO \"test\" VALUES(2, 1);") +box.execute("INSERT INTO \"test\" VALUES(20, 10);") +box.execute("INSERT INTO \"test\" VALUES(20, 100);") +s:truncate() +errinj.set("ERRINJ_WAL_IO", true) +s:format({{name='Y', type='integer'}, {name='X', type='integer'}}) +errinj.set("ERRINJ_WAL_IO", false) +box.execute("INSERT INTO \"test\" VALUES(2, 1);") +box.execute("INSERT INTO \"test\" VALUES(20, 10);") +box.execute("INSERT INTO \"test\" VALUES(20, 100);") +s:drop() diff --git a/test/sql/gh-2981-check-autoinc.result b/test/sql/gh-2981-check-autoinc.result index 9e347ca6b..7384c81e8 100644 --- a/test/sql/gh-2981-check-autoinc.result +++ b/test/sql/gh-2981-check-autoinc.result @@ -29,7 +29,7 @@ box.execute("insert into t1 values (18, null);") ... box.execute("insert into t1(s2) values (null);") --- -- error: 'Failed to execute SQL statement: CHECK constraint failed: T1' +- error: 'Failed to execute SQL statement: CHECK constraint failed: CK_CONSTRAINT_1_T1' ... box.execute("insert into t2 values (18, null);") --- @@ -37,7 +37,7 @@ box.execute("insert into t2 values (18, null);") ... box.execute("insert into t2(s2) values (null);") --- -- error: 'Failed to execute SQL statement: CHECK constraint failed: T2' +- error: 'Failed to execute SQL statement: CHECK constraint failed: CK_CONSTRAINT_1_T2' ... box.execute("insert into t2 values (24, null);") --- @@ -45,7 +45,7 @@ box.execute("insert into t2 values (24, null);") ... box.execute("insert into t2(s2) values (null);") --- -- error: 'Failed to execute SQL statement: CHECK constraint failed: T2' +- error: 'Failed to execute SQL statement: CHECK constraint failed: CK_CONSTRAINT_1_T2' ... box.execute("insert into t3 values (9, null)") --- @@ -53,7 +53,7 @@ box.execute("insert into t3 values (9, null)") ... box.execute("insert into t3(s2) values (null)") --- -- error: 'Failed to execute SQL statement: CHECK constraint failed: T3' +- error: 'Failed to execute SQL statement: CHECK constraint failed: CK_CONSTRAINT_1_T3' ... box.execute("DROP TABLE t1") --- diff --git a/test/sql/types.result b/test/sql/types.result index bc4518c01..582785413 100644 --- a/test/sql/types.result +++ b/test/sql/types.result @@ -709,7 +709,7 @@ box.execute("CREATE TABLE t1 (id INT PRIMARY KEY, a BOOLEAN CHECK (a = true));") ... box.execute("INSERT INTO t1 VALUES (1, false);") --- -- error: 'Failed to execute SQL statement: CHECK constraint failed: T1' +- error: 'Failed to execute SQL statement: CHECK constraint failed: CK_CONSTRAINT_1_T1' ... box.execute("INSERT INTO t1 VALUES (1, true);") --- diff --git a/test/sql/upgrade.result b/test/sql/upgrade.result index 3a55f7c53..5da110c14 100644 --- a/test/sql/upgrade.result +++ b/test/sql/upgrade.result @@ -188,6 +188,25 @@ i[1].opts.sql == nil --- - true ... +box.space._space:get(s.id).flags.checks == nil +--- +- true +... +check = box.space._ck_constraint:select()[1] +--- +... +check ~= nil +--- +- true +... +check.name +--- +- CK_CONSTRAINT_1_T5 +... +check.expr_str +--- +- x < 2 +... s:drop() --- ... diff --git a/test/sql/upgrade.test.lua b/test/sql/upgrade.test.lua index b76a8f373..cfda74a08 100644 --- a/test/sql/upgrade.test.lua +++ b/test/sql/upgrade.test.lua @@ -62,6 +62,11 @@ s ~= nil i = box.space._index:select(s.id) i ~= nil i[1].opts.sql == nil +box.space._space:get(s.id).flags.checks == nil +check = box.space._ck_constraint:select()[1] +check ~= nil +check.name +check.expr_str s:drop() test_run:switch('default') diff --git a/test/wal_off/alter.result b/test/wal_off/alter.result index ee280fcbb..8040efa1a 100644 --- a/test/wal_off/alter.result +++ b/test/wal_off/alter.result @@ -28,7 +28,7 @@ end; ... #spaces; --- -- 65505 +- 65504 ... -- cleanup for k, v in pairs(spaces) do -- 2.21.0 This patch introduces a new system space to persist check constraints. Format of the space: _ck_constraint (space id = 357) [<constraint name> STR, <space id> UINT, <expression string>STR] CK constraint is local to space, so every pair <CK name, space id> is unique (and it is PK in _ck_constraint space). After insertion into this space, a new instance describing check constraint is created. Check constraint holds AST of expression representing it. While space features check constraints, it isn't allowed to be dropped. The :drop() space method firstly deletes all check constraints and then removes entry from _space. Because space alter, index alter and space truncate operations cause space recreation, introduced RebuildCkConstrains object that compile new ck constraint objects, replace and remove existent instances atomically(when some compilation fails, nothing changed). In fact, in scope of this patch we don't really need to recreate ck_constraint object in such situations (patch space_def pointer in AST like we did it before is enough now, but we would recompile VDBE that represents ck constraint in future that can fail). The main motivation for these changes is the ability to support ADD CHECK CONSTRAINT operation in the future. CK constraints are are easier to manage as self-sustained objects: we mustn't the tuple describing target space to do it(unlike the current architecture). Disabled xfer optimization when some space have ck constraints because in the following patches this xfer optimisation becomes impossible. No reason to rewrite this code. Needed for #3691 --- src/box/CMakeLists.txt | 1 + src/box/alter.cc | 251 ++++++++++++++++++++++++-- src/box/alter.h | 1 + src/box/bootstrap.snap | Bin 4374 -> 4418 bytes src/box/ck_constraint.c | 114 ++++++++++++ src/box/ck_constraint.h | 163 +++++++++++++++++ src/box/errcode.h | 4 +- src/box/lua/schema.lua | 4 + src/box/lua/space.cc | 2 + src/box/lua/upgrade.lua | 43 +++++ src/box/schema.cc | 8 + src/box/schema_def.h | 10 + src/box/space.c | 2 + src/box/space.h | 5 + src/box/space_def.c | 98 +--------- src/box/space_def.h | 2 - src/box/sql.c | 86 +-------- src/box/sql.h | 36 ---- src/box/sql/build.c | 208 +++++++++++++++++---- src/box/sql/insert.c | 53 +++--- src/box/sql/parse.y | 2 +- src/box/sql/parse_def.h | 24 +++ src/box/sql/select.c | 11 +- src/box/sql/sqlInt.h | 2 +- src/box/sql/tokenize.c | 1 - test/app-tap/tarantoolctl.test.lua | 4 +- test/box-py/bootstrap.result | 7 +- test/box/access.result | 3 + test/box/access.test.lua | 1 + test/box/access_misc.result | 3 + test/box/access_sysview.result | 6 +- test/box/alter.result | 6 +- test/box/misc.result | 1 + test/sql-tap/check.test.lua | 32 ++-- test/sql-tap/fkey2.test.lua | 4 +- test/sql-tap/sql-errors.test.lua | 2 +- test/sql-tap/table.test.lua | 4 +- test/sql/checks.result | 210 ++++++++++++++++----- test/sql/checks.test.lua | 96 ++++++---- test/sql/errinj.result | 134 ++++++++++++++ test/sql/errinj.test.lua | 45 +++++ test/sql/gh-2981-check-autoinc.result | 8 +- test/sql/types.result | 2 +- test/sql/upgrade.result | 19 ++ test/sql/upgrade.test.lua | 5 + test/wal_off/alter.result | 2 +- 46 files changed, 1305 insertions(+), 420 deletions(-) create mode 100644 src/box/ck_constraint.c create mode 100644 src/box/ck_constraint.h diff --git a/src/box/CMakeLists.txt b/src/box/CMakeLists.txt index 2be0d1e35..4736a40b4 100644 --- a/src/box/CMakeLists.txt +++ b/src/box/CMakeLists.txt @@ -96,6 +96,7 @@ add_library(box STATIC space.c space_def.c sequence.c + ck_constraint.c fk_constraint.c func.c func_def.c diff --git a/src/box/alter.cc b/src/box/alter.cc index 9279426d2..2126ab369 100644 --- a/src/box/alter.cc +++ b/src/box/alter.cc @@ -29,6 +29,7 @@ * SUCH DAMAGE. */ #include "alter.h" +#include "ck_constraint.h" #include "column_mask.h" #include "schema.h" #include "user.h" @@ -529,17 +530,6 @@ space_def_new_from_tuple(struct tuple *tuple, uint32_t errcode, engine_name, engine_name_len, &opts, fields, field_count); auto def_guard = make_scoped_guard([=] { space_def_delete(def); }); - if (def->opts.checks != NULL && - sql_checks_resolve_space_def_reference(def->opts.checks, - def) != 0) { - box_error_t *err = box_error_last(); - if (box_error_code(err) != ENOMEM) { - tnt_raise(ClientError, errcode, def->name, - box_error_message(err)); - } else { - diag_raise(); - } - } struct engine *engine = engine_find_xc(def->engine_name); engine_check_space_def_xc(engine, def); def_guard.is_active = false; @@ -1380,6 +1370,71 @@ UpdateSchemaVersion::alter(struct alter_space *alter) ++schema_version; } +/** + * As ck_constraint object depends on space_def we must rebuild + * all ck constraints on space alter. + * + * To perform it transactionally, we create a list of a new ck + * constraints objects in ::prepare method that is fault-tolerant. + * Finally in ::alter or ::rollback methods we only swap thouse + * lists securely. + */ +class RebuildCkConstraints: public AlterSpaceOp +{ +public: + RebuildCkConstraints(struct alter_space *alter) : AlterSpaceOp(alter), + ck_constraint(RLIST_HEAD_INITIALIZER(ck_constraint)) {} + struct rlist ck_constraint; + virtual void prepare(struct alter_space *alter); + virtual void alter(struct alter_space *alter); + virtual void rollback(struct alter_space *alter); + virtual ~RebuildCkConstraints(); +}; + +void +RebuildCkConstraints::prepare(struct alter_space *alter) +{ + struct ck_constraint *old_ck_constraint; + rlist_foreach_entry(old_ck_constraint, &alter->old_space->ck_constraint, + link) { + struct ck_constraint *new_ck_constraint = + ck_constraint_new(old_ck_constraint->def, + alter->new_space->def); + if (new_ck_constraint == NULL) + diag_raise(); + rlist_add_entry(&ck_constraint, new_ck_constraint, link); + } +} + +void +RebuildCkConstraints::alter(struct alter_space *alter) +{ + rlist_swap(&alter->new_space->ck_constraint, &ck_constraint); + rlist_swap(&ck_constraint, &alter->old_space->ck_constraint); +} + +void +RebuildCkConstraints::rollback(struct alter_space *alter) +{ + rlist_swap(&alter->old_space->ck_constraint, &ck_constraint); + rlist_swap(&ck_constraint, &alter->new_space->ck_constraint); +} + +RebuildCkConstraints::~RebuildCkConstraints() +{ + struct ck_constraint *old_ck_constraint, *tmp; + rlist_foreach_entry_safe(old_ck_constraint, &ck_constraint, link, tmp) { + /** + * Ck constraint definition is now managed by + * other Ck constraint object. Prevent it's + * destruction as a part of ck_constraint_delete + * call. + */ + old_ck_constraint->def = NULL; + ck_constraint_delete(old_ck_constraint); + } +} + /* }}} */ /** @@ -1745,6 +1800,11 @@ on_replace_dd_space(struct trigger * /* trigger */, void *event) space_name(old_space), "the space has foreign key constraints"); } + if (!rlist_empty(&old_space->ck_constraint)) { + tnt_raise(ClientError, ER_DROP_SPACE, + space_name(old_space), + "the space has check constraints"); + } /** * The space must be deleted from the space * cache right away to achieve linearisable @@ -1842,6 +1902,7 @@ on_replace_dd_space(struct trigger * /* trigger */, void *event) def->field_count); (void) new CheckSpaceFormat(alter); (void) new ModifySpace(alter, def); + (void) new RebuildCkConstraints(alter); def_guard.is_active = false; /* Create MoveIndex ops for all space indexes. */ alter_space_move_indexes(alter, 0, old_space->index_id_max + 1); @@ -2085,6 +2146,7 @@ on_replace_dd_index(struct trigger * /* trigger */, void *event) */ alter_space_move_indexes(alter, iid + 1, old_space->index_id_max + 1); /* Add an op to update schema_version on commit. */ + (void) new RebuildCkConstraints(alter); (void) new UpdateSchemaVersion(alter); alter_space_do(txn, alter); scoped_guard.is_active = false; @@ -2152,6 +2214,7 @@ on_replace_dd_truncate(struct trigger * /* trigger */, void *event) (void) new TruncateIndex(alter, old_index->def->iid); } + (void) new RebuildCkConstraints(alter); alter_space_do(txn, alter); scoped_guard.is_active = false; } @@ -4035,6 +4098,168 @@ on_replace_dd_fk_constraint(struct trigger * /* trigger*/, void *event) } } +/** Create an instance of check constraint definition by tuple. */ +static struct ck_constraint_def * +ck_constraint_def_new_from_tuple(struct tuple *tuple) +{ + uint32_t name_len; + const char *name = + tuple_field_str_xc(tuple, BOX_CK_CONSTRAINT_FIELD_NAME, + &name_len); + if (name_len > BOX_NAME_MAX) { + tnt_raise(ClientError, ER_CREATE_CK_CONSTRAINT, + tt_cstr(name, BOX_INVALID_NAME_MAX), + "check constraint name is too long"); + } + identifier_check_xc(name, name_len); + uint32_t expr_str_len; + const char *expr_str = + tuple_field_str_xc(tuple, BOX_CK_CONSTRAINT_FIELD_EXPR_STR, + &expr_str_len); + + uint32_t name_offset, expr_str_offset; + uint32_t ck_def_sz = + ck_constraint_def_sizeof(name_len, expr_str_len, &name_offset, + &expr_str_offset); + struct ck_constraint_def *ck_def = + (struct ck_constraint_def *)malloc(ck_def_sz); + if (ck_def == NULL) + tnt_raise(OutOfMemory, ck_def_sz, "malloc", "ck_def"); + + ck_def->name = (char *)ck_def + name_offset; + ck_def->expr_str = (char *)ck_def + expr_str_offset; + memcpy(ck_def->expr_str, expr_str, expr_str_len); + ck_def->expr_str[expr_str_len] = '\0'; + memcpy(ck_def->name, name, name_len); + ck_def->name[name_len] = '\0'; + + return ck_def; +} + +/** Trigger invoked on rollback in the _ck_constraint space. */ +static void +on_replace_ck_constraint_rollback(struct trigger *trigger, void *event) +{ + struct txn_stmt *stmt = txn_last_stmt((struct txn *) event); + struct ck_constraint *ck = (struct ck_constraint *)trigger->data; + struct space *space = NULL; + if (ck != NULL) + space = space_by_id(ck->space_id); + if (stmt->old_tuple != NULL && stmt->new_tuple == NULL) { + /* Rollback DELETE check constraint. */ + assert(ck != NULL); + assert(space != NULL); + assert(space_ck_constraint_by_name(space, + ck->def->name, strlen(ck->def->name)) == NULL); + rlist_add_entry(&space->ck_constraint, ck, link); + } else if (stmt->new_tuple != NULL && stmt->old_tuple == NULL) { + /* Rollback INSERT check constraint. */ + assert(space != NULL); + assert(space_ck_constraint_by_name(space, + ck->def->name, strlen(ck->def->name)) != NULL); + rlist_del_entry(ck, link); + ck_constraint_delete(ck); + } else { + /* Rollback REPLACE check constraint. */ + assert(space != NULL); + const char *name = ck->def->name; + struct ck_constraint *new_ck = + space_ck_constraint_by_name(space, name, strlen(name)); + assert(new_ck != NULL); + rlist_del_entry(new_ck, link); + rlist_add_entry(&space->ck_constraint, ck, link); + ck_constraint_delete(new_ck); + } +} + +/** + * Trigger invoked on commit in the _ck_constraint space. + * Drop useless old check constraint object if exists. + */ +static void +on_replace_ck_constraint_commit(struct trigger *trigger, void *event) +{ + struct txn_stmt *stmt = txn_last_stmt((struct txn *) event); + struct ck_constraint *ck = (struct ck_constraint *)trigger->data; + if (stmt->old_tuple != NULL) + ck_constraint_delete(ck); +} + +/** A trigger invoked on replace in the _ck_constraint space. */ +static void +on_replace_dd_ck_constraint(struct trigger * /* trigger*/, void *event) +{ + struct txn *txn = (struct txn *) event; + txn_check_singlestatement_xc(txn, "Space _ck_constraint"); + struct txn_stmt *stmt = txn_current_stmt(txn); + struct tuple *old_tuple = stmt->old_tuple; + struct tuple *new_tuple = stmt->new_tuple; + uint32_t space_id = + tuple_field_u32_xc(old_tuple != NULL ? old_tuple : new_tuple, + BOX_CK_CONSTRAINT_FIELD_SPACE_ID); + struct space *space = space_cache_find_xc(space_id); + struct trigger *on_rollback = + txn_alter_trigger_new(on_replace_ck_constraint_rollback, NULL); + struct trigger *on_commit = + txn_alter_trigger_new(on_replace_ck_constraint_commit, NULL); + + if (new_tuple != NULL) { + bool is_deferred = + tuple_field_bool_xc(new_tuple, + BOX_CK_CONSTRAINT_FIELD_DEFERRED); + if (is_deferred) { + tnt_raise(ClientError, ER_UNSUPPORTED, "Tarantool", + "deferred ck constraints"); + } + /* Create or replace check constraint. */ + struct ck_constraint_def *ck_def = + ck_constraint_def_new_from_tuple(new_tuple); + auto ck_guard = make_scoped_guard([=] { free(ck_def); }); + /* + * FIXME: Ck constraint creation on non-empty + * space must be implemented as preparatory + * step for ALTER SPACE ADD CONSTRAINT feature. + */ + struct index *pk = space_index(space, 0); + if (pk != NULL && index_size(pk) > 0) { + tnt_raise(ClientError, ER_CREATE_CK_CONSTRAINT, + ck_def->name, + "referencing space must be empty"); + } + struct ck_constraint *new_ck_constraint = + ck_constraint_new(ck_def, space->def); + if (new_ck_constraint == NULL) + diag_raise(); + ck_guard.is_active = false; + const char *name = new_ck_constraint->def->name; + struct ck_constraint *old_ck_constraint = + space_ck_constraint_by_name(space, name, strlen(name)); + if (old_ck_constraint != NULL) + rlist_del_entry(old_ck_constraint, link); + rlist_add_entry(&space->ck_constraint, new_ck_constraint, link); + on_commit->data = old_tuple == NULL ? new_ck_constraint : + old_ck_constraint; + on_rollback->data = on_commit->data; + } else { + assert(new_tuple == NULL && old_tuple != NULL); + /* Drop check constraint. */ + uint32_t name_len; + const char *name = + tuple_field_str_xc(old_tuple, + BOX_CK_CONSTRAINT_FIELD_NAME, + &name_len); + struct ck_constraint *old_ck_constraint = + space_ck_constraint_by_name(space, name, name_len); + assert(old_ck_constraint != NULL); + rlist_del_entry(old_ck_constraint, link); + on_commit->data = old_ck_constraint; + on_rollback->data = old_ck_constraint; + } + + txn_on_rollback(txn, on_rollback); + txn_on_commit(txn, on_commit); +} + struct trigger alter_space_on_replace_space = { RLIST_LINK_INITIALIZER, on_replace_dd_space, NULL, NULL }; @@ -4103,4 +4328,8 @@ struct trigger on_replace_fk_constraint = { RLIST_LINK_INITIALIZER, on_replace_dd_fk_constraint, NULL, NULL }; +struct trigger on_replace_ck_constraint = { + RLIST_LINK_INITIALIZER, on_replace_dd_ck_constraint, NULL, NULL +}; + /* vim: set foldmethod=marker */ diff --git a/src/box/alter.h b/src/box/alter.h index 4108fa47c..b9ba7b846 100644 --- a/src/box/alter.h +++ b/src/box/alter.h @@ -46,6 +46,7 @@ extern struct trigger on_replace_sequence_data; extern struct trigger on_replace_space_sequence; extern struct trigger on_replace_trigger; extern struct trigger on_replace_fk_constraint; +extern struct trigger on_replace_ck_constraint; extern struct trigger on_stmt_begin_space; extern struct trigger on_stmt_begin_index; extern struct trigger on_stmt_begin_truncate; diff --git a/src/box/bootstrap.snap b/src/box/bootstrap.snap index 871a93f9856f97636ad55b7f5260e5c6957fef36..d988e27a189a1ec691d652f06e96d5818677d268 100644 GIT binary patch literal 4418 diff --git a/src/box/ck_constraint.c b/src/box/ck_constraint.c new file mode 100644 index 000000000..6d9f8ea44 --- /dev/null +++ b/src/box/ck_constraint.c @@ -0,0 +1,114 @@ +/* + * Copyright 2010-2019, Tarantool AUTHORS, please see AUTHORS file. + * + * Redistribution and use in source and binary forms, with or + * without modification, are permitted provided that the following + * conditions are met: + * + * 1. Redistributions of source code must retain the above + * copyright notice, this list of conditions and the + * following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following + * disclaimer in the documentation and/or other materials + * provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY <COPYRIGHT HOLDER> ``AS IS'' AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED + * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL + * <COPYRIGHT HOLDER> OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, + * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR + * BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF + * THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF + * SUCH DAMAGE. + */ +#include "ck_constraint.h" +#include "errcode.h" +#include "sql.h" +#include "sql/sqlInt.h" + +/** + * Resolve space_def references for check constraint via AST + * tree traversal. + * @param ck_constraint Check constraint object to update. + * @param space_def Space definition to use. + * @retval 0 on success. + * @retval -1 on error. + */ +static int +ck_constraint_resolve_field_names(struct Expr *expr, + struct space_def *space_def) +{ + struct Parse parser; + sql_parser_create(&parser, sql_get()); + parser.parse_only = true; + sql_resolve_self_reference(&parser, space_def, NC_IsCheck, expr, NULL); + int rc = parser.is_aborted ? -1 : 0; + sql_parser_destroy(&parser); + return rc; +} + +struct ck_constraint * +ck_constraint_new(struct ck_constraint_def *ck_constraint_def, + struct space_def *space_def) +{ + if (space_def->field_count == 0) { + diag_set(ClientError, ER_UNSUPPORTED, "Tarantool", + "CK constraint for space without format"); + return NULL; + } + struct ck_constraint *ck_constraint = malloc(sizeof(*ck_constraint)); + if (ck_constraint == NULL) { + diag_set(OutOfMemory, sizeof(*ck_constraint), "malloc", + "ck_constraint"); + return NULL; + } + ck_constraint->def = NULL; + ck_constraint->space_id = space_def->id; + rlist_create(&ck_constraint->link); + ck_constraint->expr = + sql_expr_compile(sql_get(), ck_constraint_def->expr_str, + strlen(ck_constraint_def->expr_str)); + if (ck_constraint->expr == NULL || + ck_constraint_resolve_field_names(ck_constraint->expr, + space_def) != 0) { + diag_set(ClientError, ER_CREATE_CK_CONSTRAINT, + ck_constraint_def->name, + box_error_message(box_error_last())); + goto error; + } + + ck_constraint->def = ck_constraint_def; + return ck_constraint; +error: + ck_constraint_delete(ck_constraint); + return NULL; +} + +void +ck_constraint_delete(struct ck_constraint *ck_constraint) +{ + sql_expr_delete(sql_get(), ck_constraint->expr, false); + free(ck_constraint->def); + TRASH(ck_constraint); + free(ck_constraint); +} + +struct ck_constraint * +space_ck_constraint_by_name(struct space *space, const char *name, + uint32_t name_len) +{ + struct ck_constraint *ck_constraint = NULL; + rlist_foreach_entry(ck_constraint, &space->ck_constraint, link) { + if (strlen(ck_constraint->def->name) == name_len && + memcmp(ck_constraint->def->name, name, name_len) == 0) + return ck_constraint; + } + return NULL; +} diff --git a/src/box/ck_constraint.h b/src/box/ck_constraint.h new file mode 100644 index 000000000..615612605 --- /dev/null +++ b/src/box/ck_constraint.h @@ -0,0 +1,163 @@ +#ifndef INCLUDES_BOX_CK_CONSTRAINT_H +#define INCLUDES_BOX_CK_CONSTRAINT_H +/* + * Copyright 2010-2019, Tarantool AUTHORS, please see AUTHORS file. + * + * Redistribution and use in source and binary forms, with or + * without modification, are permitted provided that the following + * conditions are met: + * + * 1. Redistributions of source code must retain the above + * copyright notice, this list of conditions and the + * following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following + * disclaimer in the documentation and/or other materials + * provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY <COPYRIGHT HOLDER> ``AS IS'' AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED + * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL + * <COPYRIGHT HOLDER> OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, + * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR + * BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF + * THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF + * SUCH DAMAGE. + */ + +#include <stdint.h> +#include "small/rlist.h" + +#if defined(__cplusplus) +extern "C" { +#endif + +struct space; +struct space_def; +struct Expr; + +/** + * Check constraint definition. + * See ck_constraint_def_sizeof() definition for implementation + * details and memory layout. + */ +struct ck_constraint_def { + /** + * The 0-terminated string, a name of the check + * constraint. Must be unique for a given space. + */ + char *name; + /** + * The 0-terminated string that defines check constraint + * expression. + * + * For instance: "field1 + field2 > 2 * 3". + */ + char *expr_str; +}; + +/** + * Structure representing check constraint. + * See ck_constraint_new() definition. + */ +struct ck_constraint { + /** The check constraint definition. */ + struct ck_constraint_def *def; + /** + * The check constraint expression AST is built for + * ck_constraint::def::expr_str with sql_expr_compile + * and resolved with sql_resolve_self_reference for + * space with space[ck_constraint::space_id] definition. + */ + struct Expr *expr; + /** + * The id of the space this check constraint is + * built for. + */ + uint32_t space_id; + /** + * Organize check constraint structs into linked list + * with space::ck_constraint. + */ + struct rlist link; +}; + +/** + * Calculate check constraint definition memory size and fields + * offsets for given arguments. + * + * Alongside with struct ck_constraint_def itself, we reserve + * memory for string containing its name and expression string. + * + * Memory layout: + * +-----------------------------+ <- Allocated memory starts here + * | struct ck_constraint_def | + * |-----------------------------| + * | name + \0 | + * |-----------------------------| + * | expr_str + \0 | + * +-----------------------------+ + * + * @param name_len The length of the name. + * @param expr_str_len The length of the expr_str. + * @param[out] name_offset The offset of the name string. + * @param[out] expr_str_offset The offset of the expr_str string. + */ +static inline uint32_t +ck_constraint_def_sizeof(uint32_t name_len, uint32_t expr_str_len, + uint32_t *name_offset, uint32_t *expr_str_offset) +{ + *name_offset = sizeof(struct ck_constraint_def); + *expr_str_offset = *name_offset + name_len + 1; + return *expr_str_offset + expr_str_len + 1; +} + +/** + * Create a new check constraint object by given check constraint + * definition and definition of the space this constraint is + * related to. + * + * @param ck_constraint_def The check constraint definition object + * to use. Expected to be allocated with + * malloc. Ck constraint object manages + * this allocation in case of successful + * creation. + * @param space_def The space definition of the space this check + * constraint must be constructed for. + * @retval not NULL Check constraint object pointer on success. + * @retval NULL Otherwise. +*/ +struct ck_constraint * +ck_constraint_new(struct ck_constraint_def *ck_constraint_def, + struct space_def *space_def); + +/** + * Destroy check constraint memory, release acquired resources. + * @param ck_constraint The check constraint object to destroy. + */ +void +ck_constraint_delete(struct ck_constraint *ck_constraint); + +/** + * Find check constraint object in space by given name and + * name_len. + * @param space The space to lookup check constraint. + * @param name The check constraint name. + * @param name_len The length of the name. + * @retval not NULL Check constrain if exists, NULL otherwise. + */ +struct ck_constraint * +space_ck_constraint_by_name(struct space *space, const char *name, + uint32_t name_len); + +#if defined(__cplusplus) +} /* extern "C" { */ +#endif + +#endif /* INCLUDES_BOX_CK_CONSTRAINT_H */ diff --git a/src/box/errcode.h b/src/box/errcode.h index 3f8cb8e0e..7878bd66b 100644 --- a/src/box/errcode.h +++ b/src/box/errcode.h @@ -245,8 +245,8 @@ struct errcode_record { /*190 */_(ER_INT_LITERAL_MAX, "Integer literal %s%s exceeds the supported range %lld - %lld") \ /*191 */_(ER_SQL_PARSER_LIMIT, "%s %d exceeds the limit (%d)") \ /*192 */_(ER_INDEX_DEF_UNSUPPORTED, "%s are prohibited in an index definition") \ - /*193 */_(ER_CK_DEF_UNSUPPORTED, "%s are prohibited in a CHECK constraint definition") \ - + /*193 */_(ER_CK_DEF_UNSUPPORTED, "%s are prohibited in a check constraint definition") \ + /*194 */_(ER_CREATE_CK_CONSTRAINT, "Failed to create check constraint '%s': %s") \ /* * !IMPORTANT! Please follow instructions at start of the file * when adding new errors. diff --git a/src/box/lua/schema.lua b/src/box/lua/schema.lua index f31cf7f2c..e01f500e6 100644 --- a/src/box/lua/schema.lua +++ b/src/box/lua/schema.lua @@ -513,6 +513,7 @@ box.schema.space.drop = function(space_id, space_name, opts) local _truncate = box.space[box.schema.TRUNCATE_ID] local _space_sequence = box.space[box.schema.SPACE_SEQUENCE_ID] local _fk_constraint = box.space[box.schema.FK_CONSTRAINT_ID] + local _ck_constraint = box.space[box.schema.CK_CONSTRAINT_ID] local sequence_tuple = _space_sequence:delete{space_id} if sequence_tuple ~= nil and sequence_tuple[3] == true then -- Delete automatically generated sequence. @@ -524,6 +525,9 @@ box.schema.space.drop = function(space_id, space_name, opts) for _, t in _fk_constraint.index.child_id:pairs({space_id}) do _fk_constraint:delete({t.name, space_id}) end + for _, t in _ck_constraint.index.space_id:pairs({space_id}) do + _ck_constraint:delete({t.name, space_id}) + end local keys = _vindex:select(space_id) for i = #keys, 1, -1 do local v = keys[i] diff --git a/src/box/lua/space.cc b/src/box/lua/space.cc index 100da0a79..ce4287bba 100644 --- a/src/box/lua/space.cc +++ b/src/box/lua/space.cc @@ -537,6 +537,8 @@ box_lua_space_init(struct lua_State *L) lua_setfield(L, -2, "TRIGGER_ID"); lua_pushnumber(L, BOX_FK_CONSTRAINT_ID); lua_setfield(L, -2, "FK_CONSTRAINT_ID"); + lua_pushnumber(L, BOX_CK_CONSTRAINT_ID); + lua_setfield(L, -2, "CK_CONSTRAINT_ID"); lua_pushnumber(L, BOX_TRUNCATE_ID); lua_setfield(L, -2, "TRUNCATE_ID"); lua_pushnumber(L, BOX_SEQUENCE_ID); diff --git a/src/box/lua/upgrade.lua b/src/box/lua/upgrade.lua index 89d6e3d52..bbb55b7e1 100644 --- a/src/box/lua/upgrade.lua +++ b/src/box/lua/upgrade.lua @@ -74,6 +74,7 @@ local function set_system_triggers(val) box.space._schema:run_triggers(val) box.space._cluster:run_triggers(val) box.space._fk_constraint:run_triggers(val) + box.space._ck_constraint:run_triggers(val) end -------------------------------------------------------------------------------- @@ -94,6 +95,7 @@ local function erase() truncate(box.space._schema) truncate(box.space._cluster) truncate(box.space._fk_constraint) + truncate(box.space._ck_constraint) end local function create_sysview(source_id, target_id) @@ -737,6 +739,46 @@ local function upgrade_to_2_1_3() end end +local function upgrade_to_2_2_0() + -- In previous Tarantool releases check constraints were + -- stored in space opts. Now we use separate space + -- _ck_constraint for this purpose. Perform legacy data + -- migration. + local MAP = setmap({}) + local _space = box.space[box.schema.SPACE_ID] + local _index = box.space[box.schema.INDEX_ID] + local _ck_constraint = box.space[box.schema.CK_CONSTRAINT_ID] + log.info("create space _ck_constraint") + local format = {{name='name', type='string'}, + {name='space_id', type='unsigned'}, + {name='is_deferred', type='boolean'}, + {name='expr_str', type='str'}} + _space:insert{_ck_constraint.id, ADMIN, '_ck_constraint', 'memtx', 0, MAP, format} + + log.info("create index primary on _ck_constraint") + _index:insert{_ck_constraint.id, 0, 'primary', 'tree', + {unique = true}, {{0, 'string'}, {1, 'unsigned'}}} + + log.info("create secondary index child_id on _ck_constraint") + _index:insert{_ck_constraint.id, 1, 'space_id', 'tree', + {unique = false}, {{1, 'unsigned'}}} + for _, space in _space:pairs() do + local flags = space.flags + if flags['checks'] ~= nil then + for i, check in pairs(flags['checks']) do + local expr_str = check.expr + local check_name = check.name or + "CK_CONSTRAINT_"..i.."_"..space.name + _ck_constraint:insert({check_name, space.id, false, expr_str}) + end + flags['checks'] = nil + _space:replace(box.tuple.new({space.id, space.owner, space.name, + space.engine, space.field_count, + flags, space.format})) + end + end +end + local function get_version() local version = box.space._schema:get{'version'} if version == nil then @@ -768,6 +810,7 @@ local function upgrade(options) {version = mkversion(2, 1, 1), func = upgrade_to_2_1_1, auto = true}, {version = mkversion(2, 1, 2), func = upgrade_to_2_1_2, auto = true}, {version = mkversion(2, 1, 3), func = upgrade_to_2_1_3, auto = true}, + {version = mkversion(2, 2, 0), func = upgrade_to_2_2_0, auto = true}, } for _, handler in ipairs(handlers) do diff --git a/src/box/schema.cc b/src/box/schema.cc index 9a55c2f14..4b20f0032 100644 --- a/src/box/schema.cc +++ b/src/box/schema.cc @@ -460,6 +460,14 @@ schema_init() sc_space_new(BOX_FK_CONSTRAINT_ID, "_fk_constraint", key_parts, 2, &on_replace_fk_constraint, NULL); + /* _ck_сonstraint - check constraints. */ + key_parts[0].fieldno = 0; /* constraint name */ + key_parts[0].type = FIELD_TYPE_STRING; + key_parts[1].fieldno = 1; /* space id */ + key_parts[1].type = FIELD_TYPE_UNSIGNED; + sc_space_new(BOX_CK_CONSTRAINT_ID, "_ck_constraint", key_parts, 2, + &on_replace_ck_constraint, NULL); + /* * _vinyl_deferred_delete - blackhole that is needed * for writing deferred DELETE statements generated by diff --git a/src/box/schema_def.h b/src/box/schema_def.h index eeeeb950b..cdd3f7f63 100644 --- a/src/box/schema_def.h +++ b/src/box/schema_def.h @@ -108,6 +108,8 @@ enum { BOX_SPACE_SEQUENCE_ID = 340, /** Space id of _fk_constraint. */ BOX_FK_CONSTRAINT_ID = 356, + /** Space id of _ck_contraint. */ + BOX_CK_CONSTRAINT_ID = 357, /** End of the reserved range of system spaces. */ BOX_SYSTEM_ID_MAX = 511, BOX_ID_NIL = 2147483647 @@ -238,6 +240,14 @@ enum { BOX_FK_CONSTRAINT_FIELD_PARENT_COLS = 8, }; +/** _ck_constraint fields. */ +enum { + BOX_CK_CONSTRAINT_FIELD_NAME = 0, + BOX_CK_CONSTRAINT_FIELD_SPACE_ID = 1, + BOX_CK_CONSTRAINT_FIELD_DEFERRED = 2, + BOX_CK_CONSTRAINT_FIELD_EXPR_STR = 3, +}; + /* * Different objects which can be subject to access * control. diff --git a/src/box/space.c b/src/box/space.c index 54ba97fba..e4e0a2264 100644 --- a/src/box/space.c +++ b/src/box/space.c @@ -165,6 +165,7 @@ space_create(struct space *space, struct engine *engine, space_fill_index_map(space); rlist_create(&space->parent_fk_constraint); rlist_create(&space->child_fk_constraint); + rlist_create(&space->ck_constraint); return 0; fail_free_indexes: @@ -225,6 +226,7 @@ space_delete(struct space *space) assert(space->sql_triggers == NULL); assert(rlist_empty(&space->parent_fk_constraint)); assert(rlist_empty(&space->child_fk_constraint)); + assert(rlist_empty(&space->ck_constraint)); space->vtab->destroy(space); } diff --git a/src/box/space.h b/src/box/space.h index 13a220d13..e8529beb3 100644 --- a/src/box/space.h +++ b/src/box/space.h @@ -193,6 +193,11 @@ struct space { * of index id. */ struct index **index; + /** + * List of check constraints linked with + * ck_constraint::link. + */ + struct rlist ck_constraint; /** * Lists of foreign key constraints. In SQL terms child * space is the "from" table i.e. the table that contains diff --git a/src/box/space_def.c b/src/box/space_def.c index d0864cc72..cb68cea1c 100644 --- a/src/box/space_def.c +++ b/src/box/space_def.c @@ -35,28 +35,12 @@ #include "sql.h" #include "msgpuck.h" -/** - * Make checks from msgpack. - * @param str pointer to array of maps - * e.g. [{"expr": "x < y", "name": "ONE"}, ..]. - * @param len array items count. - * @param[out] opt pointer to store parsing result. - * @param errcode Code of error to set if something is wrong. - * @param field_no Field number of an option in a parent element. - * @retval 0 on success. - * @retval not 0 on error. Also set diag message. - */ -static int -checks_array_decode(const char **str, uint32_t len, char *opt, uint32_t errcode, - uint32_t field_no); - const struct space_opts space_opts_default = { /* .group_id = */ 0, /* .is_temporary = */ false, /* .is_ephemeral = */ false, /* .view = */ false, /* .sql = */ NULL, - /* .checks = */ NULL, }; const struct opt_def space_opts_reg[] = { @@ -64,8 +48,7 @@ const struct opt_def space_opts_reg[] = { OPT_DEF("temporary", OPT_BOOL, struct space_opts, is_temporary), OPT_DEF("view", OPT_BOOL, struct space_opts, is_view), OPT_DEF("sql", OPT_STRPTR, struct space_opts, sql), - OPT_DEF_ARRAY("checks", struct space_opts, checks, - checks_array_decode), + OPT_DEF_LEGACY("checks"), OPT_END, }; @@ -113,16 +96,6 @@ space_def_dup_opts(struct space_def *def, const struct space_opts *opts) return -1; } } - if (opts->checks != NULL) { - def->opts.checks = sql_expr_list_dup(sql_get(), opts->checks, 0); - if (def->opts.checks == NULL) { - free(def->opts.sql); - diag_set(OutOfMemory, 0, "sql_expr_list_dup", - "def->opts.checks"); - return -1; - } - sql_checks_update_space_def_reference(def->opts.checks, def); - } return 0; } @@ -300,74 +273,5 @@ void space_opts_destroy(struct space_opts *opts) { free(opts->sql); - sql_expr_list_delete(sql_get(), opts->checks); TRASH(opts); } - -static int -checks_array_decode(const char **str, uint32_t len, char *opt, uint32_t errcode, - uint32_t field_no) -{ - char *errmsg = tt_static_buf(); - struct ExprList *checks = NULL; - const char **map = str; - struct sql *db = sql_get(); - for (uint32_t i = 0; i < len; i++) { - checks = sql_expr_list_append(db, checks, NULL); - if (checks == NULL) { - diag_set(OutOfMemory, 0, "sql_expr_list_append", - "checks"); - goto error; - } - const char *expr_name = NULL; - const char *expr_str = NULL; - uint32_t expr_name_len = 0; - uint32_t expr_str_len = 0; - uint32_t map_size = mp_decode_map(map); - for (uint32_t j = 0; j < map_size; j++) { - if (mp_typeof(**map) != MP_STR) { - diag_set(ClientError, errcode, field_no, - "key must be a string"); - goto error; - } - uint32_t key_len; - const char *key = mp_decode_str(map, &key_len); - if (mp_typeof(**map) != MP_STR) { - snprintf(errmsg, TT_STATIC_BUF_LEN, - "invalid MsgPack map field '%.*s' type", - key_len, key); - diag_set(ClientError, errcode, field_no, errmsg); - goto error; - } - if (key_len == 4 && memcmp(key, "expr", key_len) == 0) { - expr_str = mp_decode_str(map, &expr_str_len); - } else if (key_len == 4 && - memcmp(key, "name", key_len) == 0) { - expr_name = mp_decode_str(map, &expr_name_len); - } else { - snprintf(errmsg, TT_STATIC_BUF_LEN, - "invalid MsgPack map field '%.*s'", - key_len, key); - diag_set(ClientError, errcode, field_no, errmsg); - goto error; - } - } - if (sql_check_list_item_init(checks, i, expr_name, expr_name_len, - expr_str, expr_str_len) != 0) { - box_error_t *err = box_error_last(); - if (box_error_code(err) != ENOMEM) { - snprintf(errmsg, TT_STATIC_BUF_LEN, - "invalid expression specified (%s)", - box_error_message(err)); - diag_set(ClientError, errcode, field_no, - errmsg); - } - goto error; - } - } - *(struct ExprList **)opt = checks; - return 0; -error: - sql_expr_list_delete(db, checks); - return -1; -} diff --git a/src/box/space_def.h b/src/box/space_def.h index 52ff56764..71d168342 100644 --- a/src/box/space_def.h +++ b/src/box/space_def.h @@ -71,8 +71,6 @@ struct space_opts { bool is_view; /** SQL statement that produced this space. */ char *sql; - /** SQL Checks expressions list. */ - struct ExprList *checks; }; extern const struct space_opts space_opts_default; diff --git a/src/box/sql.c b/src/box/sql.c index 2310ee5e3..e4dc69c57 100644 --- a/src/box/sql.c +++ b/src/box/sql.c @@ -1034,15 +1034,8 @@ sql_encode_table_opts(struct region *region, struct space_def *def, bool is_error = false; mpstream_init(&stream, region, region_reserve_cb, region_alloc_cb, set_encode_error, &is_error); - int checks_cnt = 0; - struct ExprList_item *a; bool is_view = def->opts.is_view; - struct ExprList *checks = def->opts.checks; - if (checks != NULL) { - checks_cnt = checks->nExpr; - a = checks->a; - } - mpstream_encode_map(&stream, 2 * is_view + (checks_cnt > 0)); + mpstream_encode_map(&stream, 2 * is_view); if (is_view) { assert(def->opts.sql != NULL); @@ -1051,23 +1044,6 @@ sql_encode_table_opts(struct region *region, struct space_def *def, mpstream_encode_str(&stream, "view"); mpstream_encode_bool(&stream, true); } - if (checks_cnt > 0) { - mpstream_encode_str(&stream, "checks"); - mpstream_encode_array(&stream, checks_cnt); - } - for (int i = 0; i < checks_cnt && !is_error; ++i, ++a) { - int items = (a->pExpr != NULL) + (a->zName != NULL); - mpstream_encode_map(&stream, items); - assert(a->pExpr != NULL); - struct Expr *pExpr = a->pExpr; - assert(pExpr->u.zToken != NULL); - mpstream_encode_str(&stream, "expr"); - mpstream_encode_str(&stream, pExpr->u.zToken); - if (a->zName != NULL) { - mpstream_encode_str(&stream, "name"); - mpstream_encode_str(&stream, a->zName); - } - } mpstream_flush(&stream); if (is_error) { diag_set(OutOfMemory, stream.pos - stream.buf, @@ -1301,63 +1277,3 @@ sql_ephemeral_space_new(Parse *parser, const char *name) return space; } - -int -sql_check_list_item_init(struct ExprList *expr_list, int column, - const char *expr_name, uint32_t expr_name_len, - const char *expr_str, uint32_t expr_str_len) -{ - assert(column < expr_list->nExpr); - struct ExprList_item *item = &expr_list->a[column]; - memset(item, 0, sizeof(*item)); - if (expr_name != NULL) { - item->zName = sqlDbStrNDup(db, expr_name, expr_name_len); - if (item->zName == NULL) { - diag_set(OutOfMemory, expr_name_len, "sqlDbStrNDup", - "item->zName"); - return -1; - } - } - if (expr_str != NULL) { - item->pExpr = sql_expr_compile(db, expr_str, expr_str_len); - /* The item->zName would be released later. */ - if (item->pExpr == NULL) - return -1; - } - return 0; -} - -static int -update_space_def_callback(Walker *walker, Expr *expr) -{ - if (expr->op == TK_COLUMN && ExprHasProperty(expr, EP_Resolved)) - expr->space_def = walker->u.space_def; - return WRC_Continue; -} - -void -sql_checks_update_space_def_reference(ExprList *expr_list, - struct space_def *def) -{ - assert(expr_list != NULL); - Walker w; - memset(&w, 0, sizeof(w)); - w.xExprCallback = update_space_def_callback; - w.u.space_def = def; - for (int i = 0; i < expr_list->nExpr; i++) - sqlWalkExpr(&w, expr_list->a[i].pExpr); -} - -int -sql_checks_resolve_space_def_reference(ExprList *expr_list, - struct space_def *def) -{ - Parse parser; - sql_parser_create(&parser, sql_get()); - parser.parse_only = true; - - sql_resolve_self_reference(&parser, def, NC_IsCheck, NULL, expr_list); - int rc = parser.is_aborted ? -1 : 0; - sql_parser_destroy(&parser); - return rc; -} diff --git a/src/box/sql.h b/src/box/sql.h index 262a48bcb..4bb27da8e 100644 --- a/src/box/sql.h +++ b/src/box/sql.h @@ -285,42 +285,6 @@ sql_resolve_self_reference(struct Parse *parser, struct space_def *def, int type, struct Expr *expr, struct ExprList *expr_list); -/** - * Initialize check_list_item. - * @param expr_list ExprList with item. - * @param column index. - * @param expr_name expression name (optional). - * @param expr_name_len expresson name length (optional). - * @param expr_str expression to build string. - * @param expr_str_len expression to build string length. - * @retval 0 on success. - * @retval -1 on error. - */ -int -sql_check_list_item_init(struct ExprList *expr_list, int column, - const char *expr_name, uint32_t expr_name_len, - const char *expr_str, uint32_t expr_str_len); - -/** - * Resolve space_def references checks for expr_list. - * @param expr_list to modify. - * @param def to refer to. - * @retval 0 on success. - * @retval -1 on error. - */ -int -sql_checks_resolve_space_def_reference(struct ExprList *expr_list, - struct space_def *def); - -/** - * Update space_def references for expr_list. - * @param expr_list to modify. - * @param def to refer to. - */ -void -sql_checks_update_space_def_reference(struct ExprList *expr_list, - struct space_def *def); - /** * Initialize a new parser object. * A number of service allocations are performed on the region, diff --git a/src/box/sql/build.c b/src/box/sql/build.c index 6051a2529..3709f6b1d 100644 --- a/src/box/sql/build.c +++ b/src/box/sql/build.c @@ -43,10 +43,12 @@ * COMMIT * ROLLBACK */ +#include <ctype.h> #include "sqlInt.h" #include "vdbeInt.h" #include "tarantoolInt.h" #include "box/box.h" +#include "box/ck_constraint.h" #include "box/fk_constraint.h" #include "box/sequence.h" #include "box/session.h" @@ -638,40 +640,108 @@ primary_key_exit: return; } +/** + * Prepare a 0-terminated string in the wptr memory buffer that + * does not contain a sequence of more than one whatespace + * character. Routine enforces ' ' (space) as whitespace + * delimiter. + * The wptr buffer is expected to have str_len + 1 bytes + * (this is the expected scenario where no extra whitespace + * characters preset in the source string). + * @param wptr The destination memory buffer of size + * @a str_len + 1. + * @param str The source string to be copied. + * @param str_len The source string @a str length. + */ +static void +trim_space_snprintf(char *wptr, const char *str, uint32_t str_len) +{ + const char *str_end = str + str_len; + bool is_prev_chr_space = false; + while (str < str_end) { + if (isspace((unsigned char)*str)) { + if (!is_prev_chr_space) + *wptr++ = ' '; + is_prev_chr_space = true; + str++; + continue; + } + is_prev_chr_space = false; + *wptr++ = *str++; + } + *wptr = '\0'; +} + void -sql_add_check_constraint(struct Parse *parser) +sql_create_check_contraint(struct Parse *parser) { - struct create_ck_def *ck_def = &parser->create_ck_def; + struct create_ck_def *create_ck_def = &parser->create_ck_def; + struct ExprSpan *expr_span = create_ck_def->expr; + sql_expr_delete(parser->db, expr_span->pExpr, false); + struct alter_entity_def *alter_def = (struct alter_entity_def *) &parser->create_ck_def; assert(alter_def->entity_type == ENTITY_TYPE_CK); (void) alter_def; - struct Expr *expr = ck_def->expr->pExpr; struct space *space = parser->create_table_def.new_space; - if (space != NULL) { - expr->u.zToken = - sqlDbStrNDup(parser->db, - (char *) ck_def->expr->zStart, - (int) (ck_def->expr->zEnd - - ck_def->expr->zStart)); - if (expr->u.zToken == NULL) - goto release_expr; - space->def->opts.checks = - sql_expr_list_append(parser->db, - space->def->opts.checks, expr); - if (space->def->opts.checks == NULL) { - sqlDbFree(parser->db, expr->u.zToken); - goto release_expr; - } - struct create_entity_def *entity_def = &ck_def->base.base; - if (entity_def->name.n > 0) { - sqlExprListSetName(parser, space->def->opts.checks, - &entity_def->name, 1); + assert(space != NULL); + + /* Prepare payload for ck constraint definition. */ + struct region *region = &parser->region; + struct Token *name_token = &create_ck_def->base.base.name; + const char *name; + if (name_token->n > 0) { + name = sql_normalized_name_region_new(region, name_token->z, + name_token->n); + if (name == NULL) { + parser->is_aborted = true; + return; } } else { -release_expr: - sql_expr_delete(parser->db, expr, false); + uint32_t ck_idx = ++parser->create_table_def.check_count; + name = tt_sprintf("CK_CONSTRAINT_%d_%s", ck_idx, + space->def->name); + } + size_t name_len = strlen(name); + + uint32_t expr_str_len = (uint32_t)(create_ck_def->expr->zEnd - + create_ck_def->expr->zStart); + const char *expr_str = create_ck_def->expr->zStart; + + /* + * Allocate memory for ck constraint parse structure and + * ck constraint definition as a single memory chunk on + * region: + * + * [ck_parse][ck_def[name][expr_str]] + * |_____^ |___^ ^ + * |_________| + */ + uint32_t name_offset, expr_str_offset; + uint32_t ck_def_sz = + ck_constraint_def_sizeof(name_len, expr_str_len, &name_offset, + &expr_str_offset); + struct ck_constraint_parse *ck_parse = + region_alloc(region, sizeof(*ck_parse) + ck_def_sz); + if (ck_parse == NULL) { + diag_set(OutOfMemory, sizeof(*ck_parse) + ck_def_sz, "region", + "ck_parse"); + parser->is_aborted = true; + return; } + struct ck_constraint_def *ck_def = + (struct ck_constraint_def *)((char *)ck_parse + + sizeof(*ck_parse)); + ck_parse->ck_def = ck_def; + rlist_create(&ck_parse->link); + + ck_def->name = (char *)ck_def + name_offset; + ck_def->expr_str = (char *)ck_def + expr_str_offset; + trim_space_snprintf(ck_def->expr_str, expr_str, expr_str_len); + memcpy(ck_def->name, name, name_len); + ck_def->name[name_len] = '\0'; + + rlist_add_entry(&parser->create_table_def.new_check, ck_parse, link); } /* @@ -939,6 +1009,38 @@ emitNewSysSequenceRecord(Parse *pParse, int reg_seq_id, const char *seq_name) return first_col; } +/** + * Generate opcodes to serialize check constraint definition into + * MsgPack and insert produced tuple into _ck_constraint space. + * @param parser Parsing context. + * @param ck_def Check constraint definition to be serialized. + * @param reg_space_id The VDBE register containing space id. +*/ +static void +vdbe_emit_ck_constraint_create(struct Parse *parser, + const struct ck_constraint_def *ck_def, + uint32_t reg_space_id) +{ + struct sql *db = parser->db; + struct Vdbe *v = sqlGetVdbe(parser); + assert(v != NULL); + int ck_constraint_reg = sqlGetTempRange(parser, 5); + sqlVdbeAddOp4(v, OP_String8, 0, ck_constraint_reg, 0, + sqlDbStrDup(db, ck_def->name), P4_DYNAMIC); + sqlVdbeAddOp2(v, OP_SCopy, reg_space_id, ck_constraint_reg + 1); + sqlVdbeAddOp2(v, OP_Bool, false, ck_constraint_reg + 2); + sqlVdbeAddOp4(v, OP_String8, 0, ck_constraint_reg + 3, 0, + sqlDbStrDup(db, ck_def->expr_str), P4_DYNAMIC); + sqlVdbeAddOp3(v, OP_MakeRecord, ck_constraint_reg, 4, + ck_constraint_reg + 4); + sqlVdbeAddOp3(v, OP_SInsert, BOX_CK_CONSTRAINT_ID, 0, + ck_constraint_reg + 4); + save_record(parser, BOX_CK_CONSTRAINT_ID, ck_constraint_reg, 2, + v->nOp - 1); + VdbeComment((v, "Create CK constraint %s", ck_def->name)); + sqlReleaseTempRange(parser, ck_constraint_reg, 5); +} + int emitNewSysSpaceSequenceRecord(Parse *pParse, int space_id, const char reg_seq_id) { @@ -1108,20 +1210,18 @@ resolve_link(struct Parse *parse_context, const struct space_def *def, void sqlEndTable(struct Parse *pParse) { - sql *db = pParse->db; /* The database connection */ - - assert(!db->mallocFailed); + assert(!pParse->db->mallocFailed); struct space *new_space = pParse->create_table_def.new_space; if (new_space == NULL) return; - assert(!db->init.busy); + assert(!pParse->db->init.busy); assert(!new_space->def->opts.is_view); if (sql_space_primary_key(new_space) == NULL) { diag_set(ClientError, ER_CREATE_SPACE, new_space->def->name, "PRIMARY KEY missing"); pParse->is_aborted = true; - goto cleanup; + return; } /* @@ -1211,9 +1311,12 @@ sqlEndTable(struct Parse *pParse) fk_def->child_id = reg_space_id; vdbe_emit_fk_constraint_create(pParse, fk_def); } -cleanup: - sql_expr_list_delete(db, new_space->def->opts.checks); - new_space->def->opts.checks = NULL; + struct ck_constraint_parse *ck_parse; + rlist_foreach_entry(ck_parse, &pParse->create_table_def.new_check, + link) { + vdbe_emit_ck_constraint_create(pParse, ck_parse->ck_def, + reg_space_id); + } } void @@ -1411,6 +1514,37 @@ vdbe_emit_fk_constraint_drop(struct Parse *parse_context, char *constraint_name, sqlReleaseTempRange(parse_context, key_reg, 3); } +/** + * Generate VDBE program to remove entry from _ck_constraint space. + * + * @param parser Parsing context. + * @param ck_name Name of CK constraint to be dropped. + * @param child_id Id of table which constraint belongs to. + */ +static void +vdbe_emit_ck_constraint_drop(struct Parse *parser, const char *ck_name, + uint32_t space_id) +{ + struct Vdbe *v = sqlGetVdbe(parser); + struct sql *db = v->db; + assert(v != NULL); + int key_reg = sqlGetTempRange(parser, 3); + sqlVdbeAddOp4(v, OP_String8, 0, key_reg, 0, sqlDbStrDup(db, ck_name), + P4_DYNAMIC); + sqlVdbeAddOp2(v, OP_Integer, space_id, key_reg + 1); + const char *error_msg = + tt_sprintf(tnt_errcode_desc(ER_NO_SUCH_CONSTRAINT), ck_name); + if (vdbe_emit_halt_with_presence_test(parser, BOX_CK_CONSTRAINT_ID, 0, + key_reg, 2, ER_NO_SUCH_CONSTRAINT, + error_msg, false, + OP_Found) != 0) + return; + sqlVdbeAddOp3(v, OP_MakeRecord, key_reg, 2, key_reg + 2); + sqlVdbeAddOp2(v, OP_SDelete, BOX_CK_CONSTRAINT_ID, key_reg + 2); + VdbeComment((v, "Delete CK constraint %s", ck_name)); + sqlReleaseTempRange(parser, key_reg, 3); +} + /** * Generate code to drop a table. * This routine includes dropping triggers, sequences, @@ -1484,6 +1618,13 @@ sql_code_drop_table(struct Parse *parse_context, struct space *space, return; vdbe_emit_fk_constraint_drop(parse_context, fk_name_dup, space_id); } + /* Delete all CK constraints. */ + struct ck_constraint *ck_constraint; + rlist_foreach_entry(ck_constraint, &space->ck_constraint, link) { + vdbe_emit_ck_constraint_drop(parse_context, + ck_constraint->def->name, + space_id); + } /* * Drop all _space and _index entries that refer to the * table. @@ -2770,8 +2911,7 @@ sqlSrcListDelete(sql * db, SrcList * pList) */ assert(pItem->space == NULL || !pItem->space->def->opts.is_temporary || - (pItem->space->index == NULL && - pItem->space->def->opts.checks == NULL)); + pItem->space->index == NULL); sql_select_delete(db, pItem->pSelect); sql_expr_delete(db, pItem->pOn, false); sqlIdListDelete(db, pItem->pUsing); diff --git a/src/box/sql/insert.c b/src/box/sql/insert.c index c2aac553f..fa4a94ef7 100644 --- a/src/box/sql/insert.c +++ b/src/box/sql/insert.c @@ -36,6 +36,7 @@ #include "sqlInt.h" #include "tarantoolInt.h" #include "vdbeInt.h" +#include "box/ck_constraint.h" #include "box/session.h" #include "box/schema.h" #include "bit/bit.h" @@ -933,34 +934,27 @@ vdbe_emit_constraint_checks(struct Parse *parse_context, struct space *space, if (on_conflict == ON_CONFLICT_ACTION_DEFAULT) on_conflict = ON_CONFLICT_ACTION_ABORT; /* Test all CHECK constraints. */ - struct ExprList *checks = def->opts.checks; enum on_conflict_action on_conflict_check = on_conflict; if (on_conflict == ON_CONFLICT_ACTION_REPLACE) on_conflict_check = ON_CONFLICT_ACTION_ABORT; - if (checks != NULL) { + if (!rlist_empty(&space->ck_constraint)) parse_context->ckBase = new_tuple_reg; - for (int i = 0; i < checks->nExpr; i++) { - struct Expr *expr = checks->a[i].pExpr; - if (is_update && - checkConstraintUnchanged(expr, upd_cols)) - continue; - int all_ok = sqlVdbeMakeLabel(v); - sqlExprIfTrue(parse_context, expr, all_ok, - SQL_JUMPIFNULL); - if (on_conflict == ON_CONFLICT_ACTION_IGNORE) { - sqlVdbeGoto(v, ignore_label); - } else { - char *name = checks->a[i].zName; - if (name == NULL) - name = def->name; - sqlHaltConstraint(parse_context, - SQL_CONSTRAINT_CHECK, - on_conflict_check, name, - P4_TRANSIENT, - P5_ConstraintCheck); - } - sqlVdbeResolveLabel(v, all_ok); + struct ck_constraint *ck_constraint; + rlist_foreach_entry(ck_constraint, &space->ck_constraint, link) { + struct Expr *expr = ck_constraint->expr; + if (is_update && checkConstraintUnchanged(expr, upd_cols) != 0) + continue; + int all_ok = sqlVdbeMakeLabel(v); + sqlExprIfTrue(parse_context, expr, all_ok, SQL_JUMPIFNULL); + if (on_conflict == ON_CONFLICT_ACTION_IGNORE) { + sqlVdbeGoto(v, ignore_label); + } else { + char *name = ck_constraint->def->name; + sqlHaltConstraint(parse_context, SQL_CONSTRAINT_CHECK, + on_conflict_check, name, P4_TRANSIENT, + P5_ConstraintCheck); } + sqlVdbeResolveLabel(v, all_ok); } sql_emit_table_types(v, space->def, new_tuple_reg); /* @@ -1245,14 +1239,13 @@ xferOptimization(Parse * pParse, /* Parser context */ if (pSrcIdx == NULL) return 0; } - /* Get server checks. */ - ExprList *pCheck_src = src->def->opts.checks; - ExprList *pCheck_dest = dest->def->opts.checks; - if (pCheck_dest != NULL && - sqlExprListCompare(pCheck_src, pCheck_dest, -1) != 0) { - /* Tables have different CHECK constraints. Ticket #2252 */ + /* + * Dissallow the transfer optimization if the are check + * constraints. + */ + if (!rlist_empty(&dest->ck_constraint) || + !rlist_empty(&src->ck_constraint)) return 0; - } /* Disallow the transfer optimization if the destination table constains * any foreign key constraints. This is more restrictive than necessary. * So the extra complication to make this rule less restrictive is probably diff --git a/src/box/sql/parse.y b/src/box/sql/parse.y index 3a443a068..bc9f02599 100644 --- a/src/box/sql/parse.y +++ b/src/box/sql/parse.y @@ -290,7 +290,7 @@ ccons ::= check_constraint_def . check_constraint_def ::= cconsname(N) CHECK LP expr(X) RP. { create_ck_def_init(&pParse->create_ck_def, &N, &X); - sql_add_check_constraint(pParse); + sql_create_check_contraint(pParse); } ccons ::= cconsname(N) REFERENCES nm(T) eidlist_opt(TA) matcharg(M) refargs(R). { diff --git a/src/box/sql/parse_def.h b/src/box/sql/parse_def.h index 5899a7e4e..6c1b6fddd 100644 --- a/src/box/sql/parse_def.h +++ b/src/box/sql/parse_def.h @@ -123,6 +123,22 @@ struct fk_constraint_parse { struct rlist link; }; +/** + * Structure representing check constraint appeared within + * CREATE TABLE statement. Used only during parsing. + * All allocations are performed on region, so no cleanups are + * required. + */ +struct ck_constraint_parse { + /** + * Check constraint declared in <CREATE TABLE ...> + * statement. Must be coded after space creation. + */ + struct ck_constraint_def *ck_def; + /** Organize these structs into linked list. */ + struct rlist link; +}; + /** * Possible SQL index types. Note that PK and UNIQUE constraints * are implemented as indexes and have their own types: @@ -189,6 +205,13 @@ struct create_table_def { * Foreign key constraint appeared in CREATE TABLE stmt. */ struct rlist new_fkey; + /** + * Number of CK constraints declared within + * CREATE TABLE statement. + */ + uint32_t check_count; + /** Check constraint appeared in CREATE TABLE stmt. */ + struct rlist new_check; /** True, if table to be created has AUTOINCREMENT PK. */ bool has_autoinc; }; @@ -437,6 +460,7 @@ create_table_def_init(struct create_table_def *table_def, struct Token *name, create_entity_def_init(&table_def->base, ENTITY_TYPE_TABLE, NULL, name, if_not_exists); rlist_create(&table_def->new_fkey); + rlist_create(&table_def->new_check); } static inline void diff --git a/src/box/sql/select.c b/src/box/sql/select.c index d3472a922..996ac7ccd 100644 --- a/src/box/sql/select.c +++ b/src/box/sql/select.c @@ -6446,7 +6446,12 @@ sql_expr_extract_select(struct Parse *parser, struct Select *select) struct ExprList *expr_list = select->pEList; assert(expr_list->nExpr == 1); parser->parsed_ast_type = AST_TYPE_EXPR; - parser->parsed_ast.expr = sqlExprDup(parser->db, - expr_list->a->pExpr, - EXPRDUP_REDUCE); + /* + * Extract a copy of parsed expression. + * We cannot use EXPRDUP_REDUCE flag in sqlExprDup call + * because some compiled Expr (like Checks expressions) + * may require further resolve with sqlResolveExprNames. + */ + parser->parsed_ast.expr = + sqlExprDup(parser->db, expr_list->a->pExpr, 0); } diff --git a/src/box/sql/sqlInt.h b/src/box/sql/sqlInt.h index 997a46535..2d6936490 100644 --- a/src/box/sql/sqlInt.h +++ b/src/box/sql/sqlInt.h @@ -3347,7 +3347,7 @@ sqlAddPrimaryKey(struct Parse *parse); * @param parser Parsing context. */ void -sql_add_check_constraint(Parse *parser); +sql_create_check_contraint(Parse *parser); void sqlAddDefaultValue(Parse *, ExprSpan *); void sqlAddCollateType(Parse *, Token *); diff --git a/src/box/sql/tokenize.c b/src/box/sql/tokenize.c index 8cc35323c..3791be567 100644 --- a/src/box/sql/tokenize.c +++ b/src/box/sql/tokenize.c @@ -434,7 +434,6 @@ parser_space_delete(struct sql *db, struct space *space) assert(space->def->opts.is_temporary); for (uint32_t i = 0; i < space->index_count; ++i) index_def_delete(space->index[i]->def); - sql_expr_list_delete(db, space->def->opts.checks); } /** diff --git a/test/app-tap/tarantoolctl.test.lua b/test/app-tap/tarantoolctl.test.lua index c1e1490ca..7a72acda2 100755 --- a/test/app-tap/tarantoolctl.test.lua +++ b/test/app-tap/tarantoolctl.test.lua @@ -405,8 +405,8 @@ do check_ctlcat_xlog(test_i, dir, "--from=3 --to=6 --format=json --show-system --replica 1", "\n", 3) check_ctlcat_xlog(test_i, dir, "--from=3 --to=6 --format=json --show-system --replica 1 --replica 2", "\n", 3) check_ctlcat_xlog(test_i, dir, "--from=3 --to=6 --format=json --show-system --replica 2", "\n", 0) - check_ctlcat_snap(test_i, dir, "--space=280", "---\n", 21) - check_ctlcat_snap(test_i, dir, "--space=288", "---\n", 47) + check_ctlcat_snap(test_i, dir, "--space=280", "---\n", 22) + check_ctlcat_snap(test_i, dir, "--space=288", "---\n", 49) end) end) diff --git a/test/box-py/bootstrap.result b/test/box-py/bootstrap.result index 379f6c51f..33bb01f7c 100644 --- a/test/box-py/bootstrap.result +++ b/test/box-py/bootstrap.result @@ -4,7 +4,7 @@ box.internal.bootstrap() box.space._schema:select{} --- - - ['max_id', 511] - - ['version', 2, 1, 3] + - ['version', 2, 2, 0] ... box.space._cluster:select{} --- @@ -78,6 +78,9 @@ box.space._space:select{} {'name': 'is_deferred', 'type': 'boolean'}, {'name': 'match', 'type': 'string'}, {'name': 'on_delete', 'type': 'string'}, {'name': 'on_update', 'type': 'string'}, {'name': 'child_cols', 'type': 'array'}, {'name': 'parent_cols', 'type': 'array'}]] + - [357, 1, '_ck_constraint', 'memtx', 0, {}, [{'name': 'name', 'type': 'string'}, + {'name': 'space_id', 'type': 'unsigned'}, {'name': 'is_deferred', 'type': 'boolean'}, + {'name': 'expr_str', 'type': 'str'}]] ... box.space._index:select{} --- @@ -130,6 +133,8 @@ box.space._index:select{} - [340, 1, 'sequence', 'tree', {'unique': false}, [[1, 'unsigned']]] - [356, 0, 'primary', 'tree', {'unique': true}, [[0, 'string'], [1, 'unsigned']]] - [356, 1, 'child_id', 'tree', {'unique': false}, [[1, 'unsigned']]] + - [357, 0, 'primary', 'tree', {'unique': true}, [[0, 'string'], [1, 'unsigned']]] + - [357, 1, 'space_id', 'tree', {'unique': false}, [[1, 'unsigned']]] ... box.space._user:select{} --- diff --git a/test/box/access.result b/test/box/access.result index 9c190240f..801112799 100644 --- a/test/box/access.result +++ b/test/box/access.result @@ -1487,6 +1487,9 @@ box.schema.user.grant('tester', 'read', 'space', '_trigger') box.schema.user.grant('tester', 'read', 'space', '_fk_constraint') --- ... +box.schema.user.grant('tester', 'read', 'space', '_ck_constraint') +--- +... box.session.su("tester") --- ... diff --git a/test/box/access.test.lua b/test/box/access.test.lua index 4baeb2ef6..2f62d6f53 100644 --- a/test/box/access.test.lua +++ b/test/box/access.test.lua @@ -554,6 +554,7 @@ box.schema.user.grant('tester', 'create' , 'sequence') box.schema.user.grant('tester', 'read', 'space', '_sequence') box.schema.user.grant('tester', 'read', 'space', '_trigger') box.schema.user.grant('tester', 'read', 'space', '_fk_constraint') +box.schema.user.grant('tester', 'read', 'space', '_ck_constraint') box.session.su("tester") -- successful create s1 = box.schema.space.create("test_space") diff --git a/test/box/access_misc.result b/test/box/access_misc.result index 36ebfae09..ca54ac6d4 100644 --- a/test/box/access_misc.result +++ b/test/box/access_misc.result @@ -818,6 +818,9 @@ box.space._space:select() {'name': 'is_deferred', 'type': 'boolean'}, {'name': 'match', 'type': 'string'}, {'name': 'on_delete', 'type': 'string'}, {'name': 'on_update', 'type': 'string'}, {'name': 'child_cols', 'type': 'array'}, {'name': 'parent_cols', 'type': 'array'}]] + - [357, 1, '_ck_constraint', 'memtx', 0, {}, [{'name': 'name', 'type': 'string'}, + {'name': 'space_id', 'type': 'unsigned'}, {'name': 'is_deferred', 'type': 'boolean'}, + {'name': 'expr_str', 'type': 'str'}]] ... box.space._func:select() --- diff --git a/test/box/access_sysview.result b/test/box/access_sysview.result index c6a2b22ed..c3d1e746b 100644 --- a/test/box/access_sysview.result +++ b/test/box/access_sysview.result @@ -230,11 +230,11 @@ box.session.su('guest') ... #box.space._vspace:select{} --- -- 22 +- 23 ... #box.space._vindex:select{} --- -- 48 +- 50 ... #box.space._vuser:select{} --- @@ -262,7 +262,7 @@ box.session.su('guest') ... #box.space._vindex:select{} --- -- 48 +- 50 ... #box.space._vuser:select{} --- diff --git a/test/box/alter.result b/test/box/alter.result index c1b1de135..44630557c 100644 --- a/test/box/alter.result +++ b/test/box/alter.result @@ -107,7 +107,7 @@ space = box.space[t[1]] ... space.id --- -- 357 +- 358 ... space.field_count --- @@ -152,7 +152,7 @@ space_deleted ... space:replace{0} --- -- error: Space '357' does not exist +- error: Space '358' does not exist ... _index:insert{_space.id, 0, 'primary', 'tree', {unique=true}, {{0, 'unsigned'}}} --- @@ -230,6 +230,8 @@ _index:select{} - [340, 1, 'sequence', 'tree', {'unique': false}, [[1, 'unsigned']]] - [356, 0, 'primary', 'tree', {'unique': true}, [[0, 'string'], [1, 'unsigned']]] - [356, 1, 'child_id', 'tree', {'unique': false}, [[1, 'unsigned']]] + - [357, 0, 'primary', 'tree', {'unique': true}, [[0, 'string'], [1, 'unsigned']]] + - [357, 1, 'space_id', 'tree', {'unique': false}, [[1, 'unsigned']]] ... -- modify indexes of a system space _index:delete{_index.id, 0} diff --git a/test/box/misc.result b/test/box/misc.result index a1f7a0990..eeb63366e 100644 --- a/test/box/misc.result +++ b/test/box/misc.result @@ -522,6 +522,7 @@ t; 191: box.error.SQL_PARSER_LIMIT 192: box.error.INDEX_DEF_UNSUPPORTED 193: box.error.CK_DEF_UNSUPPORTED + 194: box.error.CREATE_CK_CONSTRAINT ... test_run:cmd("setopt delimiter ''"); --- diff --git a/test/sql-tap/check.test.lua b/test/sql-tap/check.test.lua index b01afca7c..0dda3fac8 100755 --- a/test/sql-tap/check.test.lua +++ b/test/sql-tap/check.test.lua @@ -55,7 +55,7 @@ test:do_catchsql_test( INSERT INTO t1 VALUES(6,7, 2); ]], { -- <check-1.3> - 1, "Failed to execute SQL statement: CHECK constraint failed: T1" + 1, "Failed to execute SQL statement: CHECK constraint failed: CK_CONSTRAINT_1_T1" -- </check-1.3> }) @@ -75,7 +75,7 @@ test:do_catchsql_test( INSERT INTO t1 VALUES(4,3, 2); ]], { -- <check-1.5> - 1, "Failed to execute SQL statement: CHECK constraint failed: T1" + 1, "Failed to execute SQL statement: CHECK constraint failed: CK_CONSTRAINT_2_T1" -- </check-1.5> }) @@ -147,7 +147,7 @@ test:do_catchsql_test( UPDATE t1 SET x=7 WHERE x==2 ]], { -- <check-1.12> - 1, "Failed to execute SQL statement: CHECK constraint failed: T1" + 1, "Failed to execute SQL statement: CHECK constraint failed: CK_CONSTRAINT_1_T1" -- </check-1.12> }) @@ -167,7 +167,7 @@ test:do_catchsql_test( UPDATE t1 SET x=5 WHERE x==2 ]], { -- <check-1.14> - 1, "Failed to execute SQL statement: CHECK constraint failed: T1" + 1, "Failed to execute SQL statement: CHECK constraint failed: CK_CONSTRAINT_1_T1" -- </check-1.14> }) @@ -319,7 +319,7 @@ test:do_catchsql_test( ); ]], { -- <check-3.1> - 1, "Failed to create space 'T3': Subqueries are prohibited in a CHECK constraint definition" + 1, "Failed to create check constraint 'CK_CONSTRAINT_1_T3': Subqueries are prohibited in a check constraint definition" -- </check-3.1> }) @@ -344,7 +344,7 @@ test:do_catchsql_test( ); ]], { -- <check-3.3> - 1, "Failed to create space 'T3': Can’t resolve field 'Q'" + 1, "Failed to create check constraint 'CK_CONSTRAINT_1_T3': Can’t resolve field 'Q'" -- </check-3.3> }) @@ -368,7 +368,7 @@ test:do_catchsql_test( ); ]], { -- <check-3.5> - 1, "Failed to create space 'T3': Field 'X' was not found in the space 'T2' format" + 1, "Failed to create check constraint 'CK_CONSTRAINT_1_T3': Field 'X' was not found in the space 'T2' format" -- </check-3.5> }) @@ -413,7 +413,7 @@ test:do_catchsql_test( INSERT INTO t3 VALUES(111,222,333); ]], { -- <check-3.9> - 1, "Failed to execute SQL statement: CHECK constraint failed: T3" + 1, "Failed to execute SQL statement: CHECK constraint failed: CK_CONSTRAINT_1_T3" -- </check-3.9> }) @@ -484,7 +484,7 @@ test:do_catchsql_test( UPDATE t4 SET x=0, y=1; ]], { -- <check-4.6> - 1, "Failed to execute SQL statement: CHECK constraint failed: T4" + 1, "Failed to execute SQL statement: CHECK constraint failed: CK_CONSTRAINT_1_T4" -- </check-4.6> }) @@ -504,7 +504,7 @@ test:do_catchsql_test( UPDATE t4 SET x=0, y=2; ]], { -- <check-4.9> - 1, "Failed to execute SQL statement: CHECK constraint failed: T4" + 1, "Failed to execute SQL statement: CHECK constraint failed: CK_CONSTRAINT_1_T4" -- </check-4.9> }) @@ -516,7 +516,7 @@ test:do_catchsql_test( ); ]], { -- <check-5.1> - 1, "Wrong space options (field 5): invalid expression specified (bindings are not allowed in DDL)" + 1, "Failed to create check constraint 'CK_CONSTRAINT_1_T5': bindings are not allowed in DDL" -- </check-5.1> }) @@ -528,7 +528,7 @@ test:do_catchsql_test( ); ]], { -- <check-5.2> - 1, "Wrong space options (field 5): invalid expression specified (bindings are not allowed in DDL)" + 1, "Failed to create check constraint 'CK_CONSTRAINT_1_T5': bindings are not allowed in DDL" -- </check-5.2> }) @@ -581,7 +581,7 @@ test:do_catchsql_test( UPDATE OR FAIL t1 SET x=7-x, y=y+1; ]], { -- <check-6.5> - 1, "Failed to execute SQL statement: CHECK constraint failed: T1" + 1, "Failed to execute SQL statement: CHECK constraint failed: CK_CONSTRAINT_1_T1" -- </check-6.5> }) @@ -603,7 +603,7 @@ test:do_catchsql_test( INSERT OR ROLLBACK INTO t1 VALUES(8,40.0, 10); ]], { -- <check-6.7> - 1, "Failed to execute SQL statement: CHECK constraint failed: T1" + 1, "Failed to execute SQL statement: CHECK constraint failed: CK_CONSTRAINT_1_T1" -- </check-6.7> }) @@ -636,7 +636,7 @@ test:do_catchsql_test( REPLACE INTO t1 VALUES(6,7, 11); ]], { -- <check-6.12> - 1, "Failed to execute SQL statement: CHECK constraint failed: T1" + 1, "Failed to execute SQL statement: CHECK constraint failed: CK_CONSTRAINT_1_T1" -- </check-6.12> }) @@ -700,7 +700,7 @@ test:do_catchsql_test( 7.3, " INSERT INTO t6 VALUES(11) ", { -- <7.3> - 1, "Failed to execute SQL statement: CHECK constraint failed: T6" + 1, "Failed to execute SQL statement: CHECK constraint failed: CK_CONSTRAINT_1_T6" -- </7.3> }) diff --git a/test/sql-tap/fkey2.test.lua b/test/sql-tap/fkey2.test.lua index 54e5059b3..695a379a6 100755 --- a/test/sql-tap/fkey2.test.lua +++ b/test/sql-tap/fkey2.test.lua @@ -362,7 +362,7 @@ test:do_catchsql_test( UPDATE ab SET a = 5; ]], { -- <fkey2-3.2> - 1, "Failed to execute SQL statement: CHECK constraint failed: EF" + 1, "Failed to execute SQL statement: CHECK constraint failed: CK_CONSTRAINT_1_EF" -- </fkey2-3.2> }) @@ -382,7 +382,7 @@ test:do_catchsql_test( UPDATE ab SET a = 5; ]], { -- <fkey2-3.4> - 1, "Failed to execute SQL statement: CHECK constraint failed: EF" + 1, "Failed to execute SQL statement: CHECK constraint failed: CK_CONSTRAINT_1_EF" -- </fkey2-3.4> }) diff --git a/test/sql-tap/sql-errors.test.lua b/test/sql-tap/sql-errors.test.lua index 4e173b692..d742cf6cf 100755 --- a/test/sql-tap/sql-errors.test.lua +++ b/test/sql-tap/sql-errors.test.lua @@ -313,7 +313,7 @@ test:do_catchsql_test( CREATE TABLE t27 (i INT PRIMARY KEY, CHECK(i < (SELECT * FROM t0))); ]], { -- <sql-errors-1.27> - 1,"Failed to create space 'T27': Subqueries are prohibited in a CHECK constraint definition" + 1,"Failed to create check constraint 'CK_CONSTRAINT_1_T27': Subqueries are prohibited in a check constraint definition" -- </sql-errors-1.27> }) diff --git a/test/sql-tap/table.test.lua b/test/sql-tap/table.test.lua index 5b793c0fc..066662f33 100755 --- a/test/sql-tap/table.test.lua +++ b/test/sql-tap/table.test.lua @@ -1221,7 +1221,7 @@ test:do_catchsql_test( INSERT INTO T21 VALUES(1, -1, 1); ]], { -- <table-21.3> - 1, "Failed to execute SQL statement: CHECK constraint failed: T21" + 1, "Failed to execute SQL statement: CHECK constraint failed: CK_CONSTRAINT_1_T21" -- </table-21.3> }) @@ -1231,7 +1231,7 @@ test:do_catchsql_test( INSERT INTO T21 VALUES(1, 1, -1); ]], { -- <table-21.4> - 1, "Failed to execute SQL statement: CHECK constraint failed: T21" + 1, "Failed to execute SQL statement: CHECK constraint failed: CK_CONSTRAINT_2_T21" -- </table-21.4> }) diff --git a/test/sql/checks.result b/test/sql/checks.result index f7cddec43..8da1483f6 100644 --- a/test/sql/checks.result +++ b/test/sql/checks.result @@ -18,8 +18,10 @@ box.execute('pragma sql_default_engine=\''..engine..'\'') -- -- gh-3272: Move SQL CHECK into server -- --- invalid expression -opts = {checks = {{expr = 'X><5'}}} +-- Until Tarantool version 2.2 check constraints were stored in +-- space opts. +-- Make sure that now this legacy option is ignored. +opts = {checks = {{expr = 'X>5'}}} --- ... format = {{name = 'X', type = 'unsigned'}} @@ -30,89 +32,211 @@ t = {513, 1, 'test', 'memtx', 0, opts, format} ... s = box.space._space:insert(t) --- -- error: 'Wrong space options (field 5): invalid expression specified (Syntax error - near ''<'')' ... -opts = {checks = {{expr = 'X>5'}}} +_ = box.space.test:create_index('pk') --- ... -format = {{name = 'X', type = 'unsigned'}} +-- Invalid expression test. +box.space._ck_constraint:insert({'CK_CONSTRAINT_01', 513, false, 'X><5'}) --- +- error: 'Failed to create check constraint ''CK_CONSTRAINT_01'': Syntax error near + ''<''' ... -t = {513, 1, 'test', 'memtx', 0, opts, format} +-- Unexistent space test. +box.space._ck_constraint:insert({'CK_CONSTRAINT_01', 550, false, 'X<5'}) --- +- error: Space '550' does not exist ... -s = box.space._space:insert(t) +-- Pass integer instead of expression. +box.space._ck_constraint:insert({'CK_CONSTRAINT_01', 513, false, 666}) +--- +- error: 'Tuple field 4 type does not match one required by operation: expected string' +... +-- Defered CK constraints are not supported. +box.space._ck_constraint:insert({'CK_CONSTRAINT_01', 513, true, 'X<5'}) +--- +- error: Tarantool does not support deferred ck constraints +... +-- Check constraints LUA creation test. +box.space._ck_constraint:insert({'CK_CONSTRAINT_01', 513, false, 'X<5'}) +--- +- ['CK_CONSTRAINT_01', 513, false, 'X<5'] +... +box.space._ck_constraint:count({}) +--- +- 1 +... +box.execute("INSERT INTO \"test\" VALUES(5);") +--- +- error: 'Failed to execute SQL statement: CHECK constraint failed: CK_CONSTRAINT_01' +... +box.space._ck_constraint:replace({'CK_CONSTRAINT_01', 513, false, 'X<=5'}) +--- +- ['CK_CONSTRAINT_01', 513, false, 'X<=5'] +... +box.execute("INSERT INTO \"test\" VALUES(5);") +--- +- row_count: 1 +... +box.execute("INSERT INTO \"test\" VALUES(6);") +--- +- error: 'Failed to execute SQL statement: CHECK constraint failed: CK_CONSTRAINT_01' +... +-- Can't drop table with check constraints. +box.space.test:delete({5}) --- +- [5] ... -box.space._space:delete(513) +box.space.test.index.pk:drop() +--- +... +box.space._space:delete({513}) +--- +- error: 'Can''t drop space ''test'': the space has check constraints' +... +box.space._ck_constraint:delete({'CK_CONSTRAINT_01', 513}) +--- +- ['CK_CONSTRAINT_01', 513, false, 'X<=5'] +... +box.space._space:delete({513}) --- - [513, 1, 'test', 'memtx', 0, {'checks': [{'expr': 'X>5'}]}, [{'name': 'X', 'type': 'unsigned'}]] ... -opts = {checks = {{expr = 'X>5', name = 'ONE'}}} +-- Create table with checks in sql. +box.execute("CREATE TABLE t1(x INTEGER CONSTRAINT ONE CHECK( x<5 ), y REAL CONSTRAINT TWO CHECK( y>x ), z INTEGER PRIMARY KEY);") --- +- row_count: 1 ... -format = {{name = 'X', type = 'unsigned'}} +box.space._ck_constraint:count() --- +- 2 ... -t = {513, 1, 'test', 'memtx', 0, opts, format} +box.execute("INSERT INTO t1 VALUES (7, 1, 1)") --- +- error: 'Failed to execute SQL statement: CHECK constraint failed: ONE' ... -s = box.space._space:insert(t) +box.execute("INSERT INTO t1 VALUES (2, 1, 1)") --- +- error: 'Failed to execute SQL statement: CHECK constraint failed: TWO' ... -box.space._space:delete(513) +box.execute("INSERT INTO t1 VALUES (2, 4, 1)") --- -- [513, 1, 'test', 'memtx', 0, {'checks': [{'name': 'ONE', 'expr': 'X>5'}]}, [{'name': 'X', - 'type': 'unsigned'}]] +- row_count: 1 ... --- extra invlalid field name -opts = {checks = {{expr = 'X>5', name = 'ONE', extra = 'TWO'}}} +box.execute("DROP TABLE t1") --- +- row_count: 1 ... -format = {{name = 'X', type = 'unsigned'}} +-- Test space creation rollback on spell error in ck constraint. +box.execute("CREATE TABLE first (id FLOAT PRIMARY KEY CHECK(id < 5), a INT CONSTRAINT ONE CHECK(a >< 5));") --- +- error: Syntax error near '<' ... -t = {513, 1, 'test', 'memtx', 0, opts, format} +box.space.FIRST == nil --- +- true ... -s = box.space._space:insert(t) +box.space._ck_constraint:count() == 0 --- -- error: 'Wrong space options (field 5): invalid MsgPack map field ''extra''' +- true ... -opts = {checks = {{expr_invalid_label = 'X>5'}}} +-- Ck constraints are disallowed for spaces having no format. +s = box.schema.create_space('test', {engine = engine}) --- ... -format = {{name = 'X', type = 'unsigned'}} +_ = s:create_index('pk') --- ... -t = {513, 1, 'test', 'memtx', 0, opts, format} +_ = box.space._ck_constraint:insert({'physics', s.id, false, 'X<Y'}) --- +- error: Tarantool does not support CK constraint for space without format ... -s = box.space._space:insert(t) +s:format({{name='X', type='integer'}, {name='Y', type='integer'}}) --- -- error: 'Wrong space options (field 5): invalid MsgPack map field ''expr_invalid_label''' ... --- invalid field type -opts = {checks = {{name = 123}}} +_ = box.space._ck_constraint:insert({'physics', s.id, false, 'X<Y'}) --- ... -format = {{name = 'X', type = 'unsigned'}} +box.execute("INSERT INTO \"test\" VALUES(2, 1);") --- +- error: 'Failed to execute SQL statement: CHECK constraint failed: physics' ... -t = {513, 1, 'test', 'memtx', 0, opts, format} +s:format({{name='Y', type='integer'}, {name='X', type='integer'}}) --- ... -s = box.space._space:insert(t) +box.execute("INSERT INTO \"test\" VALUES(1, 2);") +--- +- error: 'Failed to execute SQL statement: CHECK constraint failed: physics' +... +box.execute("INSERT INTO \"test\" VALUES(2, 1);") +--- +- row_count: 1 +... +s:truncate() +--- +... +box.execute("INSERT INTO \"test\" VALUES(1, 2);") +--- +- error: 'Failed to execute SQL statement: CHECK constraint failed: physics' +... +s:format({}) +--- +- error: Tarantool does not support CK constraint for space without format +... +s:format() +--- +- [{'name': 'Y', 'type': 'integer'}, {'name': 'X', 'type': 'integer'}] +... +s:format({{name='Y1', type='integer'}, {name='X1', type='integer'}}) +--- +- error: 'Failed to create check constraint ''physics'': Can’t resolve field ''X''' +... +-- Ck constraint creation is forbidden for non-empty space +s:insert({2, 1}) +--- +- [2, 1] +... +_ = box.space._ck_constraint:insert({'conflict', s.id, false, 'X>10'}) +--- +- error: 'Failed to create check constraint ''conflict'': referencing space must be + empty' +... +s:truncate() +--- +... +_ = box.space._ck_constraint:insert({'conflict', s.id, false, 'X>10'}) +--- +... +box.execute("INSERT INTO \"test\" VALUES(1, 2);") --- -- error: 'Wrong space options (field 5): invalid MsgPack map field ''name'' type' +- error: 'Failed to execute SQL statement: CHECK constraint failed: conflict' +... +box.execute("INSERT INTO \"test\" VALUES(11, 11);") +--- +- error: 'Failed to execute SQL statement: CHECK constraint failed: physics' +... +box.execute("INSERT INTO \"test\" VALUES(12, 11);") +--- +- row_count: 1 +... +s:drop() +--- +... +box.execute("CREATE TABLE T2(ID INT PRIMARY KEY, CONSTRAINT CK1 CHECK(ID > 0), CONSTRAINT CK1 CHECK(ID < 0))") +--- +- error: Duplicate key exists in unique index 'primary' in space '_ck_constraint' +... +box.space._ck_constraint:select() +--- +- [] ... -- -- gh-3611: Segfault on table creation with check referencing this table -- box.execute("CREATE TABLE w2 (s1 INT PRIMARY KEY, CHECK ((SELECT COUNT(*) FROM w2) = 0));") --- -- error: 'Failed to create space ''W2'': Space ''W2'' does not exist' +- error: 'Failed to create check constraint ''CK_CONSTRAINT_1_W2'': Subqueries are + prohibited in a check constraint definition' ... box.execute("DROP TABLE w2;") --- @@ -123,22 +247,8 @@ box.execute("DROP TABLE w2;") -- box.execute("CREATE TABLE t5(x INT PRIMARY KEY, y INT, CHECK( x*y < ? ));") --- -- error: 'Wrong space options (field 5): invalid expression specified (bindings are - not allowed in DDL)' -... -opts = {checks = {{expr = '?>5', name = 'ONE'}}} ---- -... -format = {{name = 'X', type = 'unsigned'}} ---- -... -t = {513, 1, 'test', 'memtx', 0, opts, format} ---- -... -s = box.space._space:insert(t) ---- -- error: 'Wrong space options (field 5): invalid expression specified (bindings are - not allowed in DDL)' +- error: 'Failed to create check constraint ''CK_CONSTRAINT_1_T5'': bindings are not + allowed in DDL' ... test_run:cmd("clear filter") --- diff --git a/test/sql/checks.test.lua b/test/sql/checks.test.lua index 5bfcf12f8..4029359b1 100644 --- a/test/sql/checks.test.lua +++ b/test/sql/checks.test.lua @@ -8,41 +8,79 @@ box.execute('pragma sql_default_engine=\''..engine..'\'') -- gh-3272: Move SQL CHECK into server -- --- invalid expression -opts = {checks = {{expr = 'X><5'}}} -format = {{name = 'X', type = 'unsigned'}} -t = {513, 1, 'test', 'memtx', 0, opts, format} -s = box.space._space:insert(t) - +-- Until Tarantool version 2.2 check constraints were stored in +-- space opts. +-- Make sure that now this legacy option is ignored. opts = {checks = {{expr = 'X>5'}}} format = {{name = 'X', type = 'unsigned'}} t = {513, 1, 'test', 'memtx', 0, opts, format} s = box.space._space:insert(t) -box.space._space:delete(513) +_ = box.space.test:create_index('pk') -opts = {checks = {{expr = 'X>5', name = 'ONE'}}} -format = {{name = 'X', type = 'unsigned'}} -t = {513, 1, 'test', 'memtx', 0, opts, format} -s = box.space._space:insert(t) -box.space._space:delete(513) +-- Invalid expression test. +box.space._ck_constraint:insert({'CK_CONSTRAINT_01', 513, false, 'X><5'}) +-- Unexistent space test. +box.space._ck_constraint:insert({'CK_CONSTRAINT_01', 550, false, 'X<5'}) +-- Pass integer instead of expression. +box.space._ck_constraint:insert({'CK_CONSTRAINT_01', 513, false, 666}) +-- Defered CK constraints are not supported. +box.space._ck_constraint:insert({'CK_CONSTRAINT_01', 513, true, 'X<5'}) --- extra invlalid field name -opts = {checks = {{expr = 'X>5', name = 'ONE', extra = 'TWO'}}} -format = {{name = 'X', type = 'unsigned'}} -t = {513, 1, 'test', 'memtx', 0, opts, format} -s = box.space._space:insert(t) +-- Check constraints LUA creation test. +box.space._ck_constraint:insert({'CK_CONSTRAINT_01', 513, false, 'X<5'}) +box.space._ck_constraint:count({}) -opts = {checks = {{expr_invalid_label = 'X>5'}}} -format = {{name = 'X', type = 'unsigned'}} -t = {513, 1, 'test', 'memtx', 0, opts, format} -s = box.space._space:insert(t) +box.execute("INSERT INTO \"test\" VALUES(5);") +box.space._ck_constraint:replace({'CK_CONSTRAINT_01', 513, false, 'X<=5'}) +box.execute("INSERT INTO \"test\" VALUES(5);") +box.execute("INSERT INTO \"test\" VALUES(6);") +-- Can't drop table with check constraints. +box.space.test:delete({5}) +box.space.test.index.pk:drop() +box.space._space:delete({513}) +box.space._ck_constraint:delete({'CK_CONSTRAINT_01', 513}) +box.space._space:delete({513}) --- invalid field type -opts = {checks = {{name = 123}}} -format = {{name = 'X', type = 'unsigned'}} -t = {513, 1, 'test', 'memtx', 0, opts, format} -s = box.space._space:insert(t) +-- Create table with checks in sql. +box.execute("CREATE TABLE t1(x INTEGER CONSTRAINT ONE CHECK( x<5 ), y REAL CONSTRAINT TWO CHECK( y>x ), z INTEGER PRIMARY KEY);") +box.space._ck_constraint:count() +box.execute("INSERT INTO t1 VALUES (7, 1, 1)") +box.execute("INSERT INTO t1 VALUES (2, 1, 1)") +box.execute("INSERT INTO t1 VALUES (2, 4, 1)") +box.execute("DROP TABLE t1") + +-- Test space creation rollback on spell error in ck constraint. +box.execute("CREATE TABLE first (id FLOAT PRIMARY KEY CHECK(id < 5), a INT CONSTRAINT ONE CHECK(a >< 5));") +box.space.FIRST == nil +box.space._ck_constraint:count() == 0 + +-- Ck constraints are disallowed for spaces having no format. +s = box.schema.create_space('test', {engine = engine}) +_ = s:create_index('pk') +_ = box.space._ck_constraint:insert({'physics', s.id, false, 'X<Y'}) +s:format({{name='X', type='integer'}, {name='Y', type='integer'}}) +_ = box.space._ck_constraint:insert({'physics', s.id, false, 'X<Y'}) +box.execute("INSERT INTO \"test\" VALUES(2, 1);") +s:format({{name='Y', type='integer'}, {name='X', type='integer'}}) +box.execute("INSERT INTO \"test\" VALUES(1, 2);") +box.execute("INSERT INTO \"test\" VALUES(2, 1);") +s:truncate() +box.execute("INSERT INTO \"test\" VALUES(1, 2);") +s:format({}) +s:format() +s:format({{name='Y1', type='integer'}, {name='X1', type='integer'}}) +-- Ck constraint creation is forbidden for non-empty space +s:insert({2, 1}) +_ = box.space._ck_constraint:insert({'conflict', s.id, false, 'X>10'}) +s:truncate() +_ = box.space._ck_constraint:insert({'conflict', s.id, false, 'X>10'}) +box.execute("INSERT INTO \"test\" VALUES(1, 2);") +box.execute("INSERT INTO \"test\" VALUES(11, 11);") +box.execute("INSERT INTO \"test\" VALUES(12, 11);") +s:drop() +box.execute("CREATE TABLE T2(ID INT PRIMARY KEY, CONSTRAINT CK1 CHECK(ID > 0), CONSTRAINT CK1 CHECK(ID < 0))") +box.space._ck_constraint:select() -- -- gh-3611: Segfault on table creation with check referencing this table @@ -55,10 +93,4 @@ box.execute("DROP TABLE w2;") -- box.execute("CREATE TABLE t5(x INT PRIMARY KEY, y INT, CHECK( x*y < ? ));") -opts = {checks = {{expr = '?>5', name = 'ONE'}}} -format = {{name = 'X', type = 'unsigned'}} -t = {513, 1, 'test', 'memtx', 0, opts, format} -s = box.space._space:insert(t) - - test_run:cmd("clear filter") diff --git a/test/sql/errinj.result b/test/sql/errinj.result index ee36a387b..18d1f3882 100644 --- a/test/sql/errinj.result +++ b/test/sql/errinj.result @@ -463,3 +463,137 @@ errinj.set("ERRINJ_SQL_NAME_NORMALIZATION", false) --- - ok ... +-- +-- Tests which are aimed at verifying work of commit/rollback +-- triggers on _ck_constraint space. +-- +s = box.schema.space.create('test', {format = {{name = 'X', type = 'unsigned'}}}) +--- +... +pk = box.space.test:create_index('pk') +--- +... +errinj.set("ERRINJ_WAL_IO", true) +--- +- ok +... +_ = box.space._ck_constraint:insert({'CK_CONSTRAINT_01', s.id, false, 'X<5'}) +--- +- error: Failed to write to disk +... +errinj.set("ERRINJ_WAL_IO", false) +--- +- ok +... +_ = box.space._ck_constraint:insert({'CK_CONSTRAINT_01', s.id, false, 'X<5'}) +--- +... +box.execute("INSERT INTO \"test\" VALUES(5);") +--- +- error: 'Failed to execute SQL statement: CHECK constraint failed: CK_CONSTRAINT_01' +... +errinj.set("ERRINJ_WAL_IO", true) +--- +- ok +... +_ = box.space._ck_constraint:replace({'CK_CONSTRAINT_01', s.id, false, 'X<=5'}) +--- +- error: Failed to write to disk +... +errinj.set("ERRINJ_WAL_IO", false) +--- +- ok +... +_ = box.space._ck_constraint:replace({'CK_CONSTRAINT_01', s.id, false, 'X<=5'}) +--- +... +box.execute("INSERT INTO \"test\" VALUES(5);") +--- +- row_count: 1 +... +errinj.set("ERRINJ_WAL_IO", true) +--- +- ok +... +_ = box.space._ck_constraint:delete({'CK_CONSTRAINT_01', s.id}) +--- +- error: Failed to write to disk +... +box.execute("INSERT INTO \"test\" VALUES(6);") +--- +- error: 'Failed to execute SQL statement: CHECK constraint failed: CK_CONSTRAINT_01' +... +errinj.set("ERRINJ_WAL_IO", false) +--- +- ok +... +_ = box.space._ck_constraint:delete({'CK_CONSTRAINT_01', s.id}) +--- +... +box.execute("INSERT INTO \"test\" VALUES(6);") +--- +- row_count: 1 +... +s:drop() +--- +... +-- +-- Test that failed space alter doesn't harm ck constraints +-- +s = box.schema.create_space('test') +--- +... +_ = s:create_index('pk') +--- +... +s:format({{name='X', type='integer'}, {name='Y', type='integer'}}) +--- +... +_ = box.space._ck_constraint:insert({'XlessY', s.id, false, 'X < Y'}) +--- +... +_ = box.space._ck_constraint:insert({'Xgreater10', s.id, false, 'X > 10'}) +--- +... +box.execute("INSERT INTO \"test\" VALUES(2, 1);") +--- +- error: 'Failed to execute SQL statement: CHECK constraint failed: Xgreater10' +... +box.execute("INSERT INTO \"test\" VALUES(20, 10);") +--- +- error: 'Failed to execute SQL statement: CHECK constraint failed: XlessY' +... +box.execute("INSERT INTO \"test\" VALUES(20, 100);") +--- +- row_count: 1 +... +s:truncate() +--- +... +errinj.set("ERRINJ_WAL_IO", true) +--- +- ok +... +s:format({{name='Y', type='integer'}, {name='X', type='integer'}}) +--- +- error: Failed to write to disk +... +errinj.set("ERRINJ_WAL_IO", false) +--- +- ok +... +box.execute("INSERT INTO \"test\" VALUES(2, 1);") +--- +- error: 'Failed to execute SQL statement: CHECK constraint failed: XlessY' +... +box.execute("INSERT INTO \"test\" VALUES(20, 10);") +--- +- error: 'Failed to execute SQL statement: CHECK constraint failed: XlessY' +... +box.execute("INSERT INTO \"test\" VALUES(20, 100);") +--- +- row_count: 1 +... +s:drop() +--- +... diff --git a/test/sql/errinj.test.lua b/test/sql/errinj.test.lua index 1aff6d77e..7e5550541 100644 --- a/test/sql/errinj.test.lua +++ b/test/sql/errinj.test.lua @@ -149,3 +149,48 @@ box.execute("CREATE TABLE hello (id INT primary key,x INT,y INT);") dummy_f = function(int) return 1 end box.internal.sql_create_function("counter1", "INT", dummy_f, -1, false) errinj.set("ERRINJ_SQL_NAME_NORMALIZATION", false) + +-- +-- Tests which are aimed at verifying work of commit/rollback +-- triggers on _ck_constraint space. +-- +s = box.schema.space.create('test', {format = {{name = 'X', type = 'unsigned'}}}) +pk = box.space.test:create_index('pk') + +errinj.set("ERRINJ_WAL_IO", true) +_ = box.space._ck_constraint:insert({'CK_CONSTRAINT_01', s.id, false, 'X<5'}) +errinj.set("ERRINJ_WAL_IO", false) +_ = box.space._ck_constraint:insert({'CK_CONSTRAINT_01', s.id, false, 'X<5'}) +box.execute("INSERT INTO \"test\" VALUES(5);") +errinj.set("ERRINJ_WAL_IO", true) +_ = box.space._ck_constraint:replace({'CK_CONSTRAINT_01', s.id, false, 'X<=5'}) +errinj.set("ERRINJ_WAL_IO", false) +_ = box.space._ck_constraint:replace({'CK_CONSTRAINT_01', s.id, false, 'X<=5'}) +box.execute("INSERT INTO \"test\" VALUES(5);") +errinj.set("ERRINJ_WAL_IO", true) +_ = box.space._ck_constraint:delete({'CK_CONSTRAINT_01', s.id}) +box.execute("INSERT INTO \"test\" VALUES(6);") +errinj.set("ERRINJ_WAL_IO", false) +_ = box.space._ck_constraint:delete({'CK_CONSTRAINT_01', s.id}) +box.execute("INSERT INTO \"test\" VALUES(6);") +s:drop() + +-- +-- Test that failed space alter doesn't harm ck constraints +-- +s = box.schema.create_space('test') +_ = s:create_index('pk') +s:format({{name='X', type='integer'}, {name='Y', type='integer'}}) +_ = box.space._ck_constraint:insert({'XlessY', s.id, false, 'X < Y'}) +_ = box.space._ck_constraint:insert({'Xgreater10', s.id, false, 'X > 10'}) +box.execute("INSERT INTO \"test\" VALUES(2, 1);") +box.execute("INSERT INTO \"test\" VALUES(20, 10);") +box.execute("INSERT INTO \"test\" VALUES(20, 100);") +s:truncate() +errinj.set("ERRINJ_WAL_IO", true) +s:format({{name='Y', type='integer'}, {name='X', type='integer'}}) +errinj.set("ERRINJ_WAL_IO", false) +box.execute("INSERT INTO \"test\" VALUES(2, 1);") +box.execute("INSERT INTO \"test\" VALUES(20, 10);") +box.execute("INSERT INTO \"test\" VALUES(20, 100);") +s:drop() diff --git a/test/sql/gh-2981-check-autoinc.result b/test/sql/gh-2981-check-autoinc.result index 9e347ca6b..7384c81e8 100644 --- a/test/sql/gh-2981-check-autoinc.result +++ b/test/sql/gh-2981-check-autoinc.result @@ -29,7 +29,7 @@ box.execute("insert into t1 values (18, null);") ... box.execute("insert into t1(s2) values (null);") --- -- error: 'Failed to execute SQL statement: CHECK constraint failed: T1' +- error: 'Failed to execute SQL statement: CHECK constraint failed: CK_CONSTRAINT_1_T1' ... box.execute("insert into t2 values (18, null);") --- @@ -37,7 +37,7 @@ box.execute("insert into t2 values (18, null);") ... box.execute("insert into t2(s2) values (null);") --- -- error: 'Failed to execute SQL statement: CHECK constraint failed: T2' +- error: 'Failed to execute SQL statement: CHECK constraint failed: CK_CONSTRAINT_1_T2' ... box.execute("insert into t2 values (24, null);") --- @@ -45,7 +45,7 @@ box.execute("insert into t2 values (24, null);") ... box.execute("insert into t2(s2) values (null);") --- -- error: 'Failed to execute SQL statement: CHECK constraint failed: T2' +- error: 'Failed to execute SQL statement: CHECK constraint failed: CK_CONSTRAINT_1_T2' ... box.execute("insert into t3 values (9, null)") --- @@ -53,7 +53,7 @@ box.execute("insert into t3 values (9, null)") ... box.execute("insert into t3(s2) values (null)") --- -- error: 'Failed to execute SQL statement: CHECK constraint failed: T3' +- error: 'Failed to execute SQL statement: CHECK constraint failed: CK_CONSTRAINT_1_T3' ... box.execute("DROP TABLE t1") --- diff --git a/test/sql/types.result b/test/sql/types.result index bc4518c01..582785413 100644 --- a/test/sql/types.result +++ b/test/sql/types.result @@ -709,7 +709,7 @@ box.execute("CREATE TABLE t1 (id INT PRIMARY KEY, a BOOLEAN CHECK (a = true));") ... box.execute("INSERT INTO t1 VALUES (1, false);") --- -- error: 'Failed to execute SQL statement: CHECK constraint failed: T1' +- error: 'Failed to execute SQL statement: CHECK constraint failed: CK_CONSTRAINT_1_T1' ... box.execute("INSERT INTO t1 VALUES (1, true);") --- diff --git a/test/sql/upgrade.result b/test/sql/upgrade.result index 3a55f7c53..5da110c14 100644 --- a/test/sql/upgrade.result +++ b/test/sql/upgrade.result @@ -188,6 +188,25 @@ i[1].opts.sql == nil --- - true ... +box.space._space:get(s.id).flags.checks == nil +--- +- true +... +check = box.space._ck_constraint:select()[1] +--- +... +check ~= nil +--- +- true +... +check.name +--- +- CK_CONSTRAINT_1_T5 +... +check.expr_str +--- +- x < 2 +... s:drop() --- ... diff --git a/test/sql/upgrade.test.lua b/test/sql/upgrade.test.lua index b76a8f373..cfda74a08 100644 --- a/test/sql/upgrade.test.lua +++ b/test/sql/upgrade.test.lua @@ -62,6 +62,11 @@ s ~= nil i = box.space._index:select(s.id) i ~= nil i[1].opts.sql == nil +box.space._space:get(s.id).flags.checks == nil +check = box.space._ck_constraint:select()[1] +check ~= nil +check.name +check.expr_str s:drop() test_run:switch('default') diff --git a/test/wal_off/alter.result b/test/wal_off/alter.result index ee280fcbb..8040efa1a 100644 --- a/test/wal_off/alter.result +++ b/test/wal_off/alter.result @@ -28,7 +28,7 @@ end; ... #spaces; --- -- 65505 +- 65504 ... -- cleanup for k, v in pairs(spaces) do -- 2.21.0 ^ permalink raw reply [flat|nested] 25+ messages in thread
* [tarantool-patches] Re: [PATCH v3 1/3] schema: add new system space for CHECK constraints 2019-05-07 9:53 ` Kirill Shcherbatov @ 2019-05-12 13:45 ` n.pettik 2019-05-12 15:52 ` Kirill Shcherbatov 0 siblings, 1 reply; 25+ messages in thread From: n.pettik @ 2019-05-12 13:45 UTC (permalink / raw) To: tarantool-patches; +Cc: Kirill Shcherbatov >> Nit: ck_constraint_def_sizeof() return uint32_t >> >> Also why do we not use vdbe_emit_halt_with_presence_test() >> during creation of check constraints? >> >> tarantool> create table t2(id int primary key, constraint ck1 check(id > 0), constraint ck1 check(id < 0)) >> --- >> - error: Duplicate key exists in unique index 'primary' in space '_ck_constraint' >> ... >> tarantool> create table t2(id int primary key, constraint fk1 foreign key(id) references t2, constraint fk1 foreign key(id) references t2) >> --- >> - error: Constraint FK1 already exists >> ... >> > > And this is a bug in FK > https://github.com/tarantool/tarantool/issues/4183 Still, we have an option to fix it: display proper error message and process clean-up. Please, add a mention that in scope of issue we should also use vdbe_emit_halt_with_presence_test for CK constraints >>> + if (rlist_next(dst_ck_link) != &dest->ck_constraint) >>> + return 0; >> >> After next patch this check could be removed: VDBE program for checking >> consistency of check constraints would be generated automatically. >> In SQLite xFer allows to avoid generation of this program. On the other hand, >> if you added way to temporarily disable check constraints, we would able >> to leave this check and re-enable them after query is executed. So, consider >> way of disabling/enabling check constraints - it might be useful for other >> cases as well. > > At first, there is no way to control this state as you propose. Ok, then we should come up with machinery which will allow us to disable CK constraints, but not other NoSQL triggers. > Next, disabling/enabling triggers is required for upgrade() functionality > and is not a part of public API. From user’s point of view checks are constraints. AFAIR Konstantin asked for a handle to disable check and foreign key constraints. > As in further patches ck constraint are rely > on trigger machinery, we don't need a separate controller. We shouldn’t disable all NoSQL triggers, only check constraints. > So, I've reject xfer optimization when source or destination space has > ck constraints. This optimisation doesn’t seem to be vital. On the other hand, if we can use it, why not to do so? > This patch introduces a new system space to persist check > constraints. Format of the space: > > _ck_constraint (space id = 357) > [<constraint name> STR, <space id> UINT, <expression string>STR] Nit: you forgot about is_deferred field. > Because space alter, index alter and space truncate operations > cause space recreation, introduced RebuildCkConstrains object > that compile -> compiles > new ck constraint objects, replace and remove -> replaces and removes > existent instances atomically(when some compilation fails, > nothing changed). -> is changed > In fact, in scope of this patch we don't > really need to recreate ck_constraint object in such situations > (patch space_def pointer in AST like we did it before is enough > now, but we would -> are going to > The main motivation for these changes is the ability to support > ADD CHECK CONSTRAINT operation in the future. CK constraints are > are easier to manage as self-sustained objects: we mustn’t What to do? > the tuple describing target space to do it(unlike the current > architecture). Can’t parse this sentence. Re-phrase it please. > Disabled xfer optimization when some space have ck constraints > because in the following patches this xfer optimisation becomes > impossible. No reason to rewrite this code. > > Needed for #3691 > — Please, don’t send the whole patch again, I expect only diff between versions. It takes a while to review such huge patch again. ^ permalink raw reply [flat|nested] 25+ messages in thread
* [tarantool-patches] Re: [PATCH v3 1/3] schema: add new system space for CHECK constraints 2019-05-12 13:45 ` n.pettik @ 2019-05-12 15:52 ` Kirill Shcherbatov 2019-05-12 23:04 ` n.pettik 0 siblings, 1 reply; 25+ messages in thread From: Kirill Shcherbatov @ 2019-05-12 15:52 UTC (permalink / raw) To: tarantool-patches, n.pettik >>> tarantool> create table t2(id int primary key, constraint fk1 foreign key(id) references t2, constraint fk1 foreign key(id) references t2) >>> --- >>> - error: Constraint FK1 already exists >>> ... >>> >> >> And this is a bug in FK >> https://github.com/tarantool/tarantool/issues/4183 > > Still, we have an option to fix it: display proper error message and > process clean-up. Please, add a mention that in scope of issue we > should also use vdbe_emit_halt_with_presence_test for CK constraints Can't understand what do you mean. This is a problem with incorrect usage of vdbe_emit_halt_with_presence_test #4183. We may introduce another one bug beautifying this error, but should we? Although this is unrelated problem and I don't care with error would be raised here. >> At first, there is no way to control this state as you propose. > > Ok, then we should come up with machinery which will allow us > to disable CK constraints, but not other NoSQL triggers.> >> Next, disabling/enabling triggers is required for upgrade() functionality >> and is not a part of public API. > > From user’s point of view checks are constraints. AFAIR Konstantin > asked for a handle to disable check and foreign key constraints. Again, what does turning checks on and off have to do with their server integration? >> So, I've reject xfer optimization when source or destination space has >> ck constraints. > > This optimisation doesn’t seem to be vital. On the other hand, if we can > use it, why not to do so? Because in the next patch supporting this change becomes quite expensive and pointless, we discussed it in telegram and you agreed. ^ permalink raw reply [flat|nested] 25+ messages in thread
* [tarantool-patches] Re: [PATCH v3 1/3] schema: add new system space for CHECK constraints 2019-05-12 15:52 ` Kirill Shcherbatov @ 2019-05-12 23:04 ` n.pettik 2019-05-13 7:11 ` Kirill Shcherbatov 0 siblings, 1 reply; 25+ messages in thread From: n.pettik @ 2019-05-12 23:04 UTC (permalink / raw) To: tarantool-patches; +Cc: Kirill Shcherbatov > On 12 May 2019, at 18:52, Kirill Shcherbatov <kshcherbatov@tarantool.org> wrote: > >>>> tarantool> create table t2(id int primary key, constraint fk1 foreign key(id) references t2, constraint fk1 foreign key(id) references t2) >>>> --- >>>> - error: Constraint FK1 already exists >>>> ... >>>> >>> >>> And this is a bug in FK >>> https://github.com/tarantool/tarantool/issues/4183 >> >> Still, we have an option to fix it: display proper error message and >> process clean-up. Please, add a mention that in scope of issue we >> should also use vdbe_emit_halt_with_presence_test for CK constraints > Can't understand what do you mean. This is a problem with incorrect usage of > vdbe_emit_halt_with_presence_test #4183. We may introduce another one > bug beautifying this error, but should we? I don’t ask you to add another one bug, but ask you to file this problem to not forget to fix it when smb will implement #4183. > Although this is unrelated problem and I don't care with error would > be raised here. > >>> At first, there is no way to control this state as you propose. >> >> Ok, then we should come up with machinery which will allow us >> to disable CK constraints, but not other NoSQL triggers.> >>> Next, disabling/enabling triggers is required for upgrade() functionality >>> and is not a part of public API. >> >> From user’s point of view checks are constraints. AFAIR Konstantin >> asked for a handle to disable check and foreign key constraints. > Again, what does turning checks on and off have to do with their > server integration? You broke optimisation, I suggest way (turning CK constraints on/off) to fix that. It is not about current patch, but rather about the next one. >>> So, I've reject xfer optimization when source or destination space has >>> ck constraints. >> >> This optimisation doesn’t seem to be vital. On the other hand, if we can >> use it, why not to do so? > Because in the next patch supporting this change becomes quite expensive > and pointless, we discussed it in telegram and you agreed. Agh, AFAIR we decided to turn it on when strings of expressions are equal. If you don’t really want to introduce handle disabling check constraints now, then add FIXME comment and open an issue (sql: add handle to disable constraints). That’s it. ^ permalink raw reply [flat|nested] 25+ messages in thread
* [tarantool-patches] Re: [PATCH v3 1/3] schema: add new system space for CHECK constraints 2019-05-12 23:04 ` n.pettik @ 2019-05-13 7:11 ` Kirill Shcherbatov 2019-05-13 12:29 ` n.pettik 0 siblings, 1 reply; 25+ messages in thread From: Kirill Shcherbatov @ 2019-05-13 7:11 UTC (permalink / raw) To: tarantool-patches, Nikita Pettik Ok, thank you. Now it is clear. ^ permalink raw reply [flat|nested] 25+ messages in thread
* [tarantool-patches] Re: [PATCH v3 1/3] schema: add new system space for CHECK constraints 2019-05-13 7:11 ` Kirill Shcherbatov @ 2019-05-13 12:29 ` n.pettik 2019-05-13 13:13 ` Vladislav Shpilevoy 0 siblings, 1 reply; 25+ messages in thread From: n.pettik @ 2019-05-13 12:29 UTC (permalink / raw) To: tarantool-patches; +Cc: Kirill Shcherbatov, Vladislav Shpilevoy > On 13 May 2019, at 10:11, Kirill Shcherbatov <kshcherbatov@tarantool.org> wrote: > > Ok, thank you. Now it is clear. I can say that now this particular patch LGTM. Since it is huge enough, I guess smb should briefly revise it as well. Vlad, could you take a quick look at it? ^ permalink raw reply [flat|nested] 25+ messages in thread
* [tarantool-patches] Re: [PATCH v3 1/3] schema: add new system space for CHECK constraints 2019-05-13 12:29 ` n.pettik @ 2019-05-13 13:13 ` Vladislav Shpilevoy 0 siblings, 0 replies; 25+ messages in thread From: Vladislav Shpilevoy @ 2019-05-13 13:13 UTC (permalink / raw) To: tarantool-patches, n.pettik; +Cc: Kirill Shcherbatov Yes, I will. Kirill, please, send it again in a new thread. You can keep version 3 and omit change list. On 13/05/2019 15:29, n.pettik wrote: > >> On 13 May 2019, at 10:11, Kirill Shcherbatov <kshcherbatov@tarantool.org> wrote: >> >> Ok, thank you. Now it is clear. > > I can say that now this particular patch LGTM. Since it is huge > enough, I guess smb should briefly revise it as well. Vlad, could > you take a quick look at it? > ^ permalink raw reply [flat|nested] 25+ messages in thread
* [tarantool-patches] [PATCH v3 2/3] box: run check constraint tests on space alter 2019-04-16 13:51 [tarantool-patches] [PATCH v3 0/3] box: run checks on insertions in LUA spaces Kirill Shcherbatov 2019-04-16 13:51 ` [tarantool-patches] [PATCH v3 1/3] schema: add new system space for CHECK constraints Kirill Shcherbatov @ 2019-04-16 13:51 ` Kirill Shcherbatov 2019-04-25 20:38 ` [tarantool-patches] " n.pettik 2019-04-16 13:51 ` [tarantool-patches] [PATCH v3 3/3] box: user-friendly interface to manage ck constraints Kirill Shcherbatov 2 siblings, 1 reply; 25+ messages in thread From: Kirill Shcherbatov @ 2019-04-16 13:51 UTC (permalink / raw) To: tarantool-patches, korablev; +Cc: Kirill Shcherbatov To perform ck constraints tests before insert or update space operation, we use precompiled VDBE machine is associated with each ck constraint, that is executed in on_replace trigger. Each ck constraint VDBE code is consists of 1) prologue code that maps new(or updated) tuple fields via bindings, 2) ck constraint code is generated by CK constraint AST. In case of ck constraint error the transaction is aborted and ck constraint error is handled as diag message. Each ck constraint use own on_replace trigger. Closes #3691 --- src/box/alter.cc | 25 +++- src/box/ck_constraint.c | 158 +++++++++++++++++++++++++- src/box/ck_constraint.h | 21 ++++ src/box/errcode.h | 1 + src/box/sql/insert.c | 93 ++++----------- src/box/sql/sqlInt.h | 26 +++++ src/box/sql/vdbeapi.c | 8 -- test/box/misc.result | 1 + test/sql-tap/check.test.lua | 32 +++--- test/sql-tap/fkey2.test.lua | 4 +- test/sql-tap/table.test.lua | 8 +- test/sql/checks.result | 114 +++++++++++++++++-- test/sql/checks.test.lua | 29 +++++ test/sql/errinj.result | 18 ++- test/sql/gh-2981-check-autoinc.result | 12 +- 15 files changed, 426 insertions(+), 124 deletions(-) diff --git a/src/box/alter.cc b/src/box/alter.cc index e96d502c9..57662df11 100644 --- a/src/box/alter.cc +++ b/src/box/alter.cc @@ -1410,14 +1410,29 @@ void RebuildCkConstraints::alter(struct alter_space *alter) { rlist_swap(&alter->new_space->ck_constraint, &ck_constraint); + struct ck_constraint *ck; + rlist_foreach_entry(ck, &alter->old_space->ck_constraint, link) + trigger_clear(&ck->trigger); rlist_swap(&ck_constraint, &alter->old_space->ck_constraint); + rlist_foreach_entry(ck, &alter->new_space->ck_constraint, link) { + /** + * Triggers would be swapped later on + * alter_space_do. + */ + trigger_add(&alter->old_space->on_replace, &ck->trigger); + } } void RebuildCkConstraints::rollback(struct alter_space *alter) { rlist_swap(&alter->old_space->ck_constraint, &ck_constraint); + struct ck_constraint *ck; + rlist_foreach_entry(ck, &alter->new_space->ck_constraint, link) + trigger_clear(&ck->trigger); rlist_swap(&ck_constraint, &alter->new_space->ck_constraint); + rlist_foreach_entry(ck, &alter->old_space->ck_constraint, link) + trigger_add(&alter->new_space->on_replace, &ck->trigger); } RebuildCkConstraints::~RebuildCkConstraints() @@ -4152,12 +4167,14 @@ on_replace_ck_constraint_rollback(struct trigger *trigger, void *event) assert(space_ck_constraint_by_name(space, ck->def->name, strlen(ck->def->name)) == NULL); rlist_add_entry(&space->ck_constraint, ck, link); + trigger_add(&space->on_replace, &ck->trigger); } else if (stmt->new_tuple != NULL && stmt->old_tuple == NULL) { /* Rollback INSERT check constraint. */ assert(space != NULL); assert(space_ck_constraint_by_name(space, ck->def->name, strlen(ck->def->name)) != NULL); rlist_del_entry(ck, link); + trigger_clear(&ck->trigger); ck_constraint_delete(ck); } else { /* Rollback REPLACE check constraint. */ @@ -4167,7 +4184,9 @@ on_replace_ck_constraint_rollback(struct trigger *trigger, void *event) space_ck_constraint_by_name(space, name, strlen(name)); assert(new_ck != NULL); rlist_del_entry(new_ck, link); + trigger_clear(&new_ck->trigger); rlist_add_entry(&space->ck_constraint, ck, link); + trigger_add(&space->on_replace, &ck->trigger); ck_constraint_delete(new_ck); } } @@ -4227,9 +4246,12 @@ on_replace_dd_ck_constraint(struct trigger * /* trigger*/, void *event) const char *name = new_ck_constraint->def->name; struct ck_constraint *old_ck_constraint = space_ck_constraint_by_name(space, name, strlen(name)); - if (old_ck_constraint != NULL) + if (old_ck_constraint != NULL) { rlist_del_entry(old_ck_constraint, link); + trigger_clear(&old_ck_constraint->trigger); + } rlist_add_entry(&space->ck_constraint, new_ck_constraint, link); + trigger_add(&space->on_replace, &new_ck_constraint->trigger); on_commit->data = old_ck_constraint; on_rollback->data = old_tuple == NULL ? new_ck_constraint : old_ck_constraint; @@ -4245,6 +4267,7 @@ on_replace_dd_ck_constraint(struct trigger * /* trigger*/, void *event) space_ck_constraint_by_name(space, name, name_len); assert(old_ck_constraint != NULL); rlist_del_entry(old_ck_constraint, link); + trigger_clear(&old_ck_constraint->trigger); on_commit->data = old_ck_constraint; on_rollback->data = old_ck_constraint; } diff --git a/src/box/ck_constraint.c b/src/box/ck_constraint.c index daeebb78b..26d18330b 100644 --- a/src/box/ck_constraint.c +++ b/src/box/ck_constraint.c @@ -28,11 +28,15 @@ * THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. */ +#include "bind.h" #include "ck_constraint.h" #include "errcode.h" -#include "small/rlist.h" +#include "schema.h" +#include "session.h" #include "sql.h" #include "sql/sqlInt.h" +#include "sql/vdbeInt.h" +#include "tuple.h" /** * Resolve space_def references for check constraint via AST @@ -55,6 +59,146 @@ ck_constraint_resolve_space_def(struct Expr *expr, return rc; } +/** + * Create VDBE machine for ck constraint by given definition and + * Expression AST. The generated instructions are consist of + * prologue code that maps tuple fields via bindings and ck + * constraint code is generated by given expression. + * In case of ck constraint error the VDBE execution is aborted + * and error is handled as diag message. + * @param ck_constraint_def Check constraint definition to prepare + * errors descriptions. + * @param expr Check constraint expression AST is built for + * given @ck_constraint_def, see for + * (sql_expr_compile + + * ck_constraint_resolve_space_def) implementation. + * @param space_def The space definition of the space this check + * constraint is constructed for. + * @param[out] new_tuple_var The VDBE register that contains the + * first(of allocation) variable to bind + * the corresponding first tuple field. + * @retval not NULL sql_stmt program pointer on success. + * @retval NULL otherwise. + */ +static struct sql_stmt * +ck_constraint_program_compile(struct ck_constraint_def *ck_constraint_def, + struct Expr *expr, struct space_def *space_def, + int *new_tuple_var) +{ + struct sql *db = sql_get(); + struct Parse parser; + sql_parser_create(&parser, db); + struct Vdbe *v = sqlGetVdbe(&parser); + if (v == NULL) { + diag_set(OutOfMemory, sizeof(struct Vdbe), "sqlGetVdbe", + "vdbe"); + return NULL; + } + /* Compile VDBE with default sql parameters. */ + struct session *user_session = current_session(); + uint32_t sql_flags = user_session->sql_flags; + user_session->sql_flags = default_flags; + /* + * Generate a prologue code that introduce variables to + * bind tuple fields there before execution. + */ + uint32_t field_count = space_def->field_count; + int new_tuple_reg = sqlGetTempRange(&parser, field_count); + *new_tuple_var = parser.nVar + 1; + struct Expr bind = {.op = TK_VARIABLE, .u.zToken = "?"}; + for (uint32_t i = 0; i < field_count; i++) { + sqlExprAssignVarNumber(&parser, &bind, 1); + sqlExprCodeTarget(&parser, &bind, new_tuple_reg + i); + } + /* Generate ck constraint test code. */ + vdbe_emit_ck_constraint(&parser, expr, ck_constraint_def->name, + ck_constraint_def->expr_str, new_tuple_reg); + + /* Clean-up and restore user-defined sql context. */ + bool is_error = parser.is_aborted; + sql_finish_coding(&parser); + sql_parser_destroy(&parser); + user_session->sql_flags = sql_flags; + + if (is_error) { + diag_set(ClientError, ER_CREATE_CK_CONSTRAINT, + ck_constraint_def->name, + box_error_message(box_error_last())); + sql_finalize((struct sql_stmt *) v); + return NULL; + } + return (struct sql_stmt *) v; +} + +/** + * Perform ck constraint test on new tuple data new_tuple + * before insert or replace in space space_def. + * @param ck_constraint Check constraint to perform. + * @param space_def The space definition of the space this check + * constraint is constructed for. + * @param new_tuple The tuple to be inserted in space. + * @retval 0 if check constraint test is passed, -1 otherwise. + */ +static int +ck_constraint_program_run(struct ck_constraint *ck_constraint, + const char *new_tuple) +{ + assert(new_tuple != NULL); + /* + * Prepare parameters for checks->stmt execution: + * Map new tuple fields to Vdbe memory variables in range: + * [new_tuple_var, new_tuple_var + field_count] + */ + mp_decode_array(&new_tuple); + struct space *space = space_by_id(ck_constraint->space_id); + assert(space != NULL); + for (uint32_t i = 0; i < space->def->field_count; i++) { + struct sql_bind bind; + if (sql_bind_decode(&bind, ck_constraint->new_tuple_var + i, + &new_tuple) != 0 || + sql_bind_column(ck_constraint->stmt, &bind, + ck_constraint->new_tuple_var + i) != 0) { + diag_set(ClientError, ER_CK_CONSTRAINT_FAILED, + ck_constraint->def->name, + box_error_message(box_error_last())); + return -1; + } + } + /* Checks VDBE can't expire, reset expired flag and go. */ + struct Vdbe *v = (struct Vdbe *) ck_constraint->stmt; + v->expired = 0; + while (sql_step(ck_constraint->stmt) == SQL_ROW) {} + /* + * Get VDBE execution state and reset VM to run it + * next time. + */ + return sql_reset(ck_constraint->stmt) != SQL_OK ? -1 : 0; +} + +/** + * Ck constraint trigger function. Is expected to be executed in + * space::on_replace trigger. + * + * Extracts all ck constraint related context from event and run + * bytecode implementing check constraint to test a new tuple + * before it would be inserted in destination space. + */ +static void +ck_constraint_on_replace_trigger(struct trigger *trigger, void *event) +{ + struct ck_constraint *ck_constraint = + (struct ck_constraint *) trigger->data; + struct txn *txn = (struct txn *) event; + struct txn_stmt *stmt = txn_current_stmt(txn); + assert(stmt != NULL); + struct tuple *new_tuple = stmt->new_tuple; + if (new_tuple == NULL) + return; + if (ck_constraint_program_run(ck_constraint, + tuple_data(new_tuple)) != 0) + diag_raise(); +} + struct ck_constraint * ck_constraint_new(struct ck_constraint_def *ck_constraint_def, struct space_def *space_def) @@ -71,8 +215,12 @@ ck_constraint_new(struct ck_constraint_def *ck_constraint_def, return NULL; } ck_constraint->def = NULL; + ck_constraint->stmt = NULL; ck_constraint->space_id = space_def->id; rlist_create(&ck_constraint->link); + trigger_create(&ck_constraint->trigger, + ck_constraint_on_replace_trigger, ck_constraint, + NULL); ck_constraint->expr = sql_expr_compile(sql_get(), ck_constraint_def->expr_str, strlen(ck_constraint_def->expr_str)); @@ -84,6 +232,12 @@ ck_constraint_new(struct ck_constraint_def *ck_constraint_def, box_error_message(box_error_last())); goto error; } + ck_constraint->stmt = + ck_constraint_program_compile(ck_constraint_def, + ck_constraint->expr, space_def, + &ck_constraint->new_tuple_var); + if (ck_constraint->stmt == NULL) + goto error; ck_constraint->def = ck_constraint_def; return ck_constraint; @@ -95,6 +249,8 @@ error: void ck_constraint_delete(struct ck_constraint *ck_constraint) { + assert(rlist_empty(&ck_constraint->trigger.link)); + sql_finalize(ck_constraint->stmt); sql_expr_delete(sql_get(), ck_constraint->expr, false); free(ck_constraint->def); TRASH(ck_constraint); diff --git a/src/box/ck_constraint.h b/src/box/ck_constraint.h index 615612605..66f812fcc 100644 --- a/src/box/ck_constraint.h +++ b/src/box/ck_constraint.h @@ -32,6 +32,7 @@ */ #include <stdint.h> +#include "trigger.h" #include "small/rlist.h" #if defined(__cplusplus) @@ -40,6 +41,7 @@ extern "C" { struct space; struct space_def; +struct sql_stmt; struct Expr; /** @@ -76,6 +78,25 @@ struct ck_constraint { * space with space[ck_constraint::space_id] definition. */ struct Expr *expr; + /** + * Precompiled reusable VDBE program for processing check + * constraints and setting bad exitcode and error + * message when ck condition unsatisfied. + * Program rely on new_tuple_var parameter to be bound to + * the VDBE memory before run. + */ + struct sql_stmt *stmt; + /** + * The first ck_constraint::stmt VDBE variable of the + * range space[ck_constraint::space_id]->def->field_count + * representing a new tuple to be inserted. + */ + int new_tuple_var; + /** + * Trigger object executing check constraint on space + * insert and replace. + */ + struct trigger trigger; /** * The id of the space this check constraint is * built for. diff --git a/src/box/errcode.h b/src/box/errcode.h index 7878bd66b..750cd23a0 100644 --- a/src/box/errcode.h +++ b/src/box/errcode.h @@ -247,6 +247,7 @@ struct errcode_record { /*192 */_(ER_INDEX_DEF_UNSUPPORTED, "%s are prohibited in an index definition") \ /*193 */_(ER_CK_DEF_UNSUPPORTED, "%s are prohibited in a check constraint definition") \ /*194 */_(ER_CREATE_CK_CONSTRAINT, "Failed to create check constraint '%s': %s") \ + /*195 */_(ER_CK_CONSTRAINT_FAILED, "Check constraint failed '%s': %s") \ /* * !IMPORTANT! Please follow instructions at start of the file * when adding new errors. diff --git a/src/box/sql/insert.c b/src/box/sql/insert.c index a075d4dc3..abae874b8 100644 --- a/src/box/sql/insert.c +++ b/src/box/sql/insert.c @@ -813,51 +813,27 @@ sqlInsert(Parse * pParse, /* Parser context */ sqlDbFree(db, aRegIdx); } -/* - * Meanings of bits in of pWalker->eCode for checkConstraintUnchanged() - */ -#define CKCNSTRNT_COLUMN 0x01 /* CHECK constraint uses a changing column */ - -/* This is the Walker callback from checkConstraintUnchanged(). Set - * bit 0x01 of pWalker->eCode if - * pWalker->eCode to 0 if this expression node references any of the - * columns that are being modifed by an UPDATE statement. - */ -static int -checkConstraintExprNode(Walker * pWalker, Expr * pExpr) -{ - if (pExpr->op == TK_COLUMN) { - assert(pExpr->iColumn >= 0 || pExpr->iColumn == -1); - if (pExpr->iColumn >= 0) { - if (pWalker->u.aiCol[pExpr->iColumn] >= 0) { - pWalker->eCode |= CKCNSTRNT_COLUMN; - } - } - } - return WRC_Continue; -} - -/* - * pExpr is a CHECK constraint on a row that is being UPDATE-ed. The - * only columns that are modified by the UPDATE are those for which - * aiChng[i]>=0. - * - * Return true if CHECK constraint pExpr does not use any of the - * changing columns. In other words, return true if this CHECK constraint - * can be skipped when validating the new row in the UPDATE statement. - */ -static int -checkConstraintUnchanged(Expr * pExpr, int *aiChng) +void +vdbe_emit_ck_constraint(struct Parse *parser, struct Expr *expr, + const char *name, const char *expr_str, + int new_tuple_reg) { - Walker w; - memset(&w, 0, sizeof(w)); - w.eCode = 0; - w.xExprCallback = checkConstraintExprNode; - w.u.aiCol = aiChng; - sqlWalkExpr(&w, pExpr); - testcase(w.eCode == 0); - testcase(w.eCode == CKCNSTRNT_COLUMN); - return !w.eCode; + parser->ckBase = new_tuple_reg; + struct Vdbe *v = sqlGetVdbe(parser); + const char *ck_constraint_name = sqlDbStrDup(parser->db, name); + VdbeNoopComment((v, "BEGIN: ck constraint %s test", + ck_constraint_name)); + int check_is_passed = sqlVdbeMakeLabel(v); + sqlExprIfTrue(parser, expr, check_is_passed, SQL_JUMPIFNULL); + sqlMayAbort(parser); + const char *fmt = tnt_errcode_desc(ER_CK_CONSTRAINT_FAILED); + const char *error_msg = tt_sprintf(fmt, ck_constraint_name, expr_str); + sqlVdbeAddOp4(v, OP_Halt, SQL_TARANTOOL_ERROR, + ON_CONFLICT_ACTION_ABORT, 0, + sqlDbStrDup(parser->db, error_msg), P4_DYNAMIC); + sqlVdbeChangeP5(v, ER_CK_CONSTRAINT_FAILED); + VdbeNoopComment((v, "END: ck constraint %s test", ck_constraint_name)); + sqlVdbeResolveLabel(v, check_is_passed); } void @@ -927,35 +903,6 @@ vdbe_emit_constraint_checks(struct Parse *parse_context, struct space *space, unreachable(); } } - /* - * For CHECK constraint and for INSERT/UPDATE conflict - * action DEFAULT and ABORT in fact has the same meaning. - */ - if (on_conflict == ON_CONFLICT_ACTION_DEFAULT) - on_conflict = ON_CONFLICT_ACTION_ABORT; - /* Test all CHECK constraints. */ - enum on_conflict_action on_conflict_check = on_conflict; - if (on_conflict == ON_CONFLICT_ACTION_REPLACE) - on_conflict_check = ON_CONFLICT_ACTION_ABORT; - if (!rlist_empty(&space->ck_constraint)) - parse_context->ckBase = new_tuple_reg; - struct ck_constraint *ck_constraint; - rlist_foreach_entry(ck_constraint, &space->ck_constraint, link) { - struct Expr *expr = ck_constraint->expr; - if (is_update && checkConstraintUnchanged(expr, upd_cols) != 0) - continue; - int all_ok = sqlVdbeMakeLabel(v); - sqlExprIfTrue(parse_context, expr, all_ok, SQL_JUMPIFNULL); - if (on_conflict == ON_CONFLICT_ACTION_IGNORE) { - sqlVdbeGoto(v, ignore_label); - } else { - char *name = ck_constraint->def->name; - sqlHaltConstraint(parse_context, SQL_CONSTRAINT_CHECK, - on_conflict_check, name, P4_TRANSIENT, - P5_ConstraintCheck); - } - sqlVdbeResolveLabel(v, all_ok); - } sql_emit_table_types(v, space->def, new_tuple_reg); /* * Other actions except for REPLACE and UPDATE OR IGNORE diff --git a/src/box/sql/sqlInt.h b/src/box/sql/sqlInt.h index b322602dc..46cb903be 100644 --- a/src/box/sql/sqlInt.h +++ b/src/box/sql/sqlInt.h @@ -593,6 +593,17 @@ sql_column_value(sql_stmt *, int sql_finalize(sql_stmt * pStmt); +/* + * Terminate the current execution of an SQL statement and reset + * it back to its starting state so that it can be reused. + * + * @param stmt VDBE program, may be NULL. + * @retval SQL_OK On success. + * @retval sql_ret_code Error code on error. + */ +int +sql_reset(struct sql_stmt *stmt); + int sql_exec(sql *, /* An open database */ const char *sql, /* SQL to be evaluated */ @@ -3956,6 +3967,21 @@ vdbe_emit_constraint_checks(struct Parse *parse_context, enum on_conflict_action on_conflict, int ignore_label, int *upd_cols); +/** + * Gnerate code to make check constraints tests on tuple insertion + * on INSERT, REPLACE or UPDATE operations. + * @param parser Current parsing context. + * @param expr Check constraint AST. + * @param name Check constraint name to raise error. + * @param new_tuple_reg The first ck_constraint::stmt VDBE + * register of the range + * space_def::field_count representing a + * new tuple to be inserted. + */ +void +vdbe_emit_ck_constraint(struct Parse *parser, struct Expr *expr, + const char *name, const char *expr_str, + int new_tuple_reg); /** * This routine generates code to finish the INSERT or UPDATE * operation that was started by a prior call to diff --git a/src/box/sql/vdbeapi.c b/src/box/sql/vdbeapi.c index be5c9dff9..ebedd3c42 100644 --- a/src/box/sql/vdbeapi.c +++ b/src/box/sql/vdbeapi.c @@ -132,14 +132,6 @@ sql_finalize(sql_stmt * pStmt) return rc; } -/* - * Terminate the current execution of an SQL statement and reset it - * back to its starting state so that it can be reused. A success code from - * the prior execution is returned. - * - * This routine sets the error code and string returned by - * sql_errcode(), sql_errmsg() and sql_errmsg16(). - */ int sql_reset(sql_stmt * pStmt) { diff --git a/test/box/misc.result b/test/box/misc.result index eeb63366e..c1c7ffdc6 100644 --- a/test/box/misc.result +++ b/test/box/misc.result @@ -523,6 +523,7 @@ t; 192: box.error.INDEX_DEF_UNSUPPORTED 193: box.error.CK_DEF_UNSUPPORTED 194: box.error.CREATE_CK_CONSTRAINT + 195: box.error.CK_CONSTRAINT_FAILED ... test_run:cmd("setopt delimiter ''"); --- diff --git a/test/sql-tap/check.test.lua b/test/sql-tap/check.test.lua index 0dda3fac8..1ff2eff20 100755 --- a/test/sql-tap/check.test.lua +++ b/test/sql-tap/check.test.lua @@ -55,7 +55,7 @@ test:do_catchsql_test( INSERT INTO t1 VALUES(6,7, 2); ]], { -- <check-1.3> - 1, "Failed to execute SQL statement: CHECK constraint failed: CK_CONSTRAINT_1_T1" + 1, "Failed to execute SQL statement: Check constraint failed 'CK_CONSTRAINT_1_T1': x<5" -- </check-1.3> }) @@ -75,7 +75,7 @@ test:do_catchsql_test( INSERT INTO t1 VALUES(4,3, 2); ]], { -- <check-1.5> - 1, "Failed to execute SQL statement: CHECK constraint failed: CK_CONSTRAINT_2_T1" + 1, "Failed to execute SQL statement: Check constraint failed 'CK_CONSTRAINT_2_T1': y>x" -- </check-1.5> }) @@ -147,7 +147,7 @@ test:do_catchsql_test( UPDATE t1 SET x=7 WHERE x==2 ]], { -- <check-1.12> - 1, "Failed to execute SQL statement: CHECK constraint failed: CK_CONSTRAINT_1_T1" + 1, "Check constraint failed 'CK_CONSTRAINT_1_T1': x<5" -- </check-1.12> }) @@ -167,7 +167,7 @@ test:do_catchsql_test( UPDATE t1 SET x=5 WHERE x==2 ]], { -- <check-1.14> - 1, "Failed to execute SQL statement: CHECK constraint failed: CK_CONSTRAINT_1_T1" + 1, "Check constraint failed 'CK_CONSTRAINT_1_T1': x<5" -- </check-1.14> }) @@ -206,9 +206,9 @@ test:do_execsql_test( [[ CREATE TABLE t2( id INT primary key, - x INTEGER CONSTRAINT one CHECK( typeof(coalesce(x,0))=='integer'), + x SCALAR CONSTRAINT one CHECK( typeof(coalesce(x,0))=='integer'), y REAL CONSTRAINT two CHECK( typeof(coalesce(y,0.1))=='number' ), - z TEXT CONSTRAINT three CHECK( typeof(coalesce(z,''))=='string' ) + z SCALAR CONSTRAINT three CHECK( typeof(coalesce(z,''))=='string' ) ); ]], { -- <check-2.1> @@ -246,7 +246,7 @@ test:do_catchsql_test( INSERT INTO t2 VALUES(3, 1.1, NULL, NULL); ]], { -- <check-2.4> - 1, "Failed to execute SQL statement: CHECK constraint failed: ONE" + 1, "Failed to execute SQL statement: Check constraint failed 'ONE': typeof(coalesce(x,0))=='integer'" -- </check-2.4> }) @@ -256,7 +256,7 @@ test:do_catchsql_test( INSERT INTO t2 VALUES(4, NULL, 5, NULL); ]], { -- <check-2.5> - 1, "Failed to execute SQL statement: CHECK constraint failed: TWO" + 1, "Failed to execute SQL statement: Check constraint failed 'TWO': typeof(coalesce(y,0.1))=='number'" -- </check-2.5> }) @@ -266,7 +266,7 @@ test:do_catchsql_test( INSERT INTO t2 VALUES(5, NULL, NULL, 3.14159); ]], { -- <check-2.6> - 1, "Failed to execute SQL statement: CHECK constraint failed: THREE" + 1, "Failed to execute SQL statement: Check constraint failed 'THREE': typeof(coalesce(z,''))=='string'" -- </check-2.6> }) @@ -413,7 +413,7 @@ test:do_catchsql_test( INSERT INTO t3 VALUES(111,222,333); ]], { -- <check-3.9> - 1, "Failed to execute SQL statement: CHECK constraint failed: CK_CONSTRAINT_1_T3" + 1, "Failed to execute SQL statement: Check constraint failed 'CK_CONSTRAINT_1_T3': t3.x<25" -- </check-3.9> }) @@ -484,7 +484,7 @@ test:do_catchsql_test( UPDATE t4 SET x=0, y=1; ]], { -- <check-4.6> - 1, "Failed to execute SQL statement: CHECK constraint failed: CK_CONSTRAINT_1_T4" + 1, "Check constraint failed 'CK_CONSTRAINT_1_T4': x+y==11\n OR x*y==12\n OR x/y BETWEEN 5 AND 8\n OR -x==y+10" -- </check-4.6> }) @@ -504,7 +504,7 @@ test:do_catchsql_test( UPDATE t4 SET x=0, y=2; ]], { -- <check-4.9> - 1, "Failed to execute SQL statement: CHECK constraint failed: CK_CONSTRAINT_1_T4" + 1, "Check constraint failed 'CK_CONSTRAINT_1_T4': x+y==11\n OR x*y==12\n OR x/y BETWEEN 5 AND 8\n OR -x==y+10" -- </check-4.9> }) @@ -581,7 +581,7 @@ test:do_catchsql_test( UPDATE OR FAIL t1 SET x=7-x, y=y+1; ]], { -- <check-6.5> - 1, "Failed to execute SQL statement: CHECK constraint failed: CK_CONSTRAINT_1_T1" + 1, "Check constraint failed 'CK_CONSTRAINT_1_T1': x<5" -- </check-6.5> }) @@ -603,7 +603,7 @@ test:do_catchsql_test( INSERT OR ROLLBACK INTO t1 VALUES(8,40.0, 10); ]], { -- <check-6.7> - 1, "Failed to execute SQL statement: CHECK constraint failed: CK_CONSTRAINT_1_T1" + 1, "Failed to execute SQL statement: Check constraint failed 'CK_CONSTRAINT_1_T1': x<5" -- </check-6.7> }) @@ -636,7 +636,7 @@ test:do_catchsql_test( REPLACE INTO t1 VALUES(6,7, 11); ]], { -- <check-6.12> - 1, "Failed to execute SQL statement: CHECK constraint failed: CK_CONSTRAINT_1_T1" + 1, "Failed to execute SQL statement: Check constraint failed 'CK_CONSTRAINT_1_T1': x<5" -- </check-6.12> }) @@ -700,7 +700,7 @@ test:do_catchsql_test( 7.3, " INSERT INTO t6 VALUES(11) ", { -- <7.3> - 1, "Failed to execute SQL statement: CHECK constraint failed: CK_CONSTRAINT_1_T6" + 1, "Failed to execute SQL statement: Check constraint failed 'CK_CONSTRAINT_1_T6': myfunc(a)" -- </7.3> }) diff --git a/test/sql-tap/fkey2.test.lua b/test/sql-tap/fkey2.test.lua index 695a379a6..def5f8321 100755 --- a/test/sql-tap/fkey2.test.lua +++ b/test/sql-tap/fkey2.test.lua @@ -362,7 +362,7 @@ test:do_catchsql_test( UPDATE ab SET a = 5; ]], { -- <fkey2-3.2> - 1, "Failed to execute SQL statement: CHECK constraint failed: CK_CONSTRAINT_1_EF" + 1, "Check constraint failed 'CK_CONSTRAINT_1_EF': e!=5" -- </fkey2-3.2> }) @@ -382,7 +382,7 @@ test:do_catchsql_test( UPDATE ab SET a = 5; ]], { -- <fkey2-3.4> - 1, "Failed to execute SQL statement: CHECK constraint failed: CK_CONSTRAINT_1_EF" + 1, "Check constraint failed 'CK_CONSTRAINT_1_EF': e!=5" -- </fkey2-3.4> }) diff --git a/test/sql-tap/table.test.lua b/test/sql-tap/table.test.lua index 066662f33..288724f55 100755 --- a/test/sql-tap/table.test.lua +++ b/test/sql-tap/table.test.lua @@ -1221,7 +1221,7 @@ test:do_catchsql_test( INSERT INTO T21 VALUES(1, -1, 1); ]], { -- <table-21.3> - 1, "Failed to execute SQL statement: CHECK constraint failed: CK_CONSTRAINT_1_T21" + 1, "Failed to execute SQL statement: Check constraint failed 'CK_CONSTRAINT_1_T21': B > 0" -- </table-21.3> }) @@ -1231,7 +1231,7 @@ test:do_catchsql_test( INSERT INTO T21 VALUES(1, 1, -1); ]], { -- <table-21.4> - 1, "Failed to execute SQL statement: CHECK constraint failed: CK_CONSTRAINT_2_T21" + 1, "Failed to execute SQL statement: Check constraint failed 'CK_CONSTRAINT_2_T21': C > 0" -- </table-21.4> }) @@ -1372,7 +1372,7 @@ test:do_catchsql_test( INSERT INTO T28 VALUES(0); ]], { -- <table-22.10> - 1, "Failed to execute SQL statement: CHECK constraint failed: CHECK1" + 1, "Failed to execute SQL statement: Check constraint failed 'CHECK1': id != 0" -- </table-22.10> }) @@ -1382,7 +1382,7 @@ test:do_catchsql_test( INSERT INTO T28 VALUES(9); ]], { -- <table-22.11> - 1, "Failed to execute SQL statement: CHECK constraint failed: CHECK2" + 1, "Failed to execute SQL statement: Check constraint failed 'CHECK2': id > 10" -- </table-22.11> }) diff --git a/test/sql/checks.result b/test/sql/checks.result index 0042243ea..0dd5d820f 100644 --- a/test/sql/checks.result +++ b/test/sql/checks.result @@ -63,7 +63,12 @@ box.space._ck_constraint:count({}) ... box.execute("INSERT INTO \"test\" VALUES(5);") --- -- error: 'Failed to execute SQL statement: CHECK constraint failed: CK_CONSTRAINT_01' +- error: 'Failed to execute SQL statement: Check constraint failed ''CK_CONSTRAINT_01'': + X<5' +... +box.space.test:insert({5}) +--- +- error: 'Check constraint failed ''CK_CONSTRAINT_01'': X<5' ... box.space._ck_constraint:replace({'CK_CONSTRAINT_01', 513, 'X<=5'}) --- @@ -75,7 +80,12 @@ box.execute("INSERT INTO \"test\" VALUES(5);") ... box.execute("INSERT INTO \"test\" VALUES(6);") --- -- error: 'Failed to execute SQL statement: CHECK constraint failed: CK_CONSTRAINT_01' +- error: 'Failed to execute SQL statement: Check constraint failed ''CK_CONSTRAINT_01'': + X<=5' +... +box.space.test:insert({6}) +--- +- error: 'Check constraint failed ''CK_CONSTRAINT_01'': X<=5' ... -- Can't drop table with check constraints. box.space.test:delete({5}) @@ -108,16 +118,28 @@ box.space._ck_constraint:count() ... box.execute("INSERT INTO t1 VALUES (7, 1, 1)") --- -- error: 'Failed to execute SQL statement: CHECK constraint failed: ONE' +- error: 'Failed to execute SQL statement: Check constraint failed ''ONE'': x<5' +... +box.space.T1:insert({7, 1, 1}) +--- +- error: 'Check constraint failed ''ONE'': x<5' ... box.execute("INSERT INTO t1 VALUES (2, 1, 1)") --- -- error: 'Failed to execute SQL statement: CHECK constraint failed: TWO' +- error: 'Failed to execute SQL statement: Check constraint failed ''TWO'': y>x' +... +box.space.T1:insert({2, 1, 1}) +--- +- error: 'Check constraint failed ''TWO'': y>x' ... box.execute("INSERT INTO t1 VALUES (2, 4, 1)") --- - row_count: 1 ... +box.space.T1:update({1}, {{'+', 1, 5}}) +--- +- error: 'Check constraint failed ''ONE'': x<5' +... box.execute("DROP TABLE t1") --- - row_count: 1 @@ -212,14 +234,14 @@ _ = box.space._ck_constraint:insert({'physics', s.id, 'X<Y'}) ... box.execute("INSERT INTO \"test\" VALUES(2, 1);") --- -- error: 'Failed to execute SQL statement: CHECK constraint failed: physics' +- error: 'Failed to execute SQL statement: Check constraint failed ''physics'': X<Y' ... s:format({{name='Y', type='integer'}, {name='X', type='integer'}}) --- ... box.execute("INSERT INTO \"test\" VALUES(1, 2);") --- -- error: 'Failed to execute SQL statement: CHECK constraint failed: physics' +- error: 'Failed to execute SQL statement: Check constraint failed ''physics'': X<Y' ... box.execute("INSERT INTO \"test\" VALUES(2, 1);") --- @@ -230,7 +252,7 @@ s:truncate() ... box.execute("INSERT INTO \"test\" VALUES(1, 2);") --- -- error: 'Failed to execute SQL statement: CHECK constraint failed: physics' +- error: 'Failed to execute SQL statement: Check constraint failed ''physics'': X<Y' ... s:format({}) --- @@ -262,11 +284,11 @@ _ = box.space._ck_constraint:insert({'conflict', s.id, 'X>10'}) ... box.execute("INSERT INTO \"test\" VALUES(1, 2);") --- -- error: 'Failed to execute SQL statement: CHECK constraint failed: conflict' +- error: 'Failed to execute SQL statement: Check constraint failed ''conflict'': X>10' ... box.execute("INSERT INTO \"test\" VALUES(11, 11);") --- -- error: 'Failed to execute SQL statement: CHECK constraint failed: physics' +- error: 'Failed to execute SQL statement: Check constraint failed ''physics'': X<Y' ... box.execute("INSERT INTO \"test\" VALUES(12, 11);") --- @@ -299,3 +321,77 @@ test_run:cmd("clear filter") --- - true ... +-- +-- Test ck constraint corner cases +-- +s = box.schema.create_space('test') +--- +... +_ = s:create_index('pk') +--- +... +s:format({{name='X', type='any'}, {name='Y', type='integer'}, {name='Z', type='integer', is_nullable=true}}) +--- +... +_ = box.space._ck_constraint:insert({'XlessY', s.id, 'X < Y and Y < Z'}) +--- +... +s:insert({'1', 2}) +--- +- error: 'Tuple field 1 type does not match one required by operation: expected unsigned' +... +s:insert({}) +--- +- error: Tuple field 1 required by space format is missing +... +s:insert({2, 1}) +--- +- error: 'Check constraint failed ''XlessY'': X < Y and Y < Z' +... +s:insert({1, 2}) +--- +- [1, 2] +... +s:insert({2, 3, 1}) +--- +- error: 'Check constraint failed ''XlessY'': X < Y and Y < Z' +... +s:insert({2, 3, 4}) +--- +- [2, 3, 4] +... +s:update({2}, {{'+', 2, 3}}) +--- +- error: 'Check constraint failed ''XlessY'': X < Y and Y < Z' +... +s:update({2}, {{'+', 2, 3}, {'+', 3, 3}}) +--- +- [2, 6, 7] +... +s:replace({2, 1, 3}) +--- +- error: 'Check constraint failed ''XlessY'': X < Y and Y < Z' +... +box.snapshot() +--- +- ok +... +test_run:cmd("restart server default") +s = box.space["test"] +--- +... +s:update({2}, {{'+', 2, 3}}) +--- +- error: 'Check constraint failed ''XlessY'': X < Y and Y < Z' +... +s:update({2}, {{'+', 2, 3}, {'+', 3, 3}}) +--- +- [2, 9, 10] +... +s:replace({2, 1, 3}) +--- +- error: 'Check constraint failed ''XlessY'': X < Y and Y < Z' +... +s:drop() +--- +... diff --git a/test/sql/checks.test.lua b/test/sql/checks.test.lua index 0cdfde2e9..2652f3b7d 100644 --- a/test/sql/checks.test.lua +++ b/test/sql/checks.test.lua @@ -29,9 +29,11 @@ box.space._ck_constraint:insert({'CK_CONSTRAINT_01', 513, 'X<5'}) box.space._ck_constraint:count({}) box.execute("INSERT INTO \"test\" VALUES(5);") +box.space.test:insert({5}) box.space._ck_constraint:replace({'CK_CONSTRAINT_01', 513, 'X<=5'}) box.execute("INSERT INTO \"test\" VALUES(5);") box.execute("INSERT INTO \"test\" VALUES(6);") +box.space.test:insert({6}) -- Can't drop table with check constraints. box.space.test:delete({5}) box.space.test.index.pk:drop() @@ -43,8 +45,11 @@ box.space._space:delete({513}) box.execute("CREATE TABLE t1(x INTEGER CONSTRAINT ONE CHECK( x<5 ), y REAL CONSTRAINT TWO CHECK( y>x ), z INTEGER PRIMARY KEY);") box.space._ck_constraint:count() box.execute("INSERT INTO t1 VALUES (7, 1, 1)") +box.space.T1:insert({7, 1, 1}) box.execute("INSERT INTO t1 VALUES (2, 1, 1)") +box.space.T1:insert({2, 1, 1}) box.execute("INSERT INTO t1 VALUES (2, 4, 1)") +box.space.T1:update({1}, {{'+', 1, 5}}) box.execute("DROP TABLE t1") -- Test xferOptimization for space that have CK constraints. @@ -105,3 +110,27 @@ box.execute("DROP TABLE w2;") box.execute("CREATE TABLE t5(x INT PRIMARY KEY, y INT, CHECK( x*y < ? ));") test_run:cmd("clear filter") + +-- +-- Test ck constraint corner cases +-- +s = box.schema.create_space('test') +_ = s:create_index('pk') +s:format({{name='X', type='any'}, {name='Y', type='integer'}, {name='Z', type='integer', is_nullable=true}}) +_ = box.space._ck_constraint:insert({'XlessY', s.id, 'X < Y and Y < Z'}) +s:insert({'1', 2}) +s:insert({}) +s:insert({2, 1}) +s:insert({1, 2}) +s:insert({2, 3, 1}) +s:insert({2, 3, 4}) +s:update({2}, {{'+', 2, 3}}) +s:update({2}, {{'+', 2, 3}, {'+', 3, 3}}) +s:replace({2, 1, 3}) +box.snapshot() +test_run:cmd("restart server default") +s = box.space["test"] +s:update({2}, {{'+', 2, 3}}) +s:update({2}, {{'+', 2, 3}, {'+', 3, 3}}) +s:replace({2, 1, 3}) +s:drop() diff --git a/test/sql/errinj.result b/test/sql/errinj.result index 453b8485d..e97a4525e 100644 --- a/test/sql/errinj.result +++ b/test/sql/errinj.result @@ -490,7 +490,8 @@ _ = box.space._ck_constraint:insert({'CK_CONSTRAINT_01', s.id, 'X<5'}) ... box.execute("INSERT INTO \"test\" VALUES(5);") --- -- error: 'Failed to execute SQL statement: CHECK constraint failed: CK_CONSTRAINT_01' +- error: 'Failed to execute SQL statement: Check constraint failed ''CK_CONSTRAINT_01'': + X<5' ... errinj.set("ERRINJ_WAL_IO", true) --- @@ -521,7 +522,8 @@ _ = box.space._ck_constraint:delete({'CK_CONSTRAINT_01', s.id}) ... box.execute("INSERT INTO \"test\" VALUES(6);") --- -- error: 'Failed to execute SQL statement: CHECK constraint failed: CK_CONSTRAINT_01' +- error: 'Failed to execute SQL statement: Check constraint failed ''CK_CONSTRAINT_01'': + X<=5' ... errinj.set("ERRINJ_WAL_IO", false) --- @@ -557,11 +559,13 @@ _ = box.space._ck_constraint:insert({'Xgreater10', s.id, 'X > 10'}) ... box.execute("INSERT INTO \"test\" VALUES(2, 1);") --- -- error: 'Failed to execute SQL statement: CHECK constraint failed: Xgreater10' +- error: 'Failed to execute SQL statement: Check constraint failed ''Xgreater10'': + X > 10' ... box.execute("INSERT INTO \"test\" VALUES(20, 10);") --- -- error: 'Failed to execute SQL statement: CHECK constraint failed: XlessY' +- error: 'Failed to execute SQL statement: Check constraint failed ''XlessY'': X < + Y' ... box.execute("INSERT INTO \"test\" VALUES(20, 100);") --- @@ -584,11 +588,13 @@ errinj.set("ERRINJ_WAL_IO", false) ... box.execute("INSERT INTO \"test\" VALUES(2, 1);") --- -- error: 'Failed to execute SQL statement: CHECK constraint failed: XlessY' +- error: 'Failed to execute SQL statement: Check constraint failed ''Xgreater10'': + X > 10' ... box.execute("INSERT INTO \"test\" VALUES(20, 10);") --- -- error: 'Failed to execute SQL statement: CHECK constraint failed: XlessY' +- error: 'Failed to execute SQL statement: Check constraint failed ''XlessY'': X < + Y' ... box.execute("INSERT INTO \"test\" VALUES(20, 100);") --- diff --git a/test/sql/gh-2981-check-autoinc.result b/test/sql/gh-2981-check-autoinc.result index 7384c81e8..e57789897 100644 --- a/test/sql/gh-2981-check-autoinc.result +++ b/test/sql/gh-2981-check-autoinc.result @@ -29,7 +29,8 @@ box.execute("insert into t1 values (18, null);") ... box.execute("insert into t1(s2) values (null);") --- -- error: 'Failed to execute SQL statement: CHECK constraint failed: CK_CONSTRAINT_1_T1' +- error: 'Failed to execute SQL statement: Check constraint failed ''CK_CONSTRAINT_1_T1'': + s1 <> 19' ... box.execute("insert into t2 values (18, null);") --- @@ -37,7 +38,8 @@ box.execute("insert into t2 values (18, null);") ... box.execute("insert into t2(s2) values (null);") --- -- error: 'Failed to execute SQL statement: CHECK constraint failed: CK_CONSTRAINT_1_T2' +- error: 'Failed to execute SQL statement: Check constraint failed ''CK_CONSTRAINT_1_T2'': + s1 <> 19 AND s1 <> 25' ... box.execute("insert into t2 values (24, null);") --- @@ -45,7 +47,8 @@ box.execute("insert into t2 values (24, null);") ... box.execute("insert into t2(s2) values (null);") --- -- error: 'Failed to execute SQL statement: CHECK constraint failed: CK_CONSTRAINT_1_T2' +- error: 'Failed to execute SQL statement: Check constraint failed ''CK_CONSTRAINT_1_T2'': + s1 <> 19 AND s1 <> 25' ... box.execute("insert into t3 values (9, null)") --- @@ -53,7 +56,8 @@ box.execute("insert into t3 values (9, null)") ... box.execute("insert into t3(s2) values (null)") --- -- error: 'Failed to execute SQL statement: CHECK constraint failed: CK_CONSTRAINT_1_T3' +- error: 'Failed to execute SQL statement: Check constraint failed ''CK_CONSTRAINT_1_T3'': + s1 < 10' ... box.execute("DROP TABLE t1") --- -- 2.21.0 ^ permalink raw reply [flat|nested] 25+ messages in thread
* [tarantool-patches] Re: [PATCH v3 2/3] box: run check constraint tests on space alter 2019-04-16 13:51 ` [tarantool-patches] [PATCH v3 2/3] box: run check constraint tests on space alter Kirill Shcherbatov @ 2019-04-25 20:38 ` n.pettik 2019-05-07 9:53 ` Kirill Shcherbatov 0 siblings, 1 reply; 25+ messages in thread From: n.pettik @ 2019-04-25 20:38 UTC (permalink / raw) To: tarantool-patches; +Cc: Kirill Shcherbatov > On 16 Apr 2019, at 16:51, Kirill Shcherbatov <kshcherbatov@tarantool.org> wrote: > > To perform ck constraints tests before insert or update space operation, > we use precompiled VDBE machine is associated with each ck constraint, > that is executed in on_replace trigger. Each ck constraint VDBE code is > consists of > 1) prologue code that maps new(or updated) tuple fields via bindings, > 2) ck constraint code is generated by CK constraint AST. > In case of ck constraint error the transaction is aborted and ck > constraint error is handled as diag message. > Each ck constraint use own on_replace trigger. > > Closes #3691 One unpleasant “feature” that I’ve noticed: create table t4 (id int primary key check(id > id*id)) insert into t4 values(0.5) - error: 'Failed to execute SQL statement: Check constraint failed ''CK_CONSTRAINT_1_T4'': id > id*id’ Conversion to INT occurs before tuples reaches on_replace trigger. We should discuss what to do with that. I guess you already raised this question when we were talking about typeof() functions inside check constraint. Also, I’ve noticed that check expression bytecode is not optimized. For example: create table t4 (id int primary key check(id > 6+7*3-1*31)) insert into t4 values(1) VDBE Program Listing: 102> 0 Init 0 7 0 00 Start at 7 102> 1 Variable 1 1 0 00 r[1]=parameter(1,) 102> 2 Noop 0 0 0 00 BEGIN: ck constraint CK_CONSTRAINT_1_T4 test 102> 3 Gt 3 6 1 ({type = binary}) 13 if r[1]>r[3] goto 6 102> 4 Halt 21 2 0 Check constraint failed 'CK_CONSTRAINT_1_T4': id > 6+7*3-1*31 C3 102> 5 Noop 0 0 0 00 END: ck constraint CK_CONSTRAINT_1_T4 test 102> 6 Halt 0 0 0 00 102> 7 Integer 6 4 0 00 r[4]=6 102> 8 Integer 7 6 0 00 r[6]=7 102> 9 Integer 3 7 0 00 r[7]=3 102> 10 Multiply 7 6 5 00 r[5]=r[7]*r[6] 102> 11 Add 5 4 2 00 r[2]=r[5]+r[4] 102> 12 Integer 1 4 0 00 r[4]=1 102> 13 Integer 31 7 0 00 r[7]=31 102> 14 Multiply 7 4 5 00 r[5]=r[7]*r[4] 102> 15 Subtract 5 2 3 00 r[3]=r[2]-r[5] 102> 16 Goto 0 1 0 00 Looks extremely inefficient. We could once traverse tree and avoid these calculations on each insertion. > diff --git a/src/box/alter.cc b/src/box/alter.cc > index e96d502c9..57662df11 100644 > --- a/src/box/alter.cc > +++ b/src/box/alter.cc > > diff --git a/src/box/ck_constraint.c b/src/box/ck_constraint.c > index daeebb78b..26d18330b 100644 > --- a/src/box/ck_constraint.c > +++ b/src/box/ck_constraint.c > @@ -28,11 +28,15 @@ > * THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF > * SUCH DAMAGE. > */ > +#include "bind.h" > #include "ck_constraint.h" > #include "errcode.h" > -#include "small/rlist.h" > +#include "schema.h" > +#include "session.h" > #include "sql.h" > #include "sql/sqlInt.h" > +#include "sql/vdbeInt.h" > +#include "tuple.h" > > /** > * Resolve space_def references for check constraint via AST > @@ -55,6 +59,146 @@ ck_constraint_resolve_space_def(struct Expr *expr, > > +static struct sql_stmt * > +ck_constraint_program_compile(struct ck_constraint_def *ck_constraint_def, > + struct Expr *expr, struct space_def *space_def, > + int *new_tuple_var) > > + /* Compile VDBE with default sql parameters. */ > + struct session *user_session = current_session(); > + uint32_t sql_flags = user_session->sql_flags; > + user_session->sql_flags = default_flags; > + /* > + * Generate a prologue code that introduce variables to > + * bind tuple fields there before execution. > + */ > + uint32_t field_count = space_def->field_count; > + int new_tuple_reg = sqlGetTempRange(&parser, field_count); Nit: new_tuple_reg IMHO is very general name. Mb it would be better to call it like “bind_tuple_reg”? > + *new_tuple_var = parser.nVar + 1; Won’t it be always 1? You’ve just created parser and no byte code is generated yet, so how it could be different from 1? sqlGetTempRange() doesn’t affect nVar AFAIU. > + struct Expr bind = {.op = TK_VARIABLE, .u.zToken = "?"}; > + for (uint32_t i = 0; i < field_count; i++) { > + sqlExprAssignVarNumber(&parser, &bind, 1); > + sqlExprCodeTarget(&parser, &bind, new_tuple_reg + i); You don’t need to call this huge function: all you need is this: sqlVdbeAddOp2(v, OP_Variable, bind.iColumn, new_tuple_reg + i); > + } > + /* Generate ck constraint test code. */ > + vdbe_emit_ck_constraint(&parser, expr, ck_constraint_def->name, > + ck_constraint_def->expr_str, new_tuple_reg); > + > + /* Clean-up and restore user-defined sql context. */ > + bool is_error = parser.is_aborted; > + sql_finish_coding(&parser); Can we just add OP_Halt + sqlVdbeMakeReady() ? > +/** > + * Perform ck constraint test on new tuple data new_tuple > + * before insert or replace in space space_def. > + * @param ck_constraint Check constraint to perform. > + * @param space_def The space definition of the space this check > + * constraint is constructed for. > + * @param new_tuple The tuple to be inserted in space. > + * @retval 0 if check constraint test is passed, -1 otherwise. > + */ > +static int > +ck_constraint_program_run(struct ck_constraint *ck_constraint, > + const char *new_tuple) > +{ > + assert(new_tuple != NULL); > + /* > + * Prepare parameters for checks->stmt execution: > + * Map new tuple fields to Vdbe memory variables in range: > + * [new_tuple_var, new_tuple_var + field_count] > + */ > + mp_decode_array(&new_tuple); > + struct space *space = space_by_id(ck_constraint->space_id); > + assert(space != NULL); > + for (uint32_t i = 0; i < space->def->field_count; i++) { Mm, this code differs from one on brach: uint32_t field_count = MIN(tuple_field_count, space->def->field_count); Comment (in code) please this part. How space field count could be less than tuple field count? > diff --git a/test/sql-tap/check.test.lua b/test/sql-tap/check.test.lua > index 0dda3fac8..1ff2eff20 100755 > --- a/test/sql-tap/check.test.lua > +++ b/test/sql-tap/check.test.lua > @@ -484,7 +484,7 @@ test:do_catchsql_test( > UPDATE t4 SET x=0, y=1; > ]], { > -- <check-4.6> > - 1, "Failed to execute SQL statement: CHECK constraint failed: CK_CONSTRAINT_1_T4" > + 1, "Check constraint failed 'CK_CONSTRAINT_1_T4': x+y==11\n OR x*y==12\n OR x/y BETWEEN 5 AND 8\n OR -x==y+10” Mb it is worth removing from check expr extra spaces and return carriage symbols to make it more readable? > diff --git a/test/sql/checks.test.lua b/test/sql/checks.test.lua > index 0cdfde2e9..2652f3b7d 100644 > --- a/test/sql/checks.test.lua > +++ b/test/sql/checks.test.lua > > test_run:cmd("clear filter") > + > +-- > +-- Test ck constraint corner cases > +-- > +s = box.schema.create_space('test') > +_ = s:create_index('pk') > +s:format({{name='X', type='any'}, {name='Y', type='integer'}, {name='Z', type='integer', is_nullable=true}}) > +_ = box.space._ck_constraint:insert({'XlessY', s.id, 'X < Y and Y < Z'}) > +s:insert({'1', 2}) > +s:insert({}) > +s:insert({2, 1}) > +s:insert({1, 2}) > +s:insert({2, 3, 1}) > +s:insert({2, 3, 4}) > +s:update({2}, {{'+', 2, 3}}) > +s:update({2}, {{'+', 2, 3}, {'+', 3, 3}}) > +s:replace({2, 1, 3}) > +box.snapshot() > +test_run:cmd("restart server default") > +s = box.space["test"] > +s:update({2}, {{'+', 2, 3}}) > +s:update({2}, {{'+', 2, 3}, {'+', 3, 3}}) > +s:replace({2, 1, 3}) What about NULLs and NOT NULL check? A few comment fixes: diff --git a/src/box/ck_constraint.c b/src/box/ck_constraint.c index d692e2116..f88f06f96 100644 --- a/src/box/ck_constraint.c +++ b/src/box/ck_constraint.c @@ -61,21 +61,21 @@ ck_constraint_resolve_space_def(struct Expr *expr, /** * Create VDBE machine for ck constraint by given definition and - * Expression AST. The generated instructions are consist of + * expression AST. The generated instructions consist of * prologue code that maps tuple fields via bindings and ck - * constraint code is generated by given expression. - * In case of ck constraint error the VDBE execution is aborted - * and error is handled as diag message. + * constraint code which implements given expression. + * In case of ck constraint error during VDBE execution, it is + * aborted and error is handled as diag message. * @param ck_constraint_def Check constraint definition to prepare - * errors descriptions. + * error description. * @param expr Check constraint expression AST is built for * given @ck_constraint_def, see for * (sql_expr_compile + * ck_constraint_resolve_space_def) implementation. * @param space_def The space definition of the space this check * constraint is constructed for. - * @param[out] new_tuple_var The VDBE register that contains the - * first(of allocation) variable to bind + * @param[out] new_tuple_var VDBE register that contains the + * first (of allocated) variable to bind * the corresponding first tuple field. * @retval not NULL sql_stmt program pointer on success. * @retval NULL otherwise. @@ -99,7 +99,7 @@ ck_constraint_program_compile(struct ck_constraint_def *ck_constraint_def, uint32_t sql_flags = user_session->sql_flags; user_session->sql_flags = default_flags; /* - * Generate a prologue code that introduce variables to + * Generate a prologue code that introduces variables to * bind tuple fields there before execution. */ uint32_t field_count = space_def->field_count; @@ -108,7 +108,7 @@ ck_constraint_program_compile(struct ck_constraint_def *ck_constraint_def, struct Expr bind = {.op = TK_VARIABLE, .u.zToken = "?"}; for (uint32_t i = 0; i < field_count; i++) { sqlExprAssignVarNumber(&parser, &bind, 1); - sqlExprCodeTarget(&parser, &bind, new_tuple_reg + i); + sqlVdbeAddOp2(v, OP_Variable, bind.iColumn, new_tuple_reg + i); } /* Generate ck constraint test code. */ vdbe_emit_ck_constraint(&parser, expr, ck_constraint_def->name, @@ -131,9 +131,9 @@ ck_constraint_program_compile(struct ck_constraint_def *ck_constraint_def, } /** - * Perform ck constraint test on new tuple data new_tuple + * Run bytecode implementing check constraint on new tuple * before insert or replace in space space_def. - * @param ck_constraint Check constraint to perform. + * @param ck_constraint Check constraint to run. * @param space_def The space definition of the space this check * constraint is constructed for. * @param new_tuple The tuple to be inserted in space. @@ -150,10 +150,10 @@ ck_constraint_program_run(struct ck_constraint *ck_constraint, */ struct space *space = space_by_id(ck_constraint->space_id); assert(space != NULL); - uint32_t field_count = mp_decode_array(&new_tuple); - uint32_t defined_field_count = - MIN(field_count, space->def->field_count); - for (uint32_t i = 0; i < defined_field_count; i++) { + uint32_t tuple_field_count = mp_decode_array(&new_tuple); + uint32_t field_count = + MIN(tuple_field_count, space->def->field_count); + for (uint32_t i = 0; i < field_count; i++) { struct sql_bind bind; if (sql_bind_decode(&bind, ck_constraint->new_tuple_var + i, &new_tuple) != 0 || @@ -177,12 +177,12 @@ ck_constraint_program_run(struct ck_constraint *ck_constraint, } /** - * Ck constraint trigger function. Is expected to be executed in - * space::on_replace trigger. + * Ck constraint trigger function. It ss expected to be executed + * in space::on_replace trigger. * - * Extracts all ck constraint related context from event and run - * bytecode implementing check constraint to test a new tuple - * before it would be inserted in destination space. + * It extracts all ck constraint required context from event and + * run bytecode implementing check constraint to test a new tuple + * before it will be inserted in destination space. */ static void ck_constraint_on_replace_trigger(struct trigger *trigger, void *event) diff --git a/src/box/ck_constraint.h b/src/box/ck_constraint.h index 66f812fcc..69654d4bf 100644 --- a/src/box/ck_constraint.h +++ b/src/box/ck_constraint.h @@ -82,8 +82,8 @@ struct ck_constraint { * Precompiled reusable VDBE program for processing check * constraints and setting bad exitcode and error * message when ck condition unsatisfied. - * Program rely on new_tuple_var parameter to be bound to - * the VDBE memory before run. + * Program relies on new_tuple_var parameter to be bound + * to VDBE memory before run. */ struct sql_stmt *stmt; /** @@ -93,8 +93,8 @@ struct ck_constraint { */ int new_tuple_var; /** - * Trigger object executing check constraint on space - * insert and replace. + * Trigger object executing check constraint before + * insert and replace operations. */ struct trigger trigger; ^ permalink raw reply [flat|nested] 25+ messages in thread
* [tarantool-patches] Re: [PATCH v3 2/3] box: run check constraint tests on space alter 2019-04-25 20:38 ` [tarantool-patches] " n.pettik @ 2019-05-07 9:53 ` Kirill Shcherbatov 2019-05-07 16:39 ` Konstantin Osipov 2019-05-14 16:49 ` n.pettik 0 siblings, 2 replies; 25+ messages in thread From: Kirill Shcherbatov @ 2019-05-07 9:53 UTC (permalink / raw) To: tarantool-patches, n.pettik > One unpleasant “feature” that I’ve noticed: > > create table t4 (id int primary key check(id > id*id)) > insert into t4 values(0.5) > - error: 'Failed to execute SQL statement: Check constraint failed ''CK_CONSTRAINT_1_T4'': > id > id*id’ > > Conversion to INT occurs before tuples reaches on_replace trigger. > We should discuss what to do with that. I guess you already raised > this question when we were talking about typeof() functions inside > check constraint. We should live with it ;) > Also, I’ve noticed that check expression bytecode is not optimized. > For example: > > create table t4 (id int primary key check(id > 6+7*3-1*31)) > insert into t4 values(1) > > VDBE Program Listing: > 102> 0 Init 0 7 0 00 Start at 7 > 102> 1 Variable 1 1 0 00 r[1]=parameter(1,) > 102> 2 Noop 0 0 0 00 BEGIN: ck constraint CK_CONSTRAINT_1_T4 test > 102> 3 Gt 3 6 1 ({type = binary}) 13 if r[1]>r[3] goto 6 > 102> 4 Halt 21 2 0 Check constraint failed 'CK_CONSTRAINT_1_T4': id > 6+7*3-1*31 C3 > 102> 5 Noop 0 0 0 00 END: ck constraint CK_CONSTRAINT_1_T4 test > 102> 6 Halt 0 0 0 00 > 102> 7 Integer 6 4 0 00 r[4]=6 > 102> 8 Integer 7 6 0 00 r[6]=7 > 102> 9 Integer 3 7 0 00 r[7]=3 > 102> 10 Multiply 7 6 5 00 r[5]=r[7]*r[6] > 102> 11 Add 5 4 2 00 r[2]=r[5]+r[4] > 102> 12 Integer 1 4 0 00 r[4]=1 > 102> 13 Integer 31 7 0 00 r[7]=31 > 102> 14 Multiply 7 4 5 00 r[5]=r[7]*r[4] > 102> 15 Subtract 5 2 3 00 r[3]=r[2]-r[5] > 102> 16 Goto 0 1 0 00 > > Looks extremely inefficient. We could once traverse tree and avoid > these calculations on each insertion. As we discussed before, SQL_ECEL_FACTOR doesn't solve this problem. Now SQLLite codebase that we use lacks constant propagation feature. >> + uint32_t field_count = space_def->field_count; >> + int new_tuple_reg = sqlGetTempRange(&parser, field_count); > > Nit: new_tuple_reg IMHO is very general name. Mb it would be better > to call it like “bind_tuple_reg”? Don't mind. Done > >> + *new_tuple_var = parser.nVar + 1; > > Won’t it be always 1? You’ve just created parser and no byte code is generated yet, > so how it could be different from 1? sqlGetTempRange() doesn’t affect nVar AFAIU. Don't mind. Done > >> + struct Expr bind = {.op = TK_VARIABLE, .u.zToken = "?"}; >> + for (uint32_t i = 0; i < field_count; i++) { >> + sqlExprAssignVarNumber(&parser, &bind, 1); >> + sqlExprCodeTarget(&parser, &bind, new_tuple_reg + i); > > You don’t need to call this huge function: all you need is this: > sqlVdbeAddOp2(v, OP_Variable, bind.iColumn, new_tuple_reg + i); uint32_t field_count = space_def->field_count; int bind_tuple_reg = sqlGetTempRange(&parser, field_count); for (uint32_t i = 0; i < field_count; i++) { sqlVdbeAddOp2(v, OP_Variable, ++parser.nVar, bind_tuple_reg + i); } >> + sql_finish_coding(&parser); > > Can we just add OP_Halt + sqlVdbeMakeReady() ? No, we can't. This doesn't work. > uint32_t field_count = > MIN(tuple_field_count, space->def->field_count); > > Comment (in code) please this part. How space field count could > be less than tuple field count? /* * When last format fields are nullable, they are * 'optional' i.e. they may not be present in the tuple. */ >> + 1, "Check constraint failed 'CK_CONSTRAINT_1_T4': x+y==11\n OR x*y==12\n OR x/y BETWEEN 5 AND 8\n OR -x==y+10” > > Mb it is worth removing from check expr extra spaces and > return carriage symbols to make it more readable? Consider my new helper in previous patch: +/** + * Prepare a 0-terminated string in the wptr memory buffer that + * does not contain a sequence of more than one whatespace + * character. Routine enforces ' ' (space) as whitespace + * delimiter. + * The wptr buffer is expected to have str_len + 1 bytes + * (this is the expected scenario where no extra whitespace + * characters preset in the source string). + * @param wptr The destination memory buffer of size + * @a str_len + 1. + * @param str The source string to be copied. + * @param str_len The source string @a str length. + */ +static void +trim_space_snprintf(char *wptr, const char *str, uint32_t str_len) +{ + const char *str_end = str + str_len; + bool is_prev_chr_space = false; + while (str < str_end) { + if (isspace((unsigned char)*str)) { + if (!is_prev_chr_space) + *wptr++ = ' '; + is_prev_chr_space = true; + str++; + continue; + } + is_prev_chr_space = false; + *wptr++ = *str++; + } + *wptr = '\0'; +} It is called on build >> +s:replace({2, 1, 3}) > > What about NULLs and NOT NULL check? I've added a new test (maybe as a part of previous patch) =============================================== To perform ck constraints tests before insert or update space operation, we use precompiled VDBE machine is associated with each ck constraint, that is executed in on_replace trigger. Each ck constraint VDBE code is consists of 1) prologue code that maps new(or updated) tuple fields via bindings, 2) ck constraint code is generated by CK constraint AST. In case of ck constraint error the transaction is aborted and ck constraint error is handled as diag message. Each ck constraint use own on_replace trigger. Closes #3691 --- src/box/alter.cc | 58 ++++++- src/box/ck_constraint.c | 163 ++++++++++++++++++- src/box/ck_constraint.h | 16 +- src/box/errcode.h | 1 + src/box/sql/insert.c | 92 +++-------- src/box/sql/sqlInt.h | 26 +++ src/box/sql/vdbeapi.c | 8 - test/box/misc.result | 1 + test/sql-tap/check.test.lua | 32 ++-- test/sql-tap/fkey2.test.lua | 4 +- test/sql-tap/table.test.lua | 12 +- test/sql/checks.result | 217 ++++++++++++++++++++++++-- test/sql/checks.test.lua | 61 ++++++++ test/sql/errinj.result | 18 ++- test/sql/gh-2981-check-autoinc.result | 12 +- test/sql/types.result | 3 +- 16 files changed, 586 insertions(+), 138 deletions(-) diff --git a/src/box/alter.cc b/src/box/alter.cc index 2126ab369..9c560e85d 100644 --- a/src/box/alter.cc +++ b/src/box/alter.cc @@ -1410,14 +1410,29 @@ void RebuildCkConstraints::alter(struct alter_space *alter) { rlist_swap(&alter->new_space->ck_constraint, &ck_constraint); + struct ck_constraint *ck; + rlist_foreach_entry(ck, &alter->old_space->ck_constraint, link) + trigger_clear(&ck->trigger); rlist_swap(&ck_constraint, &alter->old_space->ck_constraint); + rlist_foreach_entry(ck, &alter->new_space->ck_constraint, link) { + /** + * Triggers would be swapped later on + * alter_space_do. + */ + trigger_add(&alter->old_space->on_replace, &ck->trigger); + } } void RebuildCkConstraints::rollback(struct alter_space *alter) { rlist_swap(&alter->old_space->ck_constraint, &ck_constraint); + struct ck_constraint *ck; + rlist_foreach_entry(ck, &alter->new_space->ck_constraint, link) + trigger_clear(&ck->trigger); rlist_swap(&ck_constraint, &alter->new_space->ck_constraint); + rlist_foreach_entry(ck, &alter->old_space->ck_constraint, link) + trigger_add(&alter->new_space->on_replace, &ck->trigger); } RebuildCkConstraints::~RebuildCkConstraints() @@ -1435,6 +1450,35 @@ RebuildCkConstraints::~RebuildCkConstraints() } } +/** + * Move CK constraints from old space to the new one. + * Despite RebuildCkConstraints, this operation doesn't perform + * objects rebuild. This may be used in scenarios where space + * format doesn't change i.e. in alter index or space trim + * requests. + */ +class MoveCkConstraints: public AlterSpaceOp +{ +public: + MoveCkConstraints(struct alter_space *alter) : AlterSpaceOp(alter) {} + virtual void alter(struct alter_space *alter); + virtual void rollback(struct alter_space *alter); +}; + +void +MoveCkConstraints::alter(struct alter_space *alter) +{ + rlist_swap(&alter->new_space->ck_constraint, + &alter->old_space->ck_constraint); +} + +void +MoveCkConstraints::rollback(struct alter_space *alter) +{ + rlist_swap(&alter->new_space->ck_constraint, + &alter->old_space->ck_constraint); +} + /* }}} */ /** @@ -2145,8 +2189,8 @@ on_replace_dd_index(struct trigger * /* trigger */, void *event) * old space. */ alter_space_move_indexes(alter, iid + 1, old_space->index_id_max + 1); + (void) new MoveCkConstraints(alter); /* Add an op to update schema_version on commit. */ - (void) new RebuildCkConstraints(alter); (void) new UpdateSchemaVersion(alter); alter_space_do(txn, alter); scoped_guard.is_active = false; @@ -2214,7 +2258,7 @@ on_replace_dd_truncate(struct trigger * /* trigger */, void *event) (void) new TruncateIndex(alter, old_index->def->iid); } - (void) new RebuildCkConstraints(alter); + (void) new MoveCkConstraints(alter); alter_space_do(txn, alter); scoped_guard.is_active = false; } @@ -4152,12 +4196,14 @@ on_replace_ck_constraint_rollback(struct trigger *trigger, void *event) assert(space_ck_constraint_by_name(space, ck->def->name, strlen(ck->def->name)) == NULL); rlist_add_entry(&space->ck_constraint, ck, link); + trigger_add(&space->on_replace, &ck->trigger); } else if (stmt->new_tuple != NULL && stmt->old_tuple == NULL) { /* Rollback INSERT check constraint. */ assert(space != NULL); assert(space_ck_constraint_by_name(space, ck->def->name, strlen(ck->def->name)) != NULL); rlist_del_entry(ck, link); + trigger_clear(&ck->trigger); ck_constraint_delete(ck); } else { /* Rollback REPLACE check constraint. */ @@ -4167,7 +4213,9 @@ on_replace_ck_constraint_rollback(struct trigger *trigger, void *event) space_ck_constraint_by_name(space, name, strlen(name)); assert(new_ck != NULL); rlist_del_entry(new_ck, link); + trigger_clear(&new_ck->trigger); rlist_add_entry(&space->ck_constraint, ck, link); + trigger_add(&space->on_replace, &ck->trigger); ck_constraint_delete(new_ck); } } @@ -4234,9 +4282,12 @@ on_replace_dd_ck_constraint(struct trigger * /* trigger*/, void *event) const char *name = new_ck_constraint->def->name; struct ck_constraint *old_ck_constraint = space_ck_constraint_by_name(space, name, strlen(name)); - if (old_ck_constraint != NULL) + if (old_ck_constraint != NULL) { rlist_del_entry(old_ck_constraint, link); + trigger_clear(&old_ck_constraint->trigger); + } rlist_add_entry(&space->ck_constraint, new_ck_constraint, link); + trigger_add(&space->on_replace, &new_ck_constraint->trigger); on_commit->data = old_tuple == NULL ? new_ck_constraint : old_ck_constraint; on_rollback->data = on_commit->data; @@ -4252,6 +4303,7 @@ on_replace_dd_ck_constraint(struct trigger * /* trigger*/, void *event) space_ck_constraint_by_name(space, name, name_len); assert(old_ck_constraint != NULL); rlist_del_entry(old_ck_constraint, link); + trigger_clear(&old_ck_constraint->trigger); on_commit->data = old_ck_constraint; on_rollback->data = old_ck_constraint; } diff --git a/src/box/ck_constraint.c b/src/box/ck_constraint.c index 6d9f8ea44..52f615227 100644 --- a/src/box/ck_constraint.c +++ b/src/box/ck_constraint.c @@ -28,10 +28,15 @@ * THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. */ +#include "bind.h" #include "ck_constraint.h" #include "errcode.h" +#include "schema.h" +#include "session.h" #include "sql.h" #include "sql/sqlInt.h" +#include "sql/vdbeInt.h" +#include "tuple.h" /** * Resolve space_def references for check constraint via AST @@ -54,6 +59,143 @@ ck_constraint_resolve_field_names(struct Expr *expr, return rc; } +/** + * Create VDBE machine for ck constraint by given definition and + * expression AST. The generated instructions consist of + * prologue code that maps tuple fields via bindings and ck + * constraint code which implements given expression. + * In case of ck constraint error during VDBE execution, it is + * aborted and error is handled as diag message. + * @param ck_constraint_def Check constraint definition to prepare + * error description. + * @param expr Check constraint expression AST is built for + * given @ck_constraint_def, see for + * (sql_expr_compile + + * ck_constraint_resolve_space_def) implementation. + * @param space_def The space definition of the space this check + * constraint is constructed for. + * @retval not NULL sql_stmt program pointer on success. + * @retval NULL otherwise. + */ +static struct sql_stmt * +ck_constraint_program_compile(struct ck_constraint_def *ck_constraint_def, + struct Expr *expr, struct space_def *space_def) +{ + struct sql *db = sql_get(); + struct Parse parser; + sql_parser_create(&parser, db); + struct Vdbe *v = sqlGetVdbe(&parser); + if (v == NULL) { + diag_set(OutOfMemory, sizeof(struct Vdbe), "sqlGetVdbe", + "vdbe"); + return NULL; + } + /* Compile VDBE with default sql parameters. */ + struct session *user_session = current_session(); + uint32_t sql_flags = user_session->sql_flags; + user_session->sql_flags = default_flags; + /* + * Generate a prologue code that introduces variables to + * bind tuple fields there before execution. + */ + uint32_t field_count = space_def->field_count; + int bind_tuple_reg = sqlGetTempRange(&parser, field_count); + for (uint32_t i = 0; i < field_count; i++) { + sqlVdbeAddOp2(v, OP_Variable, ++parser.nVar, + bind_tuple_reg + i); + } + /* Generate ck constraint test code. */ + vdbe_emit_ck_constraint(&parser, expr, ck_constraint_def->name, + ck_constraint_def->expr_str, bind_tuple_reg); + + /* Clean-up and restore user-defined sql context. */ + bool is_error = parser.is_aborted; + sql_finish_coding(&parser); + sql_parser_destroy(&parser); + user_session->sql_flags = sql_flags; + + if (is_error) { + diag_set(ClientError, ER_CREATE_CK_CONSTRAINT, + ck_constraint_def->name, + box_error_message(box_error_last())); + sql_finalize((struct sql_stmt *) v); + return NULL; + } + return (struct sql_stmt *) v; +} + +/** + * Run bytecode implementing check constraint on new tuple + * before insert or replace in space space_def. + * @param ck_constraint Check constraint to run. + * @param space_def The space definition of the space this check + * constraint is constructed for. + * @param new_tuple The tuple to be inserted in space. + * @retval 0 if check constraint test is passed, -1 otherwise. + */ +static int +ck_constraint_program_run(struct ck_constraint *ck_constraint, + const char *new_tuple) +{ + /* + * Prepare parameters for checks->stmt execution: + * Map new tuple fields to Vdbe memory variables in range: + * [1, field_count] + */ + struct space *space = space_by_id(ck_constraint->space_id); + assert(space != NULL); + /* + * When last format fields are nullable, they are + * 'optional' i.e. they may not be present in the tuple. + */ + uint32_t tuple_field_count = mp_decode_array(&new_tuple); + uint32_t field_count = + MIN(tuple_field_count, space->def->field_count); + for (uint32_t i = 0; i < field_count; i++) { + struct sql_bind bind; + if (sql_bind_decode(&bind, i + 1, &new_tuple) != 0 || + sql_bind_column(ck_constraint->stmt, &bind, i + 1) != 0) { + diag_set(ClientError, ER_CK_CONSTRAINT_FAILED, + ck_constraint->def->name, + ck_constraint->def->expr_str); + return -1; + } + } + /* Checks VDBE can't expire, reset expired flag and go. */ + struct Vdbe *v = (struct Vdbe *) ck_constraint->stmt; + v->expired = 0; + while (sql_step(ck_constraint->stmt) == SQL_ROW) {} + /* + * Get VDBE execution state and reset VM to run it + * next time. + */ + return sql_reset(ck_constraint->stmt) != SQL_OK ? -1 : 0; +} + +/** + * Ck constraint trigger function. It ss expected to be executed + * in space::on_replace trigger. + * + * It extracts all ck constraint required context from event and + * run bytecode implementing check constraint to test a new tuple + * before it will be inserted in destination space. + */ +static void +ck_constraint_on_replace_trigger(struct trigger *trigger, void *event) +{ + struct ck_constraint *ck_constraint = + (struct ck_constraint *) trigger->data; + struct txn *txn = (struct txn *) event; + struct txn_stmt *stmt = txn_current_stmt(txn); + assert(stmt != NULL); + struct tuple *new_tuple = stmt->new_tuple; + if (new_tuple == NULL) + return; + if (ck_constraint_program_run(ck_constraint, + tuple_data(new_tuple)) != 0) + diag_raise(); +} + struct ck_constraint * ck_constraint_new(struct ck_constraint_def *ck_constraint_def, struct space_def *space_def) @@ -70,23 +212,33 @@ ck_constraint_new(struct ck_constraint_def *ck_constraint_def, return NULL; } ck_constraint->def = NULL; + ck_constraint->stmt = NULL; ck_constraint->space_id = space_def->id; rlist_create(&ck_constraint->link); - ck_constraint->expr = + trigger_create(&ck_constraint->trigger, + ck_constraint_on_replace_trigger, ck_constraint, + NULL); + struct Expr *expr = sql_expr_compile(sql_get(), ck_constraint_def->expr_str, strlen(ck_constraint_def->expr_str)); - if (ck_constraint->expr == NULL || - ck_constraint_resolve_field_names(ck_constraint->expr, - space_def) != 0) { + if (expr == NULL || + ck_constraint_resolve_field_names(expr, space_def) != 0) { diag_set(ClientError, ER_CREATE_CK_CONSTRAINT, ck_constraint_def->name, box_error_message(box_error_last())); goto error; } + ck_constraint->stmt = + ck_constraint_program_compile(ck_constraint_def, expr, + space_def); + if (ck_constraint->stmt == NULL) + goto error; + sql_expr_delete(sql_get(), expr, false); ck_constraint->def = ck_constraint_def; return ck_constraint; error: + sql_expr_delete(sql_get(), expr, false); ck_constraint_delete(ck_constraint); return NULL; } @@ -94,7 +246,8 @@ error: void ck_constraint_delete(struct ck_constraint *ck_constraint) { - sql_expr_delete(sql_get(), ck_constraint->expr, false); + assert(rlist_empty(&ck_constraint->trigger.link)); + sql_finalize(ck_constraint->stmt); free(ck_constraint->def); TRASH(ck_constraint); free(ck_constraint); diff --git a/src/box/ck_constraint.h b/src/box/ck_constraint.h index 615612605..abbfd3a8f 100644 --- a/src/box/ck_constraint.h +++ b/src/box/ck_constraint.h @@ -32,6 +32,7 @@ */ #include <stdint.h> +#include "trigger.h" #include "small/rlist.h" #if defined(__cplusplus) @@ -40,6 +41,7 @@ extern "C" { struct space; struct space_def; +struct sql_stmt; struct Expr; /** @@ -70,12 +72,16 @@ struct ck_constraint { /** The check constraint definition. */ struct ck_constraint_def *def; /** - * The check constraint expression AST is built for - * ck_constraint::def::expr_str with sql_expr_compile - * and resolved with sql_resolve_self_reference for - * space with space[ck_constraint::space_id] definition. + * Precompiled reusable VDBE program for processing check + * constraints and setting bad exitcode and error + * message when ck condition unsatisfied. */ - struct Expr *expr; + struct sql_stmt *stmt; + /** + * Trigger object executing check constraint before + * insert and replace operations. + */ + struct trigger trigger; /** * The id of the space this check constraint is * built for. diff --git a/src/box/errcode.h b/src/box/errcode.h index 7878bd66b..750cd23a0 100644 --- a/src/box/errcode.h +++ b/src/box/errcode.h @@ -247,6 +247,7 @@ struct errcode_record { /*192 */_(ER_INDEX_DEF_UNSUPPORTED, "%s are prohibited in an index definition") \ /*193 */_(ER_CK_DEF_UNSUPPORTED, "%s are prohibited in a check constraint definition") \ /*194 */_(ER_CREATE_CK_CONSTRAINT, "Failed to create check constraint '%s': %s") \ + /*195 */_(ER_CK_CONSTRAINT_FAILED, "Check constraint failed '%s': %s") \ /* * !IMPORTANT! Please follow instructions at start of the file * when adding new errors. diff --git a/src/box/sql/insert.c b/src/box/sql/insert.c index fa4a94ef7..c024447c2 100644 --- a/src/box/sql/insert.c +++ b/src/box/sql/insert.c @@ -813,51 +813,26 @@ sqlInsert(Parse * pParse, /* Parser context */ sqlDbFree(db, aRegIdx); } -/* - * Meanings of bits in of pWalker->eCode for checkConstraintUnchanged() - */ -#define CKCNSTRNT_COLUMN 0x01 /* CHECK constraint uses a changing column */ - -/* This is the Walker callback from checkConstraintUnchanged(). Set - * bit 0x01 of pWalker->eCode if - * pWalker->eCode to 0 if this expression node references any of the - * columns that are being modifed by an UPDATE statement. - */ -static int -checkConstraintExprNode(Walker * pWalker, Expr * pExpr) -{ - if (pExpr->op == TK_COLUMN) { - assert(pExpr->iColumn >= 0 || pExpr->iColumn == -1); - if (pExpr->iColumn >= 0) { - if (pWalker->u.aiCol[pExpr->iColumn] >= 0) { - pWalker->eCode |= CKCNSTRNT_COLUMN; - } - } - } - return WRC_Continue; -} - -/* - * pExpr is a CHECK constraint on a row that is being UPDATE-ed. The - * only columns that are modified by the UPDATE are those for which - * aiChng[i]>=0. - * - * Return true if CHECK constraint pExpr does not use any of the - * changing columns. In other words, return true if this CHECK constraint - * can be skipped when validating the new row in the UPDATE statement. - */ -static int -checkConstraintUnchanged(Expr * pExpr, int *aiChng) +void +vdbe_emit_ck_constraint(struct Parse *parser, struct Expr *expr, + const char *name, const char *expr_str, + int new_tuple_reg) { - Walker w; - memset(&w, 0, sizeof(w)); - w.eCode = 0; - w.xExprCallback = checkConstraintExprNode; - w.u.aiCol = aiChng; - sqlWalkExpr(&w, pExpr); - testcase(w.eCode == 0); - testcase(w.eCode == CKCNSTRNT_COLUMN); - return !w.eCode; + parser->ckBase = new_tuple_reg; + struct Vdbe *v = sqlGetVdbe(parser); + const char *ck_constraint_name = sqlDbStrDup(parser->db, name); + VdbeNoopComment((v, "BEGIN: ck constraint %s test", + ck_constraint_name)); + int check_is_passed = sqlVdbeMakeLabel(v); + sqlExprIfTrue(parser, expr, check_is_passed, SQL_JUMPIFNULL); + sqlMayAbort(parser); + const char *fmt = tnt_errcode_desc(ER_CK_CONSTRAINT_FAILED); + const char *error_msg = tt_sprintf(fmt, ck_constraint_name, expr_str); + sqlVdbeAddOp4(v, OP_Halt, SQL_TARANTOOL_ERROR, ON_CONFLICT_ACTION_ABORT, + 0, sqlDbStrDup(parser->db, error_msg), P4_DYNAMIC); + sqlVdbeChangeP5(v, ER_CK_CONSTRAINT_FAILED); + VdbeNoopComment((v, "END: ck constraint %s test", ck_constraint_name)); + sqlVdbeResolveLabel(v, check_is_passed); } void @@ -927,35 +902,6 @@ vdbe_emit_constraint_checks(struct Parse *parse_context, struct space *space, unreachable(); } } - /* - * For CHECK constraint and for INSERT/UPDATE conflict - * action DEFAULT and ABORT in fact has the same meaning. - */ - if (on_conflict == ON_CONFLICT_ACTION_DEFAULT) - on_conflict = ON_CONFLICT_ACTION_ABORT; - /* Test all CHECK constraints. */ - enum on_conflict_action on_conflict_check = on_conflict; - if (on_conflict == ON_CONFLICT_ACTION_REPLACE) - on_conflict_check = ON_CONFLICT_ACTION_ABORT; - if (!rlist_empty(&space->ck_constraint)) - parse_context->ckBase = new_tuple_reg; - struct ck_constraint *ck_constraint; - rlist_foreach_entry(ck_constraint, &space->ck_constraint, link) { - struct Expr *expr = ck_constraint->expr; - if (is_update && checkConstraintUnchanged(expr, upd_cols) != 0) - continue; - int all_ok = sqlVdbeMakeLabel(v); - sqlExprIfTrue(parse_context, expr, all_ok, SQL_JUMPIFNULL); - if (on_conflict == ON_CONFLICT_ACTION_IGNORE) { - sqlVdbeGoto(v, ignore_label); - } else { - char *name = ck_constraint->def->name; - sqlHaltConstraint(parse_context, SQL_CONSTRAINT_CHECK, - on_conflict_check, name, P4_TRANSIENT, - P5_ConstraintCheck); - } - sqlVdbeResolveLabel(v, all_ok); - } sql_emit_table_types(v, space->def, new_tuple_reg); /* * Other actions except for REPLACE and UPDATE OR IGNORE diff --git a/src/box/sql/sqlInt.h b/src/box/sql/sqlInt.h index 2d6936490..64f5f3d59 100644 --- a/src/box/sql/sqlInt.h +++ b/src/box/sql/sqlInt.h @@ -605,6 +605,17 @@ sql_column_value(sql_stmt *, int sql_finalize(sql_stmt * pStmt); +/* + * Terminate the current execution of an SQL statement and reset + * it back to its starting state so that it can be reused. + * + * @param stmt VDBE program, may be NULL. + * @retval SQL_OK On success. + * @retval sql_ret_code Error code on error. + */ +int +sql_reset(struct sql_stmt *stmt); + int sql_exec(sql *, /* An open database */ const char *sql, /* SQL to be evaluated */ @@ -3895,6 +3906,21 @@ vdbe_emit_constraint_checks(struct Parse *parse_context, enum on_conflict_action on_conflict, int ignore_label, int *upd_cols); +/** + * Gnerate code to make check constraints tests on tuple insertion + * on INSERT, REPLACE or UPDATE operations. + * @param parser Current parsing context. + * @param expr Check constraint AST. + * @param name Check constraint name to raise error. + * @param new_tuple_reg The first ck_constraint::stmt VDBE + * register of the range + * space_def::field_count representing a + * new tuple to be inserted. + */ +void +vdbe_emit_ck_constraint(struct Parse *parser, struct Expr *expr, + const char *name, const char *expr_str, + int new_tuple_reg); /** * This routine generates code to finish the INSERT or UPDATE * operation that was started by a prior call to diff --git a/src/box/sql/vdbeapi.c b/src/box/sql/vdbeapi.c index d2868567b..5b3ac4b6a 100644 --- a/src/box/sql/vdbeapi.c +++ b/src/box/sql/vdbeapi.c @@ -132,14 +132,6 @@ sql_finalize(sql_stmt * pStmt) return rc; } -/* - * Terminate the current execution of an SQL statement and reset it - * back to its starting state so that it can be reused. A success code from - * the prior execution is returned. - * - * This routine sets the error code and string returned by - * sql_errcode(), sql_errmsg() and sql_errmsg16(). - */ int sql_reset(sql_stmt * pStmt) { diff --git a/test/box/misc.result b/test/box/misc.result index eeb63366e..c1c7ffdc6 100644 --- a/test/box/misc.result +++ b/test/box/misc.result @@ -523,6 +523,7 @@ t; 192: box.error.INDEX_DEF_UNSUPPORTED 193: box.error.CK_DEF_UNSUPPORTED 194: box.error.CREATE_CK_CONSTRAINT + 195: box.error.CK_CONSTRAINT_FAILED ... test_run:cmd("setopt delimiter ''"); --- diff --git a/test/sql-tap/check.test.lua b/test/sql-tap/check.test.lua index 0dda3fac8..6bcc5830b 100755 --- a/test/sql-tap/check.test.lua +++ b/test/sql-tap/check.test.lua @@ -55,7 +55,7 @@ test:do_catchsql_test( INSERT INTO t1 VALUES(6,7, 2); ]], { -- <check-1.3> - 1, "Failed to execute SQL statement: CHECK constraint failed: CK_CONSTRAINT_1_T1" + 1, "Failed to execute SQL statement: Check constraint failed 'CK_CONSTRAINT_1_T1': x<5" -- </check-1.3> }) @@ -75,7 +75,7 @@ test:do_catchsql_test( INSERT INTO t1 VALUES(4,3, 2); ]], { -- <check-1.5> - 1, "Failed to execute SQL statement: CHECK constraint failed: CK_CONSTRAINT_2_T1" + 1, "Failed to execute SQL statement: Check constraint failed 'CK_CONSTRAINT_2_T1': y>x" -- </check-1.5> }) @@ -147,7 +147,7 @@ test:do_catchsql_test( UPDATE t1 SET x=7 WHERE x==2 ]], { -- <check-1.12> - 1, "Failed to execute SQL statement: CHECK constraint failed: CK_CONSTRAINT_1_T1" + 1, "Check constraint failed 'CK_CONSTRAINT_1_T1': x<5" -- </check-1.12> }) @@ -167,7 +167,7 @@ test:do_catchsql_test( UPDATE t1 SET x=5 WHERE x==2 ]], { -- <check-1.14> - 1, "Failed to execute SQL statement: CHECK constraint failed: CK_CONSTRAINT_1_T1" + 1, "Check constraint failed 'CK_CONSTRAINT_1_T1': x<5" -- </check-1.14> }) @@ -206,9 +206,9 @@ test:do_execsql_test( [[ CREATE TABLE t2( id INT primary key, - x INTEGER CONSTRAINT one CHECK( typeof(coalesce(x,0))=='integer'), + x SCALAR CONSTRAINT one CHECK( typeof(coalesce(x,0))=='integer'), y REAL CONSTRAINT two CHECK( typeof(coalesce(y,0.1))=='number' ), - z TEXT CONSTRAINT three CHECK( typeof(coalesce(z,''))=='string' ) + z SCALAR CONSTRAINT three CHECK( typeof(coalesce(z,''))=='string' ) ); ]], { -- <check-2.1> @@ -246,7 +246,7 @@ test:do_catchsql_test( INSERT INTO t2 VALUES(3, 1.1, NULL, NULL); ]], { -- <check-2.4> - 1, "Failed to execute SQL statement: CHECK constraint failed: ONE" + 1, "Failed to execute SQL statement: Check constraint failed 'ONE': typeof(coalesce(x,0))=='integer'" -- </check-2.4> }) @@ -256,7 +256,7 @@ test:do_catchsql_test( INSERT INTO t2 VALUES(4, NULL, 5, NULL); ]], { -- <check-2.5> - 1, "Failed to execute SQL statement: CHECK constraint failed: TWO" + 1, "Failed to execute SQL statement: Check constraint failed 'TWO': typeof(coalesce(y,0.1))=='number'" -- </check-2.5> }) @@ -266,7 +266,7 @@ test:do_catchsql_test( INSERT INTO t2 VALUES(5, NULL, NULL, 3.14159); ]], { -- <check-2.6> - 1, "Failed to execute SQL statement: CHECK constraint failed: THREE" + 1, "Failed to execute SQL statement: Check constraint failed 'THREE': typeof(coalesce(z,''))=='string'" -- </check-2.6> }) @@ -413,7 +413,7 @@ test:do_catchsql_test( INSERT INTO t3 VALUES(111,222,333); ]], { -- <check-3.9> - 1, "Failed to execute SQL statement: CHECK constraint failed: CK_CONSTRAINT_1_T3" + 1, "Failed to execute SQL statement: Check constraint failed 'CK_CONSTRAINT_1_T3': t3.x<25" -- </check-3.9> }) @@ -484,7 +484,7 @@ test:do_catchsql_test( UPDATE t4 SET x=0, y=1; ]], { -- <check-4.6> - 1, "Failed to execute SQL statement: CHECK constraint failed: CK_CONSTRAINT_1_T4" + 1, "Check constraint failed 'CK_CONSTRAINT_1_T4': x+y==11 OR x*y==12 OR x/y BETWEEN 5 AND 8 OR -x==y+10" -- </check-4.6> }) @@ -504,7 +504,7 @@ test:do_catchsql_test( UPDATE t4 SET x=0, y=2; ]], { -- <check-4.9> - 1, "Failed to execute SQL statement: CHECK constraint failed: CK_CONSTRAINT_1_T4" + 1, "Check constraint failed 'CK_CONSTRAINT_1_T4': x+y==11 OR x*y==12 OR x/y BETWEEN 5 AND 8 OR -x==y+10" -- </check-4.9> }) @@ -581,7 +581,7 @@ test:do_catchsql_test( UPDATE OR FAIL t1 SET x=7-x, y=y+1; ]], { -- <check-6.5> - 1, "Failed to execute SQL statement: CHECK constraint failed: CK_CONSTRAINT_1_T1" + 1, "Check constraint failed 'CK_CONSTRAINT_1_T1': x<5" -- </check-6.5> }) @@ -603,7 +603,7 @@ test:do_catchsql_test( INSERT OR ROLLBACK INTO t1 VALUES(8,40.0, 10); ]], { -- <check-6.7> - 1, "Failed to execute SQL statement: CHECK constraint failed: CK_CONSTRAINT_1_T1" + 1, "Failed to execute SQL statement: Check constraint failed 'CK_CONSTRAINT_1_T1': x<5" -- </check-6.7> }) @@ -636,7 +636,7 @@ test:do_catchsql_test( REPLACE INTO t1 VALUES(6,7, 11); ]], { -- <check-6.12> - 1, "Failed to execute SQL statement: CHECK constraint failed: CK_CONSTRAINT_1_T1" + 1, "Failed to execute SQL statement: Check constraint failed 'CK_CONSTRAINT_1_T1': x<5" -- </check-6.12> }) @@ -700,7 +700,7 @@ test:do_catchsql_test( 7.3, " INSERT INTO t6 VALUES(11) ", { -- <7.3> - 1, "Failed to execute SQL statement: CHECK constraint failed: CK_CONSTRAINT_1_T6" + 1, "Failed to execute SQL statement: Check constraint failed 'CK_CONSTRAINT_1_T6': myfunc(a)" -- </7.3> }) diff --git a/test/sql-tap/fkey2.test.lua b/test/sql-tap/fkey2.test.lua index 695a379a6..def5f8321 100755 --- a/test/sql-tap/fkey2.test.lua +++ b/test/sql-tap/fkey2.test.lua @@ -362,7 +362,7 @@ test:do_catchsql_test( UPDATE ab SET a = 5; ]], { -- <fkey2-3.2> - 1, "Failed to execute SQL statement: CHECK constraint failed: CK_CONSTRAINT_1_EF" + 1, "Check constraint failed 'CK_CONSTRAINT_1_EF': e!=5" -- </fkey2-3.2> }) @@ -382,7 +382,7 @@ test:do_catchsql_test( UPDATE ab SET a = 5; ]], { -- <fkey2-3.4> - 1, "Failed to execute SQL statement: CHECK constraint failed: CK_CONSTRAINT_1_EF" + 1, "Check constraint failed 'CK_CONSTRAINT_1_EF': e!=5" -- </fkey2-3.4> }) diff --git a/test/sql-tap/table.test.lua b/test/sql-tap/table.test.lua index 066662f33..19df1d5df 100755 --- a/test/sql-tap/table.test.lua +++ b/test/sql-tap/table.test.lua @@ -1218,20 +1218,20 @@ test:do_catchsql_test( test:do_catchsql_test( "table-21.3", [[ - INSERT INTO T21 VALUES(1, -1, 1); + INSERT INTO T21 VALUES(2, -1, 1); ]], { -- <table-21.3> - 1, "Failed to execute SQL statement: CHECK constraint failed: CK_CONSTRAINT_1_T21" + 1, "Failed to execute SQL statement: Check constraint failed 'CK_CONSTRAINT_1_T21': B > 0" -- </table-21.3> }) test:do_catchsql_test( "table-21.4", [[ - INSERT INTO T21 VALUES(1, 1, -1); + INSERT INTO T21 VALUES(2, 1, -1); ]], { -- <table-21.4> - 1, "Failed to execute SQL statement: CHECK constraint failed: CK_CONSTRAINT_2_T21" + 1, "Failed to execute SQL statement: Check constraint failed 'CK_CONSTRAINT_2_T21': C > 0" -- </table-21.4> }) @@ -1372,7 +1372,7 @@ test:do_catchsql_test( INSERT INTO T28 VALUES(0); ]], { -- <table-22.10> - 1, "Failed to execute SQL statement: CHECK constraint failed: CHECK1" + 1, "Failed to execute SQL statement: Check constraint failed 'CHECK1': id != 0" -- </table-22.10> }) @@ -1382,7 +1382,7 @@ test:do_catchsql_test( INSERT INTO T28 VALUES(9); ]], { -- <table-22.11> - 1, "Failed to execute SQL statement: CHECK constraint failed: CHECK2" + 1, "Failed to execute SQL statement: Check constraint failed 'CHECK2': id > 10" -- </table-22.11> }) diff --git a/test/sql/checks.result b/test/sql/checks.result index 8da1483f6..bd7b96435 100644 --- a/test/sql/checks.result +++ b/test/sql/checks.result @@ -68,7 +68,12 @@ box.space._ck_constraint:count({}) ... box.execute("INSERT INTO \"test\" VALUES(5);") --- -- error: 'Failed to execute SQL statement: CHECK constraint failed: CK_CONSTRAINT_01' +- error: 'Failed to execute SQL statement: Check constraint failed ''CK_CONSTRAINT_01'': + X<5' +... +box.space.test:insert({5}) +--- +- error: 'Check constraint failed ''CK_CONSTRAINT_01'': X<5' ... box.space._ck_constraint:replace({'CK_CONSTRAINT_01', 513, false, 'X<=5'}) --- @@ -80,7 +85,12 @@ box.execute("INSERT INTO \"test\" VALUES(5);") ... box.execute("INSERT INTO \"test\" VALUES(6);") --- -- error: 'Failed to execute SQL statement: CHECK constraint failed: CK_CONSTRAINT_01' +- error: 'Failed to execute SQL statement: Check constraint failed ''CK_CONSTRAINT_01'': + X<=5' +... +box.space.test:insert({6}) +--- +- error: 'Check constraint failed ''CK_CONSTRAINT_01'': X<=5' ... -- Can't drop table with check constraints. box.space.test:delete({5}) @@ -113,16 +123,28 @@ box.space._ck_constraint:count() ... box.execute("INSERT INTO t1 VALUES (7, 1, 1)") --- -- error: 'Failed to execute SQL statement: CHECK constraint failed: ONE' +- error: 'Failed to execute SQL statement: Check constraint failed ''ONE'': x<5' +... +box.space.T1:insert({7, 1, 1}) +--- +- error: 'Check constraint failed ''ONE'': x<5' ... box.execute("INSERT INTO t1 VALUES (2, 1, 1)") --- -- error: 'Failed to execute SQL statement: CHECK constraint failed: TWO' +- error: 'Failed to execute SQL statement: Check constraint failed ''TWO'': y>x' +... +box.space.T1:insert({2, 1, 1}) +--- +- error: 'Check constraint failed ''TWO'': y>x' ... box.execute("INSERT INTO t1 VALUES (2, 4, 1)") --- - row_count: 1 ... +box.space.T1:update({1}, {{'+', 1, 5}}) +--- +- error: 'Check constraint failed ''ONE'': x<5' +... box.execute("DROP TABLE t1") --- - row_count: 1 @@ -159,14 +181,14 @@ _ = box.space._ck_constraint:insert({'physics', s.id, false, 'X<Y'}) ... box.execute("INSERT INTO \"test\" VALUES(2, 1);") --- -- error: 'Failed to execute SQL statement: CHECK constraint failed: physics' +- error: 'Failed to execute SQL statement: Check constraint failed ''physics'': X<Y' ... s:format({{name='Y', type='integer'}, {name='X', type='integer'}}) --- ... box.execute("INSERT INTO \"test\" VALUES(1, 2);") --- -- error: 'Failed to execute SQL statement: CHECK constraint failed: physics' +- error: 'Failed to execute SQL statement: Check constraint failed ''physics'': X<Y' ... box.execute("INSERT INTO \"test\" VALUES(2, 1);") --- @@ -177,7 +199,7 @@ s:truncate() ... box.execute("INSERT INTO \"test\" VALUES(1, 2);") --- -- error: 'Failed to execute SQL statement: CHECK constraint failed: physics' +- error: 'Failed to execute SQL statement: Check constraint failed ''physics'': X<Y' ... s:format({}) --- @@ -209,11 +231,11 @@ _ = box.space._ck_constraint:insert({'conflict', s.id, false, 'X>10'}) ... box.execute("INSERT INTO \"test\" VALUES(1, 2);") --- -- error: 'Failed to execute SQL statement: CHECK constraint failed: conflict' +- error: 'Failed to execute SQL statement: Check constraint failed ''conflict'': X>10' ... box.execute("INSERT INTO \"test\" VALUES(11, 11);") --- -- error: 'Failed to execute SQL statement: CHECK constraint failed: physics' +- error: 'Failed to execute SQL statement: Check constraint failed ''physics'': X<Y' ... box.execute("INSERT INTO \"test\" VALUES(12, 11);") --- @@ -250,6 +272,183 @@ box.execute("CREATE TABLE t5(x INT PRIMARY KEY, y INT, CHECK( x*y < ? ));") - error: 'Failed to create check constraint ''CK_CONSTRAINT_1_T5'': bindings are not allowed in DDL' ... +-- +-- Test ck constraint corner cases +-- +s = box.schema.create_space('test', {engine = engine}) +--- +... +_ = s:create_index('pk') +--- +... +s:format({{name='X', type='any'}, {name='Y', type='integer'}, {name='Z', type='integer', is_nullable=true}}) +--- +... +ck_not_null = box.space._ck_constraint:insert({'ZnotNULL', s.id, false, 'Z IS NOT NULL'}) +--- +... +s:insert({1, 2, box.NULL}) +--- +- error: 'Check constraint failed ''ZnotNULL'': Z IS NOT NULL' +... +s:insert({1, 2}) +--- +- error: 'Check constraint failed ''ZnotNULL'': Z IS NOT NULL' +... +_ = box.space._ck_constraint:delete({'ZnotNULL', s.id}) +--- +... +_ = box.space._ck_constraint:insert({'XlessY', s.id, false, 'X < Y and Y < Z'}) +--- +... +s:insert({'1', 2}) +--- +- error: 'Tuple field 1 type does not match one required by operation: expected unsigned' +... +s:insert({}) +--- +- error: Tuple field 1 required by space format is missing +... +s:insert({2, 1}) +--- +- error: 'Check constraint failed ''XlessY'': X < Y and Y < Z' +... +s:insert({1, 2}) +--- +- [1, 2] +... +s:insert({2, 3, 1}) +--- +- error: 'Check constraint failed ''XlessY'': X < Y and Y < Z' +... +s:insert({2, 3, 4}) +--- +- [2, 3, 4] +... +s:update({2}, {{'+', 2, 3}}) +--- +- error: 'Check constraint failed ''XlessY'': X < Y and Y < Z' +... +s:update({2}, {{'+', 2, 3}, {'+', 3, 3}}) +--- +- [2, 6, 7] +... +s:replace({2, 1, 3}) +--- +- error: 'Check constraint failed ''XlessY'': X < Y and Y < Z' +... +box.snapshot() +--- +- ok +... +s = box.space["test"] +--- +... +s:update({2}, {{'+', 2, 3}}) +--- +- error: 'Check constraint failed ''XlessY'': X < Y and Y < Z' +... +s:update({2}, {{'+', 2, 3}, {'+', 3, 3}}) +--- +- [2, 9, 10] +... +s:replace({2, 1, 3}) +--- +- error: 'Check constraint failed ''XlessY'': X < Y and Y < Z' +... +s:drop() +--- +... +-- +-- Test complex CHECK constraints. +-- +s = box.schema.create_space('test', {engine = engine}) +--- +... +s:format({{name='X', type='integer'}, {name='Y', type='integer'}, {name='Z', type='integer'}}) +--- +... +_ = s:create_index('pk', {parts = {3, 'integer'}}) +--- +... +_ = s:create_index('unique', {parts = {1, 'integer'}}) +--- +... +_ = box.space._ck_constraint:insert({'complex1', s.id, false, 'x+y==11 OR x*y==12 OR x/y BETWEEN 5 AND 8 OR -x == y+10'}) +--- +... +s:insert({1, 10, 1}) +--- +- [1, 10, 1] +... +s:update({1}, {{'=', 1, 4}, {'=', 2, 3}}) +--- +- [4, 3, 1] +... +s:update({1}, {{'=', 1, 12}, {'=', 2, 2}}) +--- +- [12, 2, 1] +... +s:update({1}, {{'=', 1, 12}, {'=', 2, -22}}) +--- +- [12, -22, 1] +... +s:update({1}, {{'=', 1, 0}, {'=', 2, 1}}) +--- +- error: 'Check constraint failed ''complex1'': x+y==11 OR x*y==12 OR x/y BETWEEN + 5 AND 8 OR -x == y+10' +... +s:get({1}) +--- +- [12, -22, 1] +... +s:update({1}, {{'=', 1, 0}, {'=', 2, 2}}) +--- +- error: 'Check constraint failed ''complex1'': x+y==11 OR x*y==12 OR x/y BETWEEN + 5 AND 8 OR -x == y+10' +... +s:get({1}) +--- +- [12, -22, 1] +... +s:drop() +--- +... +s = box.schema.create_space('test', {engine = engine}) +--- +... +s:format({{name='X', type='integer'}, {name='Z', type='any'}}) +--- +... +_ = s:create_index('pk', {parts = {1, 'integer'}}) +--- +... +_ = box.space._ck_constraint:insert({'complex2', s.id, false, 'typeof(coalesce(z,0))==\'integer\''}) +--- +... +s:insert({1, 'string'}) +--- +- error: 'Check constraint failed ''complex2'': typeof(coalesce(z,0))==''integer''' +... +s:insert({1, {map=true}}) +--- +- error: 'Check constraint failed ''complex2'': typeof(coalesce(z,0))==''integer''' +... +s:insert({1, {'a', 'r','r','a','y'}}) +--- +- error: 'Check constraint failed ''complex2'': typeof(coalesce(z,0))==''integer''' +... +s:insert({1, 3.14}) +--- +- error: 'Check constraint failed ''complex2'': typeof(coalesce(z,0))==''integer''' +... +s:insert({1, 666}) +--- +- [1, 666] +... +s:drop() +--- +... test_run:cmd("clear filter") --- - true diff --git a/test/sql/checks.test.lua b/test/sql/checks.test.lua index 4029359b1..2a7510aa9 100644 --- a/test/sql/checks.test.lua +++ b/test/sql/checks.test.lua @@ -31,9 +31,11 @@ box.space._ck_constraint:insert({'CK_CONSTRAINT_01', 513, false, 'X<5'}) box.space._ck_constraint:count({}) box.execute("INSERT INTO \"test\" VALUES(5);") +box.space.test:insert({5}) box.space._ck_constraint:replace({'CK_CONSTRAINT_01', 513, false, 'X<=5'}) box.execute("INSERT INTO \"test\" VALUES(5);") box.execute("INSERT INTO \"test\" VALUES(6);") +box.space.test:insert({6}) -- Can't drop table with check constraints. box.space.test:delete({5}) box.space.test.index.pk:drop() @@ -45,8 +47,11 @@ box.space._space:delete({513}) box.execute("CREATE TABLE t1(x INTEGER CONSTRAINT ONE CHECK( x<5 ), y REAL CONSTRAINT TWO CHECK( y>x ), z INTEGER PRIMARY KEY);") box.space._ck_constraint:count() box.execute("INSERT INTO t1 VALUES (7, 1, 1)") +box.space.T1:insert({7, 1, 1}) box.execute("INSERT INTO t1 VALUES (2, 1, 1)") +box.space.T1:insert({2, 1, 1}) box.execute("INSERT INTO t1 VALUES (2, 4, 1)") +box.space.T1:update({1}, {{'+', 1, 5}}) box.execute("DROP TABLE t1") -- Test space creation rollback on spell error in ck constraint. @@ -93,4 +98,60 @@ box.execute("DROP TABLE w2;") -- box.execute("CREATE TABLE t5(x INT PRIMARY KEY, y INT, CHECK( x*y < ? ));") +-- +-- Test ck constraint corner cases +-- +s = box.schema.create_space('test', {engine = engine}) +_ = s:create_index('pk') +s:format({{name='X', type='any'}, {name='Y', type='integer'}, {name='Z', type='integer', is_nullable=true}}) +ck_not_null = box.space._ck_constraint:insert({'ZnotNULL', s.id, false, 'Z IS NOT NULL'}) +s:insert({1, 2, box.NULL}) +s:insert({1, 2}) +_ = box.space._ck_constraint:delete({'ZnotNULL', s.id}) +_ = box.space._ck_constraint:insert({'XlessY', s.id, false, 'X < Y and Y < Z'}) +s:insert({'1', 2}) +s:insert({}) +s:insert({2, 1}) +s:insert({1, 2}) +s:insert({2, 3, 1}) +s:insert({2, 3, 4}) +s:update({2}, {{'+', 2, 3}}) +s:update({2}, {{'+', 2, 3}, {'+', 3, 3}}) +s:replace({2, 1, 3}) +box.snapshot() +s = box.space["test"] +s:update({2}, {{'+', 2, 3}}) +s:update({2}, {{'+', 2, 3}, {'+', 3, 3}}) +s:replace({2, 1, 3}) +s:drop() + +-- +-- Test complex CHECK constraints. +-- +s = box.schema.create_space('test', {engine = engine}) +s:format({{name='X', type='integer'}, {name='Y', type='integer'}, {name='Z', type='integer'}}) +_ = s:create_index('pk', {parts = {3, 'integer'}}) +_ = s:create_index('unique', {parts = {1, 'integer'}}) +_ = box.space._ck_constraint:insert({'complex1', s.id, false, 'x+y==11 OR x*y==12 OR x/y BETWEEN 5 AND 8 OR -x == y+10'}) +s:insert({1, 10, 1}) +s:update({1}, {{'=', 1, 4}, {'=', 2, 3}}) +s:update({1}, {{'=', 1, 12}, {'=', 2, 2}}) +s:update({1}, {{'=', 1, 12}, {'=', 2, -22}}) +s:update({1}, {{'=', 1, 0}, {'=', 2, 1}}) +s:get({1}) +s:update({1}, {{'=', 1, 0}, {'=', 2, 2}}) +s:get({1}) +s:drop() + +s = box.schema.create_space('test', {engine = engine}) +s:format({{name='X', type='integer'}, {name='Z', type='any'}}) +_ = s:create_index('pk', {parts = {1, 'integer'}}) +_ = box.space._ck_constraint:insert({'complex2', s.id, false, 'typeof(coalesce(z,0))==\'integer\''}) +s:insert({1, 'string'}) +s:insert({1, {map=true}}) +s:insert({1, {'a', 'r','r','a','y'}}) +s:insert({1, 3.14}) +s:insert({1, 666}) +s:drop() + test_run:cmd("clear filter") diff --git a/test/sql/errinj.result b/test/sql/errinj.result index 18d1f3882..01c3e2856 100644 --- a/test/sql/errinj.result +++ b/test/sql/errinj.result @@ -490,7 +490,8 @@ _ = box.space._ck_constraint:insert({'CK_CONSTRAINT_01', s.id, false, 'X<5'}) ... box.execute("INSERT INTO \"test\" VALUES(5);") --- -- error: 'Failed to execute SQL statement: CHECK constraint failed: CK_CONSTRAINT_01' +- error: 'Failed to execute SQL statement: Check constraint failed ''CK_CONSTRAINT_01'': + X<5' ... errinj.set("ERRINJ_WAL_IO", true) --- @@ -521,7 +522,8 @@ _ = box.space._ck_constraint:delete({'CK_CONSTRAINT_01', s.id}) ... box.execute("INSERT INTO \"test\" VALUES(6);") --- -- error: 'Failed to execute SQL statement: CHECK constraint failed: CK_CONSTRAINT_01' +- error: 'Failed to execute SQL statement: Check constraint failed ''CK_CONSTRAINT_01'': + X<=5' ... errinj.set("ERRINJ_WAL_IO", false) --- @@ -557,11 +559,13 @@ _ = box.space._ck_constraint:insert({'Xgreater10', s.id, false, 'X > 10'}) ... box.execute("INSERT INTO \"test\" VALUES(2, 1);") --- -- error: 'Failed to execute SQL statement: CHECK constraint failed: Xgreater10' +- error: 'Failed to execute SQL statement: Check constraint failed ''Xgreater10'': + X > 10' ... box.execute("INSERT INTO \"test\" VALUES(20, 10);") --- -- error: 'Failed to execute SQL statement: CHECK constraint failed: XlessY' +- error: 'Failed to execute SQL statement: Check constraint failed ''XlessY'': X < + Y' ... box.execute("INSERT INTO \"test\" VALUES(20, 100);") --- @@ -584,11 +588,13 @@ errinj.set("ERRINJ_WAL_IO", false) ... box.execute("INSERT INTO \"test\" VALUES(2, 1);") --- -- error: 'Failed to execute SQL statement: CHECK constraint failed: XlessY' +- error: 'Failed to execute SQL statement: Check constraint failed ''XlessY'': X < + Y' ... box.execute("INSERT INTO \"test\" VALUES(20, 10);") --- -- error: 'Failed to execute SQL statement: CHECK constraint failed: XlessY' +- error: 'Failed to execute SQL statement: Check constraint failed ''XlessY'': X < + Y' ... box.execute("INSERT INTO \"test\" VALUES(20, 100);") --- diff --git a/test/sql/gh-2981-check-autoinc.result b/test/sql/gh-2981-check-autoinc.result index 7384c81e8..e57789897 100644 --- a/test/sql/gh-2981-check-autoinc.result +++ b/test/sql/gh-2981-check-autoinc.result @@ -29,7 +29,8 @@ box.execute("insert into t1 values (18, null);") ... box.execute("insert into t1(s2) values (null);") --- -- error: 'Failed to execute SQL statement: CHECK constraint failed: CK_CONSTRAINT_1_T1' +- error: 'Failed to execute SQL statement: Check constraint failed ''CK_CONSTRAINT_1_T1'': + s1 <> 19' ... box.execute("insert into t2 values (18, null);") --- @@ -37,7 +38,8 @@ box.execute("insert into t2 values (18, null);") ... box.execute("insert into t2(s2) values (null);") --- -- error: 'Failed to execute SQL statement: CHECK constraint failed: CK_CONSTRAINT_1_T2' +- error: 'Failed to execute SQL statement: Check constraint failed ''CK_CONSTRAINT_1_T2'': + s1 <> 19 AND s1 <> 25' ... box.execute("insert into t2 values (24, null);") --- @@ -45,7 +47,8 @@ box.execute("insert into t2 values (24, null);") ... box.execute("insert into t2(s2) values (null);") --- -- error: 'Failed to execute SQL statement: CHECK constraint failed: CK_CONSTRAINT_1_T2' +- error: 'Failed to execute SQL statement: Check constraint failed ''CK_CONSTRAINT_1_T2'': + s1 <> 19 AND s1 <> 25' ... box.execute("insert into t3 values (9, null)") --- @@ -53,7 +56,8 @@ box.execute("insert into t3 values (9, null)") ... box.execute("insert into t3(s2) values (null)") --- -- error: 'Failed to execute SQL statement: CHECK constraint failed: CK_CONSTRAINT_1_T3' +- error: 'Failed to execute SQL statement: Check constraint failed ''CK_CONSTRAINT_1_T3'': + s1 < 10' ... box.execute("DROP TABLE t1") --- diff --git a/test/sql/types.result b/test/sql/types.result index 582785413..ccbdd8c50 100644 --- a/test/sql/types.result +++ b/test/sql/types.result @@ -709,7 +709,8 @@ box.execute("CREATE TABLE t1 (id INT PRIMARY KEY, a BOOLEAN CHECK (a = true));") ... box.execute("INSERT INTO t1 VALUES (1, false);") --- -- error: 'Failed to execute SQL statement: CHECK constraint failed: CK_CONSTRAINT_1_T1' +- error: 'Failed to execute SQL statement: Check constraint failed ''CK_CONSTRAINT_1_T1'': + a = true' ... box.execute("INSERT INTO t1 VALUES (1, true);") --- -- 2.21.0 ^ permalink raw reply [flat|nested] 25+ messages in thread
* [tarantool-patches] Re: [PATCH v3 2/3] box: run check constraint tests on space alter 2019-05-07 9:53 ` Kirill Shcherbatov @ 2019-05-07 16:39 ` Konstantin Osipov 2019-05-07 17:47 ` [tarantool-patches] " Kirill Shcherbatov 2019-05-14 16:49 ` n.pettik 1 sibling, 1 reply; 25+ messages in thread From: Konstantin Osipov @ 2019-05-07 16:39 UTC (permalink / raw) To: tarantool-patches; +Cc: n.pettik * Kirill Shcherbatov <kshcherbatov@tarantool.org> [19/05/07 12:56]: > +/** > + * Create VDBE machine for ck constraint by given definition and > + * expression AST. The generated instructions consist of > + * prologue code that maps tuple fields via bindings and ck > + * constraint code which implements given expression. > + * In case of ck constraint error during VDBE execution, it is > + * aborted and error is handled as diag message. > + * @param ck_constraint_def Check constraint definition to prepare > + * error description. > + * @param expr Check constraint expression AST is built for > + * given @ck_constraint_def, see for > + * (sql_expr_compile + > + * ck_constraint_resolve_space_def) implementation. > + * @param space_def The space definition of the space this check > + * constraint is constructed for. > + * @retval not NULL sql_stmt program pointer on success. > + * @retval NULL otherwise. > + */ > +static struct sql_stmt * > +ck_constraint_program_compile(struct ck_constraint_def *ck_constraint_def, > + struct Expr *expr, struct space_def *space_def) > +{ > + struct sql *db = sql_get(); > + struct Parse parser; > + sql_parser_create(&parser, db); > + struct Vdbe *v = sqlGetVdbe(&parser); > + if (v == NULL) { > + diag_set(OutOfMemory, sizeof(struct Vdbe), "sqlGetVdbe", > + "vdbe"); > + return NULL; > + } > + /* Compile VDBE with default sql parameters. */ > + struct session *user_session = current_session(); > + uint32_t sql_flags = user_session->sql_flags; > + user_session->sql_flags = default_flags; This is a time bomb from technical debt point of view. Please pass the flags to sql_parser_create instead, which will then pass them to vdbe. > + uint32_t tuple_field_count = mp_decode_array(&new_tuple); > + uint32_t field_count = > + MIN(tuple_field_count, space->def->field_count); > + for (uint32_t i = 0; i < field_count; i++) { > + struct sql_bind bind; > + if (sql_bind_decode(&bind, i + 1, &new_tuple) != 0 || > + sql_bind_column(ck_constraint->stmt, &bind, i + 1) != 0) { > + diag_set(ClientError, ER_CK_CONSTRAINT_FAILED, > + ck_constraint->def->name, > + ck_constraint->def->expr_str); > + return -1; This looks like a pessimization to me. Depending on the code flow, some of the tuple fields may not be accessed at all. Is it really necessary to decode them so agressibvely here? Especially since you encode *all* space fields. -- Konstantin Osipov, Moscow, Russia, +7 903 626 22 32 ^ permalink raw reply [flat|nested] 25+ messages in thread
* [tarantool-patches] Re: [tarantool-patches] Re: [PATCH v3 2/3] box: run check constraint tests on space alter 2019-05-07 16:39 ` Konstantin Osipov @ 2019-05-07 17:47 ` Kirill Shcherbatov 2019-05-07 20:28 ` Konstantin Osipov 2019-05-11 12:15 ` n.pettik 0 siblings, 2 replies; 25+ messages in thread From: Kirill Shcherbatov @ 2019-05-07 17:47 UTC (permalink / raw) To: tarantool-patches, Konstantin Osipov; +Cc: n.pettik [-- Attachment #1: Type: text/plain, Size: 763 bytes --] ; > + if (sql_bind_decode(&bind, i + 1, &new_tuple) != 0 || > + sql_bind_column(ck_constraint->stmt, &bind, i + 1) != 0) { > + diag_set(ClientError, ER_CK_CONSTRAINT_FAILED, > + ck_constraint->def->name, > + ck_constraint->def->expr_str); > + return -1; >This looks like a pessimization to me. Depending on the >code flow, >some of the tuple fields may not be accessed at all. Is it >really >necessary to decode them so agressibvely here? >Especially since >you encode *all* space fields. I'll try to walk though the AST tree and prepare the map of fields that are involved in expression (on check compile operation); Here there would be binding of **used** fields. What do you think? -- Konstantin Osipov, Moscow, Russia, +7 903 626 22 32 [-- Attachment #2: Type: text/html, Size: 1068 bytes --] ^ permalink raw reply [flat|nested] 25+ messages in thread
* [tarantool-patches] Re: [PATCH v3 2/3] box: run check constraint tests on space alter 2019-05-07 17:47 ` [tarantool-patches] " Kirill Shcherbatov @ 2019-05-07 20:28 ` Konstantin Osipov 2019-05-11 12:15 ` n.pettik 1 sibling, 0 replies; 25+ messages in thread From: Konstantin Osipov @ 2019-05-07 20:28 UTC (permalink / raw) To: Kirill Shcherbatov; +Cc: tarantool-patches, n.pettik * Kirill Shcherbatov <kshcherbatov@tarantool.org> [19/05/07 20:49]: > > + if (sql_bind_decode(&bind, i + 1, &new_tuple) != 0 || > > + sql_bind_column(ck_constraint->stmt, &bind, i + 1) != 0) { > > + diag_set(ClientError, ER_CK_CONSTRAINT_FAILED, > > + ck_constraint->def->name, > > + ck_constraint->def->expr_str); > > + return -1; > >This looks like a pessimization to me. Depending on the >code flow, > >some of the tuple fields may not be accessed at all. Is it >really > >necessary to decode them so agressibvely here? > >Especially since > >you encode *all* space fields. > I'll try to walk though the AST tree and prepare the map of fields that are involved in expression (on check compile operation); > Here there would be binding of **used** fields. > What do you think? It's better to fetch the bound field upon first access. Most paths of the CHECK constraint may not touch most of the fields. -- Konstantin Osipov, Moscow, Russia, +7 903 626 22 32 ^ permalink raw reply [flat|nested] 25+ messages in thread
* [tarantool-patches] Re: [PATCH v3 2/3] box: run check constraint tests on space alter 2019-05-07 17:47 ` [tarantool-patches] " Kirill Shcherbatov 2019-05-07 20:28 ` Konstantin Osipov @ 2019-05-11 12:15 ` n.pettik 2019-05-12 21:12 ` Konstantin Osipov 1 sibling, 1 reply; 25+ messages in thread From: n.pettik @ 2019-05-11 12:15 UTC (permalink / raw) To: tarantool-patches; +Cc: Kirill Shcherbatov, Konstantin Osipov [-- Attachment #1: Type: text/plain, Size: 948 bytes --] > On 7 May 2019, at 20:47, Kirill Shcherbatov <kshcherbatov@tarantool.org> wrote: > > ; > > + if (sql_bind_decode(&bind, i + 1, &new_tuple) != 0 || > > + sql_bind_column(ck_constraint->stmt, &bind, i + 1) != 0) { > > + diag_set(ClientError, ER_CK_CONSTRAINT_FAILED, > > + ck_constraint->def->name, > > + ck_constraint->def->expr_str); > > + return -1; > >This looks like a pessimization to me. Depending on the >code flow, > >some of the tuple fields may not be accessed at all. Is it >really > >necessary to decode them so agressibvely here? > >Especially since > >you encode *all* space fields. > > I'll try to walk though the AST tree and prepare the map of fields that are involved in expression (on check compile operation); > Here there would be binding of **used** fields. > > What do you think? > To be honest, I don’t get what Konstantin really meant in his next letter. I’m ok with your approach. [-- Attachment #2: Type: text/html, Size: 1738 bytes --] ^ permalink raw reply [flat|nested] 25+ messages in thread
* [tarantool-patches] Re: [PATCH v3 2/3] box: run check constraint tests on space alter 2019-05-11 12:15 ` n.pettik @ 2019-05-12 21:12 ` Konstantin Osipov 2019-05-13 7:09 ` Kirill Shcherbatov 0 siblings, 1 reply; 25+ messages in thread From: Konstantin Osipov @ 2019-05-12 21:12 UTC (permalink / raw) To: n.pettik; +Cc: tarantool-patches, Kirill Shcherbatov * n.pettik <korablev@tarantool.org> [19/05/13 00:04]: > To be honest, I don’t get what Konstantin really meant in his next letter. > I’m ok with your approach. What is it that is unclear? -- Konstantin Osipov, Moscow, Russia, +7 903 626 22 32 ^ permalink raw reply [flat|nested] 25+ messages in thread
* [tarantool-patches] Re: [PATCH v3 2/3] box: run check constraint tests on space alter 2019-05-12 21:12 ` Konstantin Osipov @ 2019-05-13 7:09 ` Kirill Shcherbatov 2019-05-13 7:49 ` Konstantin Osipov 0 siblings, 1 reply; 25+ messages in thread From: Kirill Shcherbatov @ 2019-05-13 7:09 UTC (permalink / raw) To: tarantool-patches, Konstantin Osipov, n.pettik >> To be honest, I don’t get what Konstantin really meant in his next letter. >> I’m ok with your approach. > > What is it that is unclear? >> It's better to fetch the bound field upon first access. >> Most paths of the CHECK constraint may not touch most of the >> fields. As far as I understand, Kostya wants something like binding on demand for VDBE; maybe something like new op "OP_Fetch | struct tuple * | fieldno" = static inline const char * tuple_field(struct tuple *tuple, uint32_t fieldno) + result cache in VDBE ^ permalink raw reply [flat|nested] 25+ messages in thread
* [tarantool-patches] Re: [PATCH v3 2/3] box: run check constraint tests on space alter 2019-05-13 7:09 ` Kirill Shcherbatov @ 2019-05-13 7:49 ` Konstantin Osipov 0 siblings, 0 replies; 25+ messages in thread From: Konstantin Osipov @ 2019-05-13 7:49 UTC (permalink / raw) To: Kirill Shcherbatov; +Cc: tarantool-patches, n.pettik * Kirill Shcherbatov <kshcherbatov@tarantool.org> [19/05/13 10:13]: > >> To be honest, I don’t get what Konstantin really meant in his next letter. > >> I’m ok with your approach. > > > > What is it that is unclear? > > >> It's better to fetch the bound field upon first access. > >> Most paths of the CHECK constraint may not touch most of the > >> fields. > > As far as I understand, Kostya wants something like binding on > demand for VDBE; maybe something like new op > > "OP_Fetch | struct tuple * | fieldno" = > > static inline const char * > tuple_field(struct tuple *tuple, uint32_t fieldno) > + > result cache in VDBE Correct. -- Konstantin Osipov, Moscow, Russia, +7 903 626 22 32 ^ permalink raw reply [flat|nested] 25+ messages in thread
* [tarantool-patches] Re: [PATCH v3 2/3] box: run check constraint tests on space alter 2019-05-07 9:53 ` Kirill Shcherbatov 2019-05-07 16:39 ` Konstantin Osipov @ 2019-05-14 16:49 ` n.pettik 1 sibling, 0 replies; 25+ messages in thread From: n.pettik @ 2019-05-14 16:49 UTC (permalink / raw) To: tarantool-patches; +Cc: Kirill Shcherbatov > On 7 May 2019, at 12:53, Kirill Shcherbatov <kshcherbatov@tarantool.org> wrote: > > >> One unpleasant “feature” that I’ve noticed: >> >> create table t4 (id int primary key check(id > id*id)) >> insert into t4 values(0.5) >> - error: 'Failed to execute SQL statement: Check constraint failed ''CK_CONSTRAINT_1_T4'': >> id > id*id’ >> >> Conversion to INT occurs before tuples reaches on_replace trigger. >> We should discuss what to do with that. I guess you already raised >> this question when we were talking about typeof() functions inside >> check constraint. > We should live with it ;) Please, document this “feature”. ^ permalink raw reply [flat|nested] 25+ messages in thread
* [tarantool-patches] [PATCH v3 3/3] box: user-friendly interface to manage ck constraints 2019-04-16 13:51 [tarantool-patches] [PATCH v3 0/3] box: run checks on insertions in LUA spaces Kirill Shcherbatov 2019-04-16 13:51 ` [tarantool-patches] [PATCH v3 1/3] schema: add new system space for CHECK constraints Kirill Shcherbatov 2019-04-16 13:51 ` [tarantool-patches] [PATCH v3 2/3] box: run check constraint tests on space alter Kirill Shcherbatov @ 2019-04-16 13:51 ` Kirill Shcherbatov 2019-04-25 20:38 ` [tarantool-patches] " n.pettik 2 siblings, 1 reply; 25+ messages in thread From: Kirill Shcherbatov @ 2019-04-16 13:51 UTC (permalink / raw) To: tarantool-patches, korablev; +Cc: Kirill Shcherbatov @TarantoolBot document Title: check constraint for LUA space The check constraint is a type of integrity constraint which specifies a requirement that must be met by tuple before it is inserted into space. The constraint result must be predictable. Now it is possible to create ck constraints only for empty space having format. Constraint expression is a string that defines relations between top-level tuple fields. Take into account that all names are converted to an uppercase before resolve(like SQL does), use \" sign for names of fields that were created not with SQL. To create a new CK constraint for a space, use s = box.schema.create_space('person') _ = s:create_index('pk', {parts = {1, 'string'}}) s:format({{name='name', type='string'}, {name='age', type='integer'}, {name='experience', type='integer'}}) s:ck_constraint({'physics', '\"age\" > 14 and \"experience\" < \"age\"'}) s:insert({"James Bond", 36, 36}) --- - error: 'Check constraint failed ''physics'': "age" > 14 and "experience" < "age"' ... s:insert("James Bond", 36, 16) -- success s:insert({"Bobby", 6, 0}) --- - error: 'Check constraint failed ''physics'': "age" > 14 and "experience" < "age"' ... To list all ck constraints assosiated with space, s:ck_constraint() To replace ck constraint, use following syntax: s:ck_constraint({'physics', '\"experience\" < \"age\"'}, {'physics'}) s:insert({"Bobby", 6, 0}) -- success To drop ck constraint, use: s:ck_constraint(nil, {'physics', '\"experience\" < \"age\"'}) --- src/box/lua/schema.lua | 19 ++++++++++ test/sql/checks.result | 78 ++++++++++++++++++++++++++++++++++++++++ test/sql/checks.test.lua | 29 +++++++++++++++ 3 files changed, 126 insertions(+) diff --git a/src/box/lua/schema.lua b/src/box/lua/schema.lua index e01f500e6..69aebc01c 100644 --- a/src/box/lua/schema.lua +++ b/src/box/lua/schema.lua @@ -1533,6 +1533,25 @@ space_mt.auto_increment = function(space, tuple) table.insert(tuple, 1, max + 1) return space:insert(tuple) end +-- Manage space ck constraints +space_mt.ck_constraint = function(space, new, old) + check_space_arg(space, 'ck_constraint') + if new == nil and old == nil then + return box.space._ck_constraint.index['space_id']:select({space.id}) + end + if new == nil then + box.space._ck_constraint:delete({old[1], space.id}) + else + local msg = "Usage: space:ck_constraint({name, expr_str} | nil, " .. + "[{name, expr_str} | nil])" + if old ~= nil and old[1] ~= new[1] then + box.error(box.error.PROC_LUA, + "Error: old and new CK constraint names must " .. + "coincide. " .. msg) + end + return box.space._ck_constraint:replace({new[1], space.id, new[2]}) + end +end space_mt.pairs = function(space, key, opts) check_space_arg(space, 'pairs') diff --git a/test/sql/checks.result b/test/sql/checks.result index 0dd5d820f..301140988 100644 --- a/test/sql/checks.result +++ b/test/sql/checks.result @@ -395,3 +395,81 @@ s:replace({2, 1, 3}) s:drop() --- ... +-- Test ck constraints user-friendly creation interface +s1 = box.schema.create_space('test1') +--- +... +_ = s1:create_index('pk') +--- +... +s1:format({{name='X', type='any'}, {name='Y', type='integer'}}) +--- +... +s2 = box.schema.create_space('test2') +--- +... +_ = s2:create_index('pk') +--- +... +s2:format({{name='X', type='any'}, {name='Y', type='integer'}}) +--- +... +s1:ck_constraint({'physics', 'X < Y'}) +--- +- ['physics', 520, 'X < Y'] +... +s1:ck_constraint({'greater', 'X > 10'}) +--- +- ['greater', 520, 'X > 10'] +... +s2:ck_constraint({'physics', 'X > Y'}) +--- +- ['physics', 521, 'X > Y'] +... +s1:ck_constraint() +--- +- - ['greater', 520, 'X > 10'] + - ['physics', 520, 'X < Y'] +... +s2:ck_constraint() +--- +- - ['physics', 521, 'X > Y'] +... +s1:ck_constraint({'greater', 'X > 20'}, {'greater'}) +--- +- ['greater', 520, 'X > 20'] +... +s2:ck_constraint({'greater', 'X > 20'}, {'greater'}) +--- +- ['greater', 521, 'X > 20'] +... +s1:ck_constraint() +--- +- - ['greater', 520, 'X > 20'] + - ['physics', 520, 'X < Y'] +... +s2:ck_constraint() +--- +- - ['greater', 521, 'X > 20'] + - ['physics', 521, 'X > Y'] +... +s1:ck_constraint(nil, {'greater'}) +--- +... +s2:ck_constraint(nil, {'greater'}) +--- +... +s1:ck_constraint() +--- +- - ['physics', 520, 'X < Y'] +... +s2:ck_constraint() +--- +- - ['physics', 521, 'X > Y'] +... +s1:drop() +--- +... +s2:drop() +--- +... diff --git a/test/sql/checks.test.lua b/test/sql/checks.test.lua index 2652f3b7d..c2c5275e5 100644 --- a/test/sql/checks.test.lua +++ b/test/sql/checks.test.lua @@ -134,3 +134,32 @@ s:update({2}, {{'+', 2, 3}}) s:update({2}, {{'+', 2, 3}, {'+', 3, 3}}) s:replace({2, 1, 3}) s:drop() + +-- Test ck constraints user-friendly creation interface +s1 = box.schema.create_space('test1') +_ = s1:create_index('pk') +s1:format({{name='X', type='any'}, {name='Y', type='integer'}}) +s2 = box.schema.create_space('test2') +_ = s2:create_index('pk') +s2:format({{name='X', type='any'}, {name='Y', type='integer'}}) +s1:ck_constraint({'physics', 'X < Y'}) +s1:ck_constraint({'greater', 'X > 10'}) +s2:ck_constraint({'physics', 'X > Y'}) +s1:ck_constraint() +s2:ck_constraint() +s1:ck_constraint({'greater', 'X > 20'}, {'greater'}) +s2:ck_constraint({'greater', 'X > 20'}, {'greater'}) +s1:ck_constraint() +s2:ck_constraint() +s1:insert({2, 1}) +s1:insert({21, 20}) +s2:insert({1, 2}) +s2:insert({21, 22}) +s1:ck_constraint(nil, {'greater'}) +s2:ck_constraint(nil, {'greater'}) +s1:ck_constraint() +s2:ck_constraint() +s1:insert({2, 1}) +s2:insert({1, 2}) +s1:drop() +s2:drop() -- 2.21.0 ^ permalink raw reply [flat|nested] 25+ messages in thread
* [tarantool-patches] Re: [PATCH v3 3/3] box: user-friendly interface to manage ck constraints 2019-04-16 13:51 ` [tarantool-patches] [PATCH v3 3/3] box: user-friendly interface to manage ck constraints Kirill Shcherbatov @ 2019-04-25 20:38 ` n.pettik 2019-05-07 9:53 ` Kirill Shcherbatov 0 siblings, 1 reply; 25+ messages in thread From: n.pettik @ 2019-04-25 20:38 UTC (permalink / raw) To: tarantool-patches; +Cc: Kirill Shcherbatov > On 16 Apr 2019, at 16:51, Kirill Shcherbatov <kshcherbatov@tarantool.org> wrote: > > @TarantoolBot document > Title: check constraint for LUA space > > The check constraint is a type of integrity constraint which > specifies a requirement that must be met by tuple before it > is inserted into space. The constraint result must be predictable. Expression in check constraint must be <boolean value expression> I.e. return boolean result. > Now it is possible to create ck constraints only for empty space > having format. Constraint expression is a string that defines > relations between top-level tuple fields. > Take into account that all names are converted to an uppercase > before resolve(like SQL does), use \" sign for names of fields > that were created not with SQL. Describe please new order of execution: SQL triggers, NoSQL triggers, check constraints and fk contraints. And order between checks themselves. Original check implementation executed check constraints before NoSQL triggers. > To create a new CK constraint for a space, use > s = box.schema.create_space('person') > _ = s:create_index('pk', {parts = {1, 'string'}}) > s:format({{name='name', type='string'}, {name='age', type='integer'}, > {name='experience', type='integer'}}) > s:ck_constraint({'physics', '\"age\" > 14 and \"experience\" < > \"age\"’}) Why not s:create_ck_constraint() OR s:create_check(). Let's discuss names and methods in our server chat. So, now I won’t review code in patch since it may significantly change. ^ permalink raw reply [flat|nested] 25+ messages in thread
* [tarantool-patches] Re: [PATCH v3 3/3] box: user-friendly interface to manage ck constraints 2019-04-25 20:38 ` [tarantool-patches] " n.pettik @ 2019-05-07 9:53 ` Kirill Shcherbatov 0 siblings, 0 replies; 25+ messages in thread From: Kirill Shcherbatov @ 2019-05-07 9:53 UTC (permalink / raw) To: tarantool-patches, n.pettik > Why not s:create_ck_constraint() OR s:create_check(). > Let's discuss names and methods in our server chat. > > So, now I won’t review code in patch since it may significantly change. ====================================================== @TarantoolBot document Title: check constraint for Lua space The check constraint is a type of integrity constraint which specifies a requirement that must be met by tuple before it is inserted into space. The constraint result must be predictable. Expression in check constraint must be <boolean value expression> I.e. return boolean result. Now it is possible to create ck constraints only for empty space having format. Constraint expression is a string that defines relations between top-level tuple fields. Take into account that all names are converted to an uppercase before resolve(like SQL does), use \" sign for names of fields that were created not with SQL. The check constraints are fired on insertion to the Lua space together with Lua space triggers. The execution order of ck constraints checks and space triggers follows their creation sequence. To create a new CK constraint for a space, use s = box.schema.create_space('person') _ = s:create_index('pk', {parts = {1, 'string'}}) s:format({{name='name', type='string'}, {name='age', type='integer'}, {name='experience', type='integer'}}) s:ck_constraint({'physics', '\"age\" > 14 and \"experience\" < \"age\"'}) s:insert({"James Bond", 36, 36}) --- - error: 'Check constraint failed ''physics'': "age" > 14 and "experience" < "age"' ... s:insert("James Bond", 36, 16) -- success s:insert({"Bobby", 6, 0}) --- - error: 'Check constraint failed ''physics'': "age" > 14 and "experience" < "age"' ... To list all ck constraints associated with space, s:ck_constraint() To replace ck constraint, use following syntax: s:ck_constraint({'physics', '\"experience\" < \"age\"'}, {'physics'}) s:insert({"Bobby", 6, 0}) -- success To drop ck constraint, use: s:ck_constraint(nil, {'physics', '\"experience\" < \"age\"'}) --- src/box/alter.cc | 3 +- src/box/lua/schema.lua | 34 ++++++++++++++- src/box/lua/space.cc | 63 ++++++++++++++++++++++++++++ test/sql/checks.result | 90 ++++++++++++++++++++++++++++++++++++++++ test/sql/checks.test.lua | 29 +++++++++++++ 5 files changed, 217 insertions(+), 2 deletions(-) diff --git a/src/box/alter.cc b/src/box/alter.cc index 9c560e85d..b7a5e3650 100644 --- a/src/box/alter.cc +++ b/src/box/alter.cc @@ -4229,6 +4229,8 @@ on_replace_ck_constraint_commit(struct trigger *trigger, void *event) { struct txn_stmt *stmt = txn_last_stmt((struct txn *) event); struct ck_constraint *ck = (struct ck_constraint *)trigger->data; + struct space *space = space_by_id(ck->space_id); + trigger_run_xc(&on_alter_space, space); if (stmt->old_tuple != NULL) ck_constraint_delete(ck); } @@ -4250,7 +4252,6 @@ on_replace_dd_ck_constraint(struct trigger * /* trigger*/, void *event) txn_alter_trigger_new(on_replace_ck_constraint_rollback, NULL); struct trigger *on_commit = txn_alter_trigger_new(on_replace_ck_constraint_commit, NULL); - if (new_tuple != NULL) { bool is_deferred = tuple_field_bool_xc(new_tuple, diff --git a/src/box/lua/schema.lua b/src/box/lua/schema.lua index e01f500e6..132b2d610 100644 --- a/src/box/lua/schema.lua +++ b/src/box/lua/schema.lua @@ -1185,6 +1185,15 @@ local function check_primary_index(space) end box.internal.check_primary_index = check_primary_index -- for net.box +-- Helper function to check ck_constraint:method() usage +local function check_ck_constraint_arg(ck_constraint, method) + if type(ck_constraint) ~= 'table' or ck_constraint.name == nil then + local fmt = 'Use ck_constraint:%s(...) instead of ck_constraint.%s(...)' + error(string.format(fmt, method, method)) + end +end +box.internal.check_ck_constraint_arg = check_ck_constraint_arg + box.internal.schema_version = builtin.box_schema_version local function check_iterator_type(opts, key_is_nil) @@ -1533,7 +1542,15 @@ space_mt.auto_increment = function(space, tuple) table.insert(tuple, 1, max + 1) return space:insert(tuple) end - +-- Manage space ck constraints +space_mt.create_check_constraint = function(space, name, expr_str) + check_space_arg(space, 'create_constraint') + if name == nil or expr_str == nil then + box.error(box.error.PROC_LUA, + "Usage: space:create_constraint(name, expr_str)") + end + return box.space._ck_constraint:insert({name, space.id, false, expr_str}) +end space_mt.pairs = function(space, key, opts) check_space_arg(space, 'pairs') local pk = space.index[0] @@ -1579,10 +1596,17 @@ end space_mt.frommap = box.internal.space.frommap space_mt.__index = space_mt +local ck_constraint_mt = {} +ck_constraint_mt.drop = function(ck_constraint) + check_ck_constraint_arg(ck_constraint, 'drop') + box.space._ck_constraint:delete({ck_constraint.name, ck_constraint.space_id}) +end + box.schema.index_mt = base_index_mt box.schema.memtx_index_mt = memtx_index_mt box.schema.vinyl_index_mt = vinyl_index_mt box.schema.space_mt = space_mt +box.schema.ck_constraint_mt = ck_constraint_mt -- -- Wrap a global space/index metatable into a space/index local @@ -1628,6 +1652,14 @@ function box.schema.space.bless(space) end end end + if type(space.ck_constraint) == 'table' and space.enabled then + for j, ck_constraint in pairs(space.ck_constraint) do + if type(j) == 'string' then + setmetatable(ck_constraint, + wrap_schema_object_mt('ck_constraint_mt')) + end + end + end end local sequence_mt = {} diff --git a/src/box/lua/space.cc b/src/box/lua/space.cc index ce4287bba..9cec1cbde 100644 --- a/src/box/lua/space.cc +++ b/src/box/lua/space.cc @@ -28,6 +28,7 @@ * THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. */ +#include "box/ck_constraint.h" #include "box/lua/space.h" #include "box/lua/tuple.h" #include "box/lua/key_def.h" @@ -147,6 +148,66 @@ lbox_space_before_replace(struct lua_State *L) lbox_push_txn_stmt, lbox_pop_txn_stmt); } +/** + * Make ck_constraints available in Lua, via ck_constraint[] + * array. + * Returns a new table representing a space on top of the Lua + * stack. + */ +static void +lbox_ck_constraint(struct lua_State *L, struct space *space, int i) +{ + lua_getfield(L, i, "ck_constraint"); + if (lua_isnil(L, -1)) { + lua_pop(L, 1); + lua_pushstring(L, "ck_constraint"); + lua_newtable(L); + lua_settable(L, i); + lua_getfield(L, i, "ck_constraint"); + } else { + lua_pushnil(L); + while (lua_next(L, -2) != 0) { + size_t name_len; + const char *name = lua_tolstring(L, -2, &name_len); + /* + * Remove ck_constraint only if it was + * deleted. + */ + if (space_ck_constraint_by_name(space, name, + (uint32_t)name_len) == NULL) { + lua_pushlstring(L, name, name_len); + lua_pushnil(L); + lua_settable(L, -5); + } + lua_pop(L, 1); + } + } + struct ck_constraint *ck_constraint = NULL; + rlist_foreach_entry(ck_constraint, &space->ck_constraint, link) { + lua_getfield(L, i, ck_constraint->def->name); + if (lua_isnil(L, -1)) { + lua_pop(L, 1); + lua_pushstring(L, ck_constraint->def->name); + lua_newtable(L); + lua_settable(L, -3); + lua_getfield(L, -1, ck_constraint->def->name); + assert(!lua_isnil(L, -1)); + } + + lua_pushstring(L, ck_constraint->def->name); + lua_setfield(L, -2, "name"); + + lua_pushnumber(L, space->def->id); + lua_setfield(L, -2, "space_id"); + + lua_pushstring(L, ck_constraint->def->expr_str); + lua_setfield(L, -2, "expr_str"); + + lua_setfield(L, -2, ck_constraint->def->name); + } + lua_pop(L, 1); +} + /** * Make a single space available in Lua, * via box.space[] array. @@ -337,6 +398,8 @@ lbox_fillspace(struct lua_State *L, struct space *space, int i) lua_pop(L, 1); /* pop the index field */ + lbox_ck_constraint(L, space, i); + lua_getfield(L, LUA_GLOBALSINDEX, "box"); lua_pushstring(L, "schema"); lua_gettable(L, -2); diff --git a/test/sql/checks.result b/test/sql/checks.result index bd7b96435..44f5b2b93 100644 --- a/test/sql/checks.result +++ b/test/sql/checks.result @@ -449,6 +449,96 @@ s:insert({1, 666}) s:drop() --- ... +-- +-- Test ck constraints LUA integration. +-- +s1 = box.schema.create_space('test1') +--- +... +_ = s1:create_index('pk') +--- +... +s1:format({{name='X', type='any'}, {name='Y', type='integer'}}) +--- +... +s2 = box.schema.create_space('test2') +--- +... +_ = s2:create_index('pk') +--- +... +s2:format({{name='X', type='any'}, {name='Y', type='integer'}}) +--- +... +_ = s1:create_check_constraint('physics', 'X < Y') +--- +... +_ = s1:create_check_constraint('greater', 'X > 20') +--- +... +_ = s2:create_check_constraint('physics', 'X > Y') +--- +... +_ = s2:create_check_constraint('greater', 'X > 20') +--- +... +s1.ck_constraint.physics ~= nil +--- +- true +... +s1.ck_constraint.greater ~= nil +--- +- true +... +s2.ck_constraint.physics ~= nil +--- +- true +... +s2.ck_constraint.greater ~= nil +--- +- true +... +s1:insert({2, 1}) +--- +- error: 'Check constraint failed ''greater'': X > 20' +... +s1:insert({21, 20}) +--- +- error: 'Check constraint failed ''physics'': X < Y' +... +s2:insert({1, 2}) +--- +- error: 'Check constraint failed ''greater'': X > 20' +... +s2:insert({21, 22}) +--- +- error: 'Check constraint failed ''physics'': X > Y' +... +s2.ck_constraint.greater:drop() +--- +... +s2.ck_constraint.physics ~= nil +--- +- true +... +s2.ck_constraint.greater == nil +--- +- true +... +s1:insert({2, 1}) +--- +- error: 'Check constraint failed ''greater'': X > 20' +... +s2:insert({1, 2}) +--- +- error: 'Check constraint failed ''physics'': X > Y' +... +s1:drop() +--- +... +s2:drop() +--- +... test_run:cmd("clear filter") --- - true diff --git a/test/sql/checks.test.lua b/test/sql/checks.test.lua index 2a7510aa9..6f17d0612 100644 --- a/test/sql/checks.test.lua +++ b/test/sql/checks.test.lua @@ -154,4 +154,33 @@ s:insert({1, 3.14}) s:insert({1, 666}) s:drop() +-- +-- Test ck constraints LUA integration. +-- +s1 = box.schema.create_space('test1') +_ = s1:create_index('pk') +s1:format({{name='X', type='any'}, {name='Y', type='integer'}}) +s2 = box.schema.create_space('test2') +_ = s2:create_index('pk') +s2:format({{name='X', type='any'}, {name='Y', type='integer'}}) +_ = s1:create_check_constraint('physics', 'X < Y') +_ = s1:create_check_constraint('greater', 'X > 20') +_ = s2:create_check_constraint('physics', 'X > Y') +_ = s2:create_check_constraint('greater', 'X > 20') +s1.ck_constraint.physics ~= nil +s1.ck_constraint.greater ~= nil +s2.ck_constraint.physics ~= nil +s2.ck_constraint.greater ~= nil +s1:insert({2, 1}) +s1:insert({21, 20}) +s2:insert({1, 2}) +s2:insert({21, 22}) +s2.ck_constraint.greater:drop() +s2.ck_constraint.physics ~= nil +s2.ck_constraint.greater == nil +s1:insert({2, 1}) +s2:insert({1, 2}) +s1:drop() +s2:drop() + test_run:cmd("clear filter") -- 2.21.0 ^ permalink raw reply [flat|nested] 25+ messages in thread
* [tarantool-patches] [PATCH v3 0/3] box: run checks on insertions in LUA spaces @ 2019-05-14 15:02 Kirill Shcherbatov 2019-05-14 15:02 ` [tarantool-patches] [PATCH v3 1/3] schema: add new system space for CHECK constraints Kirill Shcherbatov 0 siblings, 1 reply; 25+ messages in thread From: Kirill Shcherbatov @ 2019-05-14 15:02 UTC (permalink / raw) To: tarantool-patches, v.shpilevoy; +Cc: Kirill Shcherbatov @v.shpilevoy > Yes, I will. Kirill, please, send it again in a new thread. You can keep > version 3 and omit change list. @kostya > It's better to fetch the bound field upon first access. > Most paths of the CHECK constraint may not touch most of the > fields. I have no idea, how, to fit it in our architecture. OP_Column has no intersections with binding machinery. Fire CK constraints for LUA spaces. To achieve this goal, we reworked data dictionary, to store ck constraints in separate space _ck_constraints and updated data migration script to migrate existent data there. This also would be useful in future to implement ALTER SPACE ADD CONSTRAINT operation. Now we do not support CK constraint creation on non-empty space. Each CK has own precompiled VDBE machine that performs this check with tuple fields mapped to it's memory with sql_bind() api. In case of ck constraint conflict detected by this VM we abort the transaction and return error to user. Finally, we introduced a LUA-wrapper that provide a user-friendly way to manage space ck constraints. Changes in version 2: - some path parts has been already merged in master - dropped commits "box: fix _trigger and _ck_constraint access check" and "sql: disallow use of TYPEOF in Check" because they are not required and simply wrong - code rebased on actual master - reworked ck_constrain_def and ck_constraint structures and methods to provide a little more consistent API - reworked structures are used on SQL parse - new user-friendly API - use on_replace trigger instead before_replace trigger to deal with already-validated tuple - many minor review fixes - many amazing new test v2: https://www.freelists.org/post/tarantool-patches/PATCH-v2-09-sql-Checks-on-server-side v1: https://www.freelists.org/post/tarantool-patches/PATCH-v1-04-sql-Checks-on-server-side Branch: http://github.com/tarantool/tarantool/tree/kshch/gh-3691-checks-on-server-side Issue: https://github.com/tarantool/tarantool/issues/3691 Kirill Shcherbatov (3): schema: add new system space for CHECK constraints box: run check constraint tests on space alter box: user-friendly interface to manage ck constraints src/box/CMakeLists.txt | 1 + src/box/alter.cc | 305 +++++++++++++++- src/box/alter.h | 1 + src/box/bootstrap.snap | Bin 4374 -> 4418 bytes src/box/ck_constraint.c | 267 ++++++++++++++ src/box/ck_constraint.h | 169 +++++++++ src/box/errcode.h | 2 + src/box/lua/schema.lua | 38 +- src/box/lua/space.cc | 65 ++++ src/box/lua/upgrade.lua | 43 +++ src/box/schema.cc | 8 + src/box/schema_def.h | 10 + src/box/space.c | 2 + src/box/space.h | 5 + src/box/space_def.c | 98 +----- src/box/space_def.h | 2 - src/box/sql.c | 86 +---- src/box/sql.h | 36 -- src/box/sql/build.c | 208 +++++++++-- src/box/sql/insert.c | 113 ++---- src/box/sql/parse.y | 2 +- src/box/sql/parse_def.h | 24 ++ src/box/sql/select.c | 11 +- src/box/sql/sqlInt.h | 28 +- src/box/sql/tokenize.c | 1 - src/box/sql/vdbeapi.c | 8 - test/app-tap/tarantoolctl.test.lua | 4 +- test/box-py/bootstrap.result | 7 +- test/box/access.result | 3 + test/box/access.test.lua | 1 + test/box/access_misc.result | 3 + test/box/access_sysview.result | 6 +- test/box/alter.result | 6 +- test/box/misc.result | 2 + test/sql-tap/check.test.lua | 42 +-- test/sql-tap/fkey2.test.lua | 4 +- test/sql-tap/sql-errors.test.lua | 2 +- test/sql-tap/table.test.lua | 12 +- test/sql/checks.result | 483 +++++++++++++++++++++++--- test/sql/checks.test.lua | 182 ++++++++-- test/sql/errinj.result | 140 ++++++++ test/sql/errinj.test.lua | 45 +++ test/sql/gh-2981-check-autoinc.result | 12 +- test/sql/types.result | 3 +- test/sql/upgrade.result | 19 + test/sql/upgrade.test.lua | 5 + test/wal_off/alter.result | 2 +- 47 files changed, 2033 insertions(+), 483 deletions(-) create mode 100644 src/box/ck_constraint.c create mode 100644 src/box/ck_constraint.h -- 2.21.0 ^ permalink raw reply [flat|nested] 25+ messages in thread
* [tarantool-patches] [PATCH v3 1/3] schema: add new system space for CHECK constraints 2019-05-14 15:02 [tarantool-patches] [PATCH v3 0/3] box: run checks on insertions in LUA spaces Kirill Shcherbatov @ 2019-05-14 15:02 ` Kirill Shcherbatov 0 siblings, 0 replies; 25+ messages in thread From: Kirill Shcherbatov @ 2019-05-14 15:02 UTC (permalink / raw) To: tarantool-patches, v.shpilevoy; +Cc: Kirill Shcherbatov This patch introduces a new system space to persist check constraints. Format of the space: _ck_constraint (space id = 357) [<constraint name> STR, <space id> UINT, <is_deferred>BOOL, <expression string>STR] CK constraint is local to space, so every pair <CK name, space id> is unique (and it is PK in _ck_constraint space). After insertion into this space, a new instance describing check constraint is created. Check constraint holds AST of expression representing it. While space features check constraints, it isn't allowed to be dropped. The :drop() space method firstly deletes all check constraints and then removes entry from _space. Because space alter, index alter and space truncate operations cause space recreation, introduced RebuildCkConstrains object that compiles new ck constraint objects, replaces and removes existent instances atomically(when some compilation fails, nothing is changed). In fact, in scope of this patch we don't really need to recreate ck_constraint object in such situations (patch space_def pointer in AST like we did it before is enough now, but we are going to recompile VDBE that represents ck constraint in future that can fail). The main motivation for these changes is the ability to support ADD CHECK CONSTRAINT operation in the future. CK constraints are are easier to manage as self-sustained objects: such change is managed with atomic insertion(unlike the current architecture). Disabled xfer optimization when some space have ck constraints because in the following patches this xfer optimisation becomes impossible. No reason to rewrite this code. Needed for #3691 --- src/box/CMakeLists.txt | 1 + src/box/alter.cc | 251 ++++++++++++++++++++++++-- src/box/alter.h | 1 + src/box/bootstrap.snap | Bin 4374 -> 4418 bytes src/box/ck_constraint.c | 114 ++++++++++++ src/box/ck_constraint.h | 163 +++++++++++++++++ src/box/errcode.h | 1 + src/box/lua/schema.lua | 4 + src/box/lua/space.cc | 2 + src/box/lua/upgrade.lua | 43 +++++ src/box/schema.cc | 8 + src/box/schema_def.h | 10 + src/box/space.c | 2 + src/box/space.h | 5 + src/box/space_def.c | 98 +--------- src/box/space_def.h | 2 - src/box/sql.c | 86 +-------- src/box/sql.h | 36 ---- src/box/sql/build.c | 208 +++++++++++++++++---- src/box/sql/insert.c | 53 +++--- src/box/sql/parse.y | 2 +- src/box/sql/parse_def.h | 24 +++ src/box/sql/select.c | 11 +- src/box/sql/sqlInt.h | 2 +- src/box/sql/tokenize.c | 1 - test/app-tap/tarantoolctl.test.lua | 4 +- test/box-py/bootstrap.result | 7 +- test/box/access.result | 3 + test/box/access.test.lua | 1 + test/box/access_misc.result | 3 + test/box/access_sysview.result | 6 +- test/box/alter.result | 6 +- test/box/misc.result | 1 + test/sql-tap/check.test.lua | 32 ++-- test/sql-tap/fkey2.test.lua | 4 +- test/sql-tap/sql-errors.test.lua | 2 +- test/sql-tap/table.test.lua | 4 +- test/sql/checks.result | 210 ++++++++++++++++----- test/sql/checks.test.lua | 96 ++++++---- test/sql/errinj.result | 134 ++++++++++++++ test/sql/errinj.test.lua | 45 +++++ test/sql/gh-2981-check-autoinc.result | 8 +- test/sql/types.result | 2 +- test/sql/upgrade.result | 19 ++ test/sql/upgrade.test.lua | 5 + test/wal_off/alter.result | 2 +- 46 files changed, 1304 insertions(+), 418 deletions(-) create mode 100644 src/box/ck_constraint.c create mode 100644 src/box/ck_constraint.h diff --git a/src/box/CMakeLists.txt b/src/box/CMakeLists.txt index 0864c3433..481842a39 100644 --- a/src/box/CMakeLists.txt +++ b/src/box/CMakeLists.txt @@ -97,6 +97,7 @@ add_library(box STATIC space.c space_def.c sequence.c + ck_constraint.c fk_constraint.c func.c func_def.c diff --git a/src/box/alter.cc b/src/box/alter.cc index 9279426d2..2126ab369 100644 --- a/src/box/alter.cc +++ b/src/box/alter.cc @@ -29,6 +29,7 @@ * SUCH DAMAGE. */ #include "alter.h" +#include "ck_constraint.h" #include "column_mask.h" #include "schema.h" #include "user.h" @@ -529,17 +530,6 @@ space_def_new_from_tuple(struct tuple *tuple, uint32_t errcode, engine_name, engine_name_len, &opts, fields, field_count); auto def_guard = make_scoped_guard([=] { space_def_delete(def); }); - if (def->opts.checks != NULL && - sql_checks_resolve_space_def_reference(def->opts.checks, - def) != 0) { - box_error_t *err = box_error_last(); - if (box_error_code(err) != ENOMEM) { - tnt_raise(ClientError, errcode, def->name, - box_error_message(err)); - } else { - diag_raise(); - } - } struct engine *engine = engine_find_xc(def->engine_name); engine_check_space_def_xc(engine, def); def_guard.is_active = false; @@ -1380,6 +1370,71 @@ UpdateSchemaVersion::alter(struct alter_space *alter) ++schema_version; } +/** + * As ck_constraint object depends on space_def we must rebuild + * all ck constraints on space alter. + * + * To perform it transactionally, we create a list of a new ck + * constraints objects in ::prepare method that is fault-tolerant. + * Finally in ::alter or ::rollback methods we only swap thouse + * lists securely. + */ +class RebuildCkConstraints: public AlterSpaceOp +{ +public: + RebuildCkConstraints(struct alter_space *alter) : AlterSpaceOp(alter), + ck_constraint(RLIST_HEAD_INITIALIZER(ck_constraint)) {} + struct rlist ck_constraint; + virtual void prepare(struct alter_space *alter); + virtual void alter(struct alter_space *alter); + virtual void rollback(struct alter_space *alter); + virtual ~RebuildCkConstraints(); +}; + +void +RebuildCkConstraints::prepare(struct alter_space *alter) +{ + struct ck_constraint *old_ck_constraint; + rlist_foreach_entry(old_ck_constraint, &alter->old_space->ck_constraint, + link) { + struct ck_constraint *new_ck_constraint = + ck_constraint_new(old_ck_constraint->def, + alter->new_space->def); + if (new_ck_constraint == NULL) + diag_raise(); + rlist_add_entry(&ck_constraint, new_ck_constraint, link); + } +} + +void +RebuildCkConstraints::alter(struct alter_space *alter) +{ + rlist_swap(&alter->new_space->ck_constraint, &ck_constraint); + rlist_swap(&ck_constraint, &alter->old_space->ck_constraint); +} + +void +RebuildCkConstraints::rollback(struct alter_space *alter) +{ + rlist_swap(&alter->old_space->ck_constraint, &ck_constraint); + rlist_swap(&ck_constraint, &alter->new_space->ck_constraint); +} + +RebuildCkConstraints::~RebuildCkConstraints() +{ + struct ck_constraint *old_ck_constraint, *tmp; + rlist_foreach_entry_safe(old_ck_constraint, &ck_constraint, link, tmp) { + /** + * Ck constraint definition is now managed by + * other Ck constraint object. Prevent it's + * destruction as a part of ck_constraint_delete + * call. + */ + old_ck_constraint->def = NULL; + ck_constraint_delete(old_ck_constraint); + } +} + /* }}} */ /** @@ -1745,6 +1800,11 @@ on_replace_dd_space(struct trigger * /* trigger */, void *event) space_name(old_space), "the space has foreign key constraints"); } + if (!rlist_empty(&old_space->ck_constraint)) { + tnt_raise(ClientError, ER_DROP_SPACE, + space_name(old_space), + "the space has check constraints"); + } /** * The space must be deleted from the space * cache right away to achieve linearisable @@ -1842,6 +1902,7 @@ on_replace_dd_space(struct trigger * /* trigger */, void *event) def->field_count); (void) new CheckSpaceFormat(alter); (void) new ModifySpace(alter, def); + (void) new RebuildCkConstraints(alter); def_guard.is_active = false; /* Create MoveIndex ops for all space indexes. */ alter_space_move_indexes(alter, 0, old_space->index_id_max + 1); @@ -2085,6 +2146,7 @@ on_replace_dd_index(struct trigger * /* trigger */, void *event) */ alter_space_move_indexes(alter, iid + 1, old_space->index_id_max + 1); /* Add an op to update schema_version on commit. */ + (void) new RebuildCkConstraints(alter); (void) new UpdateSchemaVersion(alter); alter_space_do(txn, alter); scoped_guard.is_active = false; @@ -2152,6 +2214,7 @@ on_replace_dd_truncate(struct trigger * /* trigger */, void *event) (void) new TruncateIndex(alter, old_index->def->iid); } + (void) new RebuildCkConstraints(alter); alter_space_do(txn, alter); scoped_guard.is_active = false; } @@ -4035,6 +4098,168 @@ on_replace_dd_fk_constraint(struct trigger * /* trigger*/, void *event) } } +/** Create an instance of check constraint definition by tuple. */ +static struct ck_constraint_def * +ck_constraint_def_new_from_tuple(struct tuple *tuple) +{ + uint32_t name_len; + const char *name = + tuple_field_str_xc(tuple, BOX_CK_CONSTRAINT_FIELD_NAME, + &name_len); + if (name_len > BOX_NAME_MAX) { + tnt_raise(ClientError, ER_CREATE_CK_CONSTRAINT, + tt_cstr(name, BOX_INVALID_NAME_MAX), + "check constraint name is too long"); + } + identifier_check_xc(name, name_len); + uint32_t expr_str_len; + const char *expr_str = + tuple_field_str_xc(tuple, BOX_CK_CONSTRAINT_FIELD_EXPR_STR, + &expr_str_len); + + uint32_t name_offset, expr_str_offset; + uint32_t ck_def_sz = + ck_constraint_def_sizeof(name_len, expr_str_len, &name_offset, + &expr_str_offset); + struct ck_constraint_def *ck_def = + (struct ck_constraint_def *)malloc(ck_def_sz); + if (ck_def == NULL) + tnt_raise(OutOfMemory, ck_def_sz, "malloc", "ck_def"); + + ck_def->name = (char *)ck_def + name_offset; + ck_def->expr_str = (char *)ck_def + expr_str_offset; + memcpy(ck_def->expr_str, expr_str, expr_str_len); + ck_def->expr_str[expr_str_len] = '\0'; + memcpy(ck_def->name, name, name_len); + ck_def->name[name_len] = '\0'; + + return ck_def; +} + +/** Trigger invoked on rollback in the _ck_constraint space. */ +static void +on_replace_ck_constraint_rollback(struct trigger *trigger, void *event) +{ + struct txn_stmt *stmt = txn_last_stmt((struct txn *) event); + struct ck_constraint *ck = (struct ck_constraint *)trigger->data; + struct space *space = NULL; + if (ck != NULL) + space = space_by_id(ck->space_id); + if (stmt->old_tuple != NULL && stmt->new_tuple == NULL) { + /* Rollback DELETE check constraint. */ + assert(ck != NULL); + assert(space != NULL); + assert(space_ck_constraint_by_name(space, + ck->def->name, strlen(ck->def->name)) == NULL); + rlist_add_entry(&space->ck_constraint, ck, link); + } else if (stmt->new_tuple != NULL && stmt->old_tuple == NULL) { + /* Rollback INSERT check constraint. */ + assert(space != NULL); + assert(space_ck_constraint_by_name(space, + ck->def->name, strlen(ck->def->name)) != NULL); + rlist_del_entry(ck, link); + ck_constraint_delete(ck); + } else { + /* Rollback REPLACE check constraint. */ + assert(space != NULL); + const char *name = ck->def->name; + struct ck_constraint *new_ck = + space_ck_constraint_by_name(space, name, strlen(name)); + assert(new_ck != NULL); + rlist_del_entry(new_ck, link); + rlist_add_entry(&space->ck_constraint, ck, link); + ck_constraint_delete(new_ck); + } +} + +/** + * Trigger invoked on commit in the _ck_constraint space. + * Drop useless old check constraint object if exists. + */ +static void +on_replace_ck_constraint_commit(struct trigger *trigger, void *event) +{ + struct txn_stmt *stmt = txn_last_stmt((struct txn *) event); + struct ck_constraint *ck = (struct ck_constraint *)trigger->data; + if (stmt->old_tuple != NULL) + ck_constraint_delete(ck); +} + +/** A trigger invoked on replace in the _ck_constraint space. */ +static void +on_replace_dd_ck_constraint(struct trigger * /* trigger*/, void *event) +{ + struct txn *txn = (struct txn *) event; + txn_check_singlestatement_xc(txn, "Space _ck_constraint"); + struct txn_stmt *stmt = txn_current_stmt(txn); + struct tuple *old_tuple = stmt->old_tuple; + struct tuple *new_tuple = stmt->new_tuple; + uint32_t space_id = + tuple_field_u32_xc(old_tuple != NULL ? old_tuple : new_tuple, + BOX_CK_CONSTRAINT_FIELD_SPACE_ID); + struct space *space = space_cache_find_xc(space_id); + struct trigger *on_rollback = + txn_alter_trigger_new(on_replace_ck_constraint_rollback, NULL); + struct trigger *on_commit = + txn_alter_trigger_new(on_replace_ck_constraint_commit, NULL); + + if (new_tuple != NULL) { + bool is_deferred = + tuple_field_bool_xc(new_tuple, + BOX_CK_CONSTRAINT_FIELD_DEFERRED); + if (is_deferred) { + tnt_raise(ClientError, ER_UNSUPPORTED, "Tarantool", + "deferred ck constraints"); + } + /* Create or replace check constraint. */ + struct ck_constraint_def *ck_def = + ck_constraint_def_new_from_tuple(new_tuple); + auto ck_guard = make_scoped_guard([=] { free(ck_def); }); + /* + * FIXME: Ck constraint creation on non-empty + * space must be implemented as preparatory + * step for ALTER SPACE ADD CONSTRAINT feature. + */ + struct index *pk = space_index(space, 0); + if (pk != NULL && index_size(pk) > 0) { + tnt_raise(ClientError, ER_CREATE_CK_CONSTRAINT, + ck_def->name, + "referencing space must be empty"); + } + struct ck_constraint *new_ck_constraint = + ck_constraint_new(ck_def, space->def); + if (new_ck_constraint == NULL) + diag_raise(); + ck_guard.is_active = false; + const char *name = new_ck_constraint->def->name; + struct ck_constraint *old_ck_constraint = + space_ck_constraint_by_name(space, name, strlen(name)); + if (old_ck_constraint != NULL) + rlist_del_entry(old_ck_constraint, link); + rlist_add_entry(&space->ck_constraint, new_ck_constraint, link); + on_commit->data = old_tuple == NULL ? new_ck_constraint : + old_ck_constraint; + on_rollback->data = on_commit->data; + } else { + assert(new_tuple == NULL && old_tuple != NULL); + /* Drop check constraint. */ + uint32_t name_len; + const char *name = + tuple_field_str_xc(old_tuple, + BOX_CK_CONSTRAINT_FIELD_NAME, + &name_len); + struct ck_constraint *old_ck_constraint = + space_ck_constraint_by_name(space, name, name_len); + assert(old_ck_constraint != NULL); + rlist_del_entry(old_ck_constraint, link); + on_commit->data = old_ck_constraint; + on_rollback->data = old_ck_constraint; + } + + txn_on_rollback(txn, on_rollback); + txn_on_commit(txn, on_commit); +} + struct trigger alter_space_on_replace_space = { RLIST_LINK_INITIALIZER, on_replace_dd_space, NULL, NULL }; @@ -4103,4 +4328,8 @@ struct trigger on_replace_fk_constraint = { RLIST_LINK_INITIALIZER, on_replace_dd_fk_constraint, NULL, NULL }; +struct trigger on_replace_ck_constraint = { + RLIST_LINK_INITIALIZER, on_replace_dd_ck_constraint, NULL, NULL +}; + /* vim: set foldmethod=marker */ diff --git a/src/box/alter.h b/src/box/alter.h index 4108fa47c..b9ba7b846 100644 --- a/src/box/alter.h +++ b/src/box/alter.h @@ -46,6 +46,7 @@ extern struct trigger on_replace_sequence_data; extern struct trigger on_replace_space_sequence; extern struct trigger on_replace_trigger; extern struct trigger on_replace_fk_constraint; +extern struct trigger on_replace_ck_constraint; extern struct trigger on_stmt_begin_space; extern struct trigger on_stmt_begin_index; extern struct trigger on_stmt_begin_truncate; diff --git a/src/box/bootstrap.snap b/src/box/bootstrap.snap index 871a93f9856f97636ad55b7f5260e5c6957fef36..d988e27a189a1ec691d652f06e96d5818677d268 100644 GIT binary patch literal 4418 zcmV-I5xwqHPC-x#FfK7O3RY!ub7^mGIv_GGGA=MJGBYqOXE8Q7G%_${GBFBCZgX^D zZewLSAU82NGBP)1WG!VfHZUzTWic@=Vq`UBEio`<GB`J3WH>i8FbY;fY;R+0Iv{&} z3JTS_3%bn^#{kX^F9>6$0000004TLD{Qyw)g#gNwG$BS1h*(nq000000B|q>438<) zPv^nlVgt;F;Hr2(2ofPF)034cNy(Ur$Ru(~-0(bV`C0T--gU%$)<l%fZ7tdT9+KNl zI}-u~0`vhNgb-yhF3RGa2OwZv<^zn$cz|)*L7B700hzP5K?i5;tc~GdHb=|`vN;-z z$u(X1xHu<_7*PbHfhcAWMocjPL+-w0AlqGl0hvYI&dZlsSC<R2=;S4fa&i}!E?L~- zltl};WHF1WmMx-~#R5^pqD2(bD_MHCf~EJSE4`;oR%UoAXrh2Aw7ma~WA)BGhnDy6 z`-`Va1rtzkuLM=@jbMU(mn7I%DT00Xg<$F42bAum0>Qnh0}s%CH={!Poy-B+hdA2r zl4dhJNHQ6o29m@yC5dUmGX-hZC8Ysb7Xwqq4SwgB1<Q9su5oYL)f=1Nce|oUvEc#? z*zhQd4Q*(&fK7oTn=Jt&n+*X4{(uDj4M5<(fCDU;#tg7vT2z1qtt^;AiOsl(iey|* zpkR^Qc7<z*=m~i8Kfutx5CJ-90IdZO(%MPTr3E0sr2#p)v`cFe!0pBa=yqGu<aYaE z@~>pcf0CbnBuY3xl7w?3NH{NYq$~k2QkF!Dl%-9F05B6mB-9(cuO$mIM6^~(|B3cX zfe5j2t20gK6Qs=C#LD0Y2L{9k_DW;pHg|q|7Vb5DdH<|4Z^m5Vf~z;&VAdyAt%?Ep z_`qIi%)B8Jrs)e8E4q61)>p5p7?2O_mF5M5>7Acn6pKyufxXh~sl70NQnu7h#eVAR zrRN6?3QnB#<Uzus=?NgP7n)z#&V?7YZ_$PAwrjz~%uW_t$n0RD1^RQsTk<Vl{(b1% zxgv{yE3o*l;)+kHuwuqFMHMnGDX75e*2~AAD~c)jLm>r!JyG!I--80`d3vD!i5}EL zy%W6uCZ70b!ioPSn)p|OiT@;)_!J2xW_ONALUzXpB)}Yg91iC2`|{r@;)wi27?FR7 zBJ$@Kf{6P;3~@h?5clr`5rlm_dF<Z-!oK}3Je%9Ovw5Aa%{QlGhcthvLz+I&Ax+Zc z`oWq#_+ZUBT-LOvoF4ez&EY$h!#Ce?cIDnm%MB+^xy!A!l?7+N#?s=w#{Ev0rE^$X zU+J{Q<!IXKil%wM(KJoJvFbbU(7O(Ade318j)M-I-;e|6HsC<Ixq&2Wuz@7oxEaf; z8mQWg<yf+*LeVu;=oA|iRkNmd8m7`2)R0!G3^Zsmh8Z*ogAAI~<O?yl0E2AT!VA2S z#Es;R1s7&3w7?rv6<FYnX$mXMP~fQ`OOpa`kGLHg6nL{UX?UX<9va?w!WrH;rY4x- z>9K?p^r%M?Od!V*N+8D@3FJtQwh_ir>NbvPlx?&cQ&h@6ol*9wqxQ)@{rQ>q&rtvQ zv&n0DL)Ef2Yq^%oZNsFt;jy$~8-5~BoW~R6rxTyJmN6DF#<zxX8j2W$7h_t8Qw!cJ z0`1<<-W!4lvmFSZY!^jP+@K$vN<`Fc`0eVGC)!?eZyE{OE(R@_>)iIwNm-ps9KW^O zb;dGp|0_58o>2EctvWOmbu;?5$3j1>DnvlHCEud1W=S#E^|`?>BySeCbgt>EYxC)9 z&;X__ao^2-Wx-AAOk3wyN9~+dmEO5qlHA}2rveEbaY>pb$y0xyx9&+=G-uNmm&SHS zTuo=*Ux#MO%uA}5x6Ik{V(>q#`cr$BbDO)QdG?%s5*J=w%dgL@Yufr%n7<2J^RsSg z^%a^mHO!xRZeGhe^Sb25BWOvQ*6o+P;TK-cuXw|cwCRj~b!x4>HnAW9fSfc5;KVm3 zKuenZFuCLA{go`4(Mj?nqk}|Aw&`rrIg(`7Wdw<=tH_a^ENLdA)w%#MGOI(Rh-^sh z)gHddv6xCN)4n=Fh~Qh0A@~MF2)^N)Pmmd`_G(X7YtP4Kd(~d;&GzuBVnDSQ6_x(+ zs=cVxk3F+pAAiVpeE4Cba5ieUM;{y2gO82ovByR?iiaK<y(5o|+JQ$#Gg`+T{G`JU ze$G(`Klv#K9p@Rx9Ons#9B2AQ95XIA;E-{(@dod-s-0YHxWW4xZSbxJ8@%(L#u~k& zp+@g#q|tk71{%GWaYpYc8D?0eW0Zlr7-Zlc#u&Kc4u%-Le-TFSUVzbi+C@-!g<|qC zVNQEVCzVHJY$STBLKy~BdpQ`c`V_NZl`5uGdwcfm(No2s*L{A`tk0|q1oo;_F{O}S z^ZZ<e0)m+|FW;&@v`cP&1!j_Vnz{<3iXqjW&ShwGwTe)sCSud}WK$O{U=%QAZj|jy zM>4xaT@<5vk}Q5Hrp2Jv8Giq~>VMB#uD+;ZNMF+!FSxozou}~o=aeZ@oAyJkR57Cv zE(rI78hS7lmdUDQ#B|`qLEW;!mNAt{43h>+VV1xwWiX|KDPizZ`r<RSM(KhiFG*dJ zoRGL6Z9%f&NXt<cHAh&Ct{7QSW7HI5nu=9WM8Q;16`~1<Bp`|azr|0MhLnaN7<xd6 zijV_BGZ-vLR;Sb$QG=3#kZJ{L0BFi<O3e}Xgv$EVow=9ytJhEMRmF;=@cy{|A2+3L z78~%})yu#Zxj{HKG|x|OExolc*lqcFOJ8nHt&q$ZbklXVyuaSyH;Uh`hEc-XEx-EK zzRz*C*2t-17RXKAZb5zW{%KXGqcyc!=Kag6Ta3=Dss1Mkep!`C0W@=SDS_H0Gn%=n zWl0OZPV4L7eWLBHdw18C*Rasc%`aD#5TN4s&!huzKP?r%e}1`&WB?mbpCbC{>QMn_ zzt0BLr}*WnQLGC6uxhM`TQiq&RZ=(9`P0>gQmFI8rxrpZBLDyZ5C{NM8%PjXW9E_! z699lgf`Rd}F+3Ivgct_wddxC400SUEL4qOxrGO?~oX?1{E7~y9AkrYxAkrY}n5mlj zJ4{A_;gSz()RgMq*wRMlD1+4a-FsE2XA|h+W>$Mjpdrtd8eUwU92JWV)+|H8(&Ka; z$LIKHpTa01J<EX@P^^!t!A(%?7ST7Mph0<LT2hk+{ZNO-w-SY}rau^B*dK8}qv^{a zRS^&X-Cos_v>JrgzbsT?XIH;Lr+wZ2if_ccD5A3b+3#Y8Sm`jQeotb56c#AV$^+SN ztz2aAZP6AvDRzXY%i9olg`mpO5O+{0UR@%ok~m)$%9|b~Vn{0_4(_E)>Fk8C{6opI z3cpRy3VTxMjzYLmNGXi4@uZ*iI~HzNh`PKT@mHw2ydABB(~>;-(%aRyCqlKTwEme+ zcqt#CS0=ILpdlchdi^LrfCNqSq*-&*OfM_yzCrGvm^kCvKUl7Um1XVpdcWXpa4SCU zMbAQXai&^+G~t!u@sa1O;)nR^#xLb^Z@DhQk87c2555FPxaJ}G>Adey)<z8Af>tc& zs1g?otp{X7lN??|=uUeWvy4YqsF2D43}O1;kW~Ah9AcZd4G?4h2-z5-i);T&8_O`g zN!}fER@Fl^!*b}L-{b1}nsy?WbfbP&!h~-bX~SQ1o9$|dB^LHrw8<xkrNvGJ$<j}S z5BC+>j$o&Dry?;9oj4t@4u!+>(>k-dEmV~9j?KQotu|IZ&#br&#iL?bAB^I3*wI_o zsuHZGV+&d$_>k;3FBKDBpihYnK6Uw!ku>sJ{D*74PB)c*?jqM$Xn<Qr8QexV1x;+} z3(SWbC4pURo$=!?x*e%+nyqVSY2t5@UP082KrJ(v;hf5-K@zMxk_n(}hElW@%j(#? zYMA2Dmhs<h0ZySR?ReuDTs}Zod`3DRjc*VDXbzoANG-at*uUN7EKv#UT%L}I)@zMf zNEADA7wCRzD~I^QQA1@e$>>?&+oc77abtfLWEBa#oSu!KW*qPDUE35Tlg~Y$&8Vr5 zv0kM9v}=6JykV`h>5PKS|EU#sjv;3oDKVN+8gc%Dn%Z|ZC8G=|*!$(_bAW|LU5ent zR98ImOWhXgmgvPJSJXAmQX*+BFnXY6Sv@_<DC3z^=+fxU(H7Y)=IL)pQr$z*$0!|8 zJ!vl1PH<j}o<o_qaU=^h<R7YovWg|K>y$zq!504U;6+pmkex@~kh3=vTcf!C0;C>g zLx$c=Y>ndj3y^x04H<efu{Db8FF@*n91+2#W2FOK4QI&9Fxc1yX3jP)FBUb7Fc&Ns z;$OmsPfDFfgjtR&-QZM7BVyHR>48<jK(kf?fPvsc>AWqNXnmb@GM1K-&LVP$7$IZ) zbs6amQ9bm~s$|N2OEJt?if~*>pEqtzR>h)?#$Tld#Fo9QXRh7@Dn5=kKW34C#6@2I zfIh*NywR@1tCQJU@u8liw@a)_!`9x%)$!0m-Jyoc$kUq-mxv~U9uneV84GJ1by<ap zA$<s<KPalvP%Q<-s`QWpE0Sr|(#eyN+&Y$a3)SI=A~LA%EwIDP`-ak=D;B04y%^F- z78Nr8+5*t|drYH|ypu=hwO6Hvc8%hPTg4fv&oQMKeAU8OjE*x>FUOW<FkDMfF&a)t zu?$nH!Ehx8#Hu(U#dBP#21B(J5Ub*Z6wh&`8VuD^K&*-rQas0%YA{qw0kJAhNbww3 zs=-h#1;naOD1a$*Gp<7%@~sSjE30Z#U=y>HLj^dkd{5i$UqT~PRSG{wzZ_FXdLs+Q zN!mg#b51+ZfYZqLwB7!NEHOq}25^aUDvwBU3`@6i<b^R*j0zqa!xfwn7&hjTX{9H_ z4A-8$a{8YsK}9{#hWxb|*%`(67b@js>yReJya7W?MG$NhG5*oJ9FaxJRVjwX75;P- zVT>3L{NY^RDPEt3llei_gX8g#uk$FMMq<PzaYRxz`#`y}ON0?(G$2AlAi;eh9I}r( ze}h=;<25y^RUz{<0rci-Pw_}Ij5>*=M8nlH0Q;rOP`tfzBzAGiki1NnMB|i7Cc+fs zw@Fk+_@zA|*j^vTe(X^vLL5`^mEc=9?y||s2&1(Z3d`rqSd2O9NQh@Dhf-W?#?dxa zDPffM0$_V`j1YzoHrN$ojVHkl;d}u@zeZN5)F)Qw4-@K!D<Q*>F=714V_AcZ-2a&P zb|@y65o3F3`?IVf1T}Xmlc5{8PD)e4E#(2juHVqh4Q3JJbl!$51iDYA+$CZMXDC~W zkT7bv<!d-AV^#6#CqgcUZdab!<veYpLAoc4zO?ElViY(75ddxD4a;lKj{(OYy9fcV zC@+g+V{0~IDW0yxRWm{6$7b`%IKntTcrg31<_qp~nllF&>fn9A!4>IKOR(+UN9lXr z)L}0UqvFFCk*elgR`5VNX62}lp@=_>c|!vk__arlMDeWhD9X{&DtI(Wb5v%>6P~Cb z;`hd%TXp#iOv&?@!lj3hND+0vATJ?14t6r-k%n_&P-kh5dLToTBbHrbA?3rcneivN zIlhLqKLnuQRbGMx{7q&h|C;fXNoYzKtUdwQ-z&#}g*$4ps6zk`0~Y6$X<=I^$soh^ z&Ybl8=~lvbW6?-|yqiX`&zkk~!XVTdbEk1$I|g$<J+|DwmF5FiE<8^H1@`F&m%~GU zLapfRlVeY2{Yg`eqfA0m!eI3Y!2Y5;oMzl<VGEUh#OB;-VGEUh#OB;-VGEUh#OB<2 z)+Dgn{nE&>AxZ&{ZCFf9sokMwrDo0oPWj7`Lw(_gx&V|!ZC0;}9!E%fh+&=ILWNQv I)ex=i3Y=VD7ytkO literal 4374 zcmV+x5$WzzPC-x#FfK7O3RY!ub7^mGIv_GGGA=MJF)%PKXJlnFH8e6c3Q2BrbYX5| zWjY{aWi&ElFf?W@Fl1yiEi^GRIW0J1HDWC>FgQ3jIXE~qG&V8{RzqxWV{1AfdwmKD z)w&D1%@Cdd&a`e1h@}7k0000ewJ-euP_2XjT9fP`Mi7wLIRFd*3;=q0Nziu>7-QNu z8W+2s#p7E5P7FBorA$dprX(X<N>V0~Q~&gcdL7za?!E)Y<UZ3H?vhm(vp*cYZQ213 z0s#W=0XQg&cOHO%ahVS=CgTCdX$NJ_kpnX4xIqW!c#dN@n8|6{Kqg0nF*&9yUl-+w zX%fXS8i-;BV#E}KFnR|j1KI8Z40IN8yDwm7TwX57VwW#jjFY*zc*)|HE?KmIOBS=3 zYS|)+Su7AmELub{y^`gBD_H(#y7GUzWM$S>0TTsFq3z#aICk^=b7=ejK7YHaR4@Sr z_exOZ-Uue(S4jeXlOo`kUkH@$rIK{-6bSB3AD}|>yV(?)-^m`J`4C6*JEYmH>qsW+ zasa7`38{&xOUluVD~ba$t_Gx#9Q@8N3zqMOTO{YSyE!($?{YzjVnYTRu%QtY8``jl zf!YE_wVDD&wORrTcmx#i{(uC$Hvj<(rm+Jom=YOa!72-;PhzvJAtKq9lP6cY*RE^{ z4fz6`cu#<k_dx}0AOiLtAZTv^fG#b70WR%FfJ?iyCIQ%NOnz*(B~5HLA12-_S>k;X zAMcSU>HSEO-i;vXy~vTW1i(mH5-C!aHXQ=MObC&1bMX2sS&$*3twQpjXulMQkXpAp z({w#S%G}MX41RB5Kzd*=sam)B_xrK%ANTFwW1Vp`<_Z^Fy&(tlKCdcO?5C#(_L4H= zhKv{{FkGzY>fL->y{KY8J+PNlPlauseZT0{n%V<<N#&`%Fn>|D)ZJn}_4UHzgXR{) z4q~VnVZrnS5ZDV%-+nE)nAOQ*3t1g3v_QR_@Rps6w|^h%>s*nA_f}xxeHB;ulnN_m zTT@ga+meEct8c%2-ET!PMfXrh(LJ6hy6?UR#gpgZdGb&6p8S({g6Dk`Pk7IS6W%Y; zg!f7?;e8TI_!J2xW_Io*A+vJ?5?~HJoDSyj`?9xV#GUMmI3jx@jL1IwA&R(O2qLZz zVu<Un2SNzzyMnOZ4iMH`&(+0y9Xq_!;o_b5Id!!8108MpFh`r(-1@<qJ@{ZvIa}7W zW}F^6-^uAbjnjGOaCRlvN=yC*PRUE2rG@o|`+b$g^Bd=PI;@?$+S^LEH!erpRaUgk z1CF+7+fCKqfrtNfaPxl-J9Hd$=>3Kqdba@w-R1_ZYp_9UHf_j~s)i{xWJ#85s8CeR z6na~OqG{CpPs1&pA&ss|WuRf}7-rZS1{t>A`h^%=fI+rv;RPP4aird{;KFQ$7I<W; z0t-AcO<{!@3Op5LQYr8_iQAz;fj2uN#Ust|(D29;&hSVwHNgyTlO>$6$xV`Af+j~O zL6bHTG^xqj2qYP0Bgqu1Mplt28fBeMDC_i5>tvn$>2v=+KY5>hHv5?Q)0nq8<}sJN zhV`ytvaI16cG4%#<cYD<iBCLcj75y`9WhQr5o7RTObc;p!FxrZ-TT>lLl9xM1L2eH zJ_rHI;!d1wdo|}Y4c0COEtvb=exHl7JD0eA$NP7_GVk|RZT3B%uK%*CkPy_(sIU1e z^t-A-1$<j}F6u_B#oXT~2ft9?ENbmM?yF<-=_(KbrY&*)o7~ERT<gr*?{`D-Tou*k z`CF|y_`Rt=L3ggES*_3g`K)zMZP8q9TbxwwlDKi_{=E*(&CDz6m$%H>@?y|`SJkKX zZ0|OAwR!&B_8J%7J!Zd8tZUZ3RhYjETGM0Q(rzm>YHFB2^W=TRI`g`kV-mEQmUaKt zH~hle`xS5a>DtcdN2k`>V-o`!;75Qa0i4*z<Y!3}A10UN{d*-#W^<DG$mSqXl50Dg zbB-jLaT!4(<0^7wCqtUaXd@Q@MrL$~6p;<7z1qXJI2KPCGwq`zgb20;8G>y<gkT%C z^#qy0YOnTWwf1^ywO8%c-YgG~D)v)*P*CX~uiAr3{m3)h_3?*n$A=#l3TL5qd-TCk zJ^0{g9(!<<qj>0%(L3_Us2zA@G^2Ig(N8+;=;s`D^s}FG&~ct|%yFJ@$Z@7`#4+1) z0}k0%8*lK=s`1=n!;SveXrq5M*yz9iG}ibZ4K@BhBaQ!4Gtl_Ij5GdE$uOfT9it5V z#UKO!Fvh?ie=x-O|BEpG_X3Rn(=NWSp9?SS-=Yf}Pk7x+JteG*Rw9v{jB*SQjWtMw z{nTC#cI!SxELfe2CDmS@JbCa`5a@M%zi8ek77YmORi|P}wqNt~x%xx|Fl+O6t=jWl z&ASztHSIKY6*d(+s=b}d(B^6dp~@&?v*tCE7cC%^Fl8=O4a`3>y+B<Qqd_29?4y_# zgIQ<#^{3VM_Z)NeH5EJhfN9JZT;<}<Qu_6$6e&@h_Cu{xu_6y@SmqXz!KxiJanNOh zx@F9krn;oTk{G2hN*O3&aMQt(z`T+!=((v$`hw&IsS6U9qb)}k9BDDiVuU4)(Ul@A zMNDammSPp$ViXWnxe-k;B*9PwLrU0N>?BDD0wF3wQ-U4{ngz*VFd(FQOLIg8Is$T_ z)*mT9QC_z+ckcO=_o+K`Z~s;=pW35}5jCKtIrzqg=J{=&wRsi>zb$>%+P7OXE7Tc- zZT`--f4@2Sk>a<jA(Zfb+wZ>P=QA8FHF0Va1G(G%7Tjn5Usmm6q-Gp*{~uP}Qgm9~ zzCShiVb!Gt(A-TW1;(p0n!A~0wFSK{>-*q!qH(Qz`QNsWS7`3$m#aw$O!ezeBn0q3 z?Nq=1{Brf^fHmMgMfB5EBLmKV{Tgte;+LyMF)H-CsxhK&&0NM&Mcr`cPgjYav_Zc& zb!h0@@Y~gfN2v3|rxrpZBLDyZ5C{Ns8%PjXTV_cH699lgf`QSpF+3Iwq!<S5ddxC4 z00SUEL4YCvWy~f`oXx1Qiw@(OqJ_Cm(ZF0yGu@W{E`|K0L6wJ7)jsH-QJ_W_Q`;x& zF8QjWU}GHOrdE4OU|-KA11Y*3hkE7)G|QILa5uUbPLuHfXTm6XkLv*Y=JosXyOr3u zJdsr7jfTph^!yYNmnJ1Y%F=*+N_|01mmsT>=K$*gEt9%7sAxncQiVIaDvM0s0_Q7L z2*E;8tGH%M4YO-v2hH}|;?R%v2VF4^#P`NPhr#_`+fOC5Qcf2ahGJdL7DtR?ip%5e zF`gtiTW5LR`;QUBn*?XHi!!CdUOMvcnB`7Qc6tuh9;a5zansT$j4<<LeByVl)^s_0 zTycu&^7Odil+Wc!@|gF((iT5)VQHN{+@4elJ+PhPF79CJK|He&C?9MJOewg+0Owex zela=XZE7nH|Hb!0bX2BV4w`V6J?4?sSw)BVTJkSla&Osg!jHqhW!3fsM-=BFm(%%g zJ62^3z_(Vc;j7Ysg)#wTNPP~!x^r(fjLqGn6DOo#B8KpN%aDrwPcdT~nK+2CKHzK& z&BfUMqm5yh-T>Ym)2iw@H$yyh(DR7)BAIr=9lDVoD`9ePi)`bEbQ|r)jinYgR<y|v zaiygWkYq_5rVm3PvR6r`#?DY;oCqsA9vz^=Bcyftv@L`<<(=93W>sw<ex5n>HgqS@ zvOY4c=&&<yLsi9FO@|dwiGU*6pI#~sy+D5_HrT-NA=~NnwHO80bc1ecLEObrU!edu z6lJhIv=lV4%ojMHHcA3MvQ5H|?&x-T-*m;Uo2ZGuQ+isc8%J<vklJ%vre+@4s3sGQ zmH<j4QS{eQc@@)Qmn>DkTY{HD)rRqgj$9T4T|ogl-p6msMraP5OGqucvDm2HEih3D z>=wfp0_q9f)RJP4+!EST`HCKYKt<7PB0{~PH(k2GiL*j>Cs)6~&b4cVZ{&D?Zhcb_ zJU-`fA9b$c%X)$G_^%OG^M-0NPiK@Y{;!shIfgmg<c`q{>6rP8YG&WHkuGJxlHM=R zkOM3<>QW3J#=9bvU+S<>cSH}axkBYKg%Vj$(ZC^J!YXxBMlsBsLYqdBjJAkv-A+qG zg5@5HZbqS<>WOW!c7n57bl<3nn@6-zbMjG5XjMSWlujAs5nGa64_;7P0NC6eg)FaY z{WZk!uMoHD8v=3^18W2%Mu7Ano)973OR15Fdm8|E%Apd@_Io+d)p##2ugONwL3DN+ zcxilZgt;KW5dR7`T*JUVB24sG<_4SdwglD{j!8J5FJNnRB$p$&DHGe`(=XIXN@J-@ z-^U`FiHbf(AN+BLsh7hgo$vSZxAf7+y$F-``P{(r+%ABvH=e{aAWUGt8tmvjKxXH7 z@?&xtMO@_N59kxx%G)Sv7GI`EC_ZkndHad2^j5TcJRc1uvo>ldjA(&D<q~nj;rby9 zVmGM9L$Cx$58??C(!G=#iMY1`aHkw9;cUO1@#|LB99>E~w9s8@7V*_|Z-L!y-Zzwf z4iRwU=p2<!vPj7MYZicR*<%`khHM-`^W0K3-a-X<d|=N=P5$V_;OD+<NPVY~{8|-1 zqe%Wz<J53N016^vjevzP5FYFaDU!cBH3+$G0kEzqWcXdnuOWncg|VvG5P-+nSR)`Y z3Zw_`gox>1NR33^+W@$WD&Qu??6p+L5Km$Nin^+HG&Z?0h<OKcE&YA&GvYI1g?S(` z@^LRUq&~7>o22y|wx5}oKagwd-?N^gV-2u6ajri4Kg}Y9m+HUDw0FkPa6oQm%4F=3 zen;aO^))@OGr;%cg{FUE2x{(OKBN!#d}bi}-c~@pQ~@?AZsi(EB7$W5h;f_Vp@}R~ zu1YaDF7f9V?PO%>;ScArPIdS+TuDr-9~=+wPn|~z8F@!sl29ZUk&h5py3`~g77Zda z1QOgA!yx;p>o<r&K3>&eS`~6h6T)k*w@&IX<0$k&NG!f}8(^6{8R2&xNdgY1e8s|b zNi<HWB}xo2d_#{$L~mV}3yX=t&~I}_5yL5tzfSsa<1YL~L@d7dx?os58R1n9NkaI_ zIo3`NGaiON2#Lb_o)H$y3Hi11EYPmVR9H>$Pj_RE`gLbnZT1O#4h{8+Tv;cFuo3uA zGVvPHZUT>4wu3BUnKI+ekUdK~lUH+%lx?#yukcP1>MahxY)gj5LoixFMkk)N0)%N~ zE-p-dAVAr&5G-T#-`cag8Ot5Re#*GG?>4dLFN=m729Yya7)h%h5u*T+C1OEsyl^RY z9=&k!gVhJ{3ZZ2o!oK=O4aNeNuuO7*e_Ra{TVoQZed}kR*8wQB#FB>4L%p!ia4=$e zYOuO=uXI%bH>&a%2UGEhi%6AqE-P;j9BOma-w;Ih`Yx@O3_ducSwe8ZIn+vQ2?-3= z%|9yMPJ!n;klelKR<4>U<6rW-U47{x7*d#zsvrw0JEj}b^1vXuaN4jv$A2^-Zc)<K zP@MU2;Kleeq#R$<iXQ?{@G39C0{$kmg+I+mNCY}1jz*t=_EuMcmc#XDGvBU41sMlx z&(6N*l}yPX=uUz4e|kXz)Y#m?IJ~=~7!7Fp3|R~51m>qn^BoGAo*o-+n<;~iD;J7| zQLrKBBIVi0?L%RO#>v{Vv)k7d4KXYOEKzvjJjy0iFhW+J&3wO~^YE6AxO(yzw(ydk zxP5vTci75K+-tH!hy7wCv~j)#kA)dcT=i~!v)V9cG5(s%(U4@}&rtx^8Z@h$qsLi+ QJ>-q$w`eZc2h|X*?NOjQ=l}o! diff --git a/src/box/ck_constraint.c b/src/box/ck_constraint.c new file mode 100644 index 000000000..6d9f8ea44 --- /dev/null +++ b/src/box/ck_constraint.c @@ -0,0 +1,114 @@ +/* + * Copyright 2010-2019, Tarantool AUTHORS, please see AUTHORS file. + * + * Redistribution and use in source and binary forms, with or + * without modification, are permitted provided that the following + * conditions are met: + * + * 1. Redistributions of source code must retain the above + * copyright notice, this list of conditions and the + * following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following + * disclaimer in the documentation and/or other materials + * provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY <COPYRIGHT HOLDER> ``AS IS'' AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED + * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL + * <COPYRIGHT HOLDER> OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, + * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR + * BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF + * THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF + * SUCH DAMAGE. + */ +#include "ck_constraint.h" +#include "errcode.h" +#include "sql.h" +#include "sql/sqlInt.h" + +/** + * Resolve space_def references for check constraint via AST + * tree traversal. + * @param ck_constraint Check constraint object to update. + * @param space_def Space definition to use. + * @retval 0 on success. + * @retval -1 on error. + */ +static int +ck_constraint_resolve_field_names(struct Expr *expr, + struct space_def *space_def) +{ + struct Parse parser; + sql_parser_create(&parser, sql_get()); + parser.parse_only = true; + sql_resolve_self_reference(&parser, space_def, NC_IsCheck, expr, NULL); + int rc = parser.is_aborted ? -1 : 0; + sql_parser_destroy(&parser); + return rc; +} + +struct ck_constraint * +ck_constraint_new(struct ck_constraint_def *ck_constraint_def, + struct space_def *space_def) +{ + if (space_def->field_count == 0) { + diag_set(ClientError, ER_UNSUPPORTED, "Tarantool", + "CK constraint for space without format"); + return NULL; + } + struct ck_constraint *ck_constraint = malloc(sizeof(*ck_constraint)); + if (ck_constraint == NULL) { + diag_set(OutOfMemory, sizeof(*ck_constraint), "malloc", + "ck_constraint"); + return NULL; + } + ck_constraint->def = NULL; + ck_constraint->space_id = space_def->id; + rlist_create(&ck_constraint->link); + ck_constraint->expr = + sql_expr_compile(sql_get(), ck_constraint_def->expr_str, + strlen(ck_constraint_def->expr_str)); + if (ck_constraint->expr == NULL || + ck_constraint_resolve_field_names(ck_constraint->expr, + space_def) != 0) { + diag_set(ClientError, ER_CREATE_CK_CONSTRAINT, + ck_constraint_def->name, + box_error_message(box_error_last())); + goto error; + } + + ck_constraint->def = ck_constraint_def; + return ck_constraint; +error: + ck_constraint_delete(ck_constraint); + return NULL; +} + +void +ck_constraint_delete(struct ck_constraint *ck_constraint) +{ + sql_expr_delete(sql_get(), ck_constraint->expr, false); + free(ck_constraint->def); + TRASH(ck_constraint); + free(ck_constraint); +} + +struct ck_constraint * +space_ck_constraint_by_name(struct space *space, const char *name, + uint32_t name_len) +{ + struct ck_constraint *ck_constraint = NULL; + rlist_foreach_entry(ck_constraint, &space->ck_constraint, link) { + if (strlen(ck_constraint->def->name) == name_len && + memcmp(ck_constraint->def->name, name, name_len) == 0) + return ck_constraint; + } + return NULL; +} diff --git a/src/box/ck_constraint.h b/src/box/ck_constraint.h new file mode 100644 index 000000000..615612605 --- /dev/null +++ b/src/box/ck_constraint.h @@ -0,0 +1,163 @@ +#ifndef INCLUDES_BOX_CK_CONSTRAINT_H +#define INCLUDES_BOX_CK_CONSTRAINT_H +/* + * Copyright 2010-2019, Tarantool AUTHORS, please see AUTHORS file. + * + * Redistribution and use in source and binary forms, with or + * without modification, are permitted provided that the following + * conditions are met: + * + * 1. Redistributions of source code must retain the above + * copyright notice, this list of conditions and the + * following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following + * disclaimer in the documentation and/or other materials + * provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY <COPYRIGHT HOLDER> ``AS IS'' AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED + * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL + * <COPYRIGHT HOLDER> OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, + * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR + * BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF + * THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF + * SUCH DAMAGE. + */ + +#include <stdint.h> +#include "small/rlist.h" + +#if defined(__cplusplus) +extern "C" { +#endif + +struct space; +struct space_def; +struct Expr; + +/** + * Check constraint definition. + * See ck_constraint_def_sizeof() definition for implementation + * details and memory layout. + */ +struct ck_constraint_def { + /** + * The 0-terminated string, a name of the check + * constraint. Must be unique for a given space. + */ + char *name; + /** + * The 0-terminated string that defines check constraint + * expression. + * + * For instance: "field1 + field2 > 2 * 3". + */ + char *expr_str; +}; + +/** + * Structure representing check constraint. + * See ck_constraint_new() definition. + */ +struct ck_constraint { + /** The check constraint definition. */ + struct ck_constraint_def *def; + /** + * The check constraint expression AST is built for + * ck_constraint::def::expr_str with sql_expr_compile + * and resolved with sql_resolve_self_reference for + * space with space[ck_constraint::space_id] definition. + */ + struct Expr *expr; + /** + * The id of the space this check constraint is + * built for. + */ + uint32_t space_id; + /** + * Organize check constraint structs into linked list + * with space::ck_constraint. + */ + struct rlist link; +}; + +/** + * Calculate check constraint definition memory size and fields + * offsets for given arguments. + * + * Alongside with struct ck_constraint_def itself, we reserve + * memory for string containing its name and expression string. + * + * Memory layout: + * +-----------------------------+ <- Allocated memory starts here + * | struct ck_constraint_def | + * |-----------------------------| + * | name + \0 | + * |-----------------------------| + * | expr_str + \0 | + * +-----------------------------+ + * + * @param name_len The length of the name. + * @param expr_str_len The length of the expr_str. + * @param[out] name_offset The offset of the name string. + * @param[out] expr_str_offset The offset of the expr_str string. + */ +static inline uint32_t +ck_constraint_def_sizeof(uint32_t name_len, uint32_t expr_str_len, + uint32_t *name_offset, uint32_t *expr_str_offset) +{ + *name_offset = sizeof(struct ck_constraint_def); + *expr_str_offset = *name_offset + name_len + 1; + return *expr_str_offset + expr_str_len + 1; +} + +/** + * Create a new check constraint object by given check constraint + * definition and definition of the space this constraint is + * related to. + * + * @param ck_constraint_def The check constraint definition object + * to use. Expected to be allocated with + * malloc. Ck constraint object manages + * this allocation in case of successful + * creation. + * @param space_def The space definition of the space this check + * constraint must be constructed for. + * @retval not NULL Check constraint object pointer on success. + * @retval NULL Otherwise. +*/ +struct ck_constraint * +ck_constraint_new(struct ck_constraint_def *ck_constraint_def, + struct space_def *space_def); + +/** + * Destroy check constraint memory, release acquired resources. + * @param ck_constraint The check constraint object to destroy. + */ +void +ck_constraint_delete(struct ck_constraint *ck_constraint); + +/** + * Find check constraint object in space by given name and + * name_len. + * @param space The space to lookup check constraint. + * @param name The check constraint name. + * @param name_len The length of the name. + * @retval not NULL Check constrain if exists, NULL otherwise. + */ +struct ck_constraint * +space_ck_constraint_by_name(struct space *space, const char *name, + uint32_t name_len); + +#if defined(__cplusplus) +} /* extern "C" { */ +#endif + +#endif /* INCLUDES_BOX_CK_CONSTRAINT_H */ diff --git a/src/box/errcode.h b/src/box/errcode.h index 9c15f3322..ef5139043 100644 --- a/src/box/errcode.h +++ b/src/box/errcode.h @@ -247,6 +247,7 @@ struct errcode_record { /*192 */_(ER_INDEX_DEF_UNSUPPORTED, "%s are prohibited in an index definition") \ /*193 */_(ER_CK_DEF_UNSUPPORTED, "%s are prohibited in a CHECK constraint definition") \ /*194 */_(ER_MULTIKEY_INDEX_MISMATCH, "Field %s is used as multikey in one index and as single key in another") \ + /*195 */_(ER_CREATE_CK_CONSTRAINT, "Failed to create check constraint '%s': %s") \ /* * !IMPORTANT! Please follow instructions at start of the file diff --git a/src/box/lua/schema.lua b/src/box/lua/schema.lua index f31cf7f2c..e01f500e6 100644 --- a/src/box/lua/schema.lua +++ b/src/box/lua/schema.lua @@ -513,6 +513,7 @@ box.schema.space.drop = function(space_id, space_name, opts) local _truncate = box.space[box.schema.TRUNCATE_ID] local _space_sequence = box.space[box.schema.SPACE_SEQUENCE_ID] local _fk_constraint = box.space[box.schema.FK_CONSTRAINT_ID] + local _ck_constraint = box.space[box.schema.CK_CONSTRAINT_ID] local sequence_tuple = _space_sequence:delete{space_id} if sequence_tuple ~= nil and sequence_tuple[3] == true then -- Delete automatically generated sequence. @@ -524,6 +525,9 @@ box.schema.space.drop = function(space_id, space_name, opts) for _, t in _fk_constraint.index.child_id:pairs({space_id}) do _fk_constraint:delete({t.name, space_id}) end + for _, t in _ck_constraint.index.space_id:pairs({space_id}) do + _ck_constraint:delete({t.name, space_id}) + end local keys = _vindex:select(space_id) for i = #keys, 1, -1 do local v = keys[i] diff --git a/src/box/lua/space.cc b/src/box/lua/space.cc index 100da0a79..ce4287bba 100644 --- a/src/box/lua/space.cc +++ b/src/box/lua/space.cc @@ -537,6 +537,8 @@ box_lua_space_init(struct lua_State *L) lua_setfield(L, -2, "TRIGGER_ID"); lua_pushnumber(L, BOX_FK_CONSTRAINT_ID); lua_setfield(L, -2, "FK_CONSTRAINT_ID"); + lua_pushnumber(L, BOX_CK_CONSTRAINT_ID); + lua_setfield(L, -2, "CK_CONSTRAINT_ID"); lua_pushnumber(L, BOX_TRUNCATE_ID); lua_setfield(L, -2, "TRUNCATE_ID"); lua_pushnumber(L, BOX_SEQUENCE_ID); diff --git a/src/box/lua/upgrade.lua b/src/box/lua/upgrade.lua index 89d6e3d52..bbb55b7e1 100644 --- a/src/box/lua/upgrade.lua +++ b/src/box/lua/upgrade.lua @@ -74,6 +74,7 @@ local function set_system_triggers(val) box.space._schema:run_triggers(val) box.space._cluster:run_triggers(val) box.space._fk_constraint:run_triggers(val) + box.space._ck_constraint:run_triggers(val) end -------------------------------------------------------------------------------- @@ -94,6 +95,7 @@ local function erase() truncate(box.space._schema) truncate(box.space._cluster) truncate(box.space._fk_constraint) + truncate(box.space._ck_constraint) end local function create_sysview(source_id, target_id) @@ -737,6 +739,46 @@ local function upgrade_to_2_1_3() end end +local function upgrade_to_2_2_0() + -- In previous Tarantool releases check constraints were + -- stored in space opts. Now we use separate space + -- _ck_constraint for this purpose. Perform legacy data + -- migration. + local MAP = setmap({}) + local _space = box.space[box.schema.SPACE_ID] + local _index = box.space[box.schema.INDEX_ID] + local _ck_constraint = box.space[box.schema.CK_CONSTRAINT_ID] + log.info("create space _ck_constraint") + local format = {{name='name', type='string'}, + {name='space_id', type='unsigned'}, + {name='is_deferred', type='boolean'}, + {name='expr_str', type='str'}} + _space:insert{_ck_constraint.id, ADMIN, '_ck_constraint', 'memtx', 0, MAP, format} + + log.info("create index primary on _ck_constraint") + _index:insert{_ck_constraint.id, 0, 'primary', 'tree', + {unique = true}, {{0, 'string'}, {1, 'unsigned'}}} + + log.info("create secondary index child_id on _ck_constraint") + _index:insert{_ck_constraint.id, 1, 'space_id', 'tree', + {unique = false}, {{1, 'unsigned'}}} + for _, space in _space:pairs() do + local flags = space.flags + if flags['checks'] ~= nil then + for i, check in pairs(flags['checks']) do + local expr_str = check.expr + local check_name = check.name or + "CK_CONSTRAINT_"..i.."_"..space.name + _ck_constraint:insert({check_name, space.id, false, expr_str}) + end + flags['checks'] = nil + _space:replace(box.tuple.new({space.id, space.owner, space.name, + space.engine, space.field_count, + flags, space.format})) + end + end +end + local function get_version() local version = box.space._schema:get{'version'} if version == nil then @@ -768,6 +810,7 @@ local function upgrade(options) {version = mkversion(2, 1, 1), func = upgrade_to_2_1_1, auto = true}, {version = mkversion(2, 1, 2), func = upgrade_to_2_1_2, auto = true}, {version = mkversion(2, 1, 3), func = upgrade_to_2_1_3, auto = true}, + {version = mkversion(2, 2, 0), func = upgrade_to_2_2_0, auto = true}, } for _, handler in ipairs(handlers) do diff --git a/src/box/schema.cc b/src/box/schema.cc index 9a55c2f14..4b20f0032 100644 --- a/src/box/schema.cc +++ b/src/box/schema.cc @@ -460,6 +460,14 @@ schema_init() sc_space_new(BOX_FK_CONSTRAINT_ID, "_fk_constraint", key_parts, 2, &on_replace_fk_constraint, NULL); + /* _ck_сonstraint - check constraints. */ + key_parts[0].fieldno = 0; /* constraint name */ + key_parts[0].type = FIELD_TYPE_STRING; + key_parts[1].fieldno = 1; /* space id */ + key_parts[1].type = FIELD_TYPE_UNSIGNED; + sc_space_new(BOX_CK_CONSTRAINT_ID, "_ck_constraint", key_parts, 2, + &on_replace_ck_constraint, NULL); + /* * _vinyl_deferred_delete - blackhole that is needed * for writing deferred DELETE statements generated by diff --git a/src/box/schema_def.h b/src/box/schema_def.h index eeeeb950b..cdd3f7f63 100644 --- a/src/box/schema_def.h +++ b/src/box/schema_def.h @@ -108,6 +108,8 @@ enum { BOX_SPACE_SEQUENCE_ID = 340, /** Space id of _fk_constraint. */ BOX_FK_CONSTRAINT_ID = 356, + /** Space id of _ck_contraint. */ + BOX_CK_CONSTRAINT_ID = 357, /** End of the reserved range of system spaces. */ BOX_SYSTEM_ID_MAX = 511, BOX_ID_NIL = 2147483647 @@ -238,6 +240,14 @@ enum { BOX_FK_CONSTRAINT_FIELD_PARENT_COLS = 8, }; +/** _ck_constraint fields. */ +enum { + BOX_CK_CONSTRAINT_FIELD_NAME = 0, + BOX_CK_CONSTRAINT_FIELD_SPACE_ID = 1, + BOX_CK_CONSTRAINT_FIELD_DEFERRED = 2, + BOX_CK_CONSTRAINT_FIELD_EXPR_STR = 3, +}; + /* * Different objects which can be subject to access * control. diff --git a/src/box/space.c b/src/box/space.c index 243e7da2f..a42b3a64b 100644 --- a/src/box/space.c +++ b/src/box/space.c @@ -165,6 +165,7 @@ space_create(struct space *space, struct engine *engine, space_fill_index_map(space); rlist_create(&space->parent_fk_constraint); rlist_create(&space->child_fk_constraint); + rlist_create(&space->ck_constraint); return 0; fail_free_indexes: @@ -225,6 +226,7 @@ space_delete(struct space *space) assert(space->sql_triggers == NULL); assert(rlist_empty(&space->parent_fk_constraint)); assert(rlist_empty(&space->child_fk_constraint)); + assert(rlist_empty(&space->ck_constraint)); space->vtab->destroy(space); } diff --git a/src/box/space.h b/src/box/space.h index 13a220d13..e8529beb3 100644 --- a/src/box/space.h +++ b/src/box/space.h @@ -193,6 +193,11 @@ struct space { * of index id. */ struct index **index; + /** + * List of check constraints linked with + * ck_constraint::link. + */ + struct rlist ck_constraint; /** * Lists of foreign key constraints. In SQL terms child * space is the "from" table i.e. the table that contains diff --git a/src/box/space_def.c b/src/box/space_def.c index d825def75..661131295 100644 --- a/src/box/space_def.c +++ b/src/box/space_def.c @@ -36,28 +36,12 @@ #include "msgpuck.h" #include "tt_static.h" -/** - * Make checks from msgpack. - * @param str pointer to array of maps - * e.g. [{"expr": "x < y", "name": "ONE"}, ..]. - * @param len array items count. - * @param[out] opt pointer to store parsing result. - * @param errcode Code of error to set if something is wrong. - * @param field_no Field number of an option in a parent element. - * @retval 0 on success. - * @retval not 0 on error. Also set diag message. - */ -static int -checks_array_decode(const char **str, uint32_t len, char *opt, uint32_t errcode, - uint32_t field_no); - const struct space_opts space_opts_default = { /* .group_id = */ 0, /* .is_temporary = */ false, /* .is_ephemeral = */ false, /* .view = */ false, /* .sql = */ NULL, - /* .checks = */ NULL, }; const struct opt_def space_opts_reg[] = { @@ -65,8 +49,7 @@ const struct opt_def space_opts_reg[] = { OPT_DEF("temporary", OPT_BOOL, struct space_opts, is_temporary), OPT_DEF("view", OPT_BOOL, struct space_opts, is_view), OPT_DEF("sql", OPT_STRPTR, struct space_opts, sql), - OPT_DEF_ARRAY("checks", struct space_opts, checks, - checks_array_decode), + OPT_DEF_LEGACY("checks"), OPT_END, }; @@ -114,16 +97,6 @@ space_def_dup_opts(struct space_def *def, const struct space_opts *opts) return -1; } } - if (opts->checks != NULL) { - def->opts.checks = sql_expr_list_dup(sql_get(), opts->checks, 0); - if (def->opts.checks == NULL) { - free(def->opts.sql); - diag_set(OutOfMemory, 0, "sql_expr_list_dup", - "def->opts.checks"); - return -1; - } - sql_checks_update_space_def_reference(def->opts.checks, def); - } return 0; } @@ -301,74 +274,5 @@ void space_opts_destroy(struct space_opts *opts) { free(opts->sql); - sql_expr_list_delete(sql_get(), opts->checks); TRASH(opts); } - -static int -checks_array_decode(const char **str, uint32_t len, char *opt, uint32_t errcode, - uint32_t field_no) -{ - char *errmsg = tt_static_buf(); - struct ExprList *checks = NULL; - const char **map = str; - struct sql *db = sql_get(); - for (uint32_t i = 0; i < len; i++) { - checks = sql_expr_list_append(db, checks, NULL); - if (checks == NULL) { - diag_set(OutOfMemory, 0, "sql_expr_list_append", - "checks"); - goto error; - } - const char *expr_name = NULL; - const char *expr_str = NULL; - uint32_t expr_name_len = 0; - uint32_t expr_str_len = 0; - uint32_t map_size = mp_decode_map(map); - for (uint32_t j = 0; j < map_size; j++) { - if (mp_typeof(**map) != MP_STR) { - diag_set(ClientError, errcode, field_no, - "key must be a string"); - goto error; - } - uint32_t key_len; - const char *key = mp_decode_str(map, &key_len); - if (mp_typeof(**map) != MP_STR) { - snprintf(errmsg, TT_STATIC_BUF_LEN, - "invalid MsgPack map field '%.*s' type", - key_len, key); - diag_set(ClientError, errcode, field_no, errmsg); - goto error; - } - if (key_len == 4 && memcmp(key, "expr", key_len) == 0) { - expr_str = mp_decode_str(map, &expr_str_len); - } else if (key_len == 4 && - memcmp(key, "name", key_len) == 0) { - expr_name = mp_decode_str(map, &expr_name_len); - } else { - snprintf(errmsg, TT_STATIC_BUF_LEN, - "invalid MsgPack map field '%.*s'", - key_len, key); - diag_set(ClientError, errcode, field_no, errmsg); - goto error; - } - } - if (sql_check_list_item_init(checks, i, expr_name, expr_name_len, - expr_str, expr_str_len) != 0) { - box_error_t *err = box_error_last(); - if (box_error_code(err) != ENOMEM) { - snprintf(errmsg, TT_STATIC_BUF_LEN, - "invalid expression specified (%s)", - box_error_message(err)); - diag_set(ClientError, errcode, field_no, - errmsg); - } - goto error; - } - } - *(struct ExprList **)opt = checks; - return 0; -error: - sql_expr_list_delete(db, checks); - return -1; -} diff --git a/src/box/space_def.h b/src/box/space_def.h index c6639a8d8..71356e2a6 100644 --- a/src/box/space_def.h +++ b/src/box/space_def.h @@ -71,8 +71,6 @@ struct space_opts { bool is_view; /** SQL statement that produced this space. */ char *sql; - /** SQL Checks expressions list. */ - struct ExprList *checks; }; extern const struct space_opts space_opts_default; diff --git a/src/box/sql.c b/src/box/sql.c index fbfa59992..c5123fcc0 100644 --- a/src/box/sql.c +++ b/src/box/sql.c @@ -1035,15 +1035,8 @@ sql_encode_table_opts(struct region *region, struct space_def *def, bool is_error = false; mpstream_init(&stream, region, region_reserve_cb, region_alloc_cb, set_encode_error, &is_error); - int checks_cnt = 0; - struct ExprList_item *a; bool is_view = def->opts.is_view; - struct ExprList *checks = def->opts.checks; - if (checks != NULL) { - checks_cnt = checks->nExpr; - a = checks->a; - } - mpstream_encode_map(&stream, 2 * is_view + (checks_cnt > 0)); + mpstream_encode_map(&stream, 2 * is_view); if (is_view) { assert(def->opts.sql != NULL); @@ -1052,23 +1045,6 @@ sql_encode_table_opts(struct region *region, struct space_def *def, mpstream_encode_str(&stream, "view"); mpstream_encode_bool(&stream, true); } - if (checks_cnt > 0) { - mpstream_encode_str(&stream, "checks"); - mpstream_encode_array(&stream, checks_cnt); - } - for (int i = 0; i < checks_cnt && !is_error; ++i, ++a) { - int items = (a->pExpr != NULL) + (a->zName != NULL); - mpstream_encode_map(&stream, items); - assert(a->pExpr != NULL); - struct Expr *pExpr = a->pExpr; - assert(pExpr->u.zToken != NULL); - mpstream_encode_str(&stream, "expr"); - mpstream_encode_str(&stream, pExpr->u.zToken); - if (a->zName != NULL) { - mpstream_encode_str(&stream, "name"); - mpstream_encode_str(&stream, a->zName); - } - } mpstream_flush(&stream); if (is_error) { diag_set(OutOfMemory, stream.pos - stream.buf, @@ -1302,63 +1278,3 @@ sql_ephemeral_space_new(Parse *parser, const char *name) return space; } - -int -sql_check_list_item_init(struct ExprList *expr_list, int column, - const char *expr_name, uint32_t expr_name_len, - const char *expr_str, uint32_t expr_str_len) -{ - assert(column < expr_list->nExpr); - struct ExprList_item *item = &expr_list->a[column]; - memset(item, 0, sizeof(*item)); - if (expr_name != NULL) { - item->zName = sqlDbStrNDup(db, expr_name, expr_name_len); - if (item->zName == NULL) { - diag_set(OutOfMemory, expr_name_len, "sqlDbStrNDup", - "item->zName"); - return -1; - } - } - if (expr_str != NULL) { - item->pExpr = sql_expr_compile(db, expr_str, expr_str_len); - /* The item->zName would be released later. */ - if (item->pExpr == NULL) - return -1; - } - return 0; -} - -static int -update_space_def_callback(Walker *walker, Expr *expr) -{ - if (expr->op == TK_COLUMN && ExprHasProperty(expr, EP_Resolved)) - expr->space_def = walker->u.space_def; - return WRC_Continue; -} - -void -sql_checks_update_space_def_reference(ExprList *expr_list, - struct space_def *def) -{ - assert(expr_list != NULL); - Walker w; - memset(&w, 0, sizeof(w)); - w.xExprCallback = update_space_def_callback; - w.u.space_def = def; - for (int i = 0; i < expr_list->nExpr; i++) - sqlWalkExpr(&w, expr_list->a[i].pExpr); -} - -int -sql_checks_resolve_space_def_reference(ExprList *expr_list, - struct space_def *def) -{ - Parse parser; - sql_parser_create(&parser, sql_get()); - parser.parse_only = true; - - sql_resolve_self_reference(&parser, def, NC_IsCheck, NULL, expr_list); - int rc = parser.is_aborted ? -1 : 0; - sql_parser_destroy(&parser); - return rc; -} diff --git a/src/box/sql.h b/src/box/sql.h index 262a48bcb..4bb27da8e 100644 --- a/src/box/sql.h +++ b/src/box/sql.h @@ -285,42 +285,6 @@ sql_resolve_self_reference(struct Parse *parser, struct space_def *def, int type, struct Expr *expr, struct ExprList *expr_list); -/** - * Initialize check_list_item. - * @param expr_list ExprList with item. - * @param column index. - * @param expr_name expression name (optional). - * @param expr_name_len expresson name length (optional). - * @param expr_str expression to build string. - * @param expr_str_len expression to build string length. - * @retval 0 on success. - * @retval -1 on error. - */ -int -sql_check_list_item_init(struct ExprList *expr_list, int column, - const char *expr_name, uint32_t expr_name_len, - const char *expr_str, uint32_t expr_str_len); - -/** - * Resolve space_def references checks for expr_list. - * @param expr_list to modify. - * @param def to refer to. - * @retval 0 on success. - * @retval -1 on error. - */ -int -sql_checks_resolve_space_def_reference(struct ExprList *expr_list, - struct space_def *def); - -/** - * Update space_def references for expr_list. - * @param expr_list to modify. - * @param def to refer to. - */ -void -sql_checks_update_space_def_reference(struct ExprList *expr_list, - struct space_def *def); - /** * Initialize a new parser object. * A number of service allocations are performed on the region, diff --git a/src/box/sql/build.c b/src/box/sql/build.c index 6051a2529..3709f6b1d 100644 --- a/src/box/sql/build.c +++ b/src/box/sql/build.c @@ -43,10 +43,12 @@ * COMMIT * ROLLBACK */ +#include <ctype.h> #include "sqlInt.h" #include "vdbeInt.h" #include "tarantoolInt.h" #include "box/box.h" +#include "box/ck_constraint.h" #include "box/fk_constraint.h" #include "box/sequence.h" #include "box/session.h" @@ -638,40 +640,108 @@ primary_key_exit: return; } +/** + * Prepare a 0-terminated string in the wptr memory buffer that + * does not contain a sequence of more than one whatespace + * character. Routine enforces ' ' (space) as whitespace + * delimiter. + * The wptr buffer is expected to have str_len + 1 bytes + * (this is the expected scenario where no extra whitespace + * characters preset in the source string). + * @param wptr The destination memory buffer of size + * @a str_len + 1. + * @param str The source string to be copied. + * @param str_len The source string @a str length. + */ +static void +trim_space_snprintf(char *wptr, const char *str, uint32_t str_len) +{ + const char *str_end = str + str_len; + bool is_prev_chr_space = false; + while (str < str_end) { + if (isspace((unsigned char)*str)) { + if (!is_prev_chr_space) + *wptr++ = ' '; + is_prev_chr_space = true; + str++; + continue; + } + is_prev_chr_space = false; + *wptr++ = *str++; + } + *wptr = '\0'; +} + void -sql_add_check_constraint(struct Parse *parser) +sql_create_check_contraint(struct Parse *parser) { - struct create_ck_def *ck_def = &parser->create_ck_def; + struct create_ck_def *create_ck_def = &parser->create_ck_def; + struct ExprSpan *expr_span = create_ck_def->expr; + sql_expr_delete(parser->db, expr_span->pExpr, false); + struct alter_entity_def *alter_def = (struct alter_entity_def *) &parser->create_ck_def; assert(alter_def->entity_type == ENTITY_TYPE_CK); (void) alter_def; - struct Expr *expr = ck_def->expr->pExpr; struct space *space = parser->create_table_def.new_space; - if (space != NULL) { - expr->u.zToken = - sqlDbStrNDup(parser->db, - (char *) ck_def->expr->zStart, - (int) (ck_def->expr->zEnd - - ck_def->expr->zStart)); - if (expr->u.zToken == NULL) - goto release_expr; - space->def->opts.checks = - sql_expr_list_append(parser->db, - space->def->opts.checks, expr); - if (space->def->opts.checks == NULL) { - sqlDbFree(parser->db, expr->u.zToken); - goto release_expr; - } - struct create_entity_def *entity_def = &ck_def->base.base; - if (entity_def->name.n > 0) { - sqlExprListSetName(parser, space->def->opts.checks, - &entity_def->name, 1); + assert(space != NULL); + + /* Prepare payload for ck constraint definition. */ + struct region *region = &parser->region; + struct Token *name_token = &create_ck_def->base.base.name; + const char *name; + if (name_token->n > 0) { + name = sql_normalized_name_region_new(region, name_token->z, + name_token->n); + if (name == NULL) { + parser->is_aborted = true; + return; } } else { -release_expr: - sql_expr_delete(parser->db, expr, false); + uint32_t ck_idx = ++parser->create_table_def.check_count; + name = tt_sprintf("CK_CONSTRAINT_%d_%s", ck_idx, + space->def->name); + } + size_t name_len = strlen(name); + + uint32_t expr_str_len = (uint32_t)(create_ck_def->expr->zEnd - + create_ck_def->expr->zStart); + const char *expr_str = create_ck_def->expr->zStart; + + /* + * Allocate memory for ck constraint parse structure and + * ck constraint definition as a single memory chunk on + * region: + * + * [ck_parse][ck_def[name][expr_str]] + * |_____^ |___^ ^ + * |_________| + */ + uint32_t name_offset, expr_str_offset; + uint32_t ck_def_sz = + ck_constraint_def_sizeof(name_len, expr_str_len, &name_offset, + &expr_str_offset); + struct ck_constraint_parse *ck_parse = + region_alloc(region, sizeof(*ck_parse) + ck_def_sz); + if (ck_parse == NULL) { + diag_set(OutOfMemory, sizeof(*ck_parse) + ck_def_sz, "region", + "ck_parse"); + parser->is_aborted = true; + return; } + struct ck_constraint_def *ck_def = + (struct ck_constraint_def *)((char *)ck_parse + + sizeof(*ck_parse)); + ck_parse->ck_def = ck_def; + rlist_create(&ck_parse->link); + + ck_def->name = (char *)ck_def + name_offset; + ck_def->expr_str = (char *)ck_def + expr_str_offset; + trim_space_snprintf(ck_def->expr_str, expr_str, expr_str_len); + memcpy(ck_def->name, name, name_len); + ck_def->name[name_len] = '\0'; + + rlist_add_entry(&parser->create_table_def.new_check, ck_parse, link); } /* @@ -939,6 +1009,38 @@ emitNewSysSequenceRecord(Parse *pParse, int reg_seq_id, const char *seq_name) return first_col; } +/** + * Generate opcodes to serialize check constraint definition into + * MsgPack and insert produced tuple into _ck_constraint space. + * @param parser Parsing context. + * @param ck_def Check constraint definition to be serialized. + * @param reg_space_id The VDBE register containing space id. +*/ +static void +vdbe_emit_ck_constraint_create(struct Parse *parser, + const struct ck_constraint_def *ck_def, + uint32_t reg_space_id) +{ + struct sql *db = parser->db; + struct Vdbe *v = sqlGetVdbe(parser); + assert(v != NULL); + int ck_constraint_reg = sqlGetTempRange(parser, 5); + sqlVdbeAddOp4(v, OP_String8, 0, ck_constraint_reg, 0, + sqlDbStrDup(db, ck_def->name), P4_DYNAMIC); + sqlVdbeAddOp2(v, OP_SCopy, reg_space_id, ck_constraint_reg + 1); + sqlVdbeAddOp2(v, OP_Bool, false, ck_constraint_reg + 2); + sqlVdbeAddOp4(v, OP_String8, 0, ck_constraint_reg + 3, 0, + sqlDbStrDup(db, ck_def->expr_str), P4_DYNAMIC); + sqlVdbeAddOp3(v, OP_MakeRecord, ck_constraint_reg, 4, + ck_constraint_reg + 4); + sqlVdbeAddOp3(v, OP_SInsert, BOX_CK_CONSTRAINT_ID, 0, + ck_constraint_reg + 4); + save_record(parser, BOX_CK_CONSTRAINT_ID, ck_constraint_reg, 2, + v->nOp - 1); + VdbeComment((v, "Create CK constraint %s", ck_def->name)); + sqlReleaseTempRange(parser, ck_constraint_reg, 5); +} + int emitNewSysSpaceSequenceRecord(Parse *pParse, int space_id, const char reg_seq_id) { @@ -1108,20 +1210,18 @@ resolve_link(struct Parse *parse_context, const struct space_def *def, void sqlEndTable(struct Parse *pParse) { - sql *db = pParse->db; /* The database connection */ - - assert(!db->mallocFailed); + assert(!pParse->db->mallocFailed); struct space *new_space = pParse->create_table_def.new_space; if (new_space == NULL) return; - assert(!db->init.busy); + assert(!pParse->db->init.busy); assert(!new_space->def->opts.is_view); if (sql_space_primary_key(new_space) == NULL) { diag_set(ClientError, ER_CREATE_SPACE, new_space->def->name, "PRIMARY KEY missing"); pParse->is_aborted = true; - goto cleanup; + return; } /* @@ -1211,9 +1311,12 @@ sqlEndTable(struct Parse *pParse) fk_def->child_id = reg_space_id; vdbe_emit_fk_constraint_create(pParse, fk_def); } -cleanup: - sql_expr_list_delete(db, new_space->def->opts.checks); - new_space->def->opts.checks = NULL; + struct ck_constraint_parse *ck_parse; + rlist_foreach_entry(ck_parse, &pParse->create_table_def.new_check, + link) { + vdbe_emit_ck_constraint_create(pParse, ck_parse->ck_def, + reg_space_id); + } } void @@ -1411,6 +1514,37 @@ vdbe_emit_fk_constraint_drop(struct Parse *parse_context, char *constraint_name, sqlReleaseTempRange(parse_context, key_reg, 3); } +/** + * Generate VDBE program to remove entry from _ck_constraint space. + * + * @param parser Parsing context. + * @param ck_name Name of CK constraint to be dropped. + * @param child_id Id of table which constraint belongs to. + */ +static void +vdbe_emit_ck_constraint_drop(struct Parse *parser, const char *ck_name, + uint32_t space_id) +{ + struct Vdbe *v = sqlGetVdbe(parser); + struct sql *db = v->db; + assert(v != NULL); + int key_reg = sqlGetTempRange(parser, 3); + sqlVdbeAddOp4(v, OP_String8, 0, key_reg, 0, sqlDbStrDup(db, ck_name), + P4_DYNAMIC); + sqlVdbeAddOp2(v, OP_Integer, space_id, key_reg + 1); + const char *error_msg = + tt_sprintf(tnt_errcode_desc(ER_NO_SUCH_CONSTRAINT), ck_name); + if (vdbe_emit_halt_with_presence_test(parser, BOX_CK_CONSTRAINT_ID, 0, + key_reg, 2, ER_NO_SUCH_CONSTRAINT, + error_msg, false, + OP_Found) != 0) + return; + sqlVdbeAddOp3(v, OP_MakeRecord, key_reg, 2, key_reg + 2); + sqlVdbeAddOp2(v, OP_SDelete, BOX_CK_CONSTRAINT_ID, key_reg + 2); + VdbeComment((v, "Delete CK constraint %s", ck_name)); + sqlReleaseTempRange(parser, key_reg, 3); +} + /** * Generate code to drop a table. * This routine includes dropping triggers, sequences, @@ -1484,6 +1618,13 @@ sql_code_drop_table(struct Parse *parse_context, struct space *space, return; vdbe_emit_fk_constraint_drop(parse_context, fk_name_dup, space_id); } + /* Delete all CK constraints. */ + struct ck_constraint *ck_constraint; + rlist_foreach_entry(ck_constraint, &space->ck_constraint, link) { + vdbe_emit_ck_constraint_drop(parse_context, + ck_constraint->def->name, + space_id); + } /* * Drop all _space and _index entries that refer to the * table. @@ -2770,8 +2911,7 @@ sqlSrcListDelete(sql * db, SrcList * pList) */ assert(pItem->space == NULL || !pItem->space->def->opts.is_temporary || - (pItem->space->index == NULL && - pItem->space->def->opts.checks == NULL)); + pItem->space->index == NULL); sql_select_delete(db, pItem->pSelect); sql_expr_delete(db, pItem->pOn, false); sqlIdListDelete(db, pItem->pUsing); diff --git a/src/box/sql/insert.c b/src/box/sql/insert.c index c2aac553f..fa4a94ef7 100644 --- a/src/box/sql/insert.c +++ b/src/box/sql/insert.c @@ -36,6 +36,7 @@ #include "sqlInt.h" #include "tarantoolInt.h" #include "vdbeInt.h" +#include "box/ck_constraint.h" #include "box/session.h" #include "box/schema.h" #include "bit/bit.h" @@ -933,34 +934,27 @@ vdbe_emit_constraint_checks(struct Parse *parse_context, struct space *space, if (on_conflict == ON_CONFLICT_ACTION_DEFAULT) on_conflict = ON_CONFLICT_ACTION_ABORT; /* Test all CHECK constraints. */ - struct ExprList *checks = def->opts.checks; enum on_conflict_action on_conflict_check = on_conflict; if (on_conflict == ON_CONFLICT_ACTION_REPLACE) on_conflict_check = ON_CONFLICT_ACTION_ABORT; - if (checks != NULL) { + if (!rlist_empty(&space->ck_constraint)) parse_context->ckBase = new_tuple_reg; - for (int i = 0; i < checks->nExpr; i++) { - struct Expr *expr = checks->a[i].pExpr; - if (is_update && - checkConstraintUnchanged(expr, upd_cols)) - continue; - int all_ok = sqlVdbeMakeLabel(v); - sqlExprIfTrue(parse_context, expr, all_ok, - SQL_JUMPIFNULL); - if (on_conflict == ON_CONFLICT_ACTION_IGNORE) { - sqlVdbeGoto(v, ignore_label); - } else { - char *name = checks->a[i].zName; - if (name == NULL) - name = def->name; - sqlHaltConstraint(parse_context, - SQL_CONSTRAINT_CHECK, - on_conflict_check, name, - P4_TRANSIENT, - P5_ConstraintCheck); - } - sqlVdbeResolveLabel(v, all_ok); + struct ck_constraint *ck_constraint; + rlist_foreach_entry(ck_constraint, &space->ck_constraint, link) { + struct Expr *expr = ck_constraint->expr; + if (is_update && checkConstraintUnchanged(expr, upd_cols) != 0) + continue; + int all_ok = sqlVdbeMakeLabel(v); + sqlExprIfTrue(parse_context, expr, all_ok, SQL_JUMPIFNULL); + if (on_conflict == ON_CONFLICT_ACTION_IGNORE) { + sqlVdbeGoto(v, ignore_label); + } else { + char *name = ck_constraint->def->name; + sqlHaltConstraint(parse_context, SQL_CONSTRAINT_CHECK, + on_conflict_check, name, P4_TRANSIENT, + P5_ConstraintCheck); } + sqlVdbeResolveLabel(v, all_ok); } sql_emit_table_types(v, space->def, new_tuple_reg); /* @@ -1245,14 +1239,13 @@ xferOptimization(Parse * pParse, /* Parser context */ if (pSrcIdx == NULL) return 0; } - /* Get server checks. */ - ExprList *pCheck_src = src->def->opts.checks; - ExprList *pCheck_dest = dest->def->opts.checks; - if (pCheck_dest != NULL && - sqlExprListCompare(pCheck_src, pCheck_dest, -1) != 0) { - /* Tables have different CHECK constraints. Ticket #2252 */ + /* + * Dissallow the transfer optimization if the are check + * constraints. + */ + if (!rlist_empty(&dest->ck_constraint) || + !rlist_empty(&src->ck_constraint)) return 0; - } /* Disallow the transfer optimization if the destination table constains * any foreign key constraints. This is more restrictive than necessary. * So the extra complication to make this rule less restrictive is probably diff --git a/src/box/sql/parse.y b/src/box/sql/parse.y index 3a443a068..bc9f02599 100644 --- a/src/box/sql/parse.y +++ b/src/box/sql/parse.y @@ -290,7 +290,7 @@ ccons ::= check_constraint_def . check_constraint_def ::= cconsname(N) CHECK LP expr(X) RP. { create_ck_def_init(&pParse->create_ck_def, &N, &X); - sql_add_check_constraint(pParse); + sql_create_check_contraint(pParse); } ccons ::= cconsname(N) REFERENCES nm(T) eidlist_opt(TA) matcharg(M) refargs(R). { diff --git a/src/box/sql/parse_def.h b/src/box/sql/parse_def.h index 5899a7e4e..6c1b6fddd 100644 --- a/src/box/sql/parse_def.h +++ b/src/box/sql/parse_def.h @@ -123,6 +123,22 @@ struct fk_constraint_parse { struct rlist link; }; +/** + * Structure representing check constraint appeared within + * CREATE TABLE statement. Used only during parsing. + * All allocations are performed on region, so no cleanups are + * required. + */ +struct ck_constraint_parse { + /** + * Check constraint declared in <CREATE TABLE ...> + * statement. Must be coded after space creation. + */ + struct ck_constraint_def *ck_def; + /** Organize these structs into linked list. */ + struct rlist link; +}; + /** * Possible SQL index types. Note that PK and UNIQUE constraints * are implemented as indexes and have their own types: @@ -189,6 +205,13 @@ struct create_table_def { * Foreign key constraint appeared in CREATE TABLE stmt. */ struct rlist new_fkey; + /** + * Number of CK constraints declared within + * CREATE TABLE statement. + */ + uint32_t check_count; + /** Check constraint appeared in CREATE TABLE stmt. */ + struct rlist new_check; /** True, if table to be created has AUTOINCREMENT PK. */ bool has_autoinc; }; @@ -437,6 +460,7 @@ create_table_def_init(struct create_table_def *table_def, struct Token *name, create_entity_def_init(&table_def->base, ENTITY_TYPE_TABLE, NULL, name, if_not_exists); rlist_create(&table_def->new_fkey); + rlist_create(&table_def->new_check); } static inline void diff --git a/src/box/sql/select.c b/src/box/sql/select.c index d3472a922..996ac7ccd 100644 --- a/src/box/sql/select.c +++ b/src/box/sql/select.c @@ -6446,7 +6446,12 @@ sql_expr_extract_select(struct Parse *parser, struct Select *select) struct ExprList *expr_list = select->pEList; assert(expr_list->nExpr == 1); parser->parsed_ast_type = AST_TYPE_EXPR; - parser->parsed_ast.expr = sqlExprDup(parser->db, - expr_list->a->pExpr, - EXPRDUP_REDUCE); + /* + * Extract a copy of parsed expression. + * We cannot use EXPRDUP_REDUCE flag in sqlExprDup call + * because some compiled Expr (like Checks expressions) + * may require further resolve with sqlResolveExprNames. + */ + parser->parsed_ast.expr = + sqlExprDup(parser->db, expr_list->a->pExpr, 0); } diff --git a/src/box/sql/sqlInt.h b/src/box/sql/sqlInt.h index 997a46535..2d6936490 100644 --- a/src/box/sql/sqlInt.h +++ b/src/box/sql/sqlInt.h @@ -3347,7 +3347,7 @@ sqlAddPrimaryKey(struct Parse *parse); * @param parser Parsing context. */ void -sql_add_check_constraint(Parse *parser); +sql_create_check_contraint(Parse *parser); void sqlAddDefaultValue(Parse *, ExprSpan *); void sqlAddCollateType(Parse *, Token *); diff --git a/src/box/sql/tokenize.c b/src/box/sql/tokenize.c index 8cc35323c..3791be567 100644 --- a/src/box/sql/tokenize.c +++ b/src/box/sql/tokenize.c @@ -434,7 +434,6 @@ parser_space_delete(struct sql *db, struct space *space) assert(space->def->opts.is_temporary); for (uint32_t i = 0; i < space->index_count; ++i) index_def_delete(space->index[i]->def); - sql_expr_list_delete(db, space->def->opts.checks); } /** diff --git a/test/app-tap/tarantoolctl.test.lua b/test/app-tap/tarantoolctl.test.lua index c1e1490ca..7a72acda2 100755 --- a/test/app-tap/tarantoolctl.test.lua +++ b/test/app-tap/tarantoolctl.test.lua @@ -405,8 +405,8 @@ do check_ctlcat_xlog(test_i, dir, "--from=3 --to=6 --format=json --show-system --replica 1", "\n", 3) check_ctlcat_xlog(test_i, dir, "--from=3 --to=6 --format=json --show-system --replica 1 --replica 2", "\n", 3) check_ctlcat_xlog(test_i, dir, "--from=3 --to=6 --format=json --show-system --replica 2", "\n", 0) - check_ctlcat_snap(test_i, dir, "--space=280", "---\n", 21) - check_ctlcat_snap(test_i, dir, "--space=288", "---\n", 47) + check_ctlcat_snap(test_i, dir, "--space=280", "---\n", 22) + check_ctlcat_snap(test_i, dir, "--space=288", "---\n", 49) end) end) diff --git a/test/box-py/bootstrap.result b/test/box-py/bootstrap.result index 379f6c51f..33bb01f7c 100644 --- a/test/box-py/bootstrap.result +++ b/test/box-py/bootstrap.result @@ -4,7 +4,7 @@ box.internal.bootstrap() box.space._schema:select{} --- - - ['max_id', 511] - - ['version', 2, 1, 3] + - ['version', 2, 2, 0] ... box.space._cluster:select{} --- @@ -78,6 +78,9 @@ box.space._space:select{} {'name': 'is_deferred', 'type': 'boolean'}, {'name': 'match', 'type': 'string'}, {'name': 'on_delete', 'type': 'string'}, {'name': 'on_update', 'type': 'string'}, {'name': 'child_cols', 'type': 'array'}, {'name': 'parent_cols', 'type': 'array'}]] + - [357, 1, '_ck_constraint', 'memtx', 0, {}, [{'name': 'name', 'type': 'string'}, + {'name': 'space_id', 'type': 'unsigned'}, {'name': 'is_deferred', 'type': 'boolean'}, + {'name': 'expr_str', 'type': 'str'}]] ... box.space._index:select{} --- @@ -130,6 +133,8 @@ box.space._index:select{} - [340, 1, 'sequence', 'tree', {'unique': false}, [[1, 'unsigned']]] - [356, 0, 'primary', 'tree', {'unique': true}, [[0, 'string'], [1, 'unsigned']]] - [356, 1, 'child_id', 'tree', {'unique': false}, [[1, 'unsigned']]] + - [357, 0, 'primary', 'tree', {'unique': true}, [[0, 'string'], [1, 'unsigned']]] + - [357, 1, 'space_id', 'tree', {'unique': false}, [[1, 'unsigned']]] ... box.space._user:select{} --- diff --git a/test/box/access.result b/test/box/access.result index 9c190240f..801112799 100644 --- a/test/box/access.result +++ b/test/box/access.result @@ -1487,6 +1487,9 @@ box.schema.user.grant('tester', 'read', 'space', '_trigger') box.schema.user.grant('tester', 'read', 'space', '_fk_constraint') --- ... +box.schema.user.grant('tester', 'read', 'space', '_ck_constraint') +--- +... box.session.su("tester") --- ... diff --git a/test/box/access.test.lua b/test/box/access.test.lua index 4baeb2ef6..2f62d6f53 100644 --- a/test/box/access.test.lua +++ b/test/box/access.test.lua @@ -554,6 +554,7 @@ box.schema.user.grant('tester', 'create' , 'sequence') box.schema.user.grant('tester', 'read', 'space', '_sequence') box.schema.user.grant('tester', 'read', 'space', '_trigger') box.schema.user.grant('tester', 'read', 'space', '_fk_constraint') +box.schema.user.grant('tester', 'read', 'space', '_ck_constraint') box.session.su("tester") -- successful create s1 = box.schema.space.create("test_space") diff --git a/test/box/access_misc.result b/test/box/access_misc.result index 36ebfae09..ca54ac6d4 100644 --- a/test/box/access_misc.result +++ b/test/box/access_misc.result @@ -818,6 +818,9 @@ box.space._space:select() {'name': 'is_deferred', 'type': 'boolean'}, {'name': 'match', 'type': 'string'}, {'name': 'on_delete', 'type': 'string'}, {'name': 'on_update', 'type': 'string'}, {'name': 'child_cols', 'type': 'array'}, {'name': 'parent_cols', 'type': 'array'}]] + - [357, 1, '_ck_constraint', 'memtx', 0, {}, [{'name': 'name', 'type': 'string'}, + {'name': 'space_id', 'type': 'unsigned'}, {'name': 'is_deferred', 'type': 'boolean'}, + {'name': 'expr_str', 'type': 'str'}]] ... box.space._func:select() --- diff --git a/test/box/access_sysview.result b/test/box/access_sysview.result index c6a2b22ed..c3d1e746b 100644 --- a/test/box/access_sysview.result +++ b/test/box/access_sysview.result @@ -230,11 +230,11 @@ box.session.su('guest') ... #box.space._vspace:select{} --- -- 22 +- 23 ... #box.space._vindex:select{} --- -- 48 +- 50 ... #box.space._vuser:select{} --- @@ -262,7 +262,7 @@ box.session.su('guest') ... #box.space._vindex:select{} --- -- 48 +- 50 ... #box.space._vuser:select{} --- diff --git a/test/box/alter.result b/test/box/alter.result index c1b1de135..44630557c 100644 --- a/test/box/alter.result +++ b/test/box/alter.result @@ -107,7 +107,7 @@ space = box.space[t[1]] ... space.id --- -- 357 +- 358 ... space.field_count --- @@ -152,7 +152,7 @@ space_deleted ... space:replace{0} --- -- error: Space '357' does not exist +- error: Space '358' does not exist ... _index:insert{_space.id, 0, 'primary', 'tree', {unique=true}, {{0, 'unsigned'}}} --- @@ -230,6 +230,8 @@ _index:select{} - [340, 1, 'sequence', 'tree', {'unique': false}, [[1, 'unsigned']]] - [356, 0, 'primary', 'tree', {'unique': true}, [[0, 'string'], [1, 'unsigned']]] - [356, 1, 'child_id', 'tree', {'unique': false}, [[1, 'unsigned']]] + - [357, 0, 'primary', 'tree', {'unique': true}, [[0, 'string'], [1, 'unsigned']]] + - [357, 1, 'space_id', 'tree', {'unique': false}, [[1, 'unsigned']]] ... -- modify indexes of a system space _index:delete{_index.id, 0} diff --git a/test/box/misc.result b/test/box/misc.result index 4fcd13a78..5bf419d4f 100644 --- a/test/box/misc.result +++ b/test/box/misc.result @@ -523,6 +523,7 @@ t; 192: box.error.INDEX_DEF_UNSUPPORTED 193: box.error.CK_DEF_UNSUPPORTED 194: box.error.MULTIKEY_INDEX_MISMATCH + 195: box.error.CREATE_CK_CONSTRAINT ... test_run:cmd("setopt delimiter ''"); --- diff --git a/test/sql-tap/check.test.lua b/test/sql-tap/check.test.lua index b01afca7c..b317dec05 100755 --- a/test/sql-tap/check.test.lua +++ b/test/sql-tap/check.test.lua @@ -55,7 +55,7 @@ test:do_catchsql_test( INSERT INTO t1 VALUES(6,7, 2); ]], { -- <check-1.3> - 1, "Failed to execute SQL statement: CHECK constraint failed: T1" + 1, "Failed to execute SQL statement: CHECK constraint failed: CK_CONSTRAINT_1_T1" -- </check-1.3> }) @@ -75,7 +75,7 @@ test:do_catchsql_test( INSERT INTO t1 VALUES(4,3, 2); ]], { -- <check-1.5> - 1, "Failed to execute SQL statement: CHECK constraint failed: T1" + 1, "Failed to execute SQL statement: CHECK constraint failed: CK_CONSTRAINT_2_T1" -- </check-1.5> }) @@ -147,7 +147,7 @@ test:do_catchsql_test( UPDATE t1 SET x=7 WHERE x==2 ]], { -- <check-1.12> - 1, "Failed to execute SQL statement: CHECK constraint failed: T1" + 1, "Failed to execute SQL statement: CHECK constraint failed: CK_CONSTRAINT_1_T1" -- </check-1.12> }) @@ -167,7 +167,7 @@ test:do_catchsql_test( UPDATE t1 SET x=5 WHERE x==2 ]], { -- <check-1.14> - 1, "Failed to execute SQL statement: CHECK constraint failed: T1" + 1, "Failed to execute SQL statement: CHECK constraint failed: CK_CONSTRAINT_1_T1" -- </check-1.14> }) @@ -319,7 +319,7 @@ test:do_catchsql_test( ); ]], { -- <check-3.1> - 1, "Failed to create space 'T3': Subqueries are prohibited in a CHECK constraint definition" + 1, "Failed to create check constraint 'CK_CONSTRAINT_1_T3': Subqueries are prohibited in a CHECK constraint definition" -- </check-3.1> }) @@ -344,7 +344,7 @@ test:do_catchsql_test( ); ]], { -- <check-3.3> - 1, "Failed to create space 'T3': Can’t resolve field 'Q'" + 1, "Failed to create check constraint 'CK_CONSTRAINT_1_T3': Can’t resolve field 'Q'" -- </check-3.3> }) @@ -368,7 +368,7 @@ test:do_catchsql_test( ); ]], { -- <check-3.5> - 1, "Failed to create space 'T3': Field 'X' was not found in the space 'T2' format" + 1, "Failed to create check constraint 'CK_CONSTRAINT_1_T3': Field 'X' was not found in the space 'T2' format" -- </check-3.5> }) @@ -413,7 +413,7 @@ test:do_catchsql_test( INSERT INTO t3 VALUES(111,222,333); ]], { -- <check-3.9> - 1, "Failed to execute SQL statement: CHECK constraint failed: T3" + 1, "Failed to execute SQL statement: CHECK constraint failed: CK_CONSTRAINT_1_T3" -- </check-3.9> }) @@ -484,7 +484,7 @@ test:do_catchsql_test( UPDATE t4 SET x=0, y=1; ]], { -- <check-4.6> - 1, "Failed to execute SQL statement: CHECK constraint failed: T4" + 1, "Failed to execute SQL statement: CHECK constraint failed: CK_CONSTRAINT_1_T4" -- </check-4.6> }) @@ -504,7 +504,7 @@ test:do_catchsql_test( UPDATE t4 SET x=0, y=2; ]], { -- <check-4.9> - 1, "Failed to execute SQL statement: CHECK constraint failed: T4" + 1, "Failed to execute SQL statement: CHECK constraint failed: CK_CONSTRAINT_1_T4" -- </check-4.9> }) @@ -516,7 +516,7 @@ test:do_catchsql_test( ); ]], { -- <check-5.1> - 1, "Wrong space options (field 5): invalid expression specified (bindings are not allowed in DDL)" + 1, "Failed to create check constraint 'CK_CONSTRAINT_1_T5': bindings are not allowed in DDL" -- </check-5.1> }) @@ -528,7 +528,7 @@ test:do_catchsql_test( ); ]], { -- <check-5.2> - 1, "Wrong space options (field 5): invalid expression specified (bindings are not allowed in DDL)" + 1, "Failed to create check constraint 'CK_CONSTRAINT_1_T5': bindings are not allowed in DDL" -- </check-5.2> }) @@ -581,7 +581,7 @@ test:do_catchsql_test( UPDATE OR FAIL t1 SET x=7-x, y=y+1; ]], { -- <check-6.5> - 1, "Failed to execute SQL statement: CHECK constraint failed: T1" + 1, "Failed to execute SQL statement: CHECK constraint failed: CK_CONSTRAINT_1_T1" -- </check-6.5> }) @@ -603,7 +603,7 @@ test:do_catchsql_test( INSERT OR ROLLBACK INTO t1 VALUES(8,40.0, 10); ]], { -- <check-6.7> - 1, "Failed to execute SQL statement: CHECK constraint failed: T1" + 1, "Failed to execute SQL statement: CHECK constraint failed: CK_CONSTRAINT_1_T1" -- </check-6.7> }) @@ -636,7 +636,7 @@ test:do_catchsql_test( REPLACE INTO t1 VALUES(6,7, 11); ]], { -- <check-6.12> - 1, "Failed to execute SQL statement: CHECK constraint failed: T1" + 1, "Failed to execute SQL statement: CHECK constraint failed: CK_CONSTRAINT_1_T1" -- </check-6.12> }) @@ -700,7 +700,7 @@ test:do_catchsql_test( 7.3, " INSERT INTO t6 VALUES(11) ", { -- <7.3> - 1, "Failed to execute SQL statement: CHECK constraint failed: T6" + 1, "Failed to execute SQL statement: CHECK constraint failed: CK_CONSTRAINT_1_T6" -- </7.3> }) diff --git a/test/sql-tap/fkey2.test.lua b/test/sql-tap/fkey2.test.lua index 54e5059b3..695a379a6 100755 --- a/test/sql-tap/fkey2.test.lua +++ b/test/sql-tap/fkey2.test.lua @@ -362,7 +362,7 @@ test:do_catchsql_test( UPDATE ab SET a = 5; ]], { -- <fkey2-3.2> - 1, "Failed to execute SQL statement: CHECK constraint failed: EF" + 1, "Failed to execute SQL statement: CHECK constraint failed: CK_CONSTRAINT_1_EF" -- </fkey2-3.2> }) @@ -382,7 +382,7 @@ test:do_catchsql_test( UPDATE ab SET a = 5; ]], { -- <fkey2-3.4> - 1, "Failed to execute SQL statement: CHECK constraint failed: EF" + 1, "Failed to execute SQL statement: CHECK constraint failed: CK_CONSTRAINT_1_EF" -- </fkey2-3.4> }) diff --git a/test/sql-tap/sql-errors.test.lua b/test/sql-tap/sql-errors.test.lua index 4e173b692..d742cf6cf 100755 --- a/test/sql-tap/sql-errors.test.lua +++ b/test/sql-tap/sql-errors.test.lua @@ -313,7 +313,7 @@ test:do_catchsql_test( CREATE TABLE t27 (i INT PRIMARY KEY, CHECK(i < (SELECT * FROM t0))); ]], { -- <sql-errors-1.27> - 1,"Failed to create space 'T27': Subqueries are prohibited in a CHECK constraint definition" + 1,"Failed to create check constraint 'CK_CONSTRAINT_1_T27': Subqueries are prohibited in a check constraint definition" -- </sql-errors-1.27> }) diff --git a/test/sql-tap/table.test.lua b/test/sql-tap/table.test.lua index 5b793c0fc..066662f33 100755 --- a/test/sql-tap/table.test.lua +++ b/test/sql-tap/table.test.lua @@ -1221,7 +1221,7 @@ test:do_catchsql_test( INSERT INTO T21 VALUES(1, -1, 1); ]], { -- <table-21.3> - 1, "Failed to execute SQL statement: CHECK constraint failed: T21" + 1, "Failed to execute SQL statement: CHECK constraint failed: CK_CONSTRAINT_1_T21" -- </table-21.3> }) @@ -1231,7 +1231,7 @@ test:do_catchsql_test( INSERT INTO T21 VALUES(1, 1, -1); ]], { -- <table-21.4> - 1, "Failed to execute SQL statement: CHECK constraint failed: T21" + 1, "Failed to execute SQL statement: CHECK constraint failed: CK_CONSTRAINT_2_T21" -- </table-21.4> }) diff --git a/test/sql/checks.result b/test/sql/checks.result index f7cddec43..5d02a42a6 100644 --- a/test/sql/checks.result +++ b/test/sql/checks.result @@ -18,8 +18,10 @@ box.execute('pragma sql_default_engine=\''..engine..'\'') -- -- gh-3272: Move SQL CHECK into server -- --- invalid expression -opts = {checks = {{expr = 'X><5'}}} +-- Until Tarantool version 2.2 check constraints were stored in +-- space opts. +-- Make sure that now this legacy option is ignored. +opts = {checks = {{expr = 'X>5'}}} --- ... format = {{name = 'X', type = 'unsigned'}} @@ -30,89 +32,211 @@ t = {513, 1, 'test', 'memtx', 0, opts, format} ... s = box.space._space:insert(t) --- -- error: 'Wrong space options (field 5): invalid expression specified (Syntax error - near ''<'')' ... -opts = {checks = {{expr = 'X>5'}}} +_ = box.space.test:create_index('pk') --- ... -format = {{name = 'X', type = 'unsigned'}} +-- Invalid expression test. +box.space._ck_constraint:insert({'CK_CONSTRAINT_01', 513, false, 'X><5'}) --- +- error: 'Failed to create check constraint ''CK_CONSTRAINT_01'': Syntax error near + ''<''' ... -t = {513, 1, 'test', 'memtx', 0, opts, format} +-- Unexistent space test. +box.space._ck_constraint:insert({'CK_CONSTRAINT_01', 550, false, 'X<5'}) --- +- error: Space '550' does not exist ... -s = box.space._space:insert(t) +-- Pass integer instead of expression. +box.space._ck_constraint:insert({'CK_CONSTRAINT_01', 513, false, 666}) +--- +- error: 'Tuple field 4 type does not match one required by operation: expected string' +... +-- Defered CK constraints are not supported. +box.space._ck_constraint:insert({'CK_CONSTRAINT_01', 513, true, 'X<5'}) +--- +- error: Tarantool does not support deferred ck constraints +... +-- Check constraints LUA creation test. +box.space._ck_constraint:insert({'CK_CONSTRAINT_01', 513, false, 'X<5'}) +--- +- ['CK_CONSTRAINT_01', 513, false, 'X<5'] +... +box.space._ck_constraint:count({}) +--- +- 1 +... +box.execute("INSERT INTO \"test\" VALUES(5);") +--- +- error: 'Failed to execute SQL statement: CHECK constraint failed: CK_CONSTRAINT_01' +... +box.space._ck_constraint:replace({'CK_CONSTRAINT_01', 513, false, 'X<=5'}) +--- +- ['CK_CONSTRAINT_01', 513, false, 'X<=5'] +... +box.execute("INSERT INTO \"test\" VALUES(5);") +--- +- row_count: 1 +... +box.execute("INSERT INTO \"test\" VALUES(6);") +--- +- error: 'Failed to execute SQL statement: CHECK constraint failed: CK_CONSTRAINT_01' +... +-- Can't drop table with check constraints. +box.space.test:delete({5}) --- +- [5] ... -box.space._space:delete(513) +box.space.test.index.pk:drop() +--- +... +box.space._space:delete({513}) +--- +- error: 'Can''t drop space ''test'': the space has check constraints' +... +box.space._ck_constraint:delete({'CK_CONSTRAINT_01', 513}) +--- +- ['CK_CONSTRAINT_01', 513, false, 'X<=5'] +... +box.space._space:delete({513}) --- - [513, 1, 'test', 'memtx', 0, {'checks': [{'expr': 'X>5'}]}, [{'name': 'X', 'type': 'unsigned'}]] ... -opts = {checks = {{expr = 'X>5', name = 'ONE'}}} +-- Create table with checks in sql. +box.execute("CREATE TABLE t1(x INTEGER CONSTRAINT ONE CHECK( x<5 ), y REAL CONSTRAINT TWO CHECK( y>x ), z INTEGER PRIMARY KEY);") --- +- row_count: 1 ... -format = {{name = 'X', type = 'unsigned'}} +box.space._ck_constraint:count() --- +- 2 ... -t = {513, 1, 'test', 'memtx', 0, opts, format} +box.execute("INSERT INTO t1 VALUES (7, 1, 1)") --- +- error: 'Failed to execute SQL statement: CHECK constraint failed: ONE' ... -s = box.space._space:insert(t) +box.execute("INSERT INTO t1 VALUES (2, 1, 1)") --- +- error: 'Failed to execute SQL statement: CHECK constraint failed: TWO' ... -box.space._space:delete(513) +box.execute("INSERT INTO t1 VALUES (2, 4, 1)") --- -- [513, 1, 'test', 'memtx', 0, {'checks': [{'name': 'ONE', 'expr': 'X>5'}]}, [{'name': 'X', - 'type': 'unsigned'}]] +- row_count: 1 ... --- extra invlalid field name -opts = {checks = {{expr = 'X>5', name = 'ONE', extra = 'TWO'}}} +box.execute("DROP TABLE t1") --- +- row_count: 1 ... -format = {{name = 'X', type = 'unsigned'}} +-- Test space creation rollback on spell error in ck constraint. +box.execute("CREATE TABLE first (id FLOAT PRIMARY KEY CHECK(id < 5), a INT CONSTRAINT ONE CHECK(a >< 5));") --- +- error: Syntax error near '<' ... -t = {513, 1, 'test', 'memtx', 0, opts, format} +box.space.FIRST == nil --- +- true ... -s = box.space._space:insert(t) +box.space._ck_constraint:count() == 0 --- -- error: 'Wrong space options (field 5): invalid MsgPack map field ''extra''' +- true ... -opts = {checks = {{expr_invalid_label = 'X>5'}}} +-- Ck constraints are disallowed for spaces having no format. +s = box.schema.create_space('test', {engine = engine}) --- ... -format = {{name = 'X', type = 'unsigned'}} +_ = s:create_index('pk') --- ... -t = {513, 1, 'test', 'memtx', 0, opts, format} +_ = box.space._ck_constraint:insert({'physics', s.id, false, 'X<Y'}) --- +- error: Tarantool does not support CK constraint for space without format ... -s = box.space._space:insert(t) +s:format({{name='X', type='integer'}, {name='Y', type='integer'}}) --- -- error: 'Wrong space options (field 5): invalid MsgPack map field ''expr_invalid_label''' ... --- invalid field type -opts = {checks = {{name = 123}}} +_ = box.space._ck_constraint:insert({'physics', s.id, false, 'X<Y'}) --- ... -format = {{name = 'X', type = 'unsigned'}} +box.execute("INSERT INTO \"test\" VALUES(2, 1);") --- +- error: 'Failed to execute SQL statement: CHECK constraint failed: physics' ... -t = {513, 1, 'test', 'memtx', 0, opts, format} +s:format({{name='Y', type='integer'}, {name='X', type='integer'}}) --- ... -s = box.space._space:insert(t) +box.execute("INSERT INTO \"test\" VALUES(1, 2);") +--- +- error: 'Failed to execute SQL statement: CHECK constraint failed: physics' +... +box.execute("INSERT INTO \"test\" VALUES(2, 1);") +--- +- row_count: 1 +... +s:truncate() +--- +... +box.execute("INSERT INTO \"test\" VALUES(1, 2);") +--- +- error: 'Failed to execute SQL statement: CHECK constraint failed: physics' +... +s:format({}) +--- +- error: Tarantool does not support CK constraint for space without format +... +s:format() +--- +- [{'name': 'Y', 'type': 'integer'}, {'name': 'X', 'type': 'integer'}] +... +s:format({{name='Y1', type='integer'}, {name='X1', type='integer'}}) +--- +- error: 'Failed to create check constraint ''physics'': Can’t resolve field ''X''' +... +-- Ck constraint creation is forbidden for non-empty space +s:insert({2, 1}) +--- +- [2, 1] +... +_ = box.space._ck_constraint:insert({'conflict', s.id, false, 'X>10'}) +--- +- error: 'Failed to create check constraint ''conflict'': referencing space must be + empty' +... +s:truncate() +--- +... +_ = box.space._ck_constraint:insert({'conflict', s.id, false, 'X>10'}) +--- +... +box.execute("INSERT INTO \"test\" VALUES(1, 2);") --- -- error: 'Wrong space options (field 5): invalid MsgPack map field ''name'' type' +- error: 'Failed to execute SQL statement: CHECK constraint failed: conflict' +... +box.execute("INSERT INTO \"test\" VALUES(11, 11);") +--- +- error: 'Failed to execute SQL statement: CHECK constraint failed: physics' +... +box.execute("INSERT INTO \"test\" VALUES(12, 11);") +--- +- row_count: 1 +... +s:drop() +--- +... +box.execute("CREATE TABLE T2(ID INT PRIMARY KEY, CONSTRAINT CK1 CHECK(ID > 0), CONSTRAINT CK1 CHECK(ID < 0))") +--- +- error: Duplicate key exists in unique index 'primary' in space '_ck_constraint' +... +box.space._ck_constraint:select() +--- +- [] ... -- -- gh-3611: Segfault on table creation with check referencing this table -- box.execute("CREATE TABLE w2 (s1 INT PRIMARY KEY, CHECK ((SELECT COUNT(*) FROM w2) = 0));") --- -- error: 'Failed to create space ''W2'': Space ''W2'' does not exist' +- error: 'Failed to create check constraint ''CK_CONSTRAINT_1_W2'': Subqueries are + prohibited in a CHECK constraint definition' ... box.execute("DROP TABLE w2;") --- @@ -123,22 +247,8 @@ box.execute("DROP TABLE w2;") -- box.execute("CREATE TABLE t5(x INT PRIMARY KEY, y INT, CHECK( x*y < ? ));") --- -- error: 'Wrong space options (field 5): invalid expression specified (bindings are - not allowed in DDL)' -... -opts = {checks = {{expr = '?>5', name = 'ONE'}}} ---- -... -format = {{name = 'X', type = 'unsigned'}} ---- -... -t = {513, 1, 'test', 'memtx', 0, opts, format} ---- -... -s = box.space._space:insert(t) ---- -- error: 'Wrong space options (field 5): invalid expression specified (bindings are - not allowed in DDL)' +- error: 'Failed to create check constraint ''CK_CONSTRAINT_1_T5'': bindings are not + allowed in DDL' ... test_run:cmd("clear filter") --- diff --git a/test/sql/checks.test.lua b/test/sql/checks.test.lua index 5bfcf12f8..4029359b1 100644 --- a/test/sql/checks.test.lua +++ b/test/sql/checks.test.lua @@ -8,41 +8,79 @@ box.execute('pragma sql_default_engine=\''..engine..'\'') -- gh-3272: Move SQL CHECK into server -- --- invalid expression -opts = {checks = {{expr = 'X><5'}}} -format = {{name = 'X', type = 'unsigned'}} -t = {513, 1, 'test', 'memtx', 0, opts, format} -s = box.space._space:insert(t) - +-- Until Tarantool version 2.2 check constraints were stored in +-- space opts. +-- Make sure that now this legacy option is ignored. opts = {checks = {{expr = 'X>5'}}} format = {{name = 'X', type = 'unsigned'}} t = {513, 1, 'test', 'memtx', 0, opts, format} s = box.space._space:insert(t) -box.space._space:delete(513) +_ = box.space.test:create_index('pk') -opts = {checks = {{expr = 'X>5', name = 'ONE'}}} -format = {{name = 'X', type = 'unsigned'}} -t = {513, 1, 'test', 'memtx', 0, opts, format} -s = box.space._space:insert(t) -box.space._space:delete(513) +-- Invalid expression test. +box.space._ck_constraint:insert({'CK_CONSTRAINT_01', 513, false, 'X><5'}) +-- Unexistent space test. +box.space._ck_constraint:insert({'CK_CONSTRAINT_01', 550, false, 'X<5'}) +-- Pass integer instead of expression. +box.space._ck_constraint:insert({'CK_CONSTRAINT_01', 513, false, 666}) +-- Defered CK constraints are not supported. +box.space._ck_constraint:insert({'CK_CONSTRAINT_01', 513, true, 'X<5'}) --- extra invlalid field name -opts = {checks = {{expr = 'X>5', name = 'ONE', extra = 'TWO'}}} -format = {{name = 'X', type = 'unsigned'}} -t = {513, 1, 'test', 'memtx', 0, opts, format} -s = box.space._space:insert(t) +-- Check constraints LUA creation test. +box.space._ck_constraint:insert({'CK_CONSTRAINT_01', 513, false, 'X<5'}) +box.space._ck_constraint:count({}) -opts = {checks = {{expr_invalid_label = 'X>5'}}} -format = {{name = 'X', type = 'unsigned'}} -t = {513, 1, 'test', 'memtx', 0, opts, format} -s = box.space._space:insert(t) +box.execute("INSERT INTO \"test\" VALUES(5);") +box.space._ck_constraint:replace({'CK_CONSTRAINT_01', 513, false, 'X<=5'}) +box.execute("INSERT INTO \"test\" VALUES(5);") +box.execute("INSERT INTO \"test\" VALUES(6);") +-- Can't drop table with check constraints. +box.space.test:delete({5}) +box.space.test.index.pk:drop() +box.space._space:delete({513}) +box.space._ck_constraint:delete({'CK_CONSTRAINT_01', 513}) +box.space._space:delete({513}) --- invalid field type -opts = {checks = {{name = 123}}} -format = {{name = 'X', type = 'unsigned'}} -t = {513, 1, 'test', 'memtx', 0, opts, format} -s = box.space._space:insert(t) +-- Create table with checks in sql. +box.execute("CREATE TABLE t1(x INTEGER CONSTRAINT ONE CHECK( x<5 ), y REAL CONSTRAINT TWO CHECK( y>x ), z INTEGER PRIMARY KEY);") +box.space._ck_constraint:count() +box.execute("INSERT INTO t1 VALUES (7, 1, 1)") +box.execute("INSERT INTO t1 VALUES (2, 1, 1)") +box.execute("INSERT INTO t1 VALUES (2, 4, 1)") +box.execute("DROP TABLE t1") + +-- Test space creation rollback on spell error in ck constraint. +box.execute("CREATE TABLE first (id FLOAT PRIMARY KEY CHECK(id < 5), a INT CONSTRAINT ONE CHECK(a >< 5));") +box.space.FIRST == nil +box.space._ck_constraint:count() == 0 + +-- Ck constraints are disallowed for spaces having no format. +s = box.schema.create_space('test', {engine = engine}) +_ = s:create_index('pk') +_ = box.space._ck_constraint:insert({'physics', s.id, false, 'X<Y'}) +s:format({{name='X', type='integer'}, {name='Y', type='integer'}}) +_ = box.space._ck_constraint:insert({'physics', s.id, false, 'X<Y'}) +box.execute("INSERT INTO \"test\" VALUES(2, 1);") +s:format({{name='Y', type='integer'}, {name='X', type='integer'}}) +box.execute("INSERT INTO \"test\" VALUES(1, 2);") +box.execute("INSERT INTO \"test\" VALUES(2, 1);") +s:truncate() +box.execute("INSERT INTO \"test\" VALUES(1, 2);") +s:format({}) +s:format() +s:format({{name='Y1', type='integer'}, {name='X1', type='integer'}}) +-- Ck constraint creation is forbidden for non-empty space +s:insert({2, 1}) +_ = box.space._ck_constraint:insert({'conflict', s.id, false, 'X>10'}) +s:truncate() +_ = box.space._ck_constraint:insert({'conflict', s.id, false, 'X>10'}) +box.execute("INSERT INTO \"test\" VALUES(1, 2);") +box.execute("INSERT INTO \"test\" VALUES(11, 11);") +box.execute("INSERT INTO \"test\" VALUES(12, 11);") +s:drop() +box.execute("CREATE TABLE T2(ID INT PRIMARY KEY, CONSTRAINT CK1 CHECK(ID > 0), CONSTRAINT CK1 CHECK(ID < 0))") +box.space._ck_constraint:select() -- -- gh-3611: Segfault on table creation with check referencing this table @@ -55,10 +93,4 @@ box.execute("DROP TABLE w2;") -- box.execute("CREATE TABLE t5(x INT PRIMARY KEY, y INT, CHECK( x*y < ? ));") -opts = {checks = {{expr = '?>5', name = 'ONE'}}} -format = {{name = 'X', type = 'unsigned'}} -t = {513, 1, 'test', 'memtx', 0, opts, format} -s = box.space._space:insert(t) - - test_run:cmd("clear filter") diff --git a/test/sql/errinj.result b/test/sql/errinj.result index ee36a387b..18d1f3882 100644 --- a/test/sql/errinj.result +++ b/test/sql/errinj.result @@ -463,3 +463,137 @@ errinj.set("ERRINJ_SQL_NAME_NORMALIZATION", false) --- - ok ... +-- +-- Tests which are aimed at verifying work of commit/rollback +-- triggers on _ck_constraint space. +-- +s = box.schema.space.create('test', {format = {{name = 'X', type = 'unsigned'}}}) +--- +... +pk = box.space.test:create_index('pk') +--- +... +errinj.set("ERRINJ_WAL_IO", true) +--- +- ok +... +_ = box.space._ck_constraint:insert({'CK_CONSTRAINT_01', s.id, false, 'X<5'}) +--- +- error: Failed to write to disk +... +errinj.set("ERRINJ_WAL_IO", false) +--- +- ok +... +_ = box.space._ck_constraint:insert({'CK_CONSTRAINT_01', s.id, false, 'X<5'}) +--- +... +box.execute("INSERT INTO \"test\" VALUES(5);") +--- +- error: 'Failed to execute SQL statement: CHECK constraint failed: CK_CONSTRAINT_01' +... +errinj.set("ERRINJ_WAL_IO", true) +--- +- ok +... +_ = box.space._ck_constraint:replace({'CK_CONSTRAINT_01', s.id, false, 'X<=5'}) +--- +- error: Failed to write to disk +... +errinj.set("ERRINJ_WAL_IO", false) +--- +- ok +... +_ = box.space._ck_constraint:replace({'CK_CONSTRAINT_01', s.id, false, 'X<=5'}) +--- +... +box.execute("INSERT INTO \"test\" VALUES(5);") +--- +- row_count: 1 +... +errinj.set("ERRINJ_WAL_IO", true) +--- +- ok +... +_ = box.space._ck_constraint:delete({'CK_CONSTRAINT_01', s.id}) +--- +- error: Failed to write to disk +... +box.execute("INSERT INTO \"test\" VALUES(6);") +--- +- error: 'Failed to execute SQL statement: CHECK constraint failed: CK_CONSTRAINT_01' +... +errinj.set("ERRINJ_WAL_IO", false) +--- +- ok +... +_ = box.space._ck_constraint:delete({'CK_CONSTRAINT_01', s.id}) +--- +... +box.execute("INSERT INTO \"test\" VALUES(6);") +--- +- row_count: 1 +... +s:drop() +--- +... +-- +-- Test that failed space alter doesn't harm ck constraints +-- +s = box.schema.create_space('test') +--- +... +_ = s:create_index('pk') +--- +... +s:format({{name='X', type='integer'}, {name='Y', type='integer'}}) +--- +... +_ = box.space._ck_constraint:insert({'XlessY', s.id, false, 'X < Y'}) +--- +... +_ = box.space._ck_constraint:insert({'Xgreater10', s.id, false, 'X > 10'}) +--- +... +box.execute("INSERT INTO \"test\" VALUES(2, 1);") +--- +- error: 'Failed to execute SQL statement: CHECK constraint failed: Xgreater10' +... +box.execute("INSERT INTO \"test\" VALUES(20, 10);") +--- +- error: 'Failed to execute SQL statement: CHECK constraint failed: XlessY' +... +box.execute("INSERT INTO \"test\" VALUES(20, 100);") +--- +- row_count: 1 +... +s:truncate() +--- +... +errinj.set("ERRINJ_WAL_IO", true) +--- +- ok +... +s:format({{name='Y', type='integer'}, {name='X', type='integer'}}) +--- +- error: Failed to write to disk +... +errinj.set("ERRINJ_WAL_IO", false) +--- +- ok +... +box.execute("INSERT INTO \"test\" VALUES(2, 1);") +--- +- error: 'Failed to execute SQL statement: CHECK constraint failed: XlessY' +... +box.execute("INSERT INTO \"test\" VALUES(20, 10);") +--- +- error: 'Failed to execute SQL statement: CHECK constraint failed: XlessY' +... +box.execute("INSERT INTO \"test\" VALUES(20, 100);") +--- +- row_count: 1 +... +s:drop() +--- +... diff --git a/test/sql/errinj.test.lua b/test/sql/errinj.test.lua index 1aff6d77e..7e5550541 100644 --- a/test/sql/errinj.test.lua +++ b/test/sql/errinj.test.lua @@ -149,3 +149,48 @@ box.execute("CREATE TABLE hello (id INT primary key,x INT,y INT);") dummy_f = function(int) return 1 end box.internal.sql_create_function("counter1", "INT", dummy_f, -1, false) errinj.set("ERRINJ_SQL_NAME_NORMALIZATION", false) + +-- +-- Tests which are aimed at verifying work of commit/rollback +-- triggers on _ck_constraint space. +-- +s = box.schema.space.create('test', {format = {{name = 'X', type = 'unsigned'}}}) +pk = box.space.test:create_index('pk') + +errinj.set("ERRINJ_WAL_IO", true) +_ = box.space._ck_constraint:insert({'CK_CONSTRAINT_01', s.id, false, 'X<5'}) +errinj.set("ERRINJ_WAL_IO", false) +_ = box.space._ck_constraint:insert({'CK_CONSTRAINT_01', s.id, false, 'X<5'}) +box.execute("INSERT INTO \"test\" VALUES(5);") +errinj.set("ERRINJ_WAL_IO", true) +_ = box.space._ck_constraint:replace({'CK_CONSTRAINT_01', s.id, false, 'X<=5'}) +errinj.set("ERRINJ_WAL_IO", false) +_ = box.space._ck_constraint:replace({'CK_CONSTRAINT_01', s.id, false, 'X<=5'}) +box.execute("INSERT INTO \"test\" VALUES(5);") +errinj.set("ERRINJ_WAL_IO", true) +_ = box.space._ck_constraint:delete({'CK_CONSTRAINT_01', s.id}) +box.execute("INSERT INTO \"test\" VALUES(6);") +errinj.set("ERRINJ_WAL_IO", false) +_ = box.space._ck_constraint:delete({'CK_CONSTRAINT_01', s.id}) +box.execute("INSERT INTO \"test\" VALUES(6);") +s:drop() + +-- +-- Test that failed space alter doesn't harm ck constraints +-- +s = box.schema.create_space('test') +_ = s:create_index('pk') +s:format({{name='X', type='integer'}, {name='Y', type='integer'}}) +_ = box.space._ck_constraint:insert({'XlessY', s.id, false, 'X < Y'}) +_ = box.space._ck_constraint:insert({'Xgreater10', s.id, false, 'X > 10'}) +box.execute("INSERT INTO \"test\" VALUES(2, 1);") +box.execute("INSERT INTO \"test\" VALUES(20, 10);") +box.execute("INSERT INTO \"test\" VALUES(20, 100);") +s:truncate() +errinj.set("ERRINJ_WAL_IO", true) +s:format({{name='Y', type='integer'}, {name='X', type='integer'}}) +errinj.set("ERRINJ_WAL_IO", false) +box.execute("INSERT INTO \"test\" VALUES(2, 1);") +box.execute("INSERT INTO \"test\" VALUES(20, 10);") +box.execute("INSERT INTO \"test\" VALUES(20, 100);") +s:drop() diff --git a/test/sql/gh-2981-check-autoinc.result b/test/sql/gh-2981-check-autoinc.result index 9e347ca6b..7384c81e8 100644 --- a/test/sql/gh-2981-check-autoinc.result +++ b/test/sql/gh-2981-check-autoinc.result @@ -29,7 +29,7 @@ box.execute("insert into t1 values (18, null);") ... box.execute("insert into t1(s2) values (null);") --- -- error: 'Failed to execute SQL statement: CHECK constraint failed: T1' +- error: 'Failed to execute SQL statement: CHECK constraint failed: CK_CONSTRAINT_1_T1' ... box.execute("insert into t2 values (18, null);") --- @@ -37,7 +37,7 @@ box.execute("insert into t2 values (18, null);") ... box.execute("insert into t2(s2) values (null);") --- -- error: 'Failed to execute SQL statement: CHECK constraint failed: T2' +- error: 'Failed to execute SQL statement: CHECK constraint failed: CK_CONSTRAINT_1_T2' ... box.execute("insert into t2 values (24, null);") --- @@ -45,7 +45,7 @@ box.execute("insert into t2 values (24, null);") ... box.execute("insert into t2(s2) values (null);") --- -- error: 'Failed to execute SQL statement: CHECK constraint failed: T2' +- error: 'Failed to execute SQL statement: CHECK constraint failed: CK_CONSTRAINT_1_T2' ... box.execute("insert into t3 values (9, null)") --- @@ -53,7 +53,7 @@ box.execute("insert into t3 values (9, null)") ... box.execute("insert into t3(s2) values (null)") --- -- error: 'Failed to execute SQL statement: CHECK constraint failed: T3' +- error: 'Failed to execute SQL statement: CHECK constraint failed: CK_CONSTRAINT_1_T3' ... box.execute("DROP TABLE t1") --- diff --git a/test/sql/types.result b/test/sql/types.result index bc4518c01..582785413 100644 --- a/test/sql/types.result +++ b/test/sql/types.result @@ -709,7 +709,7 @@ box.execute("CREATE TABLE t1 (id INT PRIMARY KEY, a BOOLEAN CHECK (a = true));") ... box.execute("INSERT INTO t1 VALUES (1, false);") --- -- error: 'Failed to execute SQL statement: CHECK constraint failed: T1' +- error: 'Failed to execute SQL statement: CHECK constraint failed: CK_CONSTRAINT_1_T1' ... box.execute("INSERT INTO t1 VALUES (1, true);") --- diff --git a/test/sql/upgrade.result b/test/sql/upgrade.result index 3a55f7c53..5da110c14 100644 --- a/test/sql/upgrade.result +++ b/test/sql/upgrade.result @@ -188,6 +188,25 @@ i[1].opts.sql == nil --- - true ... +box.space._space:get(s.id).flags.checks == nil +--- +- true +... +check = box.space._ck_constraint:select()[1] +--- +... +check ~= nil +--- +- true +... +check.name +--- +- CK_CONSTRAINT_1_T5 +... +check.expr_str +--- +- x < 2 +... s:drop() --- ... diff --git a/test/sql/upgrade.test.lua b/test/sql/upgrade.test.lua index b76a8f373..cfda74a08 100644 --- a/test/sql/upgrade.test.lua +++ b/test/sql/upgrade.test.lua @@ -62,6 +62,11 @@ s ~= nil i = box.space._index:select(s.id) i ~= nil i[1].opts.sql == nil +box.space._space:get(s.id).flags.checks == nil +check = box.space._ck_constraint:select()[1] +check ~= nil +check.name +check.expr_str s:drop() test_run:switch('default') diff --git a/test/wal_off/alter.result b/test/wal_off/alter.result index ee280fcbb..8040efa1a 100644 --- a/test/wal_off/alter.result +++ b/test/wal_off/alter.result @@ -28,7 +28,7 @@ end; ... #spaces; --- -- 65505 +- 65504 ... -- cleanup for k, v in pairs(spaces) do -- 2.21.0 ^ permalink raw reply [flat|nested] 25+ messages in thread
end of thread, other threads:[~2019-05-14 16:49 UTC | newest] Thread overview: 25+ messages (download: mbox.gz / follow: Atom feed) -- links below jump to the message on this page -- 2019-04-16 13:51 [tarantool-patches] [PATCH v3 0/3] box: run checks on insertions in LUA spaces Kirill Shcherbatov 2019-04-16 13:51 ` [tarantool-patches] [PATCH v3 1/3] schema: add new system space for CHECK constraints Kirill Shcherbatov 2019-04-25 20:38 ` [tarantool-patches] " n.pettik 2019-05-07 9:53 ` Kirill Shcherbatov 2019-05-12 13:45 ` n.pettik 2019-05-12 15:52 ` Kirill Shcherbatov 2019-05-12 23:04 ` n.pettik 2019-05-13 7:11 ` Kirill Shcherbatov 2019-05-13 12:29 ` n.pettik 2019-05-13 13:13 ` Vladislav Shpilevoy 2019-04-16 13:51 ` [tarantool-patches] [PATCH v3 2/3] box: run check constraint tests on space alter Kirill Shcherbatov 2019-04-25 20:38 ` [tarantool-patches] " n.pettik 2019-05-07 9:53 ` Kirill Shcherbatov 2019-05-07 16:39 ` Konstantin Osipov 2019-05-07 17:47 ` [tarantool-patches] " Kirill Shcherbatov 2019-05-07 20:28 ` Konstantin Osipov 2019-05-11 12:15 ` n.pettik 2019-05-12 21:12 ` Konstantin Osipov 2019-05-13 7:09 ` Kirill Shcherbatov 2019-05-13 7:49 ` Konstantin Osipov 2019-05-14 16:49 ` n.pettik 2019-04-16 13:51 ` [tarantool-patches] [PATCH v3 3/3] box: user-friendly interface to manage ck constraints Kirill Shcherbatov 2019-04-25 20:38 ` [tarantool-patches] " n.pettik 2019-05-07 9:53 ` Kirill Shcherbatov 2019-05-14 15:02 [tarantool-patches] [PATCH v3 0/3] box: run checks on insertions in LUA spaces Kirill Shcherbatov 2019-05-14 15:02 ` [tarantool-patches] [PATCH v3 1/3] schema: add new system space for CHECK constraints Kirill Shcherbatov
This is a public inbox, see mirroring instructions for how to clone and mirror all data and code used for this inbox