diff --git a/src/box/CMakeLists.txt b/src/box/CMakeLists.txt
index 0864c3433c1996a39489b4f57d4f0aec4980860d..481842a397e97a96e48257534aa004f2000dd1bf 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 ed9e55907530052c5f75a03b74a7ae8aeb7449e3..f72a2ed251df59e0477d954d34786e609b04ac50 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"
@@ -551,17 +552,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;
@@ -1404,6 +1394,79 @@ 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 new ck
+ * constraint objects in ::prepare method that is fault-tolerant.
+ * Finally in ::alter or ::rollback methods we only swap those
+ * lists securely.
+ */
+class RebuildCkConstraints: public AlterSpaceOp
+{
+	void space_swap_ck_constraint(struct space *old_space,
+				      struct space *new_space);
+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::space_swap_ck_constraint(struct space *old_space,
+					       struct space *new_space)
+{
+	rlist_swap(&new_space->ck_constraint, &ck_constraint);
+	rlist_swap(&ck_constraint, &old_space->ck_constraint);
+}
+
+void
+RebuildCkConstraints::alter(struct alter_space *alter)
+{
+	space_swap_ck_constraint(alter->old_space, alter->new_space);
+}
+
+void
+RebuildCkConstraints::rollback(struct alter_space *alter)
+{
+	space_swap_ck_constraint(alter->new_space, alter->old_space);
+}
+
+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);
+	}
+}
+
 /* }}} */
 
 /**
@@ -1769,6 +1832,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
@@ -1866,6 +1934,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);
@@ -2108,6 +2177,7 @@ 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 RebuildCkConstraints(alter);
 	/* Add an op to update schema_version on commit. */
 	(void) new UpdateSchemaVersion(alter);
 	alter_space_do(txn, alter);
@@ -2176,6 +2246,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;
 }
@@ -4088,6 +4159,167 @@ 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 space_id =
+		tuple_field_u32_xc(tuple, BOX_CK_CONSTRAINT_FIELD_SPACE_ID);
+	const char *language_str =
+		tuple_field_cstr_xc(tuple, BOX_CK_CONSTRAINT_FIELD_LANGUAGE);
+	enum ck_constraint_language language =
+		STR2ENUM(ck_constraint_language, language_str);
+	if (language == ck_constraint_language_MAX) {
+		tnt_raise(ClientError, ER_FUNCTION_LANGUAGE, language_str,
+			  tt_cstr(name, name_len));
+	}
+	uint32_t expr_str_len;
+	const char *expr_str =
+		tuple_field_str_xc(tuple, BOX_CK_CONSTRAINT_FIELD_CODE,
+				   &expr_str_len);
+	struct ck_constraint_def *ck_def =
+		ck_constraint_def_new(name, name_len, expr_str, expr_str_len,
+				      space_id, language);
+	if (ck_def == NULL)
+		diag_raise();
+	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->def->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([=] {
+			ck_constraint_def_delete(ck_def);
+		});
+		/*
+		 * FIXME: Ck constraint creation on non-empty
+		 * space is not implemented yet.
+		 */
+		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
 };
