From 23d638b90a960d04803bcfadc4b5c76eaf5053ef Mon Sep 17 00:00:00 2001
From: Danis Kirasirov <dbgbbu@gmail.com>
Date: Fri, 26 Jan 2024 19:30:14 +0300
Subject: [PATCH] =?UTF-8?q?=D0=A0=D0=B5=D0=B0=D0=BB=D0=B8=D0=B7=D0=BE?=
 =?UTF-8?q?=D0=B2=D0=B0=D0=BD=D0=B0=20=D1=84=D1=83=D0=BD=D0=BA=D1=86=D0=B8?=
 =?UTF-8?q?=D1=8F=20len(arr)=20=D0=B4=D0=BB=D1=8F=20<,=20>,=20=3D=3D?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 pkg/expr/mongo.go      | 40 +++++++++++++++++++++++++++++++++++++++-
 pkg/expr/mongo_test.go |  5 +++++
 2 files changed, 44 insertions(+), 1 deletion(-)

diff --git a/pkg/expr/mongo.go b/pkg/expr/mongo.go
index 0aa9b29b..303ec125 100644
--- a/pkg/expr/mongo.go
+++ b/pkg/expr/mongo.go
@@ -4,6 +4,7 @@ import (
 	"context"
 	"fmt"
 	"regexp"
+	"strconv"
 	"strings"
 
 	"github.com/expr-lang/expr"
@@ -232,6 +233,17 @@ func (c *compiler) identifier(node ast.Node) string {
 func (c *compiler) BinaryNode(node *ast.BinaryNode) interface{} {
 	switch node.Operator {
 	case "==":
+		op := c.eval(node.Right)
+		lenNode, ok := node.Left.(*ast.BuiltinNode)
+		if ok && lenNode.Name == "len" && len(lenNode.Arguments) == 1 {
+			// Оптимизация для случая len(arr) == 0, т.к. $size не использует индекс
+			if op == 0 {
+				return bson.M{c.identifier(lenNode.Arguments[0]): bson.M{"$eq": bson.A{}}}
+			}
+
+			return bson.M{c.identifier(lenNode.Arguments[0]): bson.M{"$size": op}}
+		}
+
 		return bson.M{c.identifier(node.Left): c.eval(node.Right)}
 
 	case "!=":
@@ -250,10 +262,36 @@ func (c *compiler) BinaryNode(node *ast.BinaryNode) interface{} {
 		return bson.M{c.identifier(node.Left): bson.M{"$nin": c.eval(node.Right)}}
 
 	case "<":
+		op := c.eval(node.Right)
+		lenNode, ok := node.Left.(*ast.BuiltinNode)
+		if ok && lenNode.Name == "len" && len(lenNode.Arguments) == 1 {
+			length, ok := op.(int)
+			if !ok {
+				panic("len must be compared with an integer")
+			}
+			return bson.M{c.identifier(lenNode.Arguments[0]) + "." + strconv.Itoa(length): bson.M{"$exists": false}}
+		}
+
 		return bson.M{c.identifier(node.Left): bson.M{"$lt": c.eval(node.Right)}}
 
 	case ">":
-		return bson.M{c.identifier(node.Left): bson.M{"$gt": c.eval(node.Right)}}
+		op := c.eval(node.Right)
+		lenNode, ok := node.Left.(*ast.BuiltinNode)
+		if ok && lenNode.Name == "len" && len(lenNode.Arguments) == 1 {
+			length, ok := op.(int)
+			if !ok {
+				panic("len must be compared with an integer")
+			}
+
+			// Оптимизация для случая len(arr) > 0
+			if length == 0 {
+				return bson.M{c.identifier(lenNode.Arguments[0]): bson.M{"$exists": true, "$type": "array", "$ne": bson.A{}}}
+			}
+
+			return bson.M{c.identifier(lenNode.Arguments[0]) + "." + strconv.Itoa(length): bson.M{"$exists": true}}
+		}
+
+		return bson.M{c.identifier(node.Left): bson.M{"$gt": op}}
 
 	case "<=":
 		return bson.M{c.identifier(node.Left): bson.M{"$lte": c.eval(node.Right)}}
diff --git a/pkg/expr/mongo_test.go b/pkg/expr/mongo_test.go
index f50b27c2..3e77e0d4 100644
--- a/pkg/expr/mongo_test.go
+++ b/pkg/expr/mongo_test.go
@@ -31,6 +31,11 @@ func TestConvertToMongo(t *testing.T) {
 		{"in array", "s in [1,2,3]", nil, bson.M{"s": bson.M{"$in": []interface{}{1, 2, 3}}}, false},
 		{"not in array", "s not in [1,2,3]", nil, bson.M{"s": bson.M{"$nin": []interface{}{1, 2, 3}}}, false},
 		{"exists", "exists(s)", nil, bson.M{"$or": bson.A{bson.M{"s": bson.M{"$exists": true, "$type": "array"}}, bson.M{"s": bson.M{"$ne": nil}}}}, false},
+		{"len equal", "len(s) == 1", nil, bson.M{"s": bson.M{"$size": 1}}, false},
+		{"len equal empty", "len(s) == 0", nil, bson.M{"s": bson.M{"$eq": bson.A{}}}, false},
+		{"len gt", "len(s) > 1", nil, bson.M{"s.1": bson.M{"$exists": true}}, false},
+		{"len gt 0", "len(s) > 0", nil, bson.M{"s": bson.M{"$exists": true, "$type": "array", "$ne": bson.A{}}}, false},
+		{"len lt", "len(s) < 1", nil, bson.M{"s.1": bson.M{"$exists": false}}, false},
 		{"field#1", "s.test > 3", nil, bson.M{"s.test": bson.M{"$gt": 3}}, false},
 		{"field#2", "s['test'] > 3", nil, bson.M{"s.test": bson.M{"$gt": 3}}, false},
 		{"field#3", "s[test] > 3", nil, bson.M{"s.test": bson.M{"$gt": 3}}, false},
-- 
GitLab