From 814befe821cbb2af3ac38352047da49e91d085d4 Mon Sep 17 00:00:00 2001 From: Mergen Imeev <imeevma@gmail.com> Date: Wed, 17 Nov 2021 10:21:41 +0300 Subject: [PATCH] sql: introduce operator [] This patch introduces operator [] that allows to get elements from MAP and ARRAY values. Closes #4762 Closes #4763 Part of #6251 @TarantoolBot document Title: Operator [] in SQL Operator `[]` allows to get an element of MAP and ARRAY values. Examples: ``` tarantool> box.execute([[SELECT [1, 2, 3, 4, 5][3];]]) --- - metadata: - name: COLUMN_1 type: any rows: - [3] ... tarantool> box.execute([[SELECT {'a' : 123, 7: 'asd'}['a'];]]) --- - metadata: - name: COLUMN_1 type: any rows: - [123] ... ``` The returned values is of type ANY. If the operator is applied to a value that is not a MAP or ARRAY or is NULL, an error is thrown. Example: ``` tarantool> box.execute([[SELECT 1[1];]]) --- - null - Selecting is only possible from map and array values ... ``` However, if there are two or more operators next to each other, the second or following operators do not throw an error, but instead return NULL. Example: ``` tarantool> box.execute([[select [1][1][2][3][4];]]) --- - metadata: - name: COLUMN_1 type: any rows: - [null] ... ``` --- .../unreleased/gh-6251-operator-brackets.md | 3 + extra/addopcodes.sh | 1 + src/box/sql/expr.c | 43 ++++++ src/box/sql/mem.c | 59 ++++++++ src/box/sql/mem.h | 17 +++ src/box/sql/parse.y | 27 ++++ src/box/sql/vdbe.c | 29 ++++ test/sql-luatest/containers_test.lua | 134 ++++++++++++++++++ 8 files changed, 313 insertions(+) create mode 100644 changelogs/unreleased/gh-6251-operator-brackets.md create mode 100644 test/sql-luatest/containers_test.lua diff --git a/changelogs/unreleased/gh-6251-operator-brackets.md b/changelogs/unreleased/gh-6251-operator-brackets.md new file mode 100644 index 0000000000..c48d852b19 --- /dev/null +++ b/changelogs/unreleased/gh-6251-operator-brackets.md @@ -0,0 +1,3 @@ +## feature/sql + +* Operator [] for MAP and ARRAY values is now introduced (gh-6251). diff --git a/extra/addopcodes.sh b/extra/addopcodes.sh index 3f8cfdf029..51acfe38ee 100755 --- a/extra/addopcodes.sh +++ b/extra/addopcodes.sh @@ -53,6 +53,7 @@ extras=" \ LINEFEED \ SPACE \ ILLEGAL \ + GETITEM \ " IFS=" " diff --git a/src/box/sql/expr.c b/src/box/sql/expr.c index 79b0607866..67c4cdd859 100644 --- a/src/box/sql/expr.c +++ b/src/box/sql/expr.c @@ -83,6 +83,8 @@ sql_expr_type(struct Expr *pExpr) enum field_type lhs_type = sql_expr_type(pExpr->pLeft); enum field_type rhs_type = sql_expr_type(pExpr->pRight); return sql_type_result(rhs_type, lhs_type); + case TK_GETITEM: + return FIELD_TYPE_ANY; case TK_CONCAT: return FIELD_TYPE_STRING; case TK_CASE: { @@ -3462,6 +3464,43 @@ expr_code_map(struct Parse *parser, struct Expr *expr, int reg) sqlVdbeAddOp3(vdbe, OP_Map, len, reg, result_reg); } +/** Generate opcodes for operator []. */ +static void +expr_code_getitem(struct Parse *parser, struct Expr *expr, int reg) +{ + struct Vdbe *vdbe = parser->pVdbe; + struct ExprList *list = expr->x.pList; + assert(list != NULL); + /* The last expr is the value to which the operator is applied. */ + int count = list->nExpr - 1; + struct Expr *value = list->a[count].pExpr; + + enum field_type type = value->op != TK_NULL ? sql_expr_type(value) : + field_type_MAX; + if (value->op != TK_VARIABLE && + type != FIELD_TYPE_MAP && type != FIELD_TYPE_ARRAY) { + diag_set(ClientError, ER_SQL_PARSER_GENERIC, "Selecting is " + "only possible from map and array values"); + parser->is_aborted = true; + return; + } + for (int i = 0; i < count; ++i) { + struct Expr *arg = list->a[i].pExpr; + enum field_type type = arg->op != TK_NULL ? sql_expr_type(arg) : + field_type_MAX; + if (type == FIELD_TYPE_MAP || type == FIELD_TYPE_ARRAY) { + diag_set(ClientError, ER_SQL_PARSER_GENERIC, "Map and " + "array values cannot be keys"); + parser->is_aborted = true; + return; + } + } + int reg_operands = parser->nMem + 1; + parser->nMem += count + 1; + sqlExprCodeExprList(parser, list, reg_operands, 0, SQL_ECEL_FACTOR); + sqlVdbeAddOp3(vdbe, OP_Getitem, count, reg, reg_operands); +} + /* * Erase column-cache entry number i */ @@ -3921,6 +3960,10 @@ sqlExprCodeTarget(Parse * pParse, Expr * pExpr, int target) expr_code_map(pParse, pExpr, target); return target; + case TK_GETITEM: + expr_code_getitem(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 96159a11d5..35e52fe686 100644 --- a/src/box/sql/mem.c +++ b/src/box/sql/mem.c @@ -3326,6 +3326,65 @@ mem_encode_map(const struct Mem *mems, uint32_t count, uint32_t *size, return NULL; } +/* Locate an element in a MAP or ARRAY using the given key.*/ +static int +mp_getitem(const char **data, const struct Mem *key) +{ + if (mp_typeof(**data) != MP_ARRAY && mp_typeof(**data) != MP_MAP) { + *data = NULL; + return 0; + } + if (mp_typeof(**data) == MP_ARRAY) { + uint32_t size = mp_decode_array(data); + if (!mem_is_uint(key) || key->u.u == 0 || key->u.u > size) { + *data = NULL; + return 0; + } + for (uint32_t i = 0; i < key->u.u - 1; ++i) + mp_next(data); + return 0; + } + struct Mem mem; + mem_create(&mem); + uint32_t size = mp_decode_map(data); + for (uint32_t i = 0; i < size; ++i) { + uint32_t len; + if (mem_from_mp_ephemeral(&mem, *data, &len) != 0) + return -1; + assert(mem_is_trivial(&mem) && !mem_is_metatype(&mem)); + *data += len; + if (mem_is_comparable(&mem) && + mem_cmp_scalar(&mem, key, NULL) == 0) + return 0; + mp_next(data); + } + *data = NULL; + return 0; +} + +int +mem_getitem(const struct Mem *mem, const struct Mem *keys, int count, + struct Mem *res) +{ + assert(count > 0); + assert(mem_is_map(mem) || mem_is_array(mem)); + const char *data = mem->z; + for (int i = 0; i < count && data != NULL; ++i) { + if (mp_getitem(&data, &keys[i]) != 0) + return -1; + } + if (data == NULL) { + mem_set_null(res); + return 0; + } + uint32_t len; + if (mem_from_mp(res, data, &len) != 0) + return -1; + res->flags |= MEM_Any; + assert((res->flags & (MEM_Number | MEM_Scalar)) == 0); + return 0; +} + /** * 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 70da203dd6..41bbd6e9be 100644 --- a/src/box/sql/mem.h +++ b/src/box/sql/mem.h @@ -142,6 +142,18 @@ mem_is_num(const struct Mem *mem) MEM_TYPE_DEC)) != 0; } +static inline bool +mem_is_any(const struct Mem *mem) +{ + return (mem->flags & MEM_Any) != 0; +} + +static inline bool +mem_is_container(const struct Mem *mem) +{ + return (mem->type & (MEM_TYPE_MAP | MEM_TYPE_ARRAY)) != 0; +} + static inline bool mem_is_metatype(const struct Mem *mem) { @@ -935,3 +947,8 @@ mem_encode_array(const struct Mem *mems, uint32_t count, uint32_t *size, char * mem_encode_map(const struct Mem *mems, uint32_t count, uint32_t *size, struct region *region); + +/** Return a value from ANY, MAP, or ARRAY MEM using the MEM array as keys. */ +int +mem_getitem(const struct Mem *mem, const struct Mem *keys, int count, + struct Mem *res); diff --git a/src/box/sql/parse.y b/src/box/sql/parse.y index 1788ac55ad..837ff58444 100644 --- a/src/box/sql/parse.y +++ b/src/box/sql/parse.y @@ -155,6 +155,7 @@ cmdx ::= cmd. %left CONCAT. %left COLLATE. %right BITNOT. +%right LB. ///////////////////// Begin and end transactions. //////////////////////////// @@ -1097,6 +1098,32 @@ expr(A) ::= CAST(X) LP expr(E) AS typedef(T) RP(Y). { sqlExprAttachSubtrees(pParse->db, A.pExpr, E.pExpr, 0); } +expr(A) ::= expr(X) LB getlist(Y) RB(E). { + struct Expr *expr = sql_expr_new_anon(pParse->db, TK_GETITEM); + if (expr == NULL) { + sql_expr_list_delete(pParse->db, Y); + pParse->is_aborted = true; + return; + } + Y = sql_expr_list_append(pParse->db, Y, X.pExpr); + expr->x.pList = Y; + expr->type = FIELD_TYPE_ANY; + sqlExprSetHeightAndFlags(pParse, expr); + A.pExpr = expr; + A.zStart = X.zStart; + A.zEnd = &E.z[E.n]; +} + +getlist(A) ::= getlist(A) RB LB expr(X). { + A = sql_expr_list_append(pParse->db, A, X.pExpr); +} +getlist(A) ::= expr(X). { + A = sql_expr_list_append(pParse->db, NULL, X.pExpr); +} + +%type getlist {ExprList *} +%destructor getlist {sql_expr_list_delete(pParse->db, $$);} + expr(A) ::= LB(X) exprlist(Y) RB(E). { struct Expr *expr = sql_expr_new_anon(pParse->db, TK_ARRAY); if (expr == NULL) { diff --git a/src/box/sql/vdbe.c b/src/box/sql/vdbe.c index 2f323c252b..f1a82226e2 100644 --- a/src/box/sql/vdbe.c +++ b/src/box/sql/vdbe.c @@ -1463,6 +1463,35 @@ case OP_Map: { break; } +/** + * Opcode: Getitem P1 P2 P3 * * + * Synopsis: r[P2] = value[P3@P1] + * + * Get an element from the value in register P3[P1] using values in + * registers P3, ... P3 + (P1 - 1). + */ +case OP_Getitem: { + int count = pOp->p1; + assert(count > 0); + struct Mem *value = &aMem[pOp->p3 + count]; + if (mem_is_null(value)) { + diag_set(ClientError, ER_SQL_EXECUTE, "Selecting is not " + "possible from NULL"); + goto abort_due_to_error; + } + if (mem_is_any(value) || !mem_is_container(value)) { + diag_set(ClientError, ER_SQL_TYPE_MISMATCH, mem_str(value), + "map or array"); + goto abort_due_to_error; + } + + pOut = &aMem[pOp->p2]; + struct Mem *keys = &aMem[pOp->p3]; + if (mem_getitem(value, keys, count, pOut) != 0) + goto abort_due_to_error; + break; +} + /* Opcode: Eq P1 P2 P3 P4 P5 * Synopsis: IF r[P3]==r[P1] * diff --git a/test/sql-luatest/containers_test.lua b/test/sql-luatest/containers_test.lua new file mode 100644 index 0000000000..9a01759b82 --- /dev/null +++ b/test/sql-luatest/containers_test.lua @@ -0,0 +1,134 @@ +local server = require('test.luatest_helpers.server') +local t = require('luatest') +local g = t.group() + +g.before_all(function() + g.server = server:new({alias = 'containers'}) + g.server:start() +end) + +g.after_all(function() + g.server:stop() +end) + +-- Make sure that it is possible to get elements from MAP и ARRAY. +g.test_containers_success = function() + g.server:exec(function() + local t = require('luatest') + local sql = [[SELECT [123, 234, 356, 467][2];]] + t.assert_equals(box.execute(sql).rows, {{234}}) + + sql = [[SELECT {'one' : 123, 3 : 'two', '123' : true}[3];]] + t.assert_equals(box.execute(sql).rows, {{'two'}}) + + sql = [[SELECT {'one' : [11, 22, 33], 3 : 'two'}['one'][2];]] + t.assert_equals(box.execute(sql).rows, {{22}}) + + sql = [[SELECT {'one' : 123, 3 : 'two', '123' : true}['three'];]] + t.assert_equals(box.execute(sql).rows, {{}}) + end) +end + +-- +-- Make sure that operator [] cannot get elements from values of types other +-- than MAP and ARRAY. Also, selecting from NULL throws an error. +-- +g.test_containers_error = function() + g.server:exec(function() + local t = require('luatest') + local _, err = box.execute([[SELECT 1[1];]]) + local res = "Selecting is only possible from map and array values" + t.assert_equals(err.message, res) + + _, err = box.execute([[SELECT -1[1];]]) + t.assert_equals(err.message, res) + + _, err = box.execute([[SELECT 1.1[1];]]) + t.assert_equals(err.message, res) + + _, err = box.execute([[SELECT 1.2e0[1];]]) + t.assert_equals(err.message, res) + + _, err = box.execute([[SELECT '1'[1];]]) + t.assert_equals(err.message, res) + + _, err = box.execute([[SELECT x'31'[1];]]) + t.assert_equals(err.message, res) + + _, err = box.execute([[SELECT true[1];]]) + t.assert_equals(err.message, res) + + _, err = box.execute([[SELECT uuid()[1];]]) + t.assert_equals(err.message, res) + + _, err = box.execute([[SELECT now()[1];]]) + t.assert_equals(err.message, res) + + _, err = box.execute([[SELECT (now() - now())[1];]]) + t.assert_equals(err.message, res) + + _, err = box.execute([[SELECT CAST(1 AS NUMBER)[1];]]) + t.assert_equals(err.message, res) + + _, err = box.execute([[SELECT CAST(1 AS SCALAR)[1];]]) + t.assert_equals(err.message, res) + + _, err = box.execute([[SELECT CAST(1 AS ANY)[1];]]) + t.assert_equals(err.message, res) + + _, err = box.execute([[SELECT CAST([1] AS ANY)[1];]]) + t.assert_equals(err.message, res) + + _, err = box.execute([[SELECT NULL[1];]]) + t.assert_equals(err.message, res) + + res = [[Failed to execute SQL statement: Selecting is not possible ]].. + [[from NULL]] + _, err = box.execute([[SELECT CAST(NULL AS ARRAY)[1];]]) + t.assert_equals(err.message, res) + end) +end + +-- +-- Make sure that the second and the following operators do not throw type +-- error. +-- +g.test_containers_followers = function() + g.server:exec(function() + local t = require('luatest') + local sql = [[SELECT [1, 2, 3][1][2];]] + t.assert_equals(box.execute(sql).rows, {{}}) + + sql = [[SELECT [1, 2, 3][1][2][3][4][5][6][7];]] + t.assert_equals(box.execute(sql).rows, {{}}) + + sql = [[SELECT ([1, 2, 3][1])[2];]] + local _, err = box.execute(sql) + local res = "Selecting is only possible from map and array values" + t.assert_equals(err.message, res) + end) +end + +-- Make sure that the received element is of type ANY. +g.test_containers_elem_type = function() + g.server:exec(function() + local t = require('luatest') + local sql = [[SELECT typeof([123, 234, 356, 467][2]);]] + t.assert_equals(box.execute(sql).rows, {{'any'}}) + + sql = [[SELECT [123, 234, 356, 467][2];]] + t.assert_equals(box.execute(sql).metadata[1].type, 'any') + + sql = [[SELECT typeof({'one' : 123, 3 : 'two', '123' : true}[3]);]] + t.assert_equals(box.execute(sql).rows, {{'any'}}) + + sql = [[SELECT {'one' : 123, 3 : 'two', '123' : true}[3];]] + t.assert_equals(box.execute(sql).metadata[1].type, 'any') + + sql = [[SELECT typeof({'one' : [11, 22, 33], 3 : 'two'}['one']);]] + t.assert_equals(box.execute(sql).rows, {{'any'}}) + + sql = [[SELECT {'one' : [11, 22, 33], 3 : 'two'}['one'];]] + t.assert_equals(box.execute(sql).metadata[1].type, 'any') + end) +end -- GitLab