@@ -4156,4 +4388,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 4108fa47ccbe1699f82979bd8ebd4a4e0d461622..b9ba7b846e2e14d05d6c700e97ac9eb3a4584120 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 bb8fbeba114b1e72a5585548fb7f22796931d90f..fd7c7e16920d0de05799c9265d3cc461462aca2a 100644
Binary files a/src/box/bootstrap.snap and b/src/box/bootstrap.snap differ
diff --git a/src/box/ck_constraint.c b/src/box/ck_constraint.c
new file mode 100644
index 0000000000000000000000000000000000000000..e25f4506e976a78de93bc44439f8b3affcb53603
--- /dev/null
+++ b/src/box/ck_constraint.c
@@ -0,0 +1,146 @@
+/*
+ * 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 "box/session.h"
+#include "ck_constraint.h"
+#include "errcode.h"
+#include "sql.h"
+#include "sql/sqlInt.h"
+
+const char *ck_constraint_language_strs[] = {"SQL"};
+
+struct ck_constraint_def *
+ck_constraint_def_new(const char *name, uint32_t name_len, const char *expr_str,
+		      uint32_t expr_str_len, uint32_t space_id,
+		      enum ck_constraint_language language)
+{
+	uint32_t expr_str_offset;
+	uint32_t ck_def_sz = ck_constraint_def_sizeof(name_len, expr_str_len,
+						      &expr_str_offset);
+	struct ck_constraint_def *ck_def =
+		(struct ck_constraint_def *) malloc(ck_def_sz);
+	if (ck_def == NULL) {
+		diag_set(OutOfMemory, ck_def_sz, "malloc", "ck_def");
+		return NULL;
+	}
+	ck_def->expr_str = (char *)ck_def + expr_str_offset;
+	ck_def->language = language;
+	ck_def->space_id = space_id;
+	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;
+}
+
+void
+ck_constraint_def_delete(struct ck_constraint_def *ck_def)
+{
+	free(ck_def);
+}
+
+/**
+ * Resolve space_def references for check constraint via AST
+ * tree traversal.
+ * @param expr Check constraint AST 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(), default_flags);
+	parser.parse_only = true;
+	sql_resolve_self_reference(&parser, space_def, NC_IsCheck, expr);
+	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;
+	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);
+	ck_constraint_def_delete(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 0000000000000000000000000000000000000000..a5b7b2c71a66f78313676a0645bc7e53706ea32b
--- /dev/null
+++ b/src/box/ck_constraint.h
@@ -0,0 +1,203 @@
+#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;
+
+/** Supported languages of ck constraint. */
+enum ck_constraint_language {
+  CK_CONSTRAINT_LANGUAGE_SQL,
+  ck_constraint_language_MAX,
+};
+
+/** The supported languages strings.  */
+extern const char *ck_constraint_language_strs[];
+
+/**
+ * Check constraint definition.
+ * See ck_constraint_def_sizeof() definition for implementation
+ * details and memory layout.
+ */
+struct ck_constraint_def {
+	/**
+	 * The 0-terminated string that defines check constraint
+	 * expression.
+	 *
+	 * For instance: "field1 + field2 > 2 * 3".
+	 */
+	char *expr_str;
+	/**
+	 * The id of the space this check constraint is
+	 * defined for.
+	 */
+	uint32_t space_id;
+	/** The language of ck constraint. */
+	enum ck_constraint_language language;
+	/**
+	 * The 0-terminated string, a name of the check
+	 * constraint. Must be unique for a given space.
+	 */
+	char name[0];
+};
+
+/**
+ * Structure representing ck 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;
+	/**
+	 * 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] expr_str_offset The offset of the expr_str string.
+ * @return The size of the ck constraint definition object for
+ *         given parameters.
+ */
+static inline uint32_t
+ck_constraint_def_sizeof(uint32_t name_len, uint32_t expr_str_len,
+			 uint32_t *expr_str_offset)
+{
+	*expr_str_offset = sizeof(struct ck_constraint_def) + name_len + 1;
+	return *expr_str_offset + expr_str_len + 1;
+}
+
+/**
+ * Create a new check constraint definition object with given
+ * fields.
+ *
+ * @param name The name string of a new ck constraint definition.
+ * @param name_len The length of @a name string.
+ * @param expr The check expression string.
+ * @param expr_str_len The length of the @a expr string.
+ * @param space_id The identifier of the target space.
+ * @param language The language of the @a expr string.
+ * @retval not NULL Check constraint definition object pointer
+ *                  on success.
+ * @retval NULL Otherwise. The diag message is set.
+*/
+struct ck_constraint_def *
+ck_constraint_def_new(const char *name, uint32_t name_len, const char *expr,
+		      uint32_t expr_str_len, uint32_t space_id,
+		      enum ck_constraint_language language);
+
+/**
+ * Destroy check constraint definition memory, release acquired
+ * resources.
+ * @param ck_def The check constraint definition object to
+ *               destroy.
+ */
+void
+ck_constraint_def_delete(struct ck_constraint_def *ck_def);
+
+/**
+ * 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. The diag message is set.
+*/
+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 Ck constraint pointer if exists.
+ * @retval 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 9c15f3322560c4a9e5b3eff794ce3127d405a340..1f7c81693b2e921207c420fdf400f26fa8591e75 100644
--- a/src/box/errcode.h
+++ b/src/box/errcode.h
@@ -245,8 +245,9 @@ 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 ck 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 39bd8da6d1017b711f0fca5b9eb18c4f4e464723..91fae8378bbb424dfd4b6978c434d0bd8ba8a0cc 100644
--- a/src/box/lua/schema.lua
+++ b/src/box/lua/schema.lua
@@ -514,6 +514,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.is_generated == true then
         -- Delete automatically generated sequence.
@@ -525,6 +526,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.primary:pairs({space_id}) do
+        _ck_constraint:delete({space_id, t.name})
+    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 87adaeb163353a644f43c50253d202b84cb1d9a6..509306eaeecc30cf73a89d3be4c9497aeb08c8e6 100644
--- a/src/box/lua/space.cc
+++ b/src/box/lua/space.cc
@@ -552,6 +552,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 07066269870fc9b83c462c7061010ceba5bab9ab..4ff2efd3a2d4a2bd1a95884f44726784bfae6bbc 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)
@@ -774,8 +776,46 @@ local function upgrade_sequence_to_2_2_1()
     _space_sequence:format(format)
 end
 
