From mboxrd@z Thu Jan 1 00:00:00 1970 Return-Path: Received: from [87.239.111.99] (localhost [127.0.0.1]) by dev.tarantool.org (Postfix) with ESMTP id 7FC6F6ECE3; Thu, 25 Nov 2021 11:55:34 +0300 (MSK) DKIM-Filter: OpenDKIM Filter v2.11.0 dev.tarantool.org 7FC6F6ECE3 DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/simple; d=tarantool.org; s=dev; t=1637830534; bh=EVWs4xo6nO7TdGPNP2E7GUvbk3KKfkVj6JpMO+ZLolk=; h=Date:To:Cc:References:In-Reply-To:Subject:List-Id: List-Unsubscribe:List-Archive:List-Post:List-Help:List-Subscribe: From:Reply-To:From; b=sLuZLc6mjdF21DJ533oZuoNzvkUabFNbI1lcansJDSoekOrzcNgEWAKz3/0Q+TVO/ Et4Vm3hrqWaazhFbTxOlqbSI7rhXEfrHOuB1e3UIlvZK9XOxA8LszzoWv7YQ+EuwVu 9oJ9LMrj5/NDtaOjJDLvxacpGQJhiECbqzRvOu7g= Received: from smtpng1.i.mail.ru (smtpng1.i.mail.ru [94.100.181.251]) (using TLSv1.2 with cipher ECDHE-RSA-AES256-GCM-SHA384 (256/256 bits)) (No client certificate requested) by dev.tarantool.org (Postfix) with ESMTPS id C14BB6ECE3 for ; Thu, 25 Nov 2021 11:55:31 +0300 (MSK) DKIM-Filter: OpenDKIM Filter v2.11.0 dev.tarantool.org C14BB6ECE3 Received: by smtpng1.m.smailru.net with esmtpa (envelope-from ) id 1mqAXS-0001nm-SM; Thu, 25 Nov 2021 11:55:31 +0300 Date: Thu, 25 Nov 2021 11:55:29 +0300 To: Vladislav Shpilevoy Cc: tarantool-patches@dev.tarantool.org Message-ID: <20211125085529.GB56448@tarantool.org> References: <4ecfb3439688bef76c96270624410dee8822176f.1637244389.git.imeevma@gmail.com> <662f6b87-f085-ab10-53b8-d087d9598b19@tarantool.org> MIME-Version: 1.0 Content-Type: text/plain; charset=utf-8 Content-Disposition: inline In-Reply-To: <662f6b87-f085-ab10-53b8-d087d9598b19@tarantool.org> X-4EC0790: 10 X-7564579A: 646B95376F6C166E X-77F55803: 4F1203BC0FB41BD9FE0487E502468146B8787A579DD4BF53754E468A758266C2182A05F5380850401F02F207B1483A85D99F0DCCF9EFA07C7C60E99CFDF7DFEEAA9550279EFB7F31 X-7FA49CB5: FF5795518A3D127A4AD6D5ED66289B5278DA827A17800CE72E2D36A15E1833D8EA1F7E6F0F101C67BD4B6F7A4D31EC0BCC500DACC3FED6E28638F802B75D45FF8AA50765F790063776672C316918EFDB8638F802B75D45FF36EB9D2243A4F8B5A6FCA7DBDB1FC311F39EFFDF887939037866D6147AF826D8895A243DFE674F1AF0B025C410E7CF6D117882F4460429724CE54428C33FAD305F5C1EE8F4F765FCAA867293B0326636D2E47CDBA5A96583BD4B6F7A4D31EC0BC014FD901B82EE079FA2833FD35BB23D27C277FBC8AE2E8BAA867293B0326636D2E47CDBA5A96583BA9C0B312567BB231DD303D21008E298D5E8D9A59859A8B6B372FE9A2E580EFC725E5C173C3A84C315AF0D0D4FC4FA3D35872C767BF85DA2F004C90652538430E4A6367B16DE6309 X-C1DE0DAB: 0D63561A33F958A5FA35E8C9F82CA775F2D3C0DD973D6F49ACA1FBCA8A477BB5D59269BC5F550898D99A6476B3ADF6B47008B74DF8BB9EF7333BD3B22AA88B938A852937E12ACA7506FE1F977233B9BB410CA545F18667F91A7EA1CDA0B5A7A0 X-C8649E89: 4E36BF7865823D7055A7F0CF078B5EC49A30900B95165D341C998A3771F041536D7E4F9B2EE1D75E7044DC87200C462A759F3687F7D56FCB487921107EAA9B001D7E09C32AA3244CE7393B81C12FCFB78D89E67082775B1D853296C06374E602729B2BEF169E0186 X-D57D3AED: 3ZO7eAau8CL7WIMRKs4sN3D3tLDjz0dLbV79QFUyzQ2Ujvy7cMT6pYYqY16iZVKkSc3dCLJ7zSJH7+u4VD18S7Vl4ZUrpaVfd2+vE6kuoey4m4VkSEu530nj6fImhcD4MUrOEAnl0W826KZ9Q+tr5ycPtXkTV4k65bRjmOUUP8cvGozZ33TWg5HZplvhhXbhDGzqmQDTd6OAevLeAnq3Ra9uf7zvY2zzsIhlcp/Y7m53TZgf2aB4JOg4gkr2biojycNqkYkaYUIHYIJtUL5mHQ== X-Mailru-Sender: 689FA8AB762F7393C37E3C1AEC41BA5D92E93BBB6323E30778F8FFB3007672C283D72C36FC87018B9F80AB2734326CD2FB559BB5D741EB96352A0ABBE4FDA4210A04DAD6CC59E33667EA787935ED9F1B X-Mras: Ok Subject: Re: [Tarantool-patches] [PATCH v1 2/2] sql: introduce syntax for MAP values X-BeenThere: tarantool-patches@dev.tarantool.org X-Mailman-Version: 2.1.34 Precedence: list List-Id: Tarantool development patches List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , From: Mergen Imeev via Tarantool-patches Reply-To: Mergen Imeev Errors-To: tarantool-patches-bounces@dev.tarantool.org Sender: "Tarantool-patches" Thank you for the review! My answers, diff and new patch below. Also, I added changelog and tests to show that it is possible to create an empty MAP and a map with more than 1000 key-value pairs. On Sat, Nov 20, 2021 at 01:46:57AM +0100, Vladislav Shpilevoy wrote: > Thanks for the patch! > > See 7 comments below. > > > diff --git a/src/box/sql/expr.c b/src/box/sql/expr.c > > index 74a98c550..789d8906c 100644 > > --- a/src/box/sql/expr.c > > +++ b/src/box/sql/expr.c > > @@ -3432,6 +3432,35 @@ expr_code_array(struct Parse *parser, struct Expr *expr, int reg) > > sqlVdbeAddOp3(vdbe, OP_Array, count, reg, values_reg); > > } > > > > +static void > > +expr_code_map(struct Parse *parser, struct Expr *expr, int reg) > > 1. I thought the policy was that we name functions, generating VDBE code, > using 'emit' suffix. For instance, `vdbe_emit_map()` or `sql_emit_map()`. > Don't know about prefix though. I see both vdbe_ and sql_ are used. > This is usually true, but this function is actually part of sqlExprCodeTarget(). I believe these functions were created to make sqlExprCodeTarget() more readable. All such functions are named sqlExprCode*(), code*() or expr_code _*(), for example: sqlExprCodeGetColumn(), codeReal(), expr_code_int(). Since all these functions are static, I think we should drop "expr_" prefix for them. Not in this patch, though. > > diff --git a/src/box/sql/mem.c b/src/box/sql/mem.c > > index b598fe5c2..fe7029341 100644 > > --- a/src/box/sql/mem.c > > +++ b/src/box/sql/mem.c > > @@ -3043,6 +3043,45 @@ mem_encode_array(const struct Mem *mems, uint32_t count, uint32_t *size, > > return array; > > } > > > > +char * > > +mem_encode_map(const struct Mem *mems, uint32_t count, uint32_t *size, > > + struct region *region) > > +{ > > + assert(count % 2 == 0); > > + size_t used = region_used(region); > > + bool is_error = false; > > + struct mpstream stream; > > + mpstream_init(&stream, region, region_reserve_cb, region_alloc_cb, > > + set_encode_error, &is_error); > > + mpstream_encode_map(&stream, (count + 1) / 2); > > + for (uint32_t i = 0; i < count / 2; ++i) { > > + const struct Mem *key = &mems[2 * i]; > > + const struct Mem *value = &mems[2 * i + 1]; > > + if (mem_is_metatype(key) || > > + (key->type & (MEM_TYPE_UINT | MEM_TYPE_INT | MEM_TYPE_UUID | > > + MEM_TYPE_STR)) == 0) { > > + diag_set(ClientError, ER_SQL_TYPE_MISMATCH, > > + mem_str(key), "integer, string or uuid"); > > + return NULL; > > + } > > + mem_to_mpstream(key, &stream); > > + mem_to_mpstream(value, &stream); > > + } > > + mpstream_flush(&stream); > > + if (is_error) { > > 2. The error could happen in the last moment after some allocations > were made. Better add a truncate here. > Fixed. > > + diag_set(OutOfMemory, stream.pos - stream.buf, > > + "mpstream_flush", "stream"); > > + return NULL; > > + } > > + *size = region_used(region) - used; > > + char *map = region_join(region, *size); > > + if (map == NULL) { > > 3. Ditto. And the same in mem_encode_array(). > Fixed. > > + diag_set(OutOfMemory, *size, "region_join", "map"); > > + return NULL; > > + } > > + return map; > > +} > > diff --git a/src/box/sql/parse.y b/src/box/sql/parse.y > > index 06e6244e3..db7fef71a 100644 > > --- a/src/box/sql/parse.y > > +++ b/src/box/sql/parse.y > > @@ -1140,6 +1140,37 @@ expr(A) ::= LB(X) exprlist(Y) RB(E). { > > spanSet(&A, &X, &E); > > } > > > > +expr(A) ::= LCB(X) maplist(Y) RCB(E). { > > + struct sql *db = pParse->db; > > + struct Expr *expr = sql_expr_new_dequoted(db, TK_MAP, NULL); > > 4. sql_expr_new_anon(). > Fixed. > > + if (expr == NULL) { > > + sql_expr_list_delete(db, Y); > > + pParse->is_aborted = true; > > + return; > > + } > > + expr->x.pList = Y; > > + expr->type = FIELD_TYPE_MAP; > > + sqlExprSetHeightAndFlags(pParse, expr); > > + A.pExpr = expr; > > + spanSet(&A, &X, &E); > > +} > > + > > +maplist(A) ::= nmaplist(A). > > +maplist(A) ::= . {A = 0;} > > 5. Lets remove these extra spaces between . and the code block. > Fixed. > > +nmaplist(A) ::= nmaplist(A) COMMA expr(X) COLON expr(Y). { > > + A = sql_expr_list_append(pParse->db, A, X.pExpr); > > + A = sql_expr_list_append(pParse->db, A, Y.pExpr); > > +} > > +nmaplist(A) ::= expr(X) COLON expr(Y). { > > + A = sql_expr_list_append(pParse->db, NULL, X.pExpr); > > + A = sql_expr_list_append(pParse->db, A, Y.pExpr); > > +} > > + > > +%type maplist {ExprList*} > > +%destructor maplist {sql_expr_list_delete(pParse->db, $$);} > > +%type nmaplist {ExprList*} > > +%destructor nmaplist {sql_expr_list_delete(pParse->db, $$);} > > 6. Could you please add a whitespace between type and '*'? > Fixed. > > 7. Lets add some more complicated syntax tests: > > tarantool> box.execute('SELECT {:name}', {{[':name'] = 1}}) > --- > - null > - Syntax error at line 1 near '}' > ... > > tarantool> box.execute('SELECT {:name: 5}', {{[':name'] = 1}}) > --- > - metadata: > - name: COLUMN_1 > type: map > rows: > - [{1: 5}] > ... > > tarantool> box.execute('SELECT {5::name}', {{[':name'] = 1}}) > --- > - metadata: > - name: COLUMN_1 > type: map > rows: > - [{5: 1}] > ... > > To see if the parse is able to handle both map ':' and variable ':'. Thank you! Added these tests. Diff: diff --git a/changelogs/unreleased/gh-4763-introduce-map-to-sql.md b/changelogs/unreleased/gh-4763-introduce-map-to-sql.md new file mode 100644 index 000000000..013ec8f67 --- /dev/null +++ b/changelogs/unreleased/gh-4763-introduce-map-to-sql.md @@ -0,0 +1,4 @@ +## feature/core + + * Field type MAP is now available in SQL. The syntax has also been implemented + to allow the creation of MAP values (gh-4763). diff --git a/src/box/sql/mem.c b/src/box/sql/mem.c index 645e597fa..7411b8f67 100644 --- a/src/box/sql/mem.c +++ b/src/box/sql/mem.c @@ -3096,6 +3096,7 @@ mem_encode_map(const struct Mem *mems, uint32_t count, uint32_t *size, } mpstream_flush(&stream); if (is_error) { + region_truncate(region, used); diag_set(OutOfMemory, stream.pos - stream.buf, "mpstream_flush", "stream"); return NULL; @@ -3103,6 +3104,7 @@ mem_encode_map(const struct Mem *mems, uint32_t count, uint32_t *size, *size = region_used(region) - used; char *map = region_join(region, *size); if (map == NULL) { + region_truncate(region, used); diag_set(OutOfMemory, *size, "region_join", "map"); return NULL; } diff --git a/src/box/sql/parse.y b/src/box/sql/parse.y index 1fbf0cfac..998acadea 100644 --- a/src/box/sql/parse.y +++ b/src/box/sql/parse.y @@ -1115,7 +1115,7 @@ expr(A) ::= LB(X) exprlist(Y) RB(E). { expr(A) ::= LCB(X) maplist(Y) RCB(E). { struct sql *db = pParse->db; - struct Expr *expr = sql_expr_new_dequoted(db, TK_MAP, NULL); + struct Expr *expr = sql_expr_new_anon(db, TK_MAP); if (expr == NULL) { sql_expr_list_delete(db, Y); pParse->is_aborted = true; @@ -1129,7 +1129,9 @@ expr(A) ::= LCB(X) maplist(Y) RCB(E). { } maplist(A) ::= nmaplist(A). -maplist(A) ::= . {A = 0;} +maplist(A) ::= . { + A = NULL; +} nmaplist(A) ::= nmaplist(A) COMMA expr(X) COLON expr(Y). { A = sql_expr_list_append(pParse->db, A, X.pExpr); A = sql_expr_list_append(pParse->db, A, Y.pExpr); @@ -1139,9 +1141,9 @@ nmaplist(A) ::= expr(X) COLON expr(Y). { A = sql_expr_list_append(pParse->db, A, Y.pExpr); } -%type maplist {ExprList*} +%type maplist {ExprList *} %destructor maplist {sql_expr_list_delete(pParse->db, $$);} -%type nmaplist {ExprList*} +%type nmaplist {ExprList *} %destructor nmaplist {sql_expr_list_delete(pParse->db, $$);} expr(A) ::= TRIM(X) LP trim_operands(Y) RP(E). { diff --git a/test/sql-tap/map.test.lua b/test/sql-tap/map.test.lua index de1e495f3..7791ca779 100755 --- a/test/sql-tap/map.test.lua +++ b/test/sql-tap/map.test.lua @@ -1,6 +1,6 @@ #!/usr/bin/env tarantool local test = require("sqltester") -test:plan(126) +test:plan(131) box.schema.func.create('M1', { language = 'Lua', @@ -1011,6 +1011,25 @@ test:do_execsql_test( "map" }) +test:do_execsql_test( + "map-13.4", + [[ + SELECT printf({}); + ]], { + '{}' + }) + +local map = {[0] = 0} +local str = '0: 0' +for i = 1, 1000 do map[i] = i str = str .. string.format(', %d: %d', i, i) end +test:do_execsql_test( + "map-13.5", + [[ + SELECT {]]..str..[[}; + ]], { + map + }) + -- Make sure MAP() accepts only INTEGER, STRING and UUID as keys. test:do_execsql_test( "map-13.4", @@ -1118,6 +1137,34 @@ test:do_test( "Type mismatch: can not convert double(1.5) to integer, string or uuid" }) +-- Make sure symbol ':' is properly processed by parser. +test:do_test( + "map-14.1", + function() + local res = {pcall(box.execute, [[SELECT {:name};]], {{[':name'] = 1}})} + return {tostring(res[3])} + end, { + "Syntax error at line 1 near '}'" + }) + +test:do_test( + "map-14.2", + function() + local res = box.execute([[SELECT {:name: 5}]], {{[':name'] = 1}}) + return {tostring(res.rows[1])} + end, { + "[{1: 5}]" + }) + +test:do_test( + "map-14.3", + function() + local res = box.execute([[SELECT {5::name}]], {{[':name'] = 1}}) + return {tostring(res.rows[1])} + end, { + "[{5: 1}]" + }) + box.execute([[DROP TABLE t1;]]) box.execute([[DROP TABLE t;]]) New patch: commit 3cf1a8e69375fe7627439233f1f67dcc207f6d0b Author: Mergen Imeev Date: Thu Nov 18 11:07:59 2021 +0300 sql: introduce syntax for MAP values This patch introduces a new syntax that allows to create MAP values in an SQL query. Part of #4763 @TarantoolBot document Title: Syntax for MAP in SQL The syntax for creating document values is available in SQL. You can use `{`, ':' and `}` to create a MAP value. Only INTEGER, STRING and UUID values can be keys in MAP values. Examples: ``` tarantool> box.execute("SELECT {1 : 'a', 'asd' : 1.5, uuid() : true};") --- - metadata: - name: COLUMN_1 type: map rows: - [{1: 'a', 91ca4dbb-c6d4-4468-b4a4-ab1e409dd87e: true, 'asd': 1.5}] ... ``` ``` tarantool> box.execute("SELECT {'h' : ['abc', 321], 7 : {'b' : 1.5}};") --- - metadata: - name: COLUMN_1 type: map rows: - [{7: {'b': 1.5}, 'h': ['abc', 321]}] ... ``` diff --git a/changelogs/unreleased/gh-4763-introduce-map-to-sql.md b/changelogs/unreleased/gh-4763-introduce-map-to-sql.md new file mode 100644 index 000000000..013ec8f67 --- /dev/null +++ b/changelogs/unreleased/gh-4763-introduce-map-to-sql.md @@ -0,0 +1,4 @@ +## feature/core + + * Field type MAP is now available in SQL. The syntax has also been implemented + to allow the creation of MAP values (gh-4763). diff --git a/src/box/sql/expr.c b/src/box/sql/expr.c index 8ff01dd33..e4c1dcff1 100644 --- a/src/box/sql/expr.c +++ b/src/box/sql/expr.c @@ -3439,6 +3439,35 @@ expr_code_array(struct Parse *parser, struct Expr *expr, int reg) sqlVdbeAddOp3(vdbe, OP_Array, count, reg, values_reg); } +static void +expr_code_map(struct Parse *parser, struct Expr *expr, int reg) +{ + struct Vdbe *vdbe = parser->pVdbe; + struct ExprList *list = expr->x.pList; + if (list == NULL) { + sqlVdbeAddOp3(vdbe, OP_Map, 0, reg, 0); + return; + } + int count = list->nExpr; + assert(count % 2 == 0); + for (int i = 0; i < count / 2; ++i) { + struct Expr *expr = list->a[2 * i].pExpr; + enum field_type type = sql_expr_type(expr); + if (expr->op != TK_VARIABLE && type != FIELD_TYPE_INTEGER && + type != FIELD_TYPE_UNSIGNED && type != FIELD_TYPE_STRING && + type != FIELD_TYPE_UUID) { + diag_set(ClientError, ER_SQL_PARSER_GENERIC, "Only " + "integer, string and uuid can be keys in map"); + parser->is_aborted = true; + return; + } + } + int values_reg = parser->nMem + 1; + parser->nMem += count; + sqlExprCodeExprList(parser, list, values_reg, 0, SQL_ECEL_FACTOR); + sqlVdbeAddOp3(vdbe, OP_Map, count, reg, values_reg); +} + /* * Erase column-cache entry number i */ @@ -3894,6 +3923,10 @@ sqlExprCodeTarget(Parse * pParse, Expr * pExpr, int target) expr_code_array(pParse, pExpr, target); break; + case TK_MAP: + expr_code_map(pParse, pExpr, target); + return target; + case TK_LT: case TK_LE: case TK_GT: diff --git a/src/box/sql/mem.c b/src/box/sql/mem.c index 32b8825bc..7411b8f67 100644 --- a/src/box/sql/mem.c +++ b/src/box/sql/mem.c @@ -3070,6 +3070,47 @@ mem_encode_array(const struct Mem *mems, uint32_t count, uint32_t *size, return array; } +char * +mem_encode_map(const struct Mem *mems, uint32_t count, uint32_t *size, + struct region *region) +{ + assert(count % 2 == 0); + size_t used = region_used(region); + bool is_error = false; + struct mpstream stream; + mpstream_init(&stream, region, region_reserve_cb, region_alloc_cb, + set_encode_error, &is_error); + mpstream_encode_map(&stream, (count + 1) / 2); + for (uint32_t i = 0; i < count / 2; ++i) { + const struct Mem *key = &mems[2 * i]; + const struct Mem *value = &mems[2 * i + 1]; + if (mem_is_metatype(key) || + (key->type & (MEM_TYPE_UINT | MEM_TYPE_INT | MEM_TYPE_UUID | + MEM_TYPE_STR)) == 0) { + diag_set(ClientError, ER_SQL_TYPE_MISMATCH, + mem_str(key), "integer, string or uuid"); + return NULL; + } + mem_to_mpstream(key, &stream); + mem_to_mpstream(value, &stream); + } + mpstream_flush(&stream); + if (is_error) { + region_truncate(region, used); + diag_set(OutOfMemory, stream.pos - stream.buf, + "mpstream_flush", "stream"); + return NULL; + } + *size = region_used(region) - used; + char *map = region_join(region, *size); + if (map == NULL) { + region_truncate(region, used); + diag_set(OutOfMemory, *size, "region_join", "map"); + return NULL; + } + return map; +} + /** * Allocate a sequence of initialized vdbe memory registers * on region. diff --git a/src/box/sql/mem.h b/src/box/sql/mem.h index 7f5ecf954..7e35123ca 100644 --- a/src/box/sql/mem.h +++ b/src/box/sql/mem.h @@ -874,3 +874,19 @@ mem_to_mpstream(const struct Mem *var, struct mpstream *stream); char * mem_encode_array(const struct Mem *mems, uint32_t count, uint32_t *size, struct region *region); + +/** + * Encode array of MEMs as msgpack map on region. Values in even position are + * treated as keys in MAP, values in odd position are treated as values in MAP. + * number of MEMs should be even. + * + * @param mems array of MEMs to encode. + * @param count number of elements in the array. + * @param[out] size Size of encoded msgpack map. + * @param region Region to use. + * @retval NULL on error, diag message is set. + * @retval Pointer to valid msgpack map on success. + */ +char * +mem_encode_map(const struct Mem *mems, uint32_t count, uint32_t *size, + struct region *region); diff --git a/src/box/sql/parse.y b/src/box/sql/parse.y index 15f6223b0..998acadea 100644 --- a/src/box/sql/parse.y +++ b/src/box/sql/parse.y @@ -1075,11 +1075,11 @@ expr(A) ::= VARNUM(X). { A.pExpr = expr_new_variable(pParse, &X, NULL); spanSet(&A, &X, &X); } -expr(A) ::= VARIABLE(X) id(Y). { +expr(A) ::= COLON|VARIABLE(X) id(Y). { A.pExpr = expr_new_variable(pParse, &X, &Y); spanSet(&A, &X, &Y); } -expr(A) ::= VARIABLE(X) INTEGER(Y). { +expr(A) ::= COLON|VARIABLE(X) INTEGER(Y). { A.pExpr = expr_new_variable(pParse, &X, &Y); spanSet(&A, &X, &Y); } @@ -1113,6 +1113,39 @@ expr(A) ::= LB(X) exprlist(Y) RB(E). { spanSet(&A, &X, &E); } +expr(A) ::= LCB(X) maplist(Y) RCB(E). { + struct sql *db = pParse->db; + struct Expr *expr = sql_expr_new_anon(db, TK_MAP); + if (expr == NULL) { + sql_expr_list_delete(db, Y); + pParse->is_aborted = true; + return; + } + expr->x.pList = Y; + expr->type = FIELD_TYPE_MAP; + sqlExprSetHeightAndFlags(pParse, expr); + A.pExpr = expr; + spanSet(&A, &X, &E); +} + +maplist(A) ::= nmaplist(A). +maplist(A) ::= . { + A = NULL; +} +nmaplist(A) ::= nmaplist(A) COMMA expr(X) COLON expr(Y). { + A = sql_expr_list_append(pParse->db, A, X.pExpr); + A = sql_expr_list_append(pParse->db, A, Y.pExpr); +} +nmaplist(A) ::= expr(X) COLON expr(Y). { + A = sql_expr_list_append(pParse->db, NULL, X.pExpr); + A = sql_expr_list_append(pParse->db, A, Y.pExpr); +} + +%type maplist {ExprList *} +%destructor maplist {sql_expr_list_delete(pParse->db, $$);} +%type nmaplist {ExprList *} +%destructor nmaplist {sql_expr_list_delete(pParse->db, $$);} + expr(A) ::= TRIM(X) LP trim_operands(Y) RP(E). { A.pExpr = sqlExprFunction(pParse, Y, &X); spanSet(&A, &X, &E); diff --git a/src/box/sql/tokenize.c b/src/box/sql/tokenize.c index 8bc519b9d..9e85801a3 100644 --- a/src/box/sql/tokenize.c +++ b/src/box/sql/tokenize.c @@ -58,7 +58,9 @@ #define CC_KYWD 1 /* Alphabetics or '_'. Usable in a keyword */ #define CC_ID 2 /* unicode characters usable in IDs */ #define CC_DIGIT 3 /* Digits */ -/** SQL variables: '@', '#', ':', and '$'. */ +/** Character ':'. */ +#define CC_COLON 4 +/** SQL variable special characters: '@', '#', and '$'. */ #define CC_VARALPHA 5 #define CC_VARNUM 6 /* '?'. Numeric SQL variables */ #define CC_SPACE 7 /* Space characters */ @@ -85,17 +87,21 @@ #define CC_LINEFEED 28 /* '\n' */ #define CC_LB 29 /* '[' */ #define CC_RB 30 /* ']' */ +/** Character '{'. */ +#define CC_LCB 31 +/** Character '}'. */ +#define CC_RCB 32 static const char sql_ascii_class[] = { /* x0 x1 x2 x3 x4 x5 x6 x7 x8 x9 xa xb xc xd xe xf */ /* 0x */ 27, 27, 27, 27, 27, 27, 27, 27, 27, 7, 28, 7, 7, 7, 27, 27, /* 1x */ 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, /* 2x */ 7, 15, 9, 5, 5, 22, 24, 8, 17, 18, 21, 20, 23, 11, 26, 16, -/* 3x */ 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 5, 19, 12, 14, 13, 6, +/* 3x */ 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 4, 19, 12, 14, 13, 6, /* 4x */ 5, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, /* 5x */ 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 29, 27, 30, 27, 1, /* 6x */ 27, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, -/* 7x */ 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 27, 10, 27, 25, 27, +/* 7x */ 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 31, 10, 32, 25, 27, /* 8x */ 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, /* 9x */ 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, /* Ax */ 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, @@ -228,6 +234,12 @@ sql_token(const char *z, int *type, bool *is_reserved) case CC_RB: *type = TK_RB; return 1; + case CC_LCB: + *type = TK_LCB; + return 1; + case CC_RCB: + *type = TK_RCB; + return 1; case CC_SEMI: *type = TK_SEMI; return 1; @@ -371,6 +383,9 @@ sql_token(const char *z, int *type, bool *is_reserved) case CC_VARNUM: *type = TK_VARNUM; return 1; + case CC_COLON: + *type = TK_COLON; + return 1; case CC_VARALPHA: *type = TK_VARIABLE; return 1; diff --git a/src/box/sql/vdbe.c b/src/box/sql/vdbe.c index 55e494332..86de3f98a 100644 --- a/src/box/sql/vdbe.c +++ b/src/box/sql/vdbe.c @@ -1438,6 +1438,26 @@ case OP_Array: { break; } +/** + * Opcode: Map P1 P2 P3 * * + * Synopsis: r[P2] = map(P3@P1) + * + * Construct an MAP value from P1 registers starting at reg(P3). + */ +case OP_Map: { + pOut = &aMem[pOp->p2]; + + uint32_t size; + struct region *region = &fiber()->gc; + size_t svp = region_used(region); + char *val = mem_encode_map(&aMem[pOp->p3], pOp->p1, &size, region); + if (val == NULL || mem_copy_map(pOut, val, size) != 0) { + region_truncate(region, svp); + goto abort_due_to_error; + } + break; +} + /* Opcode: Eq P1 P2 P3 P4 P5 * Synopsis: IF r[P3]==r[P1] * diff --git a/test/sql-tap/map.test.lua b/test/sql-tap/map.test.lua index 1afbb2b1d..7791ca779 100755 --- a/test/sql-tap/map.test.lua +++ b/test/sql-tap/map.test.lua @@ -1,6 +1,6 @@ #!/usr/bin/env tarantool local test = require("sqltester") -test:plan(110) +test:plan(131) box.schema.func.create('M1', { language = 'Lua', @@ -982,6 +982,189 @@ test:do_catchsql_test( 1, "Failed to execute SQL statement: wrong arguments for function ZEROBLOB()" }) +-- Make sure syntax for MAP values works as intended. +test:do_execsql_test( + "map-13.1", + [[ + SELECT {'a': a, 'g': g, 't': t, 'n': n, 'f': f, 'i': i, 'b': b, 'v': v, + 's': s, 'd': d, 'u': u} FROM t1 WHERE id = 1; + ]], { + {t = "1", f = 1, n = 1, v = "1", g = 1, b = true, s = 1, + d = require('decimal').new(1), a = {a = 1}, i = 1, + u = require('uuid').fromstr('11111111-1111-1111-1111-111111111111')} + }) + +test:do_execsql_test( + "map-13.2", + [[ + SELECT {'q': 1, 'w': true, 'e': 1.5e0, 'r': ['asd', x'32'], 't': 123.0}; + ]], { + {w = true, e = 1.5, r = {'asd', '2'}, t = require('decimal').new(123), + q = 1} + }) + +test:do_execsql_test( + "map-13.3", + [[ + SELECT typeof({1: 1}); + ]], { + "map" + }) + +test:do_execsql_test( + "map-13.4", + [[ + SELECT printf({}); + ]], { + '{}' + }) + +local map = {[0] = 0} +local str = '0: 0' +for i = 1, 1000 do map[i] = i str = str .. string.format(', %d: %d', i, i) end +test:do_execsql_test( + "map-13.5", + [[ + SELECT {]]..str..[[}; + ]], { + map + }) + +-- Make sure MAP() accepts only INTEGER, STRING and UUID as keys. +test:do_execsql_test( + "map-13.4", + [[ + SELECT {1: 1}; + ]], { + {[1] = 1} + }) + +test:do_execsql_test( + "map-13.5", + [[ + SELECT {-1: 1}; + ]], { + {[-1] = 1} + }) + +test:do_execsql_test( + "map-13.6", + [[ + SELECT {'a': 1}; + ]], { + {a = 1} + }) + +test:do_execsql_test( + "map-13.6", + [[ + SELECT typeof({UUID(): 1}); + ]], { + "map" + }) + +test:do_catchsql_test( + "map-13.7", + [[ + SELECT {1.5e0: 1}; + ]], { + 1, "Only integer, string and uuid can be keys in map" + }) + +test:do_catchsql_test( + "map-13.8", + [[ + SELECT {1.5: 1}; + ]], { + 1, "Only integer, string and uuid can be keys in map" + }) + +test:do_catchsql_test( + "map-13.9", + [[ + SELECT {x'33': 1}; + ]], { + 1, "Only integer, string and uuid can be keys in map" + }) + +test:do_catchsql_test( + "map-13.10", + [[ + SELECT {[1, 2, 3]: 1}; + ]], { + 1, "Only integer, string and uuid can be keys in map" + }) + +test:do_catchsql_test( + "map-13.11", + [[ + SELECT {{'a': 1}: 1}; + ]], { + 1, + 'Only integer, string and uuid can be keys in map' + }) + +test:do_catchsql_test( + "map-13.12", + [[ + SELECT {CAST(1 AS NUMBER): 1}; + ]], { + 1, 'Only integer, string and uuid can be keys in map' + }) + +test:do_catchsql_test( + "map-13.13", + [[ + SELECT {CAST(1 AS SCALAR): 1}; + ]], { + 1, 'Only integer, string and uuid can be keys in map' + }) + +test:do_catchsql_test( + "map-13.14", + [[ + SELECT {CAST(1 AS ANY): 1}; + ]], { + 1, 'Only integer, string and uuid can be keys in map' + }) + +test:do_test( + "map-13.15", + function() + local res = {pcall(box.execute, [[SELECT {?: 1};]], {1.5})} + return {tostring(res[3])} + end, { + "Type mismatch: can not convert double(1.5) to integer, string or uuid" + }) + +-- Make sure symbol ':' is properly processed by parser. +test:do_test( + "map-14.1", + function() + local res = {pcall(box.execute, [[SELECT {:name};]], {{[':name'] = 1}})} + return {tostring(res[3])} + end, { + "Syntax error at line 1 near '}'" + }) + +test:do_test( + "map-14.2", + function() + local res = box.execute([[SELECT {:name: 5}]], {{[':name'] = 1}}) + return {tostring(res.rows[1])} + end, { + "[{1: 5}]" + }) + +test:do_test( + "map-14.3", + function() + local res = box.execute([[SELECT {5::name}]], {{[':name'] = 1}}) + return {tostring(res.rows[1])} + end, { + "[{5: 1}]" + }) + box.execute([[DROP TABLE t1;]]) box.execute([[DROP TABLE t;]])