Repository: genjidb/genji (go)

Issue:
Add `len` function for array type
<!-- Answering these questions may help with writing the initial proposal body. Thanks! -->

### Proposal

Currently, it’s not possible to compute the length of array type in Genji

<!--
What is the current status of the proposed behavior? E.g. “Currently, it’s not possible to do X in Genji”.
What is the proposed solution? E.g. “I propose adding SQL syntax Y that does X”.
-->


Pull request description:
Add LEN builtin function
Fixes #474

I have few concerns about this PR:

* @asdine provided examples of using LEN with columns in tables. But this PR doesn't contain such tests, because tests will be required outside of `expr` package (or import of another parser will be needed). Anyway, I don't see anything bad in such test, but I don't known codebase well enough to decide is it worth it.
* Maybe error message can be improved.
* LEN function is quite expensive for ARRAY and DOCUMENT because it need to loop over elements. Probably this can be problem, but out of scope of  the mentioned issue and this PR.

Patch:
diff --git a/cmd/genji/doc/functions.go b/cmd/genji/doc/functions.go
index 16e791aa..da77f5d6 100644
--- a/cmd/genji/doc/functions.go
+++ b/cmd/genji/doc/functions.go
@@ -15,6 +15,7 @@ var builtinDocs = functionDocs{
 	"sum":    "The sum function returns the sum of all values taken by the arg1 expression in a group.",
 	"avg":    "The avg function returns the average of all values taken by the arg1 expression in a group.",
 	"typeof": "The typeof function returns the type of arg1.",
+	"len":    "Then len function returns length of the arg1 expression if arg1 evals to string, array or document, either returns NULL.",
 }
 
 var mathDocs = functionDocs{
diff --git a/internal/expr/functions/builtins.go b/internal/expr/functions/builtins.go
index cc5d431b..5670be46 100644
--- a/internal/expr/functions/builtins.go
+++ b/internal/expr/functions/builtins.go
@@ -60,6 +60,13 @@ var builtinFunctions = Definitions{
 			return &Avg{Expr: args[0]}, nil
 		},
 	},
+	"len": &definition{
+		name:  "len",
+		arity: 1,
+		constructorFn: func(args ...expr.Expr) (expr.Function, error) {
+			return &Len{Expr: args[0]}, nil
+		},
+	},
 }
 
 // BuiltinDefinitions returns a map of builtin functions.