+local function upgrade_ck_constraint_to_2_2_1()
+    -- 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._space
+    local _index = box.space._index
+    local _ck_constraint = box.space._ck_constraint
+    log.info("create space _ck_constraint")
+    local format = {{name='space_id', type='unsigned'},
+                    {name='name', type='string'},
+                    {name='is_deferred', type='boolean'},
+                    {name='language', type='str'}, {name='code', 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, 'unsigned'}, {1, 'string'}}}
+
+    for _, space in _space:pairs() do
+        local flags = space.flags
+        if flags.checks 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({space.id, check_name, false,
+                                       'SQL', expr_str})
+            end
+            flags.checks = nil
+            _space:replace({space.id, space.owner, space.name, space.engine,
+                            space.field_count, flags, space.format})
+        end
+    end
+end
+
 local function upgrade_to_2_2_1()
     upgrade_sequence_to_2_2_1()
+    upgrade_ck_constraint_to_2_2_1()
 end
 
 --------------------------------------------------------------------------------
diff --git a/src/box/schema.cc b/src/box/schema.cc
index 9a55c2f1438afa516ea8b5735ad43132f513d491..6d3153815c523f4d7ff6f73665c4d6f476337a13 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; /* space id */
+	key_parts[0].type = FIELD_TYPE_UNSIGNED;
+	key_parts[1].fieldno = 1; /* constraint name */
+	key_parts[1].type = FIELD_TYPE_STRING;
+	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 b817b49f65872ce8d49b33fafa5a28a5c8d8a07c..3648639bc773a85e0530fcfc0cca1039fa53d2cb 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 = 364,
 	/** End of the reserved range of system spaces. */
 	BOX_SYSTEM_ID_MAX = 511,
 	BOX_ID_NIL = 2147483647
@@ -240,6 +242,15 @@ enum {
 	BOX_FK_CONSTRAINT_FIELD_PARENT_COLS = 8,
 };
 
+/** _ck_constraint fields. */
+enum {
+	BOX_CK_CONSTRAINT_FIELD_SPACE_ID = 0,
+	BOX_CK_CONSTRAINT_FIELD_NAME = 1,
+	BOX_CK_CONSTRAINT_FIELD_DEFERRED = 2,
+	BOX_CK_CONSTRAINT_FIELD_LANGUAGE = 3,
+	BOX_CK_CONSTRAINT_FIELD_CODE = 4,
+};
+
 /*
  * Different objects which can be subject to access
  * control.
diff --git a/src/box/space.c b/src/box/space.c
index 243e7da2fcb653effd1058de38ab75e706c5c130..a42b3a64b357aaba7eb4b9b9b45068629855fb86 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 be1178b99292e4343c5ef15a94875370d8409c30..7ffe884b32f9507f5839ebc47ca604701644e039 100644
--- a/src/box/space.h
+++ b/src/box/space.h
@@ -197,6 +197,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 d825def75e0ae8eb86dd054f0da9008bb422d442..661131295a9ad23e55f51eecbf83dce46e2226f0 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 c6639a8d8c1ac51ab00ea562efb2570e4691631a..ac6d226c5b2b36151964f52356bc0930800708cb 100644
--- a/src/box/space_def.h
+++ b/src/box/space_def.h
@@ -40,8 +40,6 @@
 extern "C" {
 #endif /* defined(__cplusplus) */
 
-struct ExprList;
-
 /** Space options */
 struct space_opts {
 	/**
@@ -71,8 +69,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 f7b90ab5ce3a38697c571ae8b60ee8b20635b1d8..3d88ccbb4e3b39290161933b2d81312e93a6b563 100644
--- a/src/box/sql.c
+++ b/src/box/sql.c
@@ -1016,15 +1016,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);
@@ -1033,23 +1026,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,
@@ -1284,66 +1260,6 @@ 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(), default_flags);
-	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;
-}
-
 /**
  * Initialize a new vdbe_field_ref instance with given tuple
  * data.
diff --git a/src/box/sql.h b/src/box/sql.h
index 3056d8f7ecc59cdd150755805e5bcc6067042f4f..9ccecf28c244e9957793fe08eae68e7a69c57013 100644
--- a/src/box/sql.h
+++ b/src/box/sql.h
@@ -278,48 +278,10 @@ sql_expr_list_append(struct sql *db, struct ExprList *expr_list,
  * @param def The definition of space being referenced.
  * @param type NC_IsCheck or NC_IdxExpr.
  * @param expr Expression to resolve.  May be NULL.
- * @param expr_list Expression list to resolve.  May be NUL.
  */
 void
 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);
+			   int type, struct Expr *expr);
 
 /**
  * Initialize a new parser object.
diff --git a/src/box/sql/build.c b/src/box/sql/build.c
index e2353d8cc159e6d44692b8a1fea1949794ab0a09..b28e7b66f50f7ce012f8f35a1f8773b64c2c4c95 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,114 @@ sqlAddPrimaryKey(struct Parse *pParse)
 	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. When character ' or " was met, the string is copied
+ * without any changes until the next ' or " sign.
+ * The wptr buffer is expected to have str_len + 1 bytes
+ * (this is the expected scenario where no extra whitespace
+ * characters 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;
+	char quote_type = '\0';
+	bool is_prev_chr_space = false;
+	while (str < str_end) {
+		if (quote_type == '\0') {
+			if (*str == '\'' || *str == '\"') {
+				quote_type = *str;
+			} else if (isspace((unsigned char)*str)) {
+				if (!is_prev_chr_space)
+					*wptr++ = ' ';
+				is_prev_chr_space = true;
+				str++;
+				continue;
+			}
+		} else if (*str == quote_type) {
+			quote_type = '\0';
+		}
+		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;
+		(struct alter_entity_def *) 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)(expr_span->zEnd - expr_span->zStart);
+	const char *expr_str = expr_span->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 expr_str_offset;
+	uint32_t ck_def_sz = ck_constraint_def_sizeof(name_len, expr_str_len,
+						      &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->expr_str = (char *)ck_def + expr_str_offset;
+	ck_def->language = CK_CONSTRAINT_LANGUAGE_SQL;
+	ck_def->space_id = BOX_ID_NIL;
+	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);
 }
 
 /*
@@ -978,6 +1054,48 @@ emitNewSysSpaceSequenceRecord(Parse *pParse, int reg_space_id, int reg_seq_id,
 	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, 6);
+	sqlVdbeAddOp2(v, OP_SCopy, reg_space_id, ck_constraint_reg);
+	sqlVdbeAddOp4(v, OP_String8, 0, ck_constraint_reg + 1, 0,
+		      sqlDbStrDup(db, ck_def->name), P4_DYNAMIC);
+	sqlVdbeAddOp2(v, OP_Bool, false, ck_constraint_reg + 2);
+	sqlVdbeAddOp4(v, OP_String8, 0, ck_constraint_reg + 3, 0,
+		      ck_constraint_language_strs[ck_def->language], P4_STATIC);
+	sqlVdbeAddOp4(v, OP_String8, 0, ck_constraint_reg + 4, 0,
+		      sqlDbStrDup(db, ck_def->expr_str), P4_DYNAMIC);
+	sqlVdbeAddOp3(v, OP_MakeRecord, ck_constraint_reg, 5,
+		      ck_constraint_reg + 5);
+	const char *error_msg =
+		tt_sprintf(tnt_errcode_desc(ER_CONSTRAINT_EXISTS),
+					    ck_def->name);
+	if (vdbe_emit_halt_with_presence_test(parser, BOX_CK_CONSTRAINT_ID, 0,
+					      ck_constraint_reg, 2,
+					      ER_CONSTRAINT_EXISTS, error_msg,
+					      false, OP_NoConflict) != 0)
+		return;
+	sqlVdbeAddOp3(v, OP_SInsert, BOX_CK_CONSTRAINT_ID, 0,
+		      ck_constraint_reg + 5);
+	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);
+}
+
 /**
  * Generate opcodes to serialize foreign key into MsgPack and
  * insert produced tuple into _fk_constraint space.
@@ -1129,20 +1247,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;
 	}
 
 	/*
@@ -1232,9 +1348,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
@@ -1432,6 +1551,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 space_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);
+	sqlVdbeAddOp2(v, OP_Integer, space_id,  key_reg);
+	sqlVdbeAddOp4(v, OP_String8, 0, key_reg + 1, 0,
+		      sqlDbStrDup(db, ck_name), P4_DYNAMIC);
+	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,
@@ -1505,6 +1655,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.
@@ -2074,8 +2231,7 @@ index_fill_def(struct Parse *parse, struct index *index,
 	}
 	for (int i = 0; i < expr_list->nExpr; i++) {
 		struct Expr *expr = expr_list->a[i].pExpr;
-		sql_resolve_self_reference(parse, space_def, NC_IdxExpr,
-					   expr, 0);
+		sql_resolve_self_reference(parse, space_def, NC_IdxExpr, expr);
 		if (parse->is_aborted)
 			goto cleanup;
 
@@ -2791,8 +2947,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 81feca7e05f8ff8b0f6ed6a7895a208463d7d132..c4b629c6c16dcb6fcef750016e64be9483aa588e 100644
--- a/src/box/sql/insert.c
+++ b/src/box/sql/insert.c
@@ -36,9 +36,10 @@
 #include "sqlInt.h"
 #include "tarantoolInt.h"
 #include "vdbeInt.h"
-#include "box/schema.h"
+#include "box/ck_constraint.h"
 #include "bit/bit.h"
 #include "box/box.h"
+#include "box/schema.h"
 
 enum field_type *
 sql_index_type_str(struct sql *db, const struct index_def *idx_def)
@@ -918,34 +919,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);
 	/*
@@ -1230,14 +1224,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 f241b8d522cdb0a0b973822dc2e2eebbd25b20ac..e93dfe7518b1dc7b8ae40344f0a13d65c1a1877f 100644
--- a/src/box/sql/parse.y
+++ b/src/box/sql/parse.y
@@ -297,7 +297,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 5899a7e4e4062519d6f622d88d584a2a5b1a403a..6c1b6fddd4e70e5dbca8ff434485a5609ffb10a7 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/resolve.c b/src/box/sql/resolve.c
index 504096e6dd8ca0ca42ef072994e92739ffcad409..ac85b85a95fa05f2d23a584e564d095d04bdef81 100644
--- a/src/box/sql/resolve.c
+++ b/src/box/sql/resolve.c
@@ -1610,8 +1610,7 @@ sqlResolveSelectNames(Parse * pParse,	/* The parser context */
 
 void
 sql_resolve_self_reference(struct Parse *parser, struct space_def *def,
-			   int type, struct Expr *expr,
-			   struct ExprList *expr_list)
+			   int type, struct Expr *expr)
 {
 	/* Fake SrcList for parser->create_table_def */
 	SrcList sSrc;
@@ -1631,8 +1630,5 @@ sql_resolve_self_reference(struct Parse *parser, struct space_def *def,
 	sNC.pParse = parser;
 	sNC.pSrcList = &sSrc;
 	sNC.ncFlags = type;
-	if (sqlResolveExprNames(&sNC, expr) != 0)
-		return;
-	if (expr_list != NULL)
-		sqlResolveExprListNames(&sNC, expr_list);
+	sqlResolveExprNames(&sNC, expr);
 }
diff --git a/src/box/sql/select.c b/src/box/sql/select.c
index d7376ae1135756c7c6be3c2852a4633e07211db9..22aa628308f85c1ef21693befd65fb65a7084216 100644
--- a/src/box/sql/select.c
+++ b/src/box/sql/select.c
@@ -6440,7 +6440,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 7212553f612901f7724b56781badf870df84b6f3..abb7b9c2a5ef84e6a1af2a058b7bc20a428b2ba4 100644
--- a/src/box/sql/sqlInt.h
+++ b/src/box/sql/sqlInt.h
@@ -3359,7 +3359,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 11ef7b97edde07385b9f0a664ea99dad6ee05667..dbaebfefca713386895281dc373d0b016dafaf6c 100644
--- a/src/box/sql/tokenize.c
+++ b/src/box/sql/tokenize.c
@@ -435,7 +435,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 c1e1490ca1b3c2a430b6d77336eb8afa7ed80007..4777d8a6f567e8b55c2a3d8adf91d91d5e2fd34c 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", 48)
         end)
     end)
 
