diff --git a/changelogs/unreleased/gh-8098-show-create-table.md b/changelogs/unreleased/gh-8098-show-create-table.md
new file mode 100644
index 0000000000000000000000000000000000000000..c4f42ea04a6d75e1af51a18194296965a07444f3
--- /dev/null
+++ b/changelogs/unreleased/gh-8098-show-create-table.md
@@ -0,0 +1,3 @@
+## feature/sql
+
+* Introduced the `SHOW CREATE TABLE` statement (gh-8098).
diff --git a/extra/mkkeywordhash.c b/extra/mkkeywordhash.c
index 8c392e0edd760633b70a563b67e3afd1db4bfe8c..12379554ffab532aadf6d9e56b62b4debdc26dc3 100644
--- a/extra/mkkeywordhash.c
+++ b/extra/mkkeywordhash.c
@@ -257,6 +257,7 @@ static Keyword aKeywordTable[] = {
   { "BOTH",                   "TK_BOTH",        true  },
   { "INTERVAL",               "TK_INTERVAL",    true  },
   { "SEQSCAN",                "TK_SEQSCAN",     false },
+  { "SHOW",                   "TK_SHOW",        false },
 };
 
 /* Number of keywords */
diff --git a/src/box/CMakeLists.txt b/src/box/CMakeLists.txt
index ede060b2ca8af6c051620c18ee9f5cf59e1109ee..a7661670d7323c73dd5362a36d1691cf24690b2c 100644
--- a/src/box/CMakeLists.txt
+++ b/src/box/CMakeLists.txt
@@ -64,6 +64,7 @@ set(sql_sources
     sql/resolve.c
     sql/port.c
     sql/select.c
+    sql/show.c
     sql/tokenize.c
     sql/treeview.c
     sql/trigger.c
diff --git a/src/box/sql.h b/src/box/sql.h
index ccc47e6b2c5ed75b4a40a42e6d88b1b80cb8687c..de0e8bada768d79789d65c2d5bb9c8e003886584 100644
--- a/src/box/sql.h
+++ b/src/box/sql.h
@@ -413,6 +413,11 @@ vdbe_field_ref_create(struct vdbe_field_ref *ref, uint32_t capacity);
 bool
 func_sql_expr_has_single_arg(const struct func *base, const char *name);
 
+/** Check that all SQL EXPR function arguments exist in the space definition. */
+bool
+func_sql_expr_check_fields(const struct func *base,
+			   const struct space_def *def);
+
 /** Returns the SQL flags used during session initialization. */
 uint32_t
 sql_default_session_flags(void);
diff --git a/src/box/sql/build.c b/src/box/sql/build.c
index a11593ac771af1079c01ad5f2c1a4d2c630692dc..f0e7041a8d4e8b198a254a8969164971e202a935 100644
--- a/src/box/sql/build.c
+++ b/src/box/sql/build.c
@@ -3395,3 +3395,69 @@ sql_setting_set(struct Parse *parse_context, struct Token *name,
 	sqlVdbeAddOp4(vdbe, OP_SetSession, target, 0, 0, key, P4_DYNAMIC);
 	return;
 }
+
+/** Emit VDBE instructions for "SHOW CREATE TABLE table_name;" statement. */
+void
+sql_emit_show_create_table_one(struct Parse *parse, struct Token *name)
+{
+	struct Vdbe *v = sqlGetVdbe(parse);
+	char *space_name = sql_name_from_token(name);
+	sqlVdbeSetNumCols(v, 2);
+	vdbe_metadata_set_col_name(v, 0, "STATEMENTS");
+	vdbe_metadata_set_col_type(v, 0, "array");
+	vdbe_metadata_set_col_name(v, 1, "ERRORS");
+	vdbe_metadata_set_col_type(v, 1, "array");
+
+	int cursor = parse->nTab++;
+	int space_reg = ++parse->nMem;
+	int name_reg = ++parse->nMem;
+	sqlVdbeAddOp2(v, OP_OpenSpace, space_reg, BOX_VSPACE_ID);
+	sqlVdbeAddOp3(v, OP_IteratorOpen, cursor, 2, space_reg);
+	sqlVdbeChangeP5(v, OPFLAG_SEEKEQ);
+	sqlVdbeAddOp4(v, OP_String8, 0, name_reg, 0, space_name, P4_DYNAMIC);
+	int addr1 = sqlVdbeAddOp4Int(v, OP_SeekGE, cursor, 0, name_reg, 1);
+	int addr2 = sqlVdbeAddOp4Int(v, OP_IdxGT, cursor, 0, name_reg, 1);
+	int space_id_reg = ++parse->nMem;
+	sqlVdbeAddOp3(v, OP_Column, cursor, BOX_SPACE_FIELD_ID, space_id_reg);
+	int result_reg = ++parse->nMem;
+	++parse->nMem;
+	sqlVdbeAddOp2(v, OP_ShowCreateTable, space_id_reg, result_reg);
+	sqlVdbeAddOp2(v, OP_ResultRow, result_reg, 2);
+	int addr3 = sqlVdbeAddOp0(v, OP_Goto);
+	sqlVdbeJumpHere(v, addr1);
+	sqlVdbeJumpHere(v, addr2);
+
+	char *err = sqlMPrintf(tnt_errcode_desc(ER_NO_SUCH_SPACE), space_name);
+	sqlVdbeAddOp4(v, OP_SetDiag, ER_NO_SUCH_SPACE, 0, 0, err, P4_DYNAMIC);
+	sqlVdbeAddOp2(v, OP_Halt, -1, ON_CONFLICT_ACTION_ABORT);
+	sqlVdbeJumpHere(v, addr3);
+}
+
+/** Emit VDBE instructions for "SHOW CREATE TABLE;" statement. */
+void
+sql_emit_show_create_table_all(struct Parse *parse)
+{
+	struct Vdbe *v = sqlGetVdbe(parse);
+	sqlVdbeSetNumCols(v, 2);
+	vdbe_metadata_set_col_name(v, 0, "STATEMENTS");
+	vdbe_metadata_set_col_type(v, 0, "array");
+	vdbe_metadata_set_col_name(v, 1, "ERRORS");
+	vdbe_metadata_set_col_type(v, 1, "array");
+
+	int cursor = parse->nTab++;
+	int space_reg = ++parse->nMem;
+	int key_reg = ++parse->nMem;
+	sqlVdbeAddOp2(v, OP_OpenSpace, space_reg, BOX_VSPACE_ID);
+	sqlVdbeAddOp3(v, OP_IteratorOpen, cursor, 0, space_reg);
+	sqlVdbeAddOp2(v, OP_Integer, BOX_SYSTEM_ID_MAX, key_reg);
+	int addr1 = sqlVdbeAddOp4Int(v, OP_SeekGT, cursor, 0, key_reg, 1);
+	int space_id_reg = ++parse->nMem;
+	int addr2 = sqlVdbeAddOp3(v, OP_Column, cursor, BOX_SPACE_FIELD_ID,
+				  space_id_reg);
+	int result_reg = ++parse->nMem;
+	++parse->nMem;
+	sqlVdbeAddOp2(v, OP_ShowCreateTable, space_id_reg, result_reg);
+	sqlVdbeAddOp2(v, OP_ResultRow, result_reg, 2);
+	sqlVdbeAddOp2(v, OP_Next, cursor, addr2);
+	sqlVdbeJumpHere(v, addr1);
+}
diff --git a/src/box/sql/func.c b/src/box/sql/func.c
index af611e9df33d34038f85112d2ef54267e59c92b6..de0333929dea12573cc75e31e54b625e178d5be4 100644
--- a/src/box/sql/func.c
+++ b/src/box/sql/func.c
@@ -2510,3 +2510,24 @@ func_sql_expr_has_single_arg(const struct func *base, const char *name)
 	}
 	return true;
 }
+
+bool
+func_sql_expr_check_fields(const struct func *base, const struct space_def *def)
+{
+	assert(base->def->language == FUNC_LANGUAGE_SQL_EXPR);
+	struct func_sql_expr *func = (struct func_sql_expr *)base;
+	struct Vdbe *v = func->stmt;
+	for (int i = 0; i < v->nOp; ++i) {
+		if (v->aOp[i].opcode != OP_FetchByName)
+			continue;
+		const char *name = v->aOp[i].p4.z;
+		bool is_exists = false;
+		for (size_t j = 0; j < def->field_count && !is_exists; ++j) {
+			const char *field_name = def->fields[j].name;
+			is_exists = strcmp(name, field_name) == 0;
+		}
+		if (!is_exists)
+			return false;
+	}
+	return true;
+}
diff --git a/src/box/sql/parse.y b/src/box/sql/parse.y
index 4274ffb5d332ad81cad0c009a0989206b8cf66d4..0e1ea5c186a73f3ca09c90f19602b033ec50d4f7 100644
--- a/src/box/sql/parse.y
+++ b/src/box/sql/parse.y
@@ -255,7 +255,7 @@ columnlist ::= tcons.
   CONFLICT DEFERRED END ENGINE FAIL
   IGNORE INITIALLY INSTEAD NO MATCH PLAN
   QUERY KEY OFFSET RAISE RELEASE REPLACE RESTRICT
-  RENAME CTIME_KW IF ENABLE DISABLE UUID
+  RENAME CTIME_KW IF ENABLE DISABLE UUID SHOW
   .
 %wildcard WILDCARD.
 
@@ -1498,6 +1498,15 @@ cmd ::= FUNCTION_KW(T) expr(E). {
   pParse->parsed_ast_type = AST_TYPE_EXPR;
   pParse->parsed_ast.expr = sqlExprDup(E.pExpr, 0);
 }