@@ -652,3 +659,61 @@ func (s *AvgAggregator) Eval(_ *environment.Environment) (types.Value, error) {
 func (s *AvgAggregator) String() string {
 	return s.Fn.String()
 }
+
+// Len represents the len() function.
+// It returns the length of string, array or document.
+// For other types len() returns NULL.
+type Len struct {
+	Expr expr.Expr
+}
+
+// Eval extracts the average value from the given document and returns it.
+func (s *Len) Eval(env *environment.Environment) (types.Value, error) {
+	val, err := s.Expr.Eval(env)
+	if err != nil {
+		return nil, err
+	}
+	var length int
+	switch val.Type() {
+	case types.TextValue:
+		length = len(types.As[string](val))
+	case types.ArrayValue:
+		arrayLen, err := document.ArrayLength(types.As[types.Array](val))
+		if err != nil {
+			return nil, err
+		}
+		length = arrayLen
+	case types.DocumentValue:
+		docLen, err := document.Length(types.As[types.Document](val))
+		if err != nil {
+			return nil, err
+		}
+		length = docLen
+	default:
+		return types.NewNullValue(), nil
+	}
+
+	return types.NewIntegerValue(int64(length)), nil
+}
+
+// IsEqual compares this expression with the other expression and returns
+// true if they are equal.
+func (s *Len) IsEqual(other expr.Expr) bool {
+	if other == nil {
+		return false
+	}
+
+	o, ok := other.(*Len)
+	if !ok {
+		return false
+	}
+
+	return expr.Equal(s.Expr, o.Expr)
+}
+
+func (s *Len) Params() []expr.Expr { return []expr.Expr{s.Expr} }
+
+// String returns the literal representation of len.
+func (s *Len) String() string {
+	return fmt.Sprintf("LEN(%v)", s.Expr)
+}


Test patch:
diff --git a/internal/expr/functions/testdata/builtin_functions.sql b/internal/expr/functions/testdata/builtin_functions.sql
index a4255d33..44485319 100644
--- a/internal/expr/functions/testdata/builtin_functions.sql
+++ b/internal/expr/functions/testdata/builtin_functions.sql
@@ -33,4 +33,3 @@
 
 > typeof(NULL)
 'null'
-
diff --git a/sqltests/SELECT/len.sql b/sqltests/SELECT/len.sql
new file mode 100644
index 00000000..f9696c54
--- /dev/null
+++ b/sqltests/SELECT/len.sql
@@ -0,0 +1,155 @@
+-- setup:
+CREATE TABLE foo(
+  a TEXT,
+  b ARRAY,
+  c (
+      ...
+  )
+);
+INSERT INTO foo VALUES (
+  "hello",
+  [1, 2, 3, [4, 5]],
+  {
+    a: 1,
+    b: 2,
+    c: {
+      d: 3
+    }
+  }
+);
+
+-- test: text field
+SELECT len(a) FROM foo;
+/* result:
+{
+    "LEN(a)": 5
+}
+*/
+
+-- test: array field
+SELECT len(b) FROM foo;
+/* result:
+{
+    "LEN(b)": 4
+}
+*/
+
+-- test: document
+SELECT len(c) FROM foo;
+/* result:
+{
+    "LEN(c)": 3
+}
+*/
+
+-- test: subarray
+SELECT len(b[3]) FROM foo;
+/* result:
+{
+    "LEN(b[3])": 2
+}
+*/
+
+-- test: subdocument
+SELECT len(c.c) FROM foo;
+/* result:
+{
+    "LEN(c.c)": 1
+}
+*/
+
+-- test: text expr
+SELECT len("hello, world!");
+/* result:
+{
+  "LEN(\"hello, world!\")": 13
+}
+*/
+
+-- test: zero text expr
+SELECT len('');
+/* result:
+{
+  "LEN(\"\")": 0
+}
+*/
+
+-- test: array expr
+SELECT len([1, 2, 3, 4, 5]);
+/* result:
+{
+  "LEN([1, 2, 3, 4, 5])": 5
+}
+*/
+
+-- test: empty array expr
+SELECT len([]);
+/* result:
+{
+  "LEN([])": 0
+}
+*/
+
+-- test: mixed type array expr
+SELECT len([1, 2, 3, [1, 2, 3]]);
+/* result:
+{
+  "LEN([1, 2, 3, [1, 2, 3]])": 4
+}
+*/
+
+-- test: document expr
+SELECT len({'a': 1, 'b': 2, 'c': 3});
+/* result:
+{
+  "LEN({a: 1, b: 2, c: 3})": 3
+}
+*/
+
+-- test: empty document expr
+SELECT len({});
+/* result:
+{
+  "LEN({})": 0
+}
+*/
+
+-- test: integer expr
+SELECT len(10);
+/* result:
+{
+  "LEN(10)": NULL
+}
+*/
+
+-- test: float expr
+SELECT len(1.0);
+/* result:
+{
+  "LEN(1.0)": NULL
+}
+*/
+
+-- test: NULL expr
+SELECT len(NULL);
+/* result:
+{
+  "LEN(NULL)": NULL
+}
+*/
+
+-- test: NULL expr
+SELECT len(NULL);
+/* result:
+{
+  "LEN(NULL)": NULL
+}
+*/
+
+-- test: blob expr
+SELECT len('\x323232') as l;
+/* result:
+{
+  "l": NULL
+}
+*/