diff --git a/test/box-py/bootstrap.result b/test/box-py/bootstrap.result
index 0684914c01d95487d94ac536fc527da57a18ab22..21fcbea7e7321d0ec35bc494d1eb5401fa39c84a 100644
--- a/test/box-py/bootstrap.result
+++ b/test/box-py/bootstrap.result
@@ -79,6 +79,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'}]]
+  - [364, 1, '_ck_constraint', 'memtx', 0, {}, [{'name': 'space_id', 'type': 'unsigned'},
+      {'name': 'name', 'type': 'string'}, {'name': 'is_deferred', 'type': 'boolean'},
+      {'name': 'language', 'type': 'str'}, {'name': 'code', 'type': 'str'}]]
 ...
 box.space._index:select{}
 ---
@@ -131,6 +134,7 @@ 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']]]
+  - [364, 0, 'primary', 'tree', {'unique': true}, [[0, 'unsigned'], [1, 'string']]]
 ...
 box.space._user:select{}
 ---
diff --git a/test/box/access.result b/test/box/access.result
index 44a74e17cbe9d6faed7e3b14e79d5cbcc0b956e9..0ebc6a350e9450c4682d15dc815d3e3dea9d1c87 100644
--- a/test/box/access.result
+++ b/test/box/access.result
@@ -1493,6 +1493,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 ee408f53ef3ff76024293ae4f2f20a8e2fcb57e8..ad63a1016f169bd5111807a9f7a17ef5f5203e84 100644
--- a/test/box/access.test.lua
+++ b/test/box/access.test.lua
@@ -556,6 +556,7 @@ box.schema.user.grant('tester', 'read', 'space', '_sequence')
 box.schema.user.grant('tester', 'read', 'space', '_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 24bdd9d63ebedf60112d0819a894856167e568cd..b8ddcb53b14d644dd1877490e664c1c95cd7a69a 100644
--- a/test/box/access_misc.result
+++ b/test/box/access_misc.result
@@ -819,6 +819,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'}]]
+  - [364, 1, '_ck_constraint', 'memtx', 0, {}, [{'name': 'space_id', 'type': 'unsigned'},
+      {'name': 'name', 'type': 'string'}, {'name': 'is_deferred', 'type': 'boolean'},
+      {'name': 'language', 'type': 'str'}, {'name': 'code', 'type': 'str'}]]
 ...
 box.space._func:select()
 ---
diff --git a/test/box/access_sysview.result b/test/box/access_sysview.result
index c6a2b22ed272e0f9839706967edc9e4121b31fab..65a9da8a5901f6ba0bbd6c27d2aafff2101d6309 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
+- 49
 ...
 #box.space._vuser:select{}
 ---
@@ -262,7 +262,7 @@ box.session.su('guest')
 ...
 #box.space._vindex:select{}
 ---
-- 48
+- 49
 ...
 #box.space._vuser:select{}
 ---
diff --git a/test/box/alter.result b/test/box/alter.result
index e83c0b7effa1a2e39944f705531071232bf6be91..97dfe7c82e78e01fece6fbf3c03fb7e91d4f29fb 100644
--- a/test/box/alter.result
+++ b/test/box/alter.result
@@ -92,7 +92,7 @@ space = box.space[t[1]]
 ...
 space.id
 ---
-- 357
+- 365
 ...
 space.field_count
 ---
@@ -137,7 +137,7 @@ space_deleted
 ...
 space:replace{0}
 ---
-- error: Space '357' does not exist
+- error: Space '365' does not exist
 ...
 _index:insert{_space.id, 0, 'primary', 'tree', {unique=true}, {{0, 'unsigned'}}}
 ---
@@ -215,6 +215,7 @@ _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']]]
+  - [364, 0, 'primary', 'tree', {'unique': true}, [[0, 'unsigned'], [1, 'string']]]
 ...
 -- modify indexes of a system space
 _index:delete{_index.id, 0}
diff --git a/test/box/misc.result b/test/box/misc.result
index 4fcd13a78bec449f3f0aa889cecd8c2708ef5763..5bf419d4f1de55cff0a869c83145cd1e1dee3d50 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 b01afca7ca1b530a5b43490cd2b5d000bb50f7a1..ede77c63055caae570d9fbf725d3d4065eaaee68 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 ck 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 54e5059b3a03b87d4427ae18ca2eaaa14e8a2dc6..695a379a62347bff577c396bfedfb7ba27934f6f 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 9357c406bfd64b3327bcc3ae88e34fdbc0344efd..a8d39472d2e538e652b07dcd35474e38f019b89c 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 ck constraint definition"
 		-- </sql-errors-1.27>
 	})
 
