Skip to content
Snippets Groups Projects
Commit eae58bf4 authored by Pavel Antonov's avatar Pavel Antonov :asterisk:
Browse files

feat(core): Добавлены функции exists и len для языка запросов expr

Close #PRXS-1883
parents d0069ab4 235e56b6
No related branches found
No related tags found
No related merge requests found
......@@ -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()")
}
}
......@@ -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},
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment