diff --git a/pkg/expr/mongo.go b/pkg/expr/mongo.go index 397043c2fc9e99ad26498b3aecbf87280ac795a1..597180a671e80c2a5bc79bb8efd9bbee940120e4 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" @@ -213,7 +214,7 @@ func (c *compiler) UnaryNode(node *ast.UnaryNode) interface{} { return bson.M{c.identifier(nodeIn.Left): bson.M{"$nin": c.eval(nodeIn.Right)}} } - return bson.M{"$not": c.compile(node.Node)} + return bson.M{"$nor": bson.A{c.compile(node.Node)}} default: panic(fmt.Sprintf("unknown operator (%v)", node.Operator)) } @@ -230,6 +231,10 @@ func (c *compiler) identifier(node ast.Node) string { } func (c *compiler) BinaryNode(node *ast.BinaryNode) interface{} { + if result := c.handleLenNode(node); result != nil { + return result + } + switch node.Operator { case "==": return bson.M{c.identifier(node.Left): c.eval(node.Right)} @@ -404,6 +409,12 @@ func (c *compiler) CallNode(node *ast.CallNode) interface{} { } return bson.M{fields: bson.M{"$in": array}} + case "exists": + if len(node.Arguments) != 1 { + panic("exists() expects exactly 1 argument") + } + field := c.identifier(node.Arguments[0]) + return bson.M{field: bson.M{"$exists": true}} case "icontains": v := c.identifier(node.Arguments[0]) @@ -641,3 +652,50 @@ func (c *compiler) PairNode(node *ast.PairNode) interface{} { //c.compile(node.Key) //c.compile(node.Value) } + +// handleLenNode получает узел AST и возвращает запрос для mongo, +// если узел представляет вызов функции len, и nil в противном случае. +func (c *compiler) handleLenNode(node *ast.BinaryNode) bson.M { + lenNode, ok := node.Left.(*ast.BuiltinNode) + if !ok || lenNode.Name != "len" { + return nil + } + + if len(lenNode.Arguments) != 1 { + panic("len() expects exactly 1 argument") + } + + length, ok := c.eval(node.Right).(int) + if !ok { + panic("len() can only be compared with number value") + } + if length < 0 { + panic("len() can only be compared with non-negative number") + } + + field := c.identifier(lenNode.Arguments[0]) + switch op := node.Operator; { + case (op == "==" || op == "<=") && length == 0: + return bson.M{field: bson.M{"$eq": bson.A{}}} + case (op == "!=" || op == ">") && length == 0: + return bson.M{field: bson.M{"$exists": true, "$type": "array", "$ne": bson.A{}}} + case op == ">=" && length == 0: + return bson.M{field: bson.M{"$exists": true, "$type": "array"}} + case op == "<" && length == 0: + panic("invalid comparison: len() cannot be less than 0") + case op == "==": + return bson.M{field: bson.M{"$size": length}} + case op == "!=": + return bson.M{field: bson.M{"$not": bson.M{"$size": length}, "$type": "array"}} + case op == ">": + return bson.M{field + "." + strconv.Itoa(length): bson.M{"$exists": true}} + case op == ">=": + return bson.M{field + "." + strconv.Itoa(length-1): bson.M{"$exists": true}} + case op == "<": + return bson.M{field + "." + strconv.Itoa(length-1): bson.M{"$exists": false}, field: bson.M{"$type": "array"}} + case op == "<=": + return bson.M{field + "." + strconv.Itoa(length): bson.M{"$exists": false}, field: bson.M{"$type": "array"}} + default: + panic("invalid comparison operator with len()") + } +} diff --git a/pkg/expr/mongo_test.go b/pkg/expr/mongo_test.go index dadf30e720ce88cd51591989dd4e0082f3f0883f..dee6e668c490a6d375a77c532862f5389b1bd5f4 100644 --- a/pkg/expr/mongo_test.go +++ b/pkg/expr/mongo_test.go @@ -30,6 +30,25 @@ func TestConvertToMongo(t *testing.T) { {"equal", "s == 3", nil, bson.M{"s": 3}, false}, {"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#1", "exists(s)", nil, bson.M{"s": bson.M{"$exists": true}}, false}, + {"exists#2", "exists(s, s)", nil, nil, true}, + {"len#1", "len(s)", nil, nil, true}, + {"len#2", "len(s) <> 1", nil, nil, true}, + {"len#3", "len(s) == -1", nil, nil, true}, + {"len#4", "len(s, s) == -1", nil, nil, true}, + {"len#5", "len(s) == s", nil, nil, true}, + {"len eq", "len(s) == 1", nil, bson.M{"s": bson.M{"$size": 1}}, false}, + {"len eq zero", "len(s) == 0", nil, bson.M{"s": bson.M{"$eq": bson.A{}}}, false}, + {"len ne", "len(s) != 1", nil, bson.M{"s": bson.M{"$not": bson.M{"$size": 1}, "$type": "array"}}, false}, + {"len ne zero", "len(s) != 0", nil, bson.M{"s": bson.M{"$exists": true, "$ne": bson.A{}, "$type": "array"}}, false}, + {"len gt", "len(s) > 1", nil, bson.M{"s.1": bson.M{"$exists": true}}, false}, + {"len gt zero", "len(s) > 0", nil, bson.M{"s": bson.M{"$exists": true, "$type": "array", "$ne": bson.A{}}}, false}, + {"len gte", "len(s) >= 1", nil, bson.M{"s.0": bson.M{"$exists": true}}, false}, + {"len gte zero", "len(s) >= 0", nil, bson.M{"s": bson.M{"$exists": true, "$type": "array"}}, false}, + {"len lt", "len(s) < 1", nil, bson.M{"s.0": bson.M{"$exists": false}, "s": bson.M{"$type": "array"}}, false}, + {"len lt zero", "len(s) < 0", nil, nil, true}, + {"len lte", "len(s) <= 1", nil, bson.M{"s.1": bson.M{"$exists": false}, "s": bson.M{"$type": "array"}}, false}, + {"len lte zero", "len(s) <= 0", nil, bson.M{"s": bson.M{"$eq": bson.A{}}}, 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}, @@ -46,6 +65,8 @@ func TestConvertToMongo(t *testing.T) { {"iendsWith", "iendsWith(s, 'some')", nil, bson.M{"s": bson.M{"$regex": ".*some$", "$options": "i"}}, false}, {"iendsWith . + () $ {} ^", "iendsWith(s,'. + () $ {} ^')", nil, bson.M{"s": bson.M{"$regex": ".*\\. \\+ \\(\\) \\$ \\{\\} \\^$", "$options": "i"}}, false}, {"or", "s==2 || s > 10", nil, bson.M{"$or": bson.A{bson.M{"s": 2}, bson.M{"s": bson.M{"$gt": 10}}}}, false}, + {"not#1", "not icontains(s, 'some')", nil, bson.M{"$nor": bson.A{bson.M{"s": bson.M{"$options": "i", "$regex": "some"}}}}, false}, + {"not#2", "not (s.test > 3)", nil, bson.M{"$nor": bson.A{bson.M{"s.test": bson.M{"$gt": 3}}}}, false}, {"search", "search('some') || s > 10", nil, bson.M{"$or": bson.A{bson.M{"$text": bson.M{"$search": "some"}}, bson.M{"s": bson.M{"$gt": 10}}}}, false}, {"vars:or", "s== a + 2 || s > a + 10", map[string]interface{}{"a": 100}, bson.M{"$or": bson.A{bson.M{"s": 102}, bson.M{"s": bson.M{"$gt": 110}}}}, false}, {"near", "near(a, [55.5, 37.5], 1000)", map[string]interface{}{"a": []interface{}{55, 37}}, bson.M{"a.geometry": bson.M{"$near": bson.D{{Key: "$geometry", Value: map[string]interface{}{"coordinates": []interface{}{55.5, 37.5}, "type": "Point"}}, {Key: "$maxDistance", Value: 1000}}}}, false},