diff --git a/test/sql-tap/table.test.lua b/test/sql-tap/table.test.lua
index 5b793c0fc98c8b1cd1854a0dcfad10e4409a16a4..066662f33ac7753e49c9420cffe4e3b6aa70be3c 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 f7cddec434268c67bb6a7de5408befa8a56e6858..efe6264288c2d41c95ddac7db5b95fb33ebff7f0 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,219 @@ 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({513, 'CK_CONSTRAINT_01', false, 'SQL', 'X><5'})
 ---
+- error: 'Failed to create check constraint ''CK_CONSTRAINT_01'': Syntax error near
+    ''<'''
 ...
-t = {513, 1, 'test', 'memtx', 0, opts, format}
+-- Non-existent space test.
+box.space._ck_constraint:insert({550, 'CK_CONSTRAINT_01', false, 'SQL', 'X<5'})
 ---
+- error: Space '550' does not exist
 ...
-s = box.space._space:insert(t)
+-- Pass integer instead of expression.
+box.space._ck_constraint:insert({513, 'CK_CONSTRAINT_01', false, 'SQL', 666})
+---
+- error: 'Tuple field 5 type does not match one required by operation: expected string'
+...
+-- Deferred CK constraints are not supported.
+box.space._ck_constraint:insert({513, 'CK_CONSTRAINT_01', true, 'SQL', 'X<5'})
+---
+- error: Tarantool does not support deferred ck constraints
+...
+-- The only supported language is SQL.
+box.space._ck_constraint:insert({513, 'CK_CONSTRAINT_01', false, 'LUA', 'X<5'})
+---
+- error: Unsupported language 'LUA' specified for function 'CK_CONSTRAINT_01'
+...
+-- Check constraints LUA creation test.
+box.space._ck_constraint:insert({513, 'CK_CONSTRAINT_01', false, 'SQL', 'X<5'})
+---
+- [513, 'CK_CONSTRAINT_01', false, 'SQL', '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({513, 'CK_CONSTRAINT_01', false, 'SQL', 'X<=5'})
+---
+- [513, 'CK_CONSTRAINT_01', false, 'SQL', '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'
 ...
-box.space._space:delete(513)
+-- 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({513, 'CK_CONSTRAINT_01'})
+---
+- [513, 'CK_CONSTRAINT_01', false, 'SQL', '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({s.id, 'physics', false, 'SQL', '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({s.id, 'physics', false, 'SQL', '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({s.id, 'conflict', false, 'SQL', 'X>10'})
+---
+- error: 'Failed to create check constraint ''conflict'': referencing space must be
+    empty'
+...
+s:truncate()
+---
+...
+_ = box.space._ck_constraint:insert({s.id, 'conflict', false, 'SQL', 'X>10'})
 ---
-- error: 'Wrong space options (field 5): invalid MsgPack map field ''name'' type'
+...
+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()
+---
+...
+box.execute("CREATE TABLE T2(ID INT PRIMARY KEY, CONSTRAINT CK1 CHECK(ID > 0), CONSTRAINT CK1 CHECK(ID < 0))")
+---
+- error: Constraint CK1 already exists
+...
+box.space.T2:drop()
+---
+...
+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 ck constraint definition'
 ...
 box.execute("DROP TABLE w2;")
 ---
@@ -123,22 +255,33 @@ 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)'
+- error: 'Failed to create check constraint ''CK_CONSTRAINT_1_T5'': bindings are not
+    allowed in DDL'
 ...
-opts = {checks = {{expr = '?>5', name = 'ONE'}}}
+-- Test trim CK constraint code correctness.
+box.execute("CREATE TABLE t1(x TEXT PRIMARY KEY CHECK(x    LIKE     '1  a'))")
 ---
+- row_count: 1
 ...
-format = {{name = 'X', type = 'unsigned'}}
+box.space._ck_constraint:select()[1].code
 ---
+- x LIKE '1  a'
 ...
-t = {513, 1, 'test', 'memtx', 0, opts, format}
+box.execute("INSERT INTO t1 VALUES('1 a')")
 ---
+- error: 'Failed to execute SQL statement: CHECK constraint failed: CK_CONSTRAINT_1_T1'
 ...
-s = box.space._space:insert(t)
+box.execute("INSERT INTO t1 VALUES('1   a')")
+---
+- error: 'Failed to execute SQL statement: CHECK constraint failed: CK_CONSTRAINT_1_T1'
+...
+box.execute("INSERT INTO t1 VALUES('1  a')")
+---
+- row_count: 1
+...
+box.execute("DROP TABLE t1")
 ---
-- error: 'Wrong space options (field 5): invalid expression specified (bindings are
-    not allowed in DDL)'
+- row_count: 1
 ...
 test_run:cmd("clear filter")
 ---
diff --git a/test/sql/checks.test.lua b/test/sql/checks.test.lua
index 5bfcf12f8dbe2a918352ffe3ff37647572aab5d6..399caf134658c669d95752034aa48ba5d9727879 100644
--- a/test/sql/checks.test.lua
+++ b/test/sql/checks.test.lua
@@ -8,41 +8,81 @@ 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({513, 'CK_CONSTRAINT_01', false, 'SQL', 'X><5'})
+-- Non-existent space test.
+box.space._ck_constraint:insert({550, 'CK_CONSTRAINT_01', false, 'SQL', 'X<5'})
+-- Pass integer instead of expression.
+box.space._ck_constraint:insert({513, 'CK_CONSTRAINT_01', false, 'SQL', 666})
+-- Deferred CK constraints are not supported.
+box.space._ck_constraint:insert({513, 'CK_CONSTRAINT_01', true, 'SQL', 'X<5'})
+-- The only supported language is SQL.
+box.space._ck_constraint:insert({513, 'CK_CONSTRAINT_01', false, 'LUA', '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({513, 'CK_CONSTRAINT_01', false, 'SQL', '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({513, 'CK_CONSTRAINT_01', false, 'SQL', '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({513, 'CK_CONSTRAINT_01'})
+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({s.id, 'physics', false, 'SQL', 'X<Y'})
+s:format({{name='X', type='integer'}, {name='Y', type='integer'}})
+_ = box.space._ck_constraint:insert({s.id, 'physics', false, 'SQL', '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({s.id, 'conflict', false, 'SQL', 'X>10'})
+s:truncate()
+_ = box.space._ck_constraint:insert({s.id, 'conflict', false, 'SQL', '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.T2:drop()
+box.space._ck_constraint:select()
 
 --
 -- gh-3611: Segfault on table creation with check referencing this table
@@ -55,10 +95,12 @@ 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 trim CK constraint code correctness.
+box.execute("CREATE TABLE t1(x TEXT PRIMARY KEY CHECK(x    LIKE     '1  a'))")
+box.space._ck_constraint:select()[1].code
+box.execute("INSERT INTO t1 VALUES('1 a')")
+box.execute("INSERT INTO t1 VALUES('1   a')")
+box.execute("INSERT INTO t1 VALUES('1  a')")
+box.execute("DROP TABLE t1")
 
 test_run:cmd("clear filter")
diff --git a/test/sql/errinj.result b/test/sql/errinj.result
index ee36a387b8030397165a6edfb0dfa3586b5a4e68..414e3c476b84c2e0fd8ea03addcccc17570cf702 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({s.id, 'CK_CONSTRAINT_01', false, 'SQL', 'X<5'})
+---
+- error: Failed to write to disk
+...
+errinj.set("ERRINJ_WAL_IO", false)
+---
+- ok
+...
+_ = box.space._ck_constraint:insert({s.id, 'CK_CONSTRAINT_01', false, 'SQL', '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({s.id, 'CK_CONSTRAINT_01', false, 'SQL', 'X<=5'})
+---
+- error: Failed to write to disk
+...
+errinj.set("ERRINJ_WAL_IO", false)
+---
+- ok
+...
+_ = box.space._ck_constraint:replace({s.id, 'CK_CONSTRAINT_01', false, 'SQL', 'X<=5'})
+---
+...
+box.execute("INSERT INTO \"test\" VALUES(5);")
+---
+- row_count: 1
+...
+errinj.set("ERRINJ_WAL_IO", true)
+---
+- ok
+...
+_ = box.space._ck_constraint:delete({s.id, 'CK_CONSTRAINT_01'})
+---
+- 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({s.id, 'CK_CONSTRAINT_01'})
+---
+...
+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({s.id, 'XlessY', false, 'SQL', 'X < Y'})
+---
+...
+_ = box.space._ck_constraint:insert({s.id, 'Xgreater10', false, 'SQL', 'X > 10'})
+---
+...
+box.execute("INSERT INTO \"test\" VALUES(1, 2);")
+---
+- 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(1, 2);")
+---
+- 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:drop()
+---
+...
diff --git a/test/sql/errinj.test.lua b/test/sql/errinj.test.lua
index 1aff6d77eb728ac630d15b3cab54ff25f3988e61..48b80a4430eceea4b85061392e984b89174a3314 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({s.id, 'CK_CONSTRAINT_01', false, 'SQL', 'X<5'})
+errinj.set("ERRINJ_WAL_IO", false)
+_ = box.space._ck_constraint:insert({s.id, 'CK_CONSTRAINT_01', false, 'SQL', 'X<5'})
+box.execute("INSERT INTO \"test\" VALUES(5);")
+errinj.set("ERRINJ_WAL_IO", true)
+_ = box.space._ck_constraint:replace({s.id, 'CK_CONSTRAINT_01', false, 'SQL', 'X<=5'})
+errinj.set("ERRINJ_WAL_IO", false)
+_ = box.space._ck_constraint:replace({s.id, 'CK_CONSTRAINT_01', false, 'SQL', 'X<=5'})
+box.execute("INSERT INTO \"test\" VALUES(5);")
+errinj.set("ERRINJ_WAL_IO", true)
+_ = box.space._ck_constraint:delete({s.id, 'CK_CONSTRAINT_01'})
+box.execute("INSERT INTO \"test\" VALUES(6);")
+errinj.set("ERRINJ_WAL_IO", false)
+_ = box.space._ck_constraint:delete({s.id, 'CK_CONSTRAINT_01'})
+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({s.id, 'XlessY', false, 'SQL', 'X < Y'})
+_ = box.space._ck_constraint:insert({s.id, 'Xgreater10', false, 'SQL', 'X > 10'})
+box.execute("INSERT INTO \"test\" VALUES(1, 2);")
+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(1, 2);")
+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 9e347ca6ba3d1245cd7eb62a437dd17dcd03d89c..7384c81e8b07b69a7715f010390a4f50b88f6825 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 a53d6f7cec240adabca369e966161db21549d411..1c9ef5468ee91636487f4580a9e303350fef8731 100644
--- a/test/sql/types.result
+++ b/test/sql/types.result
@@ -741,7 +741,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 3a55f7c533641c92ff79c858b774555fd9a0879c..f0997e17f80e70d8b6245a06abec2192957002f6 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.code
+---
+- x < 2
+...
 s:drop()
 ---
 ...
diff --git a/test/sql/upgrade.test.lua b/test/sql/upgrade.test.lua
index b76a8f37358784da51e2993b4f19e67173ddec99..37425ae21428718666e2f7e31f7d7c5b5972bb8c 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.code
 s:drop()
 
 test_run:switch('default')
diff --git a/test/wal_off/alter.result b/test/wal_off/alter.result
index ee280fcbbd1d8d488bd497d4e059a8a7168c166c..8040efa1ac71d2571342fadf7f713e1a322cb5bc 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