+
+//////////////////////////// The SHOW CREATE TABLE command /////////////////////
+cmd ::= SHOW CREATE TABLE nm(X). {
+  sql_emit_show_create_table_one(pParse, &X);
+}
+cmd ::= SHOW CREATE TABLE. {
+  sql_emit_show_create_table_all(pParse);
+}
+
 //////////////////////////// The CREATE TRIGGER command /////////////////////
 
 cmd ::= createkw trigger_decl(A) BEGIN trigger_cmd_list(S) END(Z). {
diff --git a/src/box/sql/show.c b/src/box/sql/show.c
new file mode 100644
index 0000000000000000000000000000000000000000..668c2f1a1d18d98a4f276342a987f4d7c11123d3
--- /dev/null
+++ b/src/box/sql/show.c
@@ -0,0 +1,496 @@
+/*
+ * SPDX-License-Identifier: BSD-2-Clause
+ *
+ * Copyright 2010-2023, Tarantool AUTHORS, please see AUTHORS file.
+ */
+#include <ctype.h>
+
+#include "sqlInt.h"
+#include "mem.h"
+#include "box/schema.h"
+#include "box/sequence.h"
+#include "box/coll_id_cache.h"
+#include "box/tuple_constraint_def.h"
+
+/** An objected used to accumulate a statement. */
+struct sql_desc {
+	/** Accumulate the string representation of the statement. */
+	struct StrAccum acc;
+	/** MEM where the array of compiled statements should be inserted. */
+	struct Mem *ret;
+	/** MEM where the array of compiled errors should be inserted. */
+	struct Mem *err;
+	/** Array of compiled but not encoded statements. */
+	char **statements;
+	/** Array of compiled but not encoded errors. */
+	char **errors;
+	/** Number of compiled statements. */
+	uint32_t statement_count;
+	/** Number of compiled errors. */
+	uint32_t error_count;
+};
+
+/** Initialize the object used to accumulate a statement. */
+static void
+sql_desc_initialize(struct sql_desc *desc, struct Mem *ret, struct Mem *err)
+{
+	sqlStrAccumInit(&desc->acc, NULL, 0, SQL_MAX_LENGTH);
+	desc->statements = NULL;
+	desc->statement_count = 0;
+	desc->errors = NULL;
+	desc->error_count = 0;
+	desc->ret = ret;
+	desc->err = err;
+}
+
+/** Append a new string to the object used to accumulate a statement. */
+CFORMAT(printf, 2, 3) static void
+sql_desc_append(struct sql_desc *desc, const char *fmt, ...)
+{
+	va_list ap;
+	va_start(ap, fmt);
+	sqlVXPrintf(&desc->acc, fmt, ap);
+	va_end(ap);
+}
+
+/** Append a name to the object used to accumulate a statement. */
+static void
+sql_desc_append_name(struct sql_desc *desc, const char *name)
+{
+	char *escaped = sql_escaped_name_new(name);
+	assert(escaped[0] == '"' && escaped[strlen(escaped) - 1] == '"');
+	char *normalized = sql_normalized_name_new(name, strlen(name));
+	if (isalpha(name[0]) && strlen(escaped) == strlen(name) + 2 &&
+	    strcmp(normalized, name) == 0)
+		sqlXPrintf(&desc->acc, "%s", normalized);
+	else
+		sqlXPrintf(&desc->acc, "%s", escaped);
+	sql_xfree(normalized);
+	sql_xfree(escaped);
+}
+
+/** Append a new error to the object used to accumulate a statement. */
+static void
+sql_desc_error(struct sql_desc *desc, const char *type, const char *name,
+	       const char *error)
+{
+	char *str = sqlMPrintf("Problem with %s '%s': %s.", type, name, error);
+	uint32_t id = desc->error_count;
+	++desc->error_count;
+	uint32_t size = desc->error_count * sizeof(desc->errors);
+	desc->errors = sql_xrealloc(desc->errors, size);
+	desc->errors[id] = str;
+}
+
+/** Complete the statement and add it to the array of compiled statements. */
+static void
+sql_desc_finish_statement(struct sql_desc *desc)
+{
+	char *str = sqlStrAccumFinish(&desc->acc);
+	sqlStrAccumInit(&desc->acc, NULL, 0, SQL_MAX_LENGTH);
+	uint32_t id = desc->statement_count;
+	++desc->statement_count;
+	uint32_t size = desc->statement_count * sizeof(desc->statements);
+	desc->statements = sql_xrealloc(desc->statements, size);
+	desc->statements[id] = str;
+}
+
+/** Finalize a described statement. */
+static void
+sql_desc_finalize(struct sql_desc *desc)
+{
+	sqlStrAccumReset(&desc->acc);
+	if (desc->error_count > 0) {
+		uint32_t size = mp_sizeof_array(desc->error_count);
+		for (uint32_t i = 0; i < desc->error_count; ++i)
+			size += mp_sizeof_str(strlen(desc->errors[i]));
+		char *buf = sql_xmalloc(size);
+		char *end = mp_encode_array(buf, desc->error_count);
+		for (uint32_t i = 0; i < desc->error_count; ++i) {
+			end = mp_encode_str0(end, desc->errors[i]);
+			sql_xfree(desc->errors[i]);
+		}
+		sql_xfree(desc->errors);
+		assert(end - buf == size);
+		mem_set_array_allocated(desc->err, buf, size);
+	} else {
+		mem_set_null(desc->err);
+	}
+
+	uint32_t size = mp_sizeof_array(desc->statement_count);
+	for (uint32_t i = 0; i < desc->statement_count; ++i)
+		size += mp_sizeof_str(strlen(desc->statements[i]));
+	char *buf = sql_xmalloc(size);
+	char *end = mp_encode_array(buf, desc->statement_count);
+	for (uint32_t i = 0; i < desc->statement_count; ++i) {
+		end = mp_encode_str0(end, desc->statements[i]);
+		sql_xfree(desc->statements[i]);
+	}
+	sql_xfree(desc->statements);
+	assert(end - buf == size);
+	mem_set_array_allocated(desc->ret, buf, size);
+}
+
+/** Add a field foreign key constraint to the statement description. */
+static void
+sql_describe_field_foreign_key(struct sql_desc *desc,
+			       const struct tuple_constraint_def *cdef)
+{
+	assert(cdef->type == CONSTR_FKEY && cdef->fkey.field_mapping_size == 0);
+	const struct space *foreign_space = space_by_id(cdef->fkey.space_id);
+	assert(foreign_space != NULL);
+	const struct tuple_constraint_field_id *field = &cdef->fkey.field;
+	if (field->name_len == 0 &&
+	    field->id >= foreign_space->def->field_count) {
+		const char *err = "foreign field is unnamed";
+		sql_desc_error(desc, "foreign key", cdef->name, err);
+		return;
+	}
+
+	const char *field_name = field->name_len > 0 ? field->name :
+				 foreign_space->def->fields[field->id].name;
+	sql_desc_append(desc, " CONSTRAINT ");
+	sql_desc_append_name(desc, cdef->name);
+	sql_desc_append(desc, " REFERENCES ");
+	sql_desc_append_name(desc, foreign_space->def->name);
+	sql_desc_append(desc, "(");
+	sql_desc_append_name(desc, field_name);
+	sql_desc_append(desc, ")");
+}
+
+/** Add a tuple foreign key constraint to the statement description. */
+static void
+sql_describe_tuple_foreign_key(struct sql_desc *desc,
+			       const struct space_def *def, int i)
+{
+	struct tuple_constraint_def *cdef = &def->opts.constraint_def[i];
+	assert(cdef->type == CONSTR_FKEY && cdef->fkey.field_mapping_size > 0);
+	const struct space *foreign_space = space_by_id(cdef->fkey.space_id);
+	assert(foreign_space != NULL);
+	bool is_error = false;
+	for (uint32_t i = 0; i < cdef->fkey.field_mapping_size; ++i) {
+		const struct tuple_constraint_field_id *field =
+			&cdef->fkey.field_mapping[i].local_field;
+		if (field->name_len == 0 &&
+		    field->id >= foreign_space->def->field_count) {
+			sql_desc_error(desc, "foreign key", cdef->name,
+				       "local field is unnamed");
+			is_error = true;
+		}
+		field = &cdef->fkey.field_mapping[i].foreign_field;
+		if (field->name_len == 0 &&
+		    field->id >= foreign_space->def->field_count) {
+			sql_desc_error(desc, "foreign key", cdef->name,
+				       "foreign field is unnamed");
+			is_error = true;
+		}
+	}
+	if (is_error)
+		return;
+
+	assert(def->field_count != 0);
+	sql_desc_append(desc, ",\nCONSTRAINT ");
+	sql_desc_append_name(desc, cdef->name);
+	sql_desc_append(desc, " FOREIGN KEY(");
+	for (uint32_t i = 0; i < cdef->fkey.field_mapping_size; ++i) {
+		const struct tuple_constraint_field_id *field =
+			&cdef->fkey.field_mapping[i].local_field;
+		const char *field_name = field->name_len != 0 ? field->name :
+					 def->fields[field->id].name;
+		if (i > 0)
+			sql_desc_append(desc, ", ");
+		sql_desc_append_name(desc, field_name);
+	}
+
+	sql_desc_append(desc, ") REFERENCES ");
+	sql_desc_append_name(desc, foreign_space->def->name);
+	sql_desc_append(desc, "(");
+	for (uint32_t i = 0; i < cdef->fkey.field_mapping_size; ++i) {
+		const struct tuple_constraint_field_id *field =
+			&cdef->fkey.field_mapping[i].foreign_field;
+		assert(field->name_len != 0 || field->id < def->field_count);
+		const char *field_name = field->name_len != 0 ? field->name :
+					 def->fields[field->id].name;
+		if (i > 0)
+			sql_desc_append(desc, ", ");
+		sql_desc_append_name(desc, field_name);
+	}
+	sql_desc_append(desc, ")");
+}
+
+/** Add a field check constraint to the statement description. */
+static void
+sql_describe_field_check(struct sql_desc *desc, const char *field_name,
+			 const struct tuple_constraint_def *cdef)
+{
+	assert(cdef->type == CONSTR_FUNC);
+	const struct func *func = func_by_id(cdef->func.id);
+	if (func->def->language != FUNC_LANGUAGE_SQL_EXPR) {
+		sql_desc_error(desc, "check constraint", cdef->name,
+			       "wrong constraint expression");
+		return;
+	}
+	if (!func_sql_expr_has_single_arg(func, field_name)) {
+		sql_desc_error(desc, "check constraint", cdef->name,
+			       "wrong field name in constraint expression");
+		return;
+	}
+
+	sql_desc_append(desc, " CONSTRAINT ");
+	sql_desc_append_name(desc, cdef->name);
+	sql_desc_append(desc, " CHECK(%s)", func->def->body);
+}
+
+/** Add a tuple check constraint to the statement description. */
+static void
+sql_describe_tuple_check(struct sql_desc *desc, const struct space_def *def,
+			 int i)
+{
+	struct tuple_constraint_def *cdef = &def->opts.constraint_def[i];
+	assert(cdef->type == CONSTR_FUNC);
+	const struct func *func = func_by_id(cdef->func.id);
+	if (func->def->language != FUNC_LANGUAGE_SQL_EXPR) {
+		sql_desc_error(desc, "check constraint", cdef->name,
+			       "wrong constraint expression");
+		return;
+	}
+	if (!func_sql_expr_check_fields(func, def)) {
+		sql_desc_error(desc, "check constraint", cdef->name,
+			       "wrong field name in constraint expression");
+		return;
+	}
+	if (i != 0 || def->field_count != 0)
+		sql_desc_append(desc, ",");
+	sql_desc_append(desc, "\nCONSTRAINT ");
+	sql_desc_append_name(desc, cdef->name);
+	sql_desc_append(desc, " CHECK(%s)", func->def->body);
+}
+
+/** Add a field to the statement description. */
+static void
+sql_describe_field(struct sql_desc *desc, const struct field_def *field)
+{
+	sql_desc_append(desc, "\n");
+	sql_desc_append_name(desc, field->name);
+	char *field_type = strtoupperdup(field_type_strs[field->type]);
+	sql_desc_append(desc, " %s", field_type);
+	free(field_type);
+
+	if (field->coll_id != 0) {
+		struct coll_id *coll_id = coll_by_id(field->coll_id);
+		if (coll_id == NULL) {
+			sql_desc_error(desc, "collation",
+				       tt_sprintf("%d", field->coll_id),
+				       "collation does not exist");
+		} else {
+			sql_desc_append(desc, " COLLATE ");
+			sql_desc_append_name(desc, coll_id->name);
+		}
+	}
+	if (!field->is_nullable)
+		sql_desc_append(desc, " NOT NULL");
+	if (field->default_value != NULL)
+		sql_desc_append(desc, " DEFAULT(%s)", field->default_value);
+	for (uint32_t i = 0; i < field->constraint_count; ++i) {
+		struct tuple_constraint_def *cdef = &field->constraint_def[i];
+		assert(cdef->type == CONSTR_FKEY || cdef->type == CONSTR_FUNC);
+		if (cdef->type == CONSTR_FKEY)
+			sql_describe_field_foreign_key(desc, cdef);
+		else
+			sql_describe_field_check(desc, field->name, cdef);
+	}
+}
+
+/** Add a primary key to the statement description. */
+static void
+sql_describe_primary_key(struct sql_desc *desc, const struct space *space)
+{
+	if (space->index_count == 0) {
+		const char *err = "primary key is not defined";
+		sql_desc_error(desc, "space", space->def->name, err);
+		return;
+	}
+
+	const struct index *pk = space->index[0];
+	assert(pk->def->opts.is_unique);
+	bool is_error = false;
+	if (pk->def->type != TREE) {
+		const char *err = "primary key has unsupported index type";
+		sql_desc_error(desc, "space", space->def->name, err);
+		is_error = true;
+	}
+
+	for (uint32_t i = 0; i < pk->def->key_def->part_count; ++i) {
+		uint32_t fieldno = pk->def->key_def->parts[i].fieldno;
+		if (fieldno >= space->def->field_count) {
+			const char *err = tt_sprintf("field %u is unnamed",
+						     fieldno + 1);
+			sql_desc_error(desc, "primary key", pk->def->name, err);
+			is_error = true;
+			continue;
+		}
+		struct field_def *field = &space->def->fields[fieldno];
+		if (pk->def->key_def->parts[i].type != field->type) {
+			const char *err =
+				tt_sprintf("field '%s' and related part are of "
+					   "different types", field->name);
+			sql_desc_error(desc, "primary key", pk->def->name, err);
+			is_error = true;
+		}
+		if (pk->def->key_def->parts[i].coll_id != field->coll_id) {
+			const char *err =
+				tt_sprintf("field '%s' and related part have "
+					   "different collations", field->name);
+			sql_desc_error(desc, "primary key", pk->def->name, err);
+			is_error = true;
+		}
+	}
+	if (is_error)
+		return;
+
+	bool has_sequence = false;
+	if (space->sequence != NULL) {
+		struct sequence_def *sdef = space->sequence->def;
+		if (sdef->step != 1 || sdef->min != 0 || sdef->start != 1 ||
+		    sdef->max != INT64_MAX || sdef->cache != 0 || sdef->cycle ||
+		    strcmp(sdef->name, space->def->name) != 0) {
+			const char *err = "unsupported sequence definition";
+			sql_desc_error(desc, "sequence", sdef->name, err);
+		} else if (space->sequence_fieldno > space->def->field_count) {
+			const char *err =
+				"sequence is attached to unnamed field";
+			sql_desc_error(desc, "sequence", sdef->name, err);
+		} else {
+			has_sequence = true;
+		}
+	}
+
+	sql_desc_append(desc, ",\nCONSTRAINT ");
+	sql_desc_append_name(desc, pk->def->name);
+	sql_desc_append(desc, " PRIMARY KEY(");
+	for (uint32_t i = 0; i < pk->def->key_def->part_count; ++i) {
+		uint32_t fieldno = pk->def->key_def->parts[i].fieldno;
+		if (i > 0)
+			sql_desc_append(desc, ", ");
+		sql_desc_append_name(desc, space->def->fields[fieldno].name);
+		if (has_sequence && fieldno == space->sequence_fieldno)
+			sql_desc_append(desc, " AUTOINCREMENT");
+	}
+	sql_desc_append(desc, ")");
+}
+
+/** Add a index to the statement description. */
+static void
+sql_describe_index(struct sql_desc *desc, const struct space *space,
+		   const struct index *index)
+{
+	assert(index != NULL);
+	bool is_error = false;
+	if (index->def->type != TREE) {
+		const char *err = "unsupported index type";
+		sql_desc_error(desc, "index", index->def->name, err);
+		is_error = true;
+	}
+	for (uint32_t i = 0; i < index->def->key_def->part_count; ++i) {
+		uint32_t fieldno = index->def->key_def->parts[i].fieldno;
+		if (fieldno >= space->def->field_count) {
+			const char *err = tt_sprintf("field %u is unnamed",
+						     fieldno + 1);
+			sql_desc_error(desc, "index", index->def->name, err);
+			is_error = true;
+			continue;
+		}
+		struct field_def *field = &space->def->fields[fieldno];
+		if (index->def->key_def->parts[i].type != field->type) {
+			const char *err =
+				tt_sprintf("field '%s' and related part are of "
+					   "different types", field->name);
+			sql_desc_error(desc, "index", index->def->name, err);
+			is_error = true;
+		}
+		if (index->def->key_def->parts[i].coll_id != field->coll_id) {
+			const char *err =
+				tt_sprintf("field '%s' and related part have "
+					   "different collations", field->name);
+			sql_desc_error(desc, "index", index->def->name, err);
+			is_error = true;
+		}
+	}
+	if (is_error)
+		return;
+
+	if (!index->def->opts.is_unique)
+		sql_desc_append(desc, "CREATE INDEX ");
+	else
+		sql_desc_append(desc, "CREATE UNIQUE INDEX ");
+	sql_desc_append_name(desc, index->def->name);
+	sql_desc_append(desc, " ON ");
+	sql_desc_append_name(desc, space->def->name);
+	sql_desc_append(desc, "(");
+	for (uint32_t i = 0; i < index->def->key_def->part_count; ++i) {
+		uint32_t fieldno = index->def->key_def->parts[i].fieldno;
+		if (i > 0)
+			sql_desc_append(desc, ", ");
+		sql_desc_append_name(desc, space->def->fields[fieldno].name);
+	}
+	sql_desc_append(desc, ");");
+	sql_desc_finish_statement(desc);
+}
+
+/** Add the table to the statement description. */
+static void
+sql_describe_table(struct sql_desc *desc, const struct space *space)
+{
+	struct space_def *def = space->def;
+	sql_desc_append(desc, "CREATE TABLE ");
+	sql_desc_append_name(desc, def->name);
+
+	if (def->field_count + def->opts.constraint_count > 0)
+		sql_desc_append(desc, "(");
+
+	if (def->field_count == 0)
+		sql_desc_error(desc, "space", def->name, "format is missing");
+	else
+		sql_describe_field(desc, &def->fields[0]);
+	for (uint32_t i = 1; i < def->field_count; ++i) {
+		sql_desc_append(desc, ",");
+		sql_describe_field(desc, &def->fields[i]);
+	}
+
+	sql_describe_primary_key(desc, space);
+
+	for (uint32_t i = 0; i < def->opts.constraint_count; ++i) {
+		assert(def->opts.constraint_def[i].type == CONSTR_FKEY ||
+		       def->opts.constraint_def[i].type == CONSTR_FUNC);
+		if (def->opts.constraint_def[i].type == CONSTR_FKEY)
+			sql_describe_tuple_foreign_key(desc, def, i);
+		else
+			sql_describe_tuple_check(desc, def, i);
+	}
+
+	if (def->field_count + def->opts.constraint_count > 0)
+		sql_desc_append(desc, ")");
+
+	if (space_is_memtx(space))
+		sql_desc_append(desc, "\nWITH ENGINE = 'memtx'");
+	else if (space_is_vinyl(space))
+		sql_desc_append(desc, "\nWITH ENGINE = 'vinyl'");
+	else
+		sql_desc_error(desc, "space", def->name, "wrong space engine");
+	sql_desc_append(desc, ";");
+	sql_desc_finish_statement(desc);
+}
+
+void
+sql_show_create_table(uint32_t space_id, struct Mem *ret, struct Mem *err)
+{
+	struct space *space = space_by_id(space_id);
+	assert(space != NULL);
+
+	struct sql_desc desc;
+	sql_desc_initialize(&desc, ret, err);
+	sql_describe_table(&desc, space);
+	for (uint32_t i = 1; i < space->index_count; ++i)
+		sql_describe_index(&desc, space, space->index[i]);
+	sql_desc_finalize(&desc);
+}
diff --git a/src/box/sql/sqlInt.h b/src/box/sql/sqlInt.h
index 8ab70fa6cd3ee419587eb2d270572fc6171ff760..578bc8e526b01e9c0fce0f21a353ba30fee016d3 100644
--- a/src/box/sql/sqlInt.h
+++ b/src/box/sql/sqlInt.h
@@ -2504,6 +2504,13 @@ sql_normalized_name_new(const char *name, int len);
 char *
 sql_normalized_name_region_new(struct region *r, const char *name, int len);
 
+/**
+ * Return an escaped version of the original name in memory allocated with
+ * sql_xmalloc().
+ */
+char *
+sql_escaped_name_new(const char *name);
+
 int sqlKeywordCode(const unsigned char *, int);
 int sqlRunParser(Parse *, const char *);
 
@@ -2651,6 +2658,18 @@ void
 sqlPragma(struct Parse *pParse, struct Token *pragma, struct Token *table,
 	  struct Token *index);
 
+/** Emit VDBE instructions for "SHOW CREATE TABLE table_name;" statement. */
+void
+sql_emit_show_create_table_one(struct Parse *parse, struct Token *name);
+
+/** Emit VDBE instructions for "SHOW CREATE TABLE;" statement. */
+void
+sql_emit_show_create_table_all(struct Parse *parse);
+
+/** Generate a CREATE TABLE statement for the space with the given ID. */
+void
+sql_show_create_table(uint32_t space_id, struct Mem *ret, struct Mem *err);
+
 /**
  * Return true if given column is part of primary key.
  * If field number is less than 63, corresponding bit
diff --git a/src/box/sql/util.c b/src/box/sql/util.c
index f333f44fde562570f74df39e3580f095770ca6db..8a9379332f20f778256a42274985d61f10da11a3 100644
--- a/src/box/sql/util.c
+++ b/src/box/sql/util.c
@@ -1244,3 +1244,25 @@ sqlVListNameToNum(VList * pIn, const char *zName, int nName)
 	} while (i < mx);
 	return 0;
 }
+
+char *
+sql_escaped_name_new(const char *name)
+{
+	size_t len = strlen(name);
+	size_t count = 0;
+	for (size_t i = 0; i < len; ++i) {
+		if (name[i] == '"')
+			++count;
+	}
+	size_t size = len + count + 2;
+	char *buf = sql_xmalloc(size + 1);
+	buf[0] = '"';
+	for (size_t i = 0, j = 1; i < len; ++i) {
+		buf[j++] = name[i];
+		if (name[i] == '"')
+			buf[j++] = '"';
+	}
+	buf[size - 1] = '"';
+	buf[size] = '\0';
+	return buf;
+}
diff --git a/src/box/sql/vdbe.c b/src/box/sql/vdbe.c
index e3c18e32a2d3bbbc8bea7e7fdf5398c4f73cf4da..93246148b8ee8dd8343861b57a9a3187ce91d299 100644
--- a/src/box/sql/vdbe.c
+++ b/src/box/sql/vdbe.c
@@ -4398,6 +4398,21 @@ case OP_SetSession: {
 	break;
 }
 
+/**
+ * Opcode: ShowCreateTable P1 P2 * * *
+ * Synopsis: r[P2, P2 + 1]=description of a space with ID == r[P1]
+ *
+ * Set the space description with the identifier from register P1 to register
+ * P2. All errors detected during the construction of the description are set to
+ * register P2 + 1.
+ */
+case OP_ShowCreateTable: {
+	struct Mem *ret = &aMem[pOp->p2];
+	struct Mem *err = &aMem[pOp->p2 + 1];
+	sql_show_create_table(aMem[pOp->p1].u.i, ret, err);
+	break;
+}
+
 /* Opcode: Noop * * * * *
  *
  * Do nothing.  This instruction is often useful as a jump
diff --git a/test/sql-luatest/show_create_table_test.lua b/test/sql-luatest/show_create_table_test.lua
new file mode 100644
index 0000000000000000000000000000000000000000..e20102931e0f4e7662bfa11b403cc2052ea1c824
--- /dev/null
+++ b/test/sql-luatest/show_create_table_test.lua
@@ -0,0 +1,521 @@
+local server = require('luatest.server')
+local t = require('luatest')
+
+local g = t.group()
+
+g.before_all(function()
+    g.server = server:new({alias = 'master'})
+    g.server:start()
+    g.server:exec(function()
+        rawset(_G, 'check', function(table_raw_name, res, err)
+            local sql = 'SHOW CREATE TABLE '..table_raw_name..';'
+            local ret = box.execute(sql)
+            t.assert_equals(ret.rows[1][1], res)
+            t.assert_equals(ret.rows[1][2], err)
+        end)
+    end)
+end)
+
+g.after_all(function()
+    g.server:stop()
+end)
+
+g.test_show_create_table_one = function()
+    g.server:exec(function()
+        local _, err = box.execute('SHOW CREATE TABLE t;')
+        t.assert_equals(err.message, [[Space 'T' does not exist]])
+
+        box.execute('CREATE TABLE t(i INT PRIMARY KEY, a INT);')
+        local res = {'CREATE TABLE T(\nI INTEGER NOT NULL,\nA INTEGER,\n'..
+                     'CONSTRAINT "pk_unnamed_T_1" PRIMARY KEY(I))\n'..
+                     "WITH ENGINE = 'memtx';"}
+        _G.check('t', res)
+        box.execute('DROP TABLE t;')
+
+        local sql = [[CREATE TABLE t(i INT CONSTRAINT c0 PRIMARY KEY,
+                                     a STRING CONSTRAINT c1 REFERENCES t(i)
+                                     CONSTRAINT c2 UNIQUE,
+                                     b UUID NOT NULL DEFAULT(uuid()),
+                                     CONSTRAINT c3 CHECK(i * a < 100),
+                                     CONSTRAINT c4 UNIQUE (a, b),
+                                     CONSTRAINT c5 FOREIGN KEY(i, a)
+                                     REFERENCES t(a, b))
+                      WITH ENGINE = 'vinyl';]]
+        box.execute(sql)
+        res = {'CREATE TABLE T(\nI INTEGER NOT NULL,\n'..
+               'A STRING CONSTRAINT C1 REFERENCES T(I),\n'..
+               'B UUID NOT NULL DEFAULT(uuid()),\n'..
+               'CONSTRAINT C0 PRIMARY KEY(I),\n'..
+               'CONSTRAINT C3 CHECK(i * a < 100),\n'..
+               'CONSTRAINT C5 FOREIGN KEY(I, A) REFERENCES T(A, B))\n'..
+               "WITH ENGINE = 'vinyl';",
+               'CREATE UNIQUE INDEX C2 ON T(A);',
+               'CREATE UNIQUE INDEX C4 ON T(A, B);'}
+        _G.check('t', res)
+        box.execute('DROP TABLE t;')
+
+        -- Make sure SHOW, INCLUDING and ERRORS can be used as names.
+        sql = [[CREATE TABLE show(a INT PRIMARY KEY, b INT);]]
+        local ret = box.execute(sql)
+        t.assert(ret ~= nil);
+        box.execute([[DROP TABLE show;]])
+    end)
+end
+
+g.test_show_create_table_all = function()
+    g.server:exec(function()
+        local res = box.execute('SHOW CREATE TABLE;')
+        t.assert_equals(res.rows, {})
+
+        -- Make sure that a description of all non-system spaces is displayed.
+        box.execute('CREATE TABLE t1(i INT PRIMARY KEY, a INT);')
+        box.execute('CREATE TABLE t2(i INT PRIMARY KEY, a INT);')
+        box.execute('CREATE TABLE t3(i INT PRIMARY KEY, a INT);')
+        box.schema.space.create('a')
+        local ret = box.execute('SHOW CREATE TABLE;')
+        local res1 = {'CREATE TABLE T1(\nI INTEGER NOT NULL,\nA INTEGER,\n'..
+                      'CONSTRAINT "pk_unnamed_T1_1" PRIMARY KEY(I))\n'..
+                      "WITH ENGINE = 'memtx';"}
+        local res2 = {'CREATE TABLE T2(\nI INTEGER NOT NULL,\nA INTEGER,\n'..
+                      'CONSTRAINT "pk_unnamed_T2_1" PRIMARY KEY(I))\n'..
+                      "WITH ENGINE = 'memtx';"}
+        local res3 = {'CREATE TABLE T3(\nI INTEGER NOT NULL,\nA INTEGER,\n'..
+                      'CONSTRAINT "pk_unnamed_T3_1" PRIMARY KEY(I))\n'..
+                      "WITH ENGINE = 'memtx';"}
+        local res4 = {"CREATE TABLE \"a\"\nWITH ENGINE = 'memtx';"}
+        local err4 = {"Problem with space 'a': format is missing.",
+                      "Problem with space 'a': primary key is not defined."}
+        t.assert_equals(#ret.rows, 4)
+        t.assert_equals(ret.rows[1][1], res1)
+        t.assert_equals(ret.rows[1][2])
+        t.assert_equals(ret.rows[2][1], res2)
+        t.assert_equals(ret.rows[2][2])
+        t.assert_equals(ret.rows[3][1], res3)
+        t.assert_equals(ret.rows[3][2])
+        t.assert_equals(ret.rows[4][1], res4)
+        t.assert_equals(ret.rows[4][2], err4)
+
+        box.space.a:drop()
+        box.execute('DROP TABLE t1;')
+        box.execute('DROP TABLE t2;')
+        box.execute('DROP TABLE t3;')
+    end)
+end
+
+g.test_space_from_lua = function()
+    g.server:exec(function()
+        -- Working example.
+        local s = box.schema.space.create('a', {format = {{'i', 'integer'}}})
+        s:create_index('i', {parts = {{'i', 'integer'}}})
+        local res = {'CREATE TABLE "a"(\n"i" INTEGER NOT NULL,\n'..
+                     'CONSTRAINT "i" PRIMARY KEY("i"))\n'..
+                     "WITH ENGINE = 'memtx';"}
+        _G.check('"a"', res)
+        s:drop()
+
+        -- No columns defined.
+        s = box.schema.space.create('a');
+        s:create_index('i', {parts = {{1, 'integer'}}})
+        res = {"CREATE TABLE \"a\"\nWITH ENGINE = 'memtx';"}
+        local err = {"Problem with space 'a': format is missing.",
+                     "Problem with primary key 'i': field 1 is unnamed."}
+        _G.check('"a"', res, err)
+        s:drop()
+
+        -- No indexes defined.
+        s = box.schema.space.create('a', {format = {{'i', 'integer'}}})
+        res = {'CREATE TABLE "a"(\n"i" INTEGER NOT NULL)\n'..
+               "WITH ENGINE = 'memtx';"}
+        err = {"Problem with space 'a': primary key is not defined."}
+        _G.check('"a"', res, err)
+        s:drop()
+
+        -- Unsupported type of index.
+        s = box.schema.space.create('a', {format = {{'i', 'integer'}}})
+        s:create_index('i', {type = 'hash', parts = {{'i', 'integer'}}})
+        res = {'CREATE TABLE "a"(\n"i" INTEGER NOT NULL)\n'..
+               "WITH ENGINE = 'memtx';"}
+        err = {"Problem with space 'a': primary key has unsupported index "..
+               "type."}
+        _G.check('"a"', res, err)
+        s:drop()
+
+        -- Parts of PK contains unnnamed columns.
+        s = box.schema.space.create('a', {format = {{'i', 'integer'}}})
+        s:create_index('i', {parts = {{2, 'integer'}}})
+        res = {'CREATE TABLE "a"(\n"i" INTEGER NOT NULL)\n'..
+               "WITH ENGINE = 'memtx';"}
+        err = {"Problem with primary key 'i': field 2 is unnamed."}
+        _G.check('"a"', res, err)
+        s:drop()
+
+        -- Type of the part in PK different from type of the field.
+        s = box.schema.space.create('a', {format = {{'i', 'integer'}}})
+        s:create_index('i', {parts = {{'i', 'unsigned'}}})
+        res = {'CREATE TABLE "a"(\n"i" INTEGER NOT NULL)\n'..
+               "WITH ENGINE = 'memtx';"}
+        err = {"Problem with primary key 'i': field 'i' and related part are "..
+               "of different types."}
+        _G.check('"a"', res, err)
+        s:drop()
+
+        -- Collation of the part in PK different from collation of the field.
+        s = box.schema.space.create('a', {format = {{'i', 'string',
+                                          collation = "unicode_ci"}}})
+        s:create_index('i', {parts = {{'i', 'string', collation = "binary"}}})
+        res = {'CREATE TABLE "a"(\n"i" STRING COLLATE "unicode_ci" '..
+               "NOT NULL)\nWITH ENGINE = 'memtx';"}
+        err = {"Problem with primary key 'i': field 'i' and related part "..
+               "have different collations."}
+        _G.check('"a"', res, err)
+        s:drop()
+
+        --
+        -- Spaces with an engine other than "memtx" and "vinyl" cannot be
+        -- created with CREATE TABLE.
+        --
+        res = {'CREATE TABLE "_vspace"(\n'..
+               '"id" UNSIGNED NOT NULL,\n'..
+               '"owner" UNSIGNED NOT NULL,\n'..
+               '"name" STRING NOT NULL,\n'..
+               '"engine" STRING NOT NULL,\n'..
+               '"field_count" UNSIGNED NOT NULL,\n'..
+               '"flags" MAP NOT NULL,\n'..
+               '"format" ARRAY NOT NULL,\n'..
+               'CONSTRAINT "primary" PRIMARY KEY("id"));',
+               'CREATE INDEX "owner" ON "_vspace"("owner");',
+               'CREATE UNIQUE INDEX "name" ON "_vspace"("name");'}
+        err = {"Problem with space '_vspace': wrong space engine."}
+        _G.check('"_vspace"', res, err)
+
+        -- Make sure the table, field, and PK names are properly escaped.
+        s = box.schema.space.create('"A"', {format = {{'"i', 'integer'}}})
+        s:create_index('123', {parts = {{'"i', 'integer'}}})
+        res = {'CREATE TABLE """A"""(\n"""i" INTEGER NOT NULL,\n'..
+               'CONSTRAINT "123" PRIMARY KEY("""i"))\nWITH ENGINE = \'memtx\';'}
+        _G.check('"""A"""', res)
+        s:drop()
+    end)
+end
+
+g.test_field_foreign_key_from_lua = function()
+    g.server:exec(function()
+        local format = {{'i', 'integer'}}
+        box.schema.space.create('a', {format = format})
+
+        -- Working example.
+        format[1].foreign_key = {a = {space = 'a', field = 'i'}}
+        box.schema.space.create('b', {format = format})
+        box.space.b:create_index('i', {parts = {{'i', 'integer'}}})
+        local res = {'CREATE TABLE "b"(\n"i" INTEGER NOT NULL '..
+                     'CONSTRAINT "a" REFERENCES "a"("i"),\n'..
+                     'CONSTRAINT "i" PRIMARY KEY("i"))\n'..
+                     "WITH ENGINE = 'memtx';"}
+        _G.check('"b"', res)
+
+        -- Wrong foreign field defined by id in foreign_key.
+        format[1].foreign_key.a = {space = 'a', field = 5}
+        box.space.b:format(format)
+        local err = {"Problem with foreign key 'a': foreign field is unnamed."}
+        res = {'CREATE TABLE "b"(\n"i" INTEGER NOT NULL,\n'..
+               'CONSTRAINT "i" PRIMARY KEY("i"))\nWITH ENGINE = \'memtx\';'}
+        _G.check('"b"', res, err)
+
+        -- Make sure field foreign key constraint name is properly escaped.
+        format[1].foreign_key = {['"'] = {space = 'a', field = 'i'}}
+        box.space.b:format(format)
+        res = {'CREATE TABLE "b"(\n"i" INTEGER NOT NULL '..
+               'CONSTRAINT """" REFERENCES "a"("i"),\n'..
+               'CONSTRAINT "i" PRIMARY KEY("i"))\nWITH ENGINE = \'memtx\';'}
+        _G.check('"b"', res)
+
+        box.space.b:drop()
+        box.space.a:drop()
+    end)
+end
+
+g.test_tuple_foreign_key_from_lua = function()
+    g.server:exec(function()
+        local opts = {format = {{'i', 'integer'}}}
+        box.schema.space.create('a', opts)
+
+        -- Working example.
+        opts.foreign_key = {a = {space = 'a', field = {i = 'i'}}}
+        box.schema.space.create('b', opts)
+        box.space.b:create_index('i', {parts = {{'i', 'integer'}}})
+        local res = {'CREATE TABLE "b"(\n"i" INTEGER NOT NULL,\n'..
+                     'CONSTRAINT "i" PRIMARY KEY("i"),\n'..
+                     'CONSTRAINT "a" FOREIGN KEY("i") REFERENCES "a"("i"))\n'..
+                     "WITH ENGINE = 'memtx';"}
+        _G.check('"b"', res)
+
+        -- Wrong foreign field defined by id in foreign_key.
+        opts.foreign_key.a = {space = 'a', field = {[5] = 'i'}}
+        box.space.b:alter(opts)
+        res = {'CREATE TABLE "b"(\n"i" INTEGER NOT NULL,\n'..
+               'CONSTRAINT "i" PRIMARY KEY("i"))\nWITH ENGINE = \'memtx\';'}
+        local err = {"Problem with foreign key 'a': local field is unnamed."}
+        _G.check('"b"', res, err)
+
+        -- Wrong foreign field defined by id in foreign_key.
+        opts.foreign_key.a = {space = 'a', field = {i = 5}}
+        box.space.b:alter(opts)
+        err = {"Problem with foreign key 'a': foreign field is unnamed."}
+        res = {'CREATE TABLE "b"(\n"i" INTEGER NOT NULL,\n'..
+               'CONSTRAINT "i" PRIMARY KEY("i"))\nWITH ENGINE = \'memtx\';'}
+        _G.check('"b"', res, err)
+
+        -- Make sure tuple foreign key constraint name is properly escaped.
+        opts.foreign_key = {['a"b"c'] = {space = 'a', field = {i = 'i'}}}
+        box.space.b:alter(opts)
+        res = {'CREATE TABLE "b"(\n"i" INTEGER NOT NULL,\n'..
+               'CONSTRAINT "i" PRIMARY KEY("i"),\n'..
+               'CONSTRAINT "a""b""c" FOREIGN KEY("i") REFERENCES "a"("i"))\n'..
+               "WITH ENGINE = 'memtx';"}
+        _G.check('"b"', res)
+
+        box.space.b:drop()
+        box.space.a:drop()
+    end)
+end
+
+g.test_field_check_from_lua = function()
+    g.server:exec(function()
+        box.schema.func.create('f', {body = '"i" > 10', language = 'SQL_EXPR',
+                                     is_deterministic = true})
+        box.schema.func.create('f1', {body = 'function(a) return a > 10 end',
+                                     is_deterministic = true})
+        box.schema.func.create('f2', {body = '"b" > 10', language = 'SQL_EXPR',
+                                     is_deterministic = true})
+
+        -- Working example.
+        local format = {{'i', 'integer', constraint = {a = 'f'}}}
+        box.schema.space.create('a', {format = format})
+        box.space.a:create_index('i', {parts = {{'i', 'integer'}}})
+        local res = {'CREATE TABLE "a"(\n"i" INTEGER NOT NULL '..
+                     'CONSTRAINT "a" CHECK("i" > 10),\n'..
+                     'CONSTRAINT "i" PRIMARY KEY("i"))\n'..
+                     "WITH ENGINE = 'memtx';"}
+        _G.check('"a"', res)
+
+        -- Wrong function type.
+        format[1].constraint.a = 'f1'
+        box.space.a:format(format)
+        res = {'CREATE TABLE "a"(\n"i" INTEGER NOT NULL,\n'..
+               'CONSTRAINT "i" PRIMARY KEY("i"))\nWITH ENGINE = \'memtx\';'}
+        local err = {"Problem with check constraint 'a': wrong constraint "..
+                     "expression."}
+        _G.check('"a"', res, err)
+
+        -- Wrong field name in the function.
+        format[1].constraint.a = 'f2'
+        box.space.a:format(format)
+        res = {'CREATE TABLE "a"(\n"i" INTEGER NOT NULL,\n'..
+               'CONSTRAINT "i" PRIMARY KEY("i"))\nWITH ENGINE = \'memtx\';'}
+        err = {"Problem with check constraint 'a': wrong field name in "..
+               "constraint expression."}
+        _G.check('"a"', res, err)
+
+        -- Make sure field check constraint name is properly escaped.
+        format[1].constraint = {['""'] = 'f'}
+        box.space.a:format(format)
+        res = {'CREATE TABLE "a"(\n"i" INTEGER NOT NULL '..
+               'CONSTRAINT """""" CHECK("i" > 10),\n'..
+               'CONSTRAINT "i" PRIMARY KEY("i"))\nWITH ENGINE = \'memtx\';'}
+        _G.check('"a"', res)
+
+        box.space.a:drop()
+        box.func.f:drop()
+        box.func.f1:drop()
+        box.func.f2:drop()
+    end)
+end
+
+g.test_tuple_check_from_lua = function()
+    g.server:exec(function()
+        box.schema.func.create('f', {body = '"i" > 10', language = 'SQL_EXPR',
+                                     is_deterministic = true})
+        box.schema.func.create('f1', {body = 'function(a) return a > 10 end',
+                                     is_deterministic = true})
+        box.schema.func.create('f2', {body = '1 > 0', language = 'SQL_EXPR',
+                                     is_deterministic = true})
+        box.schema.func.create('f3', {body = 'k > l', language = 'SQL_EXPR',
+                                     is_deterministic = true})
+
+        -- Working example.
+        local opts = {format = {{'i', 'integer'}}, constraint = {a = 'f'}}
+        box.schema.space.create('a', opts)
+        box.space.a:create_index('i', {parts = {{'i', 'integer'}}})
+        local res = {'CREATE TABLE "a"(\n"i" INTEGER NOT NULL,\n'..
+                     'CONSTRAINT "i" PRIMARY KEY("i"),\n'..
+                     'CONSTRAINT "a" CHECK("i" > 10))\n'..
+                     "WITH ENGINE = 'memtx';"}
+        _G.check('"a"', res)
+
+        -- Wrong function type.
+        opts.constraint.a = 'f1'
+        box.space.a:alter(opts)
+        res = {'CREATE TABLE "a"(\n"i" INTEGER NOT NULL,\n'..
+               'CONSTRAINT "i" PRIMARY KEY("i"))\nWITH ENGINE = \'memtx\';'}
+        local err = {"Problem with check constraint 'a': wrong constraint "..
+                     "expression."}
+        _G.check('"a"', res, err)
+
+        -- Make sure tuple check constraint name is properly escaped.
+        opts.constraint = {['"a"'] = 'f'}
+        box.space.a:alter(opts)
+        res = {'CREATE TABLE "a"(\n"i" INTEGER NOT NULL,\n'..
+               'CONSTRAINT "i" PRIMARY KEY("i"),\n'..
+               'CONSTRAINT """a""" CHECK("i" > 10))\nWITH ENGINE = \'memtx\';'}
+        _G.check('"a"', res)
+
+        -- Wrong function arguments.
+        opts.constraint = {a = 'f3'}
+        box.space.a:alter(opts)
+        res = {'CREATE TABLE "a"(\n"i" INTEGER NOT NULL,\n'..
+               'CONSTRAINT "i" PRIMARY KEY("i"))\nWITH ENGINE = \'memtx\';'}
+        err = {"Problem with check constraint 'a': wrong field name in "..
+               "constraint expression."}
+        _G.check('"a"', res, err)
+        box.space.a:drop()
+
+        -- The columns are not defined, but a constraint is set.
+        box.schema.space.create('a', {constraint = {a = 'f2'}})
+        res = {'CREATE TABLE "a"(\nCONSTRAINT "a" CHECK(1 > 0))\n'..
+               'WITH ENGINE = \'memtx\';'}
+        err = {"Problem with space 'a': format is missing.",
+               "Problem with space 'a': primary key is not defined."}
+        _G.check('"a"', res, err)
+        box.space.a:drop()
+
+        box.func.f:drop()
+        box.func.f1:drop()
+        box.func.f2:drop()
+        box.func.f3:drop()
+    end)
+end
+
+g.test_wrong_collation = function()
+    g.server:exec(function()
+        local map = setmetatable({}, { __serialize = 'map' })
+        local col_def = {'col1', 1, 'BINARY', '', map}
+        local col = box.space._collation:auto_increment(col_def)
+        t.assert(col ~= nil)
+
+        -- Working example.
+        local format = {{'i', 'string', collation = 'col1'}}
+        box.schema.space.create('a', {format = format})
+        local parts = {{'i', 'string', collation = 'col1'}}
+        box.space.a:create_index('i', {parts = parts})
+        local res = {'CREATE TABLE "a"(\n"i" STRING COLLATE "col1"'..
+                     ' NOT NULL,\nCONSTRAINT "i" PRIMARY KEY("i"))\n'..
+                     "WITH ENGINE = 'memtx';"}
+        _G.check('"a"', res)
+
+        -- Collations does not exists.
+        box.space._collation:delete(col.id)
+        res = {'CREATE TABLE "a"(\n"i" STRING NOT NULL,\n'..
+               'CONSTRAINT "i" PRIMARY KEY("i"))\nWITH ENGINE = \'memtx\';'}
+        local err = {"Problem with collation '277': collation does not exist."}
+        _G.check('"a"', res, err)
+
+        box.space._collation:insert(col)
+        box.space.a:drop()
+        box.space._collation:delete(col.id)
+
+        -- Make sure collation name is properly escaped.
+        col_def = {'"c"ol"2', 1, 'BINARY', '', map}
+        col = box.space._collation:auto_increment(col_def)
+        t.assert(col ~= nil)
+        format = {{'i', 'string', collation = '"c"ol"2'}}
+        box.schema.space.create('a', {format = format})
+        parts = {{'i', 'string', collation = '"c"ol"2'}}
+        box.space.a:create_index('i', {parts = parts})
+        res = {'CREATE TABLE "a"(\n"i" STRING COLLATE """c""ol""2" '..
+               'NOT NULL,\nCONSTRAINT "i" PRIMARY KEY("i"))\n'..
+               "WITH ENGINE = 'memtx';"}
+        _G.check('"a"', res)
+
+        box.space.a:drop()
+        box.space._collation:delete(col.id)
+    end)
+end
+
+g.test_index_from_lua = function()
+    g.server:exec(function()
+        local format = {{'i', 'integer'}, {'s', 'string', collation = 'binary'}}
+        local s = box.schema.space.create('a', {format = format})
+        s:create_index('i', {parts = {{'i', 'integer'}}})
+        local res = {'CREATE TABLE "a"(\n"i" INTEGER NOT NULL,\n'..
+                     '"s" STRING COLLATE "binary" NOT NULL,\n'..
+                     'CONSTRAINT "i" PRIMARY KEY("i"))\n'..
+                     "WITH ENGINE = 'memtx';"}
+        _G.check('"a"', res)
+
+        -- Working example.
+        s:create_index('i1', {parts = {{'i', 'integer'}}})
+        res = {'CREATE TABLE "a"(\n"i" INTEGER NOT NULL,\n'..
+               '"s" STRING COLLATE "binary" NOT NULL,\n'..
+               'CONSTRAINT "i" PRIMARY KEY("i"))\nWITH ENGINE = \'memtx\';',
+               'CREATE UNIQUE INDEX "i1" ON "a"("i");'}
+        _G.check('"a"', res)
+        s.index.i1:drop()
+
+        -- Unsupported type of the index.
+        s:create_index('i1', {parts = {{'i', 'integer'}}, type = 'HASH'})
+        local err = {"Problem with index 'i1': unsupported index type."}
+        res = {'CREATE TABLE "a"(\n"i" INTEGER NOT NULL,\n'..
+               '"s" STRING COLLATE "binary" NOT NULL,\n'..
+               'CONSTRAINT "i" PRIMARY KEY("i"))\nWITH ENGINE = \'memtx\';'}
+        _G.check('"a"', res, err)
+        s.index.i1:drop()
+
+        -- Non-unique index.
+        s:create_index('i1', {parts = {{'i', 'integer'}}, unique = false})
+        res = {'CREATE TABLE "a"(\n"i" INTEGER NOT NULL,\n'..
+               '"s" STRING COLLATE "binary" NOT NULL,\n'..
+               'CONSTRAINT "i" PRIMARY KEY("i"))\nWITH ENGINE = \'memtx\';',
+               'CREATE INDEX "i1" ON "a"("i");'}
+        _G.check('"a"', res)
+        s.index.i1:drop()
+
+        -- Parts contains an unnamed field.
+        s:create_index('i1', {parts = {{5, 'integer'}}})
+        err = {"Problem with index 'i1': field 5 is unnamed."}
+        res = {'CREATE TABLE "a"(\n"i" INTEGER NOT NULL,\n'..
+               '"s" STRING COLLATE "binary" NOT NULL,\n'..
+               'CONSTRAINT "i" PRIMARY KEY("i"))\nWITH ENGINE = \'memtx\';'}
+        _G.check('"a"', res, err)
+        s.index.i1:drop()
+
+        -- Type of the part in index different from type of the field.
+        s:create_index('i1', {parts = {{'i', 'unsigned'}}})
+        err = {"Problem with index 'i1': field 'i' and related part are of "..
+               "different types."}
+        res = {'CREATE TABLE "a"(\n"i" INTEGER NOT NULL,\n'..
+               '"s" STRING COLLATE "binary" NOT NULL,\n'..
+               'CONSTRAINT "i" PRIMARY KEY("i"))\nWITH ENGINE = \'memtx\';'}
+        _G.check('"a"', res, err)
+        s.index.i1:drop()
+
+        -- Collation of the part in index different from collation of the field.
+        s:create_index('i1', {parts = {{'s', 'string', collation = "unicode"}}})
+        err = {"Problem with index 'i1': field 's' and related part have "..
+               "different collations."}
+        res = {'CREATE TABLE "a"(\n"i" INTEGER NOT NULL,\n'..
+               '"s" STRING COLLATE "binary" NOT NULL,\n'..
+               'CONSTRAINT "i" PRIMARY KEY("i"))\nWITH ENGINE = \'memtx\';'}
+        _G.check('"a"', res, err)
+        s.index.i1:drop()
+
+        -- Make sure index name is properly escaped.
+        s:create_index('i7"', {parts = {{'i', 'integer'}}})
+        res = {'CREATE TABLE "a"(\n"i" INTEGER NOT NULL,\n'..
+               '"s" STRING COLLATE "binary" NOT NULL,\n'..
+               'CONSTRAINT "i" PRIMARY KEY("i"))\nWITH ENGINE = \'memtx\';',
+               'CREATE UNIQUE INDEX "i7""" ON "a"("i");'}
+        _G.check('"a"', res)
+
+        s:drop()
+    end)
+end