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