From 5ab5ce2a669526c64de52b8e7eab9975b57941a5 Mon Sep 17 00:00:00 2001 From: Mergen Imeev <imeevma@gmail.com> Date: Mon, 9 Jan 2023 15:26:34 +0300 Subject: [PATCH] sql: introduce SHOW CREATE TABLE statement This patch introduces the SHOW CREATE TABLE statement. This statement can be used to obtain a description of a space in the form of a corresponding CREATE TABLE and CREATE INDEX statements. Closes #8098 @TarantoolBot document Title: SHOW CREATE TABLE statement Statement can be used to obtain a description of a space in the form of a corresponding `CREATE TABLE` and `CREATE INDEX` statements. Result will be in form of set of statements and set of found errors. If errors were not detected, set of the statements should be enough to completely serialize space definition. There is two types of `SHOW CREATE TABLE` statement: 1) Get a description of a single space: ``` SHOW CREATE TABLE table_name; ``` This statement can be used to obtain a description of a space in the form of the corresponding `CREATE TABLE` and `CREATE INDEX` statements. The result will be in the form of a set of statements and a set of found errors. If no errors are found, the set of statements should be sufficient to fully serialize the space definition. Otherwise, it will certainly not be a complete space definition, and a `CREATE TABLE` statement is generally not guaranteed to be syntactically correct. 2) Get descriptions of all available non-system spaces: ``` SHOW CREATE TABLE; ``` This statement returns descriptions for each available non-system space in the form described above. --- .../unreleased/gh-8098-show-create-table.md | 3 + extra/mkkeywordhash.c | 1 + src/box/CMakeLists.txt | 1 + src/box/sql.h | 5 + src/box/sql/build.c | 66 +++ src/box/sql/func.c | 21 + src/box/sql/parse.y | 11 +- src/box/sql/show.c | 496 +++++++++++++++++ src/box/sql/sqlInt.h | 19 + src/box/sql/util.c | 22 + src/box/sql/vdbe.c | 15 + test/sql-luatest/show_create_table_test.lua | 521 ++++++++++++++++++ 12 files changed, 1180 insertions(+), 1 deletion(-) create mode 100644 changelogs/unreleased/gh-8098-show-create-table.md create mode 100644 src/box/sql/show.c create mode 100644 test/sql-luatest/show_create_table_test.lua 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 0000000000..c4f42ea04a --- /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 8c392e0edd..12379554ff 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 ede060b2ca..a7661670d7 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 ccc47e6b2c..de0e8bada7 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 a11593ac77..f0e7041a8d 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 af611e9df3..de0333929d 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 4274ffb5d3..0e1ea5c186 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 0000000000..668c2f1a1d --- /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 8ab70fa6cd..578bc8e526 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 f333f44fde..8a9379332f 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 e3c18e32a2..93246148b8 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 0000000000..e20102931e --- /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 -- GitLab