diff --git a/changelogs/unreleased/gh-8793-rework-sql-defaults.md b/changelogs/unreleased/gh-8793-rework-sql-defaults.md index 50f52d39038c7bd9807b7b25f713738d1d0ec994..ce92025b31bb96036ae1f6561b6c2a224676fee4 100644 --- a/changelogs/unreleased/gh-8793-rework-sql-defaults.md +++ b/changelogs/unreleased/gh-8793-rework-sql-defaults.md @@ -4,3 +4,5 @@ * `SQL_EXPR` functions can now be set as a default value (gh-8793). * A literal set as the default value can no longer have a `+` or `-` sign unless the literal is numeric (gh-8793). +* **[Breaking change]** SQL now uses the BOX mechanism for default + values (gh-8793). diff --git a/src/box/sql.c b/src/box/sql.c index 1a3168c6ce08917f96f1d92461096f38a4849a76..74c63f522b44c578f702eca0655cfd9aac753989 100644 --- a/src/box/sql.c +++ b/src/box/sql.c @@ -1619,6 +1619,20 @@ sql_check_create(const char *name, uint32_t space_id, uint32_t func_id, return sql_constraint_create(name, space_id, path, value); } +int +sql_add_default(uint32_t space_id, uint32_t fieldno, uint32_t func_id) +{ + const char *path = tt_sprintf("format[%u].default_func", fieldno + 1); + const int ops_size = 128; + char ops[ops_size]; + const char *ops_end = ops + mp_format(ops, ops_size, "[[%s%s%u]]", "!", + path, func_id); + const int key_size = 16; + char key[key_size]; + const char *key_end = key + mp_format(key, key_size, "[%u]", space_id); + return box_update(BOX_SPACE_ID, 0, key, key_end, ops, ops_end, 0, NULL); +} + const struct space * sql_space_by_token(const struct Token *name) { diff --git a/src/box/sql/build.c b/src/box/sql/build.c index 6f544980a788903a89272142cf2f4b87ea56064d..908c751e558b6e71195e0021d38d54e53b97b629 100644 --- a/src/box/sql/build.c +++ b/src/box/sql/build.c @@ -366,6 +366,18 @@ sql_create_column_start(struct Parse *parse) static void vdbe_emit_create_constraints(struct Parse *parse, int reg_space_id); +/** Emit VDBE code to add SQL_EXPR function as default value. */ +static void +vdbe_emit_create_defaults(struct Parse *parser, int reg_space_id) +{ + struct Vdbe *v = sqlGetVdbe(parser); + for (uint32_t i = 0; i < parser->default_func_count; ++i) { + sqlVdbeAddOp3(v, OP_AddFuncDefault, reg_space_id, + parser->default_funcs[i].reg_func_id, + parser->default_funcs[i].fieldno); + } +} + void sql_create_column_end(struct Parse *parse) { @@ -424,6 +436,7 @@ sql_create_column_end(struct Parse *parse) sqlVdbeChangeP5(v, OPFLAG_NCHANGE); sqlReleaseTempRange(parse, tuple_reg, box_space_field_MAX + 1); vdbe_emit_create_constraints(parse, key_reg); + vdbe_emit_create_defaults(parse, key_reg); } void @@ -450,45 +463,6 @@ sql_column_add_nullable_action(struct Parse *parser, field->is_nullable = action_is_nullable(nullable_action); } -/* - * The expression is the default value for the most recently added - * column. - * - * Default value expressions must be constant. Raise an exception if this - * is not the case. - * - * This routine is called by the parser while in the middle of - * parsing a <CREATE TABLE> or an <ALTER TABLE ADD COLUMN> - * statement. - */ -void -sqlAddDefaultValue(Parse * pParse, ExprSpan * pSpan) -{ - struct space *p = pParse->create_column_def.space; - if (p != NULL) { - assert(p->def->opts.is_ephemeral); - struct space_def *def = p->def; - if (!sqlExprIsConstantOrFunction - (pSpan->pExpr, sql_get()->init.busy)) { - const char *column_name = - def->fields[def->field_count - 1].name; - diag_set(ClientError, ER_CREATE_SPACE, def->name, - tt_sprintf("default value of column '%s' is "\ - "not constant", column_name)); - pParse->is_aborted = true; - } else { - assert(def != NULL); - struct field_def *field = - &def->fields[def->field_count - 1]; - struct region *region = &pParse->region; - size_t len = pSpan->zEnd - pSpan->zStart + 1; - field->sql_default_value = xregion_alloc(region, len); - strlcpy(field->sql_default_value, pSpan->zStart, len); - } - } - sql_expr_delete(pSpan->pExpr); -} - /** Fetch negative integer value from the expr. */ static int sql_expr_nint(const struct Expr *expr, int64_t *res) @@ -1261,6 +1235,38 @@ vdbe_emit_ck_constraint_create(struct Parse *parser, sqlVdbeCountChanges(v); } +void +sql_add_func_default(struct Parse *parser, struct ExprSpan *span) +{ + assert(parser->create_column_def.space != NULL); + struct space_def *def = parser->create_column_def.space->def; + uint32_t fieldno = def->field_count - 1; + const char *field_name = def->fields[fieldno].name; + + if (!sqlExprIsConstantOrFunction(span->pExpr, sql_get()->init.busy)) { + diag_set(ClientError, ER_CREATE_SPACE, def->name, + tt_sprintf("default value of column '%s' is not " + "constant", field_name)); + parser->is_aborted = true; + sql_expr_delete(span->pExpr); + return; + } + sql_expr_delete(span->pExpr); + + char *name = sqlMPrintf("default_%s_%s", def->name, field_name); + char *body = sql_xstrndup(span->zStart, span->zEnd - span->zStart); + int reg_id = ++parser->nMem; + vdbe_emit_create_function(parser, reg_id, name, body); + sql_xfree(name); + sql_xfree(body); + int id = parser->default_func_count++; + size_t size = parser->default_func_count * + sizeof(*parser->default_funcs); + parser->default_funcs = sql_xrealloc(parser->default_funcs, size); + parser->default_funcs[id].fieldno = fieldno; + parser->default_funcs[id].reg_func_id = reg_id; +} + /** * Generate opcodes to create foreign key constraint. * @@ -1508,6 +1514,7 @@ sqlEndTable(struct Parse *pParse) int reg_space_id = getNewSpaceId(pParse); vdbe_emit_space_create(pParse, reg_space_id, name_reg, new_space); vdbe_emit_create_constraints(pParse, reg_space_id); + vdbe_emit_create_defaults(pParse, reg_space_id); } void diff --git a/src/box/sql/parse.y b/src/box/sql/parse.y index be67cb53a2ed2b4d9ce6b637f31dc4345b217827..bef339117933fadf05e139f5faf808a506a2fda0 100644 --- a/src/box/sql/parse.y +++ b/src/box/sql/parse.y @@ -293,7 +293,7 @@ ccons ::= DEFAULT LP expr(X) RP. { if (sql_expr_is_term(X.pExpr)) sql_add_term_default(pParse, &X); else - sqlAddDefaultValue(pParse, &X); + sql_add_func_default(pParse, &X); } ccons ::= DEFAULT PLUS number(X). {sql_add_term_default(pParse, &X);} ccons ::= DEFAULT MINUS(A) number(X). { diff --git a/src/box/sql/prepare.c b/src/box/sql/prepare.c index 6b10db2fd632223327b655195795a661eaad82d1..2e2647641c3ec6b4ce17b03b17f339c8039ec007 100644 --- a/src/box/sql/prepare.c +++ b/src/box/sql/prepare.c @@ -202,6 +202,7 @@ sql_parser_destroy(Parse *parser) { assert(parser != NULL); assert(!parser->parse_only || parser->pVdbe == NULL); + sql_xfree(parser->default_funcs); sql_xfree(parser->aLabel); sql_expr_list_delete(parser->pConstExpr); struct create_fk_constraint_parse_def *create_fk_constraint_parse_def = diff --git a/src/box/sql/sqlInt.h b/src/box/sql/sqlInt.h index 4244b7a2d9028bb16ad03e077120c2fa706cbdb6..8d2325e06102ab7a2e7670df992c4d062bccaf33 100644 --- a/src/box/sql/sqlInt.h +++ b/src/box/sql/sqlInt.h @@ -1942,6 +1942,14 @@ enum ast_type { ast_type_MAX }; +/** Information about the expressions that will be used as default values. */ +struct sql_default_func { + /** Fieldno of the field to which default value will be added. */ + uint32_t fieldno; + /** Register that will contain the ID of the new SQL EXPR functions. */ + int reg_func_id; +}; + /* * An SQL parser context. A copy of this structure is passed through * the parser and down into all the parser action routine in order to @@ -2068,6 +2076,10 @@ struct Parse { */ struct create_table_def create_table_def; struct create_column_def create_column_def; + /** Array of default function descriptions. */ + struct sql_default_func *default_funcs; + /** Length of array of default function descriptions. */ + uint32_t default_func_count; /** AST of parsed SQL statement. */ struct sql_ast ast; /* @@ -2750,7 +2762,10 @@ sql_create_check_contraint(struct Parse *parser, bool is_field_ck); void sql_add_term_default(struct Parse *parser, struct ExprSpan *expr_span); -void sqlAddDefaultValue(Parse *, ExprSpan *); +/** Add a default expression to the last created column. */ +void +sql_add_func_default(struct Parse *parser, struct ExprSpan *span); + void sqlAddCollateType(Parse *, Token *); /** diff --git a/src/box/sql/tarantoolInt.h b/src/box/sql/tarantoolInt.h index 6677bac4c8a5a271a8935dc2d28adac45a3125cc..79cdc6b236f5e3fbc0e030446c3f3a580a7cb03f 100644 --- a/src/box/sql/tarantoolInt.h +++ b/src/box/sql/tarantoolInt.h @@ -229,6 +229,10 @@ int sql_check_create(const char *name, uint32_t space_id, uint32_t func_id, uint32_t fieldno, bool is_field_ck); +/** Add SQL_EXPR function as the field default value to space format. */ +int +sql_add_default(uint32_t space_id, uint32_t fieldno, uint32_t func_id); + /** * Encode index parts of given foreign key constraint into * MsgPack on @region. diff --git a/src/box/sql/vdbe.c b/src/box/sql/vdbe.c index 6f4f27d316a4e3aacb5e53ad17db80b89d279e7a..bc49ca82f03b900031b0fac6909a51ea40f31874 100644 --- a/src/box/sql/vdbe.c +++ b/src/box/sql/vdbe.c @@ -2246,6 +2246,21 @@ case OP_DropFieldCheck: { break; } +/** + * Opcode: AddFuncDefault P1 P2 P3 * * + * Synopsis: Add function r[P2] as default for field P3 of box.space[r[P1]] + */ +case OP_AddFuncDefault: { + assert(aMem[pOp->p1].type == MEM_TYPE_UINT); + uint32_t space_id = aMem[pOp->p1].u.u; + uint32_t fieldno = pOp->p3; + assert(aMem[pOp->p2].type == MEM_TYPE_UINT); + uint32_t func_id = aMem[pOp->p2].u.u; + if (sql_add_default(space_id, fieldno, func_id) != 0) + goto abort_due_to_error; + break; +} + /* Opcode: Savepoint P1 * P3 P4 * * * Open, release or rollback the savepoint named by parameter P4, depending diff --git a/test/sql-luatest/defaults_test.lua b/test/sql-luatest/defaults_test.lua index b4e2543b3161ed9d38aea84e4fd2fa988aca24f0..9f864a8bed026a0ddaecc90fc64193552b157c25 100644 --- a/test/sql-luatest/defaults_test.lua +++ b/test/sql-luatest/defaults_test.lua @@ -245,3 +245,21 @@ g.test_new_defaults = function() box.space.T:drop() end) end + +g.test_new_func_defaults = function() + g.server:exec(function() + -- Make sure an expression works correctly as a default value. + local sql = [[CREATE TABLE T(I INT PRIMARY KEY, A ANY DEFAULT(9 * 7));]] + box.execute(sql) + local s = box.space.T + local func = box.func.default_T_A + t.assert_equals(func.body, '9 * 7') + t.assert_equals(s:format()[2].default_func, func.id) + s:insert({1}) + t.assert_equals(s:select(), {{1, 63}}) + box.execute([[INSERT INTO T VALUES (2, NULL);]]) + t.assert_equals(s:select(), {{1, 63}, {2, 63}}) + s:drop() + func:drop() + end) +end diff --git a/test/sql-tap/func.test.lua b/test/sql-tap/func.test.lua index 43c71b9ee5c995069657a8e987cb12676706af5c..599428bcd3f372a737c7a1f4d02ab33165735b19 100755 --- a/test/sql-tap/func.test.lua +++ b/test/sql-tap/func.test.lua @@ -2426,11 +2426,8 @@ test:do_catchsql_test( test:do_test( "func-28.1", function() - test:execsql([[ - CREATE TABLE t28(id INT primary key, x INT, y INT DEFAULT(nosuchfunc(1))); - ]]) return test:catchsql([[ - INSERT INTO t28(id, x) VALUES(1, 1); + CREATE TABLE t28(id INT primary key, x INT, y INT DEFAULT(nosuchfunc(1))); ]]) end, { -- <func-28.1> diff --git a/test/sql-tap/func5.test.lua b/test/sql-tap/func5.test.lua index 7fb5bfadb030f7be11515801b6b902f9c79fb087..f0aff08bca70e949c5705ea83f564814791bdc32 100755 --- a/test/sql-tap/func5.test.lua +++ b/test/sql-tap/func5.test.lua @@ -323,30 +323,26 @@ box.func.counter2:drop() local body = 'function(x) return 1 end' box.schema.func.create('f1', {language = 'Lua', returns = 'number', body = body, param_list = {}, exports = {'LUA'}}); -box.execute([[CREATE TABLE t01(i INT PRIMARY KEY, a INT DEFAULT(f1(1)));]]) test:do_catchsql_test( "func-7.1", [[ - INSERT INTO t01(i) VALUES(1); + CREATE TABLE t01(i INT PRIMARY KEY, a INT DEFAULT(f1(1))); ]], { 1, "function f1() is not available in SQL" }) box.schema.func.create('f2', {language = 'Lua', returns = 'number', body = body, exports = {'LUA', 'SQL'}}); -box.execute([[CREATE TABLE t02(i INT PRIMARY KEY, a INT DEFAULT(f2(1)));]]) test:do_catchsql_test( "func-7.2", [[ - INSERT INTO t02(i) VALUES(1); + CREATE TABLE t02(i INT PRIMARY KEY, a INT DEFAULT(f2(1))); ]], { 1, "Wrong number of arguments is passed to f2(): expected 0, got 1" }) box.func.f1:drop() box.func.f2:drop() -box.space.t01:drop() -box.space.t02:drop() -- -- gh-6105: Make sure that functions that were described in _func but were not diff --git a/test/sql-tap/uuid.test.lua b/test/sql-tap/uuid.test.lua index 6f694933c7edab042e08c964c3f61a6f0329ab79..485c1d54a42060befa9fdd7e553ababa0248be76 100755 --- a/test/sql-tap/uuid.test.lua +++ b/test/sql-tap/uuid.test.lua @@ -900,7 +900,7 @@ test:do_execsql_test( INSERT INTO t10 VALUES (4, NULL); SELECT * FROM t10 WHERE i = 4; ]], { - 4, '' + 4, uuid1 }) -- Check that UPDATE of UUID field works.