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