mirror of
https://github.com/SecurityBrewery/catalyst.git
synced 2026-01-25 07:23:28 +01:00
Release catalyst
This commit is contained in:
182
caql/blevebuilder.go
Normal file
182
caql/blevebuilder.go
Normal file
@@ -0,0 +1,182 @@
|
||||
package caql
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"strconv"
|
||||
|
||||
"github.com/SecurityBrewery/catalyst/generated/caql/parser"
|
||||
)
|
||||
|
||||
var TooComplexError = errors.New("unsupported features for index queries, use advanced search instead")
|
||||
|
||||
type bleveBuilder struct {
|
||||
*parser.BaseCAQLParserListener
|
||||
stack []string
|
||||
err error
|
||||
}
|
||||
|
||||
// push is a helper function for pushing new node to the listener Stack.
|
||||
func (s *bleveBuilder) push(i string) {
|
||||
s.stack = append(s.stack, i)
|
||||
}
|
||||
|
||||
// pop is a helper function for poping a node from the listener Stack.
|
||||
func (s *bleveBuilder) pop() (n string) {
|
||||
// Check that we have nodes in the stack.
|
||||
size := len(s.stack)
|
||||
if size < 1 {
|
||||
panic(ErrStack)
|
||||
}
|
||||
|
||||
// Pop the last value from the Stack.
|
||||
n, s.stack = s.stack[size-1], s.stack[:size-1]
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (s *bleveBuilder) binaryPop() (interface{}, interface{}) {
|
||||
right, left := s.pop(), s.pop()
|
||||
return left, right
|
||||
}
|
||||
|
||||
// ExitExpression is called when production expression is exited.
|
||||
func (s *bleveBuilder) ExitExpression(ctx *parser.ExpressionContext) {
|
||||
switch {
|
||||
case ctx.Value_literal() != nil:
|
||||
// pass
|
||||
case ctx.Reference() != nil:
|
||||
// pass
|
||||
case ctx.Operator_unary() != nil:
|
||||
s.err = TooComplexError
|
||||
return
|
||||
|
||||
case ctx.T_PLUS() != nil:
|
||||
fallthrough
|
||||
case ctx.T_MINUS() != nil:
|
||||
fallthrough
|
||||
case ctx.T_TIMES() != nil:
|
||||
fallthrough
|
||||
case ctx.T_DIV() != nil:
|
||||
fallthrough
|
||||
case ctx.T_MOD() != nil:
|
||||
s.err = TooComplexError
|
||||
return
|
||||
|
||||
case ctx.T_RANGE() != nil:
|
||||
s.err = TooComplexError
|
||||
return
|
||||
|
||||
case ctx.T_LT() != nil && ctx.GetEq_op() == nil:
|
||||
left, right := s.binaryPop()
|
||||
s.push(fmt.Sprintf("%s:<%s", left, right))
|
||||
case ctx.T_GT() != nil && ctx.GetEq_op() == nil:
|
||||
left, right := s.binaryPop()
|
||||
s.push(fmt.Sprintf("%s:>%s", left, right))
|
||||
case ctx.T_LE() != nil && ctx.GetEq_op() == nil:
|
||||
left, right := s.binaryPop()
|
||||
s.push(fmt.Sprintf("%s:<=%s", left, right))
|
||||
case ctx.T_GE() != nil && ctx.GetEq_op() == nil:
|
||||
left, right := s.binaryPop()
|
||||
s.push(fmt.Sprintf("%s:>=%s", left, right))
|
||||
|
||||
case ctx.T_IN() != nil && ctx.GetEq_op() == nil:
|
||||
s.err = TooComplexError
|
||||
return
|
||||
|
||||
case ctx.T_EQ() != nil && ctx.GetEq_op() == nil:
|
||||
left, right := s.binaryPop()
|
||||
s.push(fmt.Sprintf("%s:%s", left, right))
|
||||
case ctx.T_NE() != nil && ctx.GetEq_op() == nil:
|
||||
left, right := s.binaryPop()
|
||||
s.push(fmt.Sprintf("-%s:%s", left, right))
|
||||
|
||||
case ctx.T_ALL() != nil && ctx.GetEq_op() != nil:
|
||||
fallthrough
|
||||
case ctx.T_ANY() != nil && ctx.GetEq_op() != nil:
|
||||
fallthrough
|
||||
case ctx.T_NONE() != nil && ctx.GetEq_op() != nil:
|
||||
s.err = TooComplexError
|
||||
return
|
||||
|
||||
case ctx.T_ALL() != nil && ctx.T_NOT() != nil && ctx.T_IN() != nil:
|
||||
fallthrough
|
||||
case ctx.T_ANY() != nil && ctx.T_NOT() != nil && ctx.T_IN() != nil:
|
||||
fallthrough
|
||||
case ctx.T_NONE() != nil && ctx.T_NOT() != nil && ctx.T_IN() != nil:
|
||||
s.err = TooComplexError
|
||||
return
|
||||
|
||||
case ctx.T_LIKE() != nil:
|
||||
s.err = errors.New("index queries are like queries by default")
|
||||
return
|
||||
|
||||
case ctx.T_REGEX_MATCH() != nil:
|
||||
left, right := s.binaryPop()
|
||||
if ctx.T_NOT() != nil {
|
||||
s.err = TooComplexError
|
||||
return
|
||||
} else {
|
||||
s.push(fmt.Sprintf("%s:/%s/", left, right))
|
||||
}
|
||||
case ctx.T_REGEX_NON_MATCH() != nil:
|
||||
s.err = errors.New("index query cannot contain regex non matches, use advanced search instead")
|
||||
return
|
||||
|
||||
case ctx.T_AND() != nil:
|
||||
left, right := s.binaryPop()
|
||||
s.push(fmt.Sprintf("%s %s", left, right))
|
||||
case ctx.T_OR() != nil:
|
||||
s.err = errors.New("index query cannot contain OR, use advanced search instead")
|
||||
return
|
||||
|
||||
case ctx.T_QUESTION() != nil && len(ctx.AllExpression()) == 3:
|
||||
s.err = errors.New("index query cannot contain ternary operations, use advanced search instead")
|
||||
return
|
||||
case ctx.T_QUESTION() != nil && len(ctx.AllExpression()) == 2:
|
||||
s.err = errors.New("index query cannot contain ternary operations, use advanced search instead")
|
||||
return
|
||||
|
||||
default:
|
||||
panic("unknown expression")
|
||||
}
|
||||
}
|
||||
|
||||
// ExitReference is called when production reference is exited.
|
||||
func (s *bleveBuilder) ExitReference(ctx *parser.ReferenceContext) {
|
||||
switch {
|
||||
case ctx.DOT() != nil:
|
||||
reference := s.pop()
|
||||
|
||||
s.push(fmt.Sprintf("%s.%s", reference, ctx.T_STRING().GetText()))
|
||||
case ctx.T_STRING() != nil:
|
||||
s.push(ctx.T_STRING().GetText())
|
||||
case ctx.Compound_value() != nil:
|
||||
s.err = TooComplexError
|
||||
return
|
||||
case ctx.Function_call() != nil:
|
||||
s.err = TooComplexError
|
||||
return
|
||||
case ctx.T_OPEN() != nil:
|
||||
s.err = TooComplexError
|
||||
return
|
||||
case ctx.T_ARRAY_OPEN() != nil:
|
||||
s.err = TooComplexError
|
||||
return
|
||||
default:
|
||||
panic(fmt.Sprintf("unexpected value: %s", ctx.GetText()))
|
||||
}
|
||||
}
|
||||
|
||||
// ExitValue_literal is called when production value_literal is exited.
|
||||
func (s *bleveBuilder) ExitValue_literal(ctx *parser.Value_literalContext) {
|
||||
if ctx.T_QUOTED_STRING() != nil {
|
||||
st, err := unquote(ctx.GetText())
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
s.push(strconv.Quote(st))
|
||||
} else {
|
||||
s.push(ctx.GetText())
|
||||
}
|
||||
}
|
||||
50
caql/blevebuilder_test.go
Normal file
50
caql/blevebuilder_test.go
Normal file
@@ -0,0 +1,50 @@
|
||||
package caql
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestBleveBuilder(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
saql string
|
||||
wantBleve string
|
||||
wantParseErr bool
|
||||
wantRebuildErr bool
|
||||
}{
|
||||
{name: "Search 1", saql: `"Bob"`, wantBleve: `"Bob"`},
|
||||
{name: "Search 2", saql: `"Bob" AND title == 'Name'`, wantBleve: `"Bob" title:"Name"`},
|
||||
{name: "Search 3", saql: `"Bob" OR title == 'Name'`, wantRebuildErr: true},
|
||||
{name: "Search 4", saql: `title == 'malware' AND 'wannacry'`, wantBleve: `title:"malware" "wannacry"`},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
parser := &Parser{}
|
||||
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
expr, err := parser.Parse(tt.saql)
|
||||
if (err != nil) != tt.wantParseErr {
|
||||
t.Errorf("Parse() error = %v, wantErr %v", err, tt.wantParseErr)
|
||||
if expr != nil {
|
||||
t.Error(expr.String())
|
||||
}
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
got, err := expr.BleveString()
|
||||
if (err != nil) != tt.wantRebuildErr {
|
||||
t.Error(expr.String())
|
||||
t.Errorf("String() error = %v, wantErr %v", err, tt.wantParseErr)
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if got != tt.wantBleve {
|
||||
t.Errorf("String() got = %v, want %v", got, tt.wantBleve)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
317
caql/builder.go
Normal file
317
caql/builder.go
Normal file
@@ -0,0 +1,317 @@
|
||||
package caql
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/SecurityBrewery/catalyst/generated/caql/parser"
|
||||
)
|
||||
|
||||
type Searcher interface {
|
||||
Search(term string) (ids []string, err error)
|
||||
}
|
||||
|
||||
type aqlBuilder struct {
|
||||
*parser.BaseCAQLParserListener
|
||||
searcher Searcher
|
||||
stack []string
|
||||
prefix string
|
||||
}
|
||||
|
||||
// push is a helper function for pushing new node to the listener Stack.
|
||||
func (s *aqlBuilder) push(i string) {
|
||||
s.stack = append(s.stack, i)
|
||||
}
|
||||
|
||||
// pop is a helper function for poping a node from the listener Stack.
|
||||
func (s *aqlBuilder) pop() (n string) {
|
||||
// Check that we have nodes in the stack.
|
||||
size := len(s.stack)
|
||||
if size < 1 {
|
||||
panic(ErrStack)
|
||||
}
|
||||
|
||||
// Pop the last value from the Stack.
|
||||
n, s.stack = s.stack[size-1], s.stack[:size-1]
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (s *aqlBuilder) binaryPop() (string, string) {
|
||||
right, left := s.pop(), s.pop()
|
||||
return left, right
|
||||
}
|
||||
|
||||
// ExitExpression is called when production expression is exited.
|
||||
func (s *aqlBuilder) ExitExpression(ctx *parser.ExpressionContext) {
|
||||
switch {
|
||||
case ctx.Value_literal() != nil:
|
||||
if ctx.GetParent().GetParent() == nil {
|
||||
s.push(s.toBoolString(s.pop()))
|
||||
}
|
||||
case ctx.Reference() != nil:
|
||||
ref := s.pop()
|
||||
if ref == "d.id" {
|
||||
s.push("d._key")
|
||||
} else {
|
||||
s.push(ref)
|
||||
}
|
||||
// pass
|
||||
case ctx.Operator_unary() != nil:
|
||||
s.push(s.toBoolString(s.pop()))
|
||||
|
||||
case ctx.T_PLUS() != nil:
|
||||
left, right := s.binaryPop()
|
||||
s.push(fmt.Sprintf("%s + %s", left, right))
|
||||
case ctx.T_MINUS() != nil:
|
||||
left, right := s.binaryPop()
|
||||
s.push(fmt.Sprintf("%s - %s", left, right))
|
||||
case ctx.T_TIMES() != nil:
|
||||
left, right := s.binaryPop()
|
||||
s.push(fmt.Sprintf("%s * %s", left, right))
|
||||
case ctx.T_DIV() != nil:
|
||||
left, right := s.binaryPop()
|
||||
s.push(fmt.Sprintf("%s / %s", left, right))
|
||||
case ctx.T_MOD() != nil:
|
||||
left, right := s.binaryPop()
|
||||
s.push(fmt.Sprintf("%s %% %s", left, right))
|
||||
|
||||
case ctx.T_RANGE() != nil:
|
||||
left, right := s.binaryPop()
|
||||
s.push(fmt.Sprintf("%s..%s", left, right))
|
||||
|
||||
case ctx.T_LT() != nil && ctx.GetEq_op() == nil:
|
||||
left, right := s.binaryPop()
|
||||
s.push(fmt.Sprintf("%s < %s", left, right))
|
||||
case ctx.T_GT() != nil && ctx.GetEq_op() == nil:
|
||||
left, right := s.binaryPop()
|
||||
s.push(fmt.Sprintf("%s > %s", left, right))
|
||||
case ctx.T_LE() != nil && ctx.GetEq_op() == nil:
|
||||
left, right := s.binaryPop()
|
||||
s.push(fmt.Sprintf("%s <= %s", left, right))
|
||||
case ctx.T_GE() != nil && ctx.GetEq_op() == nil:
|
||||
left, right := s.binaryPop()
|
||||
s.push(fmt.Sprintf("%s >= %s", left, right))
|
||||
|
||||
case ctx.T_IN() != nil && ctx.GetEq_op() == nil:
|
||||
left, right := s.binaryPop()
|
||||
if ctx.T_NOT() != nil {
|
||||
s.push(fmt.Sprintf("%s NOT IN %s", left, right))
|
||||
} else {
|
||||
s.push(fmt.Sprintf("%s IN %s", left, right))
|
||||
}
|
||||
|
||||
case ctx.T_EQ() != nil && ctx.GetEq_op() == nil:
|
||||
left, right := s.binaryPop()
|
||||
s.push(fmt.Sprintf("%s == %s", left, right))
|
||||
case ctx.T_NE() != nil && ctx.GetEq_op() == nil:
|
||||
left, right := s.binaryPop()
|
||||
s.push(fmt.Sprintf("%s != %s", left, right))
|
||||
|
||||
case ctx.T_ALL() != nil && ctx.GetEq_op() != nil:
|
||||
right, left := s.pop(), s.pop()
|
||||
s.push(fmt.Sprintf("%s ALL %s %s", left, ctx.GetEq_op().GetText(), right))
|
||||
case ctx.T_ANY() != nil && ctx.GetEq_op() != nil:
|
||||
right, left := s.pop(), s.pop()
|
||||
s.push(fmt.Sprintf("%s ANY %s %s", left, ctx.GetEq_op().GetText(), right))
|
||||
case ctx.T_NONE() != nil && ctx.GetEq_op() != nil:
|
||||
right, left := s.pop(), s.pop()
|
||||
s.push(fmt.Sprintf("%s NONE %s %s", left, ctx.GetEq_op().GetText(), right))
|
||||
|
||||
case ctx.T_ALL() != nil && ctx.T_NOT() != nil && ctx.T_IN() != nil:
|
||||
right, left := s.pop(), s.pop()
|
||||
s.push(fmt.Sprintf("%s ALL IN %s", left, right))
|
||||
case ctx.T_ANY() != nil && ctx.T_NOT() != nil && ctx.T_IN() != nil:
|
||||
right, left := s.pop(), s.pop()
|
||||
s.push(fmt.Sprintf("%s ANY IN %s", left, right))
|
||||
case ctx.T_NONE() != nil && ctx.T_NOT() != nil && ctx.T_IN() != nil:
|
||||
right, left := s.pop(), s.pop()
|
||||
s.push(fmt.Sprintf("%s NONE IN %s", left, right))
|
||||
|
||||
case ctx.T_LIKE() != nil:
|
||||
left, right := s.binaryPop()
|
||||
if ctx.T_NOT() != nil {
|
||||
s.push(fmt.Sprintf("%s NOT LIKE %s", left, right))
|
||||
} else {
|
||||
s.push(fmt.Sprintf("%s LIKE %s", left, right))
|
||||
}
|
||||
case ctx.T_REGEX_MATCH() != nil:
|
||||
left, right := s.binaryPop()
|
||||
if ctx.T_NOT() != nil {
|
||||
s.push(fmt.Sprintf("%s NOT =~ %s", left, right))
|
||||
} else {
|
||||
s.push(fmt.Sprintf("%s =~ %s", left, right))
|
||||
}
|
||||
case ctx.T_REGEX_NON_MATCH() != nil:
|
||||
left, right := s.binaryPop()
|
||||
if ctx.T_NOT() != nil {
|
||||
s.push(fmt.Sprintf("%s NOT !~ %s", left, right))
|
||||
} else {
|
||||
s.push(fmt.Sprintf("%s !~ %s", left, right))
|
||||
}
|
||||
|
||||
case ctx.T_AND() != nil:
|
||||
left, right := s.binaryPop()
|
||||
left = s.toBoolString(left)
|
||||
right = s.toBoolString(right)
|
||||
s.push(fmt.Sprintf("%s AND %s", left, right))
|
||||
case ctx.T_OR() != nil:
|
||||
left, right := s.binaryPop()
|
||||
left = s.toBoolString(left)
|
||||
right = s.toBoolString(right)
|
||||
s.push(fmt.Sprintf("%s OR %s", left, right))
|
||||
|
||||
case ctx.T_QUESTION() != nil && len(ctx.AllExpression()) == 3:
|
||||
right, middle, left := s.pop(), s.pop(), s.pop()
|
||||
s.push(fmt.Sprintf("%s ? %s : %s", left, middle, right))
|
||||
case ctx.T_QUESTION() != nil && len(ctx.AllExpression()) == 2:
|
||||
right, left := s.pop(), s.pop()
|
||||
s.push(fmt.Sprintf("%s ? : %s", left, right))
|
||||
|
||||
default:
|
||||
panic("unknown expression")
|
||||
}
|
||||
}
|
||||
|
||||
func (s *aqlBuilder) toBoolString(v string) string {
|
||||
_, err := unquote(v)
|
||||
if err == nil {
|
||||
ids, err := s.searcher.Search(v)
|
||||
if err != nil {
|
||||
panic("invalid search " + err.Error())
|
||||
}
|
||||
return fmt.Sprintf(`d._key IN ["%s"]`, strings.Join(ids, `","`))
|
||||
}
|
||||
return v
|
||||
}
|
||||
|
||||
// ExitOperator_unary is called when production operator_unary is exited.
|
||||
func (s *aqlBuilder) ExitOperator_unary(ctx *parser.Operator_unaryContext) {
|
||||
value := s.pop()
|
||||
switch {
|
||||
case ctx.T_PLUS() != nil:
|
||||
s.push(value)
|
||||
case ctx.T_MINUS() != nil:
|
||||
s.push(fmt.Sprintf("-%s", value))
|
||||
case ctx.T_NOT() != nil:
|
||||
s.push(fmt.Sprintf("NOT %s", value))
|
||||
default:
|
||||
panic(fmt.Sprintf("unexpected operation: %s", ctx.GetText()))
|
||||
}
|
||||
}
|
||||
|
||||
// ExitReference is called when production reference is exited.
|
||||
func (s *aqlBuilder) ExitReference(ctx *parser.ReferenceContext) {
|
||||
switch {
|
||||
case ctx.DOT() != nil:
|
||||
reference := s.pop()
|
||||
if s.prefix != "" && !strings.HasPrefix(reference, s.prefix) {
|
||||
reference = s.prefix + reference
|
||||
}
|
||||
s.push(fmt.Sprintf("%s.%s", reference, ctx.T_STRING().GetText()))
|
||||
case ctx.T_STRING() != nil:
|
||||
reference := ctx.T_STRING().GetText()
|
||||
if s.prefix != "" && !strings.HasPrefix(reference, s.prefix) {
|
||||
reference = s.prefix + reference
|
||||
}
|
||||
s.push(reference)
|
||||
case ctx.Compound_value() != nil:
|
||||
// pass
|
||||
case ctx.Function_call() != nil:
|
||||
// pass
|
||||
case ctx.T_OPEN() != nil:
|
||||
s.push(fmt.Sprintf("(%s)", s.pop()))
|
||||
case ctx.T_ARRAY_OPEN() != nil:
|
||||
key := s.pop()
|
||||
reference := s.pop()
|
||||
|
||||
s.push(fmt.Sprintf("%s[%s]", reference, key))
|
||||
default:
|
||||
panic(fmt.Sprintf("unexpected value: %s", ctx.GetText()))
|
||||
}
|
||||
}
|
||||
|
||||
// ExitCompound_value is called when production compound_value is exited.
|
||||
func (s *aqlBuilder) ExitCompound_value(ctx *parser.Compound_valueContext) {
|
||||
// pass
|
||||
}
|
||||
|
||||
// ExitFunction_call is called when production function_call is exited.
|
||||
func (s *aqlBuilder) ExitFunction_call(ctx *parser.Function_callContext) {
|
||||
var array []string
|
||||
for range ctx.AllExpression() {
|
||||
// prepend element
|
||||
array = append([]string{s.pop()}, array...)
|
||||
}
|
||||
parameter := strings.Join(array, ", ")
|
||||
|
||||
if !stringSliceContains(functionNames, strings.ToUpper(ctx.T_STRING().GetText())) {
|
||||
panic("unknown function")
|
||||
}
|
||||
|
||||
s.push(fmt.Sprintf("%s(%s)", strings.ToUpper(ctx.T_STRING().GetText()), parameter))
|
||||
}
|
||||
|
||||
// ExitValue_literal is called when production value_literal is exited.
|
||||
func (s *aqlBuilder) ExitValue_literal(ctx *parser.Value_literalContext) {
|
||||
if ctx.T_QUOTED_STRING() != nil {
|
||||
st, err := unquote(ctx.GetText())
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
s.push(strconv.Quote(st))
|
||||
} else {
|
||||
s.push(ctx.GetText())
|
||||
}
|
||||
}
|
||||
|
||||
// ExitArray is called when production array is exited.
|
||||
func (s *aqlBuilder) ExitArray(ctx *parser.ArrayContext) {
|
||||
var elements []string
|
||||
for range ctx.AllExpression() {
|
||||
// elements = append(elements, s.pop())
|
||||
elements = append([]string{s.pop()}, elements...)
|
||||
}
|
||||
s.push("[" + strings.Join(elements, ", ") + "]")
|
||||
}
|
||||
|
||||
// ExitObject is called when production object is exited.
|
||||
func (s *aqlBuilder) ExitObject(ctx *parser.ObjectContext) {
|
||||
var elements []string
|
||||
for range ctx.AllObject_element() {
|
||||
key, value := s.pop(), s.pop()
|
||||
|
||||
elements = append([]string{fmt.Sprintf("%s: %v", key, value)}, elements...)
|
||||
}
|
||||
// s.push(object)
|
||||
s.push("{" + strings.Join(elements, ", ") + "}")
|
||||
}
|
||||
|
||||
// ExitObject_element is called when production object_element is exited.
|
||||
func (s *aqlBuilder) ExitObject_element(ctx *parser.Object_elementContext) {
|
||||
switch {
|
||||
case ctx.T_STRING() != nil:
|
||||
s.push(ctx.GetText())
|
||||
s.push(ctx.GetText())
|
||||
case ctx.Object_element_name() != nil, ctx.T_ARRAY_OPEN() != nil:
|
||||
key, value := s.pop(), s.pop()
|
||||
|
||||
s.push(key)
|
||||
s.push(value)
|
||||
default:
|
||||
panic(fmt.Sprintf("unexpected value: %s", ctx.GetText()))
|
||||
}
|
||||
}
|
||||
|
||||
// ExitObject_element_name is called when production object_element_name is exited.
|
||||
func (s *aqlBuilder) ExitObject_element_name(ctx *parser.Object_element_nameContext) {
|
||||
switch {
|
||||
case ctx.T_STRING() != nil:
|
||||
s.push(ctx.T_STRING().GetText())
|
||||
case ctx.T_QUOTED_STRING() != nil:
|
||||
s.push(ctx.T_QUOTED_STRING().GetText())
|
||||
default:
|
||||
panic(fmt.Sprintf("unexpected value: %s", ctx.GetText()))
|
||||
}
|
||||
}
|
||||
8
caql/errors.go
Normal file
8
caql/errors.go
Normal file
@@ -0,0 +1,8 @@
|
||||
package caql
|
||||
|
||||
import "errors"
|
||||
|
||||
var (
|
||||
ErrStack = errors.New("unexpected operator stack")
|
||||
ErrUndefined = errors.New("variable not defined")
|
||||
)
|
||||
750
caql/function.go
Normal file
750
caql/function.go
Normal file
@@ -0,0 +1,750 @@
|
||||
package caql
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"math"
|
||||
"math/rand"
|
||||
"sort"
|
||||
"strings"
|
||||
"unicode/utf8"
|
||||
|
||||
"github.com/imdario/mergo"
|
||||
|
||||
"github.com/SecurityBrewery/catalyst/generated/caql/parser"
|
||||
)
|
||||
|
||||
func (s *aqlInterpreter) function(ctx *parser.Function_callContext) {
|
||||
switch strings.ToUpper(ctx.T_STRING().GetText()) {
|
||||
|
||||
default:
|
||||
s.appendErrors(errors.New("unknown function"))
|
||||
|
||||
// Array https://www.arangodb.com/docs/stable/aql/functions-array.html
|
||||
case "APPEND":
|
||||
u := false
|
||||
if len(ctx.AllExpression()) == 3 {
|
||||
u = s.pop().(bool)
|
||||
}
|
||||
seen := map[interface{}]bool{}
|
||||
values, anyArray := s.pop().([]interface{}), s.pop().([]interface{})
|
||||
|
||||
if u {
|
||||
for _, e := range anyArray {
|
||||
seen[e] = true
|
||||
}
|
||||
}
|
||||
|
||||
for _, e := range values {
|
||||
_, ok := seen[e]
|
||||
if !ok || !u {
|
||||
seen[e] = true
|
||||
anyArray = append(anyArray, e)
|
||||
}
|
||||
}
|
||||
s.push(anyArray)
|
||||
case "COUNT_DISTINCT", "COUNT_UNIQUE":
|
||||
count := 0
|
||||
seen := map[interface{}]bool{}
|
||||
array := s.pop().([]interface{})
|
||||
for _, e := range array {
|
||||
_, ok := seen[e]
|
||||
if !ok {
|
||||
seen[e] = true
|
||||
count += 1
|
||||
}
|
||||
}
|
||||
s.push(float64(count))
|
||||
case "FIRST":
|
||||
array := s.pop().([]interface{})
|
||||
if len(array) == 0 {
|
||||
s.push(nil)
|
||||
} else {
|
||||
s.push(array[0])
|
||||
}
|
||||
// case "FLATTEN":
|
||||
// case "INTERLEAVE":
|
||||
case "INTERSECTION":
|
||||
iset := New(s.pop().([]interface{})...)
|
||||
|
||||
for i := 1; i < len(ctx.AllExpression()); i++ {
|
||||
iset = iset.Intersection(New(s.pop().([]interface{})...))
|
||||
}
|
||||
|
||||
s.push(iset.Values())
|
||||
// case "JACCARD":
|
||||
case "LAST":
|
||||
array := s.pop().([]interface{})
|
||||
if len(array) == 0 {
|
||||
s.push(nil)
|
||||
} else {
|
||||
s.push(array[len(array)-1])
|
||||
}
|
||||
case "COUNT", "LENGTH":
|
||||
switch v := s.pop().(type) {
|
||||
case nil:
|
||||
s.push(float64(0))
|
||||
case bool:
|
||||
if v {
|
||||
s.push(float64(1))
|
||||
} else {
|
||||
s.push(float64(0))
|
||||
}
|
||||
case float64:
|
||||
s.push(float64(len(fmt.Sprint(v))))
|
||||
case string:
|
||||
s.push(float64(utf8.RuneCountInString(v)))
|
||||
case []interface{}:
|
||||
s.push(float64(len(v)))
|
||||
case map[string]interface{}:
|
||||
s.push(float64(len(v)))
|
||||
default:
|
||||
panic("unknown type")
|
||||
}
|
||||
case "MINUS":
|
||||
var sets []*Set
|
||||
for i := 0; i < len(ctx.AllExpression()); i++ {
|
||||
sets = append(sets, New(s.pop().([]interface{})...))
|
||||
}
|
||||
|
||||
iset := sets[len(sets)-1]
|
||||
// for i := len(sets)-1; i > 0; i-- {
|
||||
for i := 0; i < len(sets)-1; i++ {
|
||||
iset = iset.Minus(sets[i])
|
||||
}
|
||||
|
||||
s.push(iset.Values())
|
||||
case "NTH":
|
||||
pos := s.pop().(float64)
|
||||
array := s.pop().([]interface{})
|
||||
if int(pos) >= len(array) || pos < 0 {
|
||||
s.push(nil)
|
||||
} else {
|
||||
s.push(array[int64(pos)])
|
||||
}
|
||||
// case "OUTERSECTION":
|
||||
// array := s.pop().([]interface{})
|
||||
// union := New(array...)
|
||||
// intersection := New(s.pop().([]interface{})...)
|
||||
// for i := 1; i < len(ctx.AllExpression()); i++ {
|
||||
// array = s.pop().([]interface{})
|
||||
// union = union.Union(New(array...))
|
||||
// intersection = intersection.Intersection(New(array...))
|
||||
// }
|
||||
// s.push(union.Minus(intersection).Values())
|
||||
case "POP":
|
||||
array := s.pop().([]interface{})
|
||||
s.push(array[:len(array)-1])
|
||||
case "POSITION", "CONTAINS_ARRAY":
|
||||
returnIndex := false
|
||||
if len(ctx.AllExpression()) == 3 {
|
||||
returnIndex = s.pop().(bool)
|
||||
}
|
||||
search := s.pop()
|
||||
array := s.pop().([]interface{})
|
||||
|
||||
for idx, e := range array {
|
||||
if e == search {
|
||||
if returnIndex {
|
||||
s.push(float64(idx))
|
||||
} else {
|
||||
s.push(true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if returnIndex {
|
||||
s.push(float64(-1))
|
||||
} else {
|
||||
s.push(false)
|
||||
}
|
||||
case "PUSH":
|
||||
u := false
|
||||
if len(ctx.AllExpression()) == 3 {
|
||||
u = s.pop().(bool)
|
||||
}
|
||||
element := s.pop()
|
||||
array := s.pop().([]interface{})
|
||||
|
||||
if u && contains(array, element) {
|
||||
s.push(array)
|
||||
} else {
|
||||
s.push(append(array, element))
|
||||
}
|
||||
case "REMOVE_NTH":
|
||||
position := s.pop().(float64)
|
||||
anyArray := s.pop().([]interface{})
|
||||
|
||||
if position < 0 {
|
||||
position = float64(len(anyArray) + int(position))
|
||||
}
|
||||
|
||||
result := []interface{}{}
|
||||
for idx, e := range anyArray {
|
||||
if idx != int(position) {
|
||||
result = append(result, e)
|
||||
}
|
||||
}
|
||||
s.push(result)
|
||||
case "REPLACE_NTH":
|
||||
defaultPaddingValue := ""
|
||||
if len(ctx.AllExpression()) == 4 {
|
||||
defaultPaddingValue = s.pop().(string)
|
||||
}
|
||||
replaceValue := s.pop().(string)
|
||||
position := s.pop().(float64)
|
||||
anyArray := s.pop().([]interface{})
|
||||
|
||||
if position < 0 {
|
||||
position = float64(len(anyArray) + int(position))
|
||||
if position < 0 {
|
||||
position = 0
|
||||
}
|
||||
}
|
||||
|
||||
switch {
|
||||
case int(position) < len(anyArray):
|
||||
anyArray[int(position)] = replaceValue
|
||||
case int(position) == len(anyArray):
|
||||
anyArray = append(anyArray, replaceValue)
|
||||
default:
|
||||
if defaultPaddingValue == "" {
|
||||
panic("missing defaultPaddingValue")
|
||||
}
|
||||
for len(anyArray) < int(position) {
|
||||
anyArray = append(anyArray, defaultPaddingValue)
|
||||
}
|
||||
anyArray = append(anyArray, replaceValue)
|
||||
}
|
||||
|
||||
s.push(anyArray)
|
||||
case "REMOVE_VALUE":
|
||||
limit := math.Inf(1)
|
||||
if len(ctx.AllExpression()) == 3 {
|
||||
limit = s.pop().(float64)
|
||||
}
|
||||
value := s.pop()
|
||||
array := s.pop().([]interface{})
|
||||
result := []interface{}{}
|
||||
for idx, e := range array {
|
||||
if e != value || float64(idx) > limit {
|
||||
result = append(result, e)
|
||||
}
|
||||
}
|
||||
s.push(result)
|
||||
case "REMOVE_VALUES":
|
||||
values := s.pop().([]interface{})
|
||||
array := s.pop().([]interface{})
|
||||
result := []interface{}{}
|
||||
for _, e := range array {
|
||||
if !contains(values, e) {
|
||||
result = append(result, e)
|
||||
}
|
||||
}
|
||||
s.push(result)
|
||||
case "REVERSE":
|
||||
array := s.pop().([]interface{})
|
||||
var reverse []interface{}
|
||||
for _, e := range array {
|
||||
reverse = append([]interface{}{e}, reverse...)
|
||||
}
|
||||
s.push(reverse)
|
||||
case "SHIFT":
|
||||
s.push(s.pop().([]interface{})[1:])
|
||||
case "SLICE":
|
||||
length := float64(-1)
|
||||
full := true
|
||||
if len(ctx.AllExpression()) == 3 {
|
||||
length = s.pop().(float64)
|
||||
full = false
|
||||
}
|
||||
start := int64(s.pop().(float64))
|
||||
array := s.pop().([]interface{})
|
||||
|
||||
if start < 0 {
|
||||
start = int64(len(array)) + start
|
||||
}
|
||||
if full {
|
||||
length = float64(int64(len(array)) - start)
|
||||
}
|
||||
|
||||
end := int64(0)
|
||||
if length < 0 {
|
||||
end = int64(len(array)) + int64(length)
|
||||
} else {
|
||||
end = start + int64(length)
|
||||
}
|
||||
s.push(array[start:end])
|
||||
case "SORTED":
|
||||
array := s.pop().([]interface{})
|
||||
sort.Slice(array, func(i, j int) bool { return lt(array[i], array[j]) })
|
||||
s.push(array)
|
||||
case "SORTED_UNIQUE":
|
||||
array := s.pop().([]interface{})
|
||||
sort.Slice(array, func(i, j int) bool { return lt(array[i], array[j]) })
|
||||
s.push(unique(array))
|
||||
case "UNION":
|
||||
array := s.pop().([]interface{})
|
||||
|
||||
for i := 1; i < len(ctx.AllExpression()); i++ {
|
||||
array = append(array, s.pop().([]interface{})...)
|
||||
}
|
||||
|
||||
sort.Slice(array, func(i, j int) bool { return lt(array[i], array[j]) })
|
||||
s.push(array)
|
||||
case "UNION_DISTINCT":
|
||||
iset := New(s.pop().([]interface{})...)
|
||||
|
||||
for i := 1; i < len(ctx.AllExpression()); i++ {
|
||||
iset = iset.Union(New(s.pop().([]interface{})...))
|
||||
}
|
||||
|
||||
s.push(unique(iset.Values()))
|
||||
case "UNIQUE":
|
||||
s.push(unique(s.pop().([]interface{})))
|
||||
case "UNSHIFT":
|
||||
u := false
|
||||
if len(ctx.AllExpression()) == 3 {
|
||||
u = s.pop().(bool)
|
||||
}
|
||||
element := s.pop()
|
||||
array := s.pop().([]interface{})
|
||||
if u && contains(array, element) {
|
||||
s.push(array)
|
||||
} else {
|
||||
s.push(append([]interface{}{element}, array...))
|
||||
}
|
||||
|
||||
// Bit https://www.arangodb.com/docs/stable/aql/functions-bit.html
|
||||
// case "BIT_AND":
|
||||
// case "BIT_CONSTRUCT":
|
||||
// case "BIT_DECONSTRUCT":
|
||||
// case "BIT_FROM_STRING":
|
||||
// case "BIT_NEGATE":
|
||||
// case "BIT_OR":
|
||||
// case "BIT_POPCOUNT":
|
||||
// case "BIT_SHIFT_LEFT":
|
||||
// case "BIT_SHIFT_RIGHT":
|
||||
// case "BIT_TEST":
|
||||
// case "BIT_TO_STRING":
|
||||
// case "BIT_XOR":
|
||||
|
||||
// Date https://www.arangodb.com/docs/stable/aql/functions-date.html
|
||||
// case "DATE_NOW":
|
||||
// case "DATE_ISO8601":
|
||||
// case "DATE_TIMESTAMP":
|
||||
// case "IS_DATESTRING":
|
||||
|
||||
// case "DATE_DAYOFWEEK":
|
||||
// case "DATE_YEAR":
|
||||
// case "DATE_MONTH":
|
||||
// case "DATE_DAY":
|
||||
// case "DATE_HOUR":
|
||||
// case "DATE_MINUTE":
|
||||
// case "DATE_SECOND":
|
||||
// case "DATE_MILLISECOND":
|
||||
|
||||
// case "DATE_DAYOFYEAR":
|
||||
// case "DATE_ISOWEEK":
|
||||
// case "DATE_LEAPYEAR":
|
||||
// case "DATE_QUARTER":
|
||||
// case "DATE_DAYS_IN_MONTH":
|
||||
// case "DATE_TRUNC":
|
||||
// case "DATE_ROUND":
|
||||
// case "DATE_FORMAT":
|
||||
|
||||
// case "DATE_ADD":
|
||||
// case "DATE_SUBTRACT":
|
||||
// case "DATE_DIFF":
|
||||
// case "DATE_COMPARE":
|
||||
|
||||
// Document https://www.arangodb.com/docs/stable/aql/functions-document.html
|
||||
case "ATTRIBUTES":
|
||||
if len(ctx.AllExpression()) == 3 {
|
||||
s.pop() // always sort
|
||||
}
|
||||
removeInternal := false
|
||||
if len(ctx.AllExpression()) >= 2 {
|
||||
removeInternal = s.pop().(bool)
|
||||
}
|
||||
var keys []interface{}
|
||||
for k := range s.pop().(map[string]interface{}) {
|
||||
isInternalKey := strings.HasPrefix(k, "_")
|
||||
if !removeInternal || !isInternalKey {
|
||||
keys = append(keys, k)
|
||||
}
|
||||
}
|
||||
sort.Slice(keys, func(i, j int) bool { return lt(keys[i], keys[j]) })
|
||||
s.push(keys)
|
||||
// case "COUNT":
|
||||
case "HAS":
|
||||
right, left := s.pop(), s.pop()
|
||||
_, ok := left.(map[string]interface{})[right.(string)]
|
||||
s.push(ok)
|
||||
// case "KEEP":
|
||||
// case "LENGTH":
|
||||
// case "MATCHES":
|
||||
case "MERGE":
|
||||
var docs []map[string]interface{}
|
||||
if len(ctx.AllExpression()) == 1 {
|
||||
for _, doc := range s.pop().([]interface{}) {
|
||||
docs = append([]map[string]interface{}{doc.(map[string]interface{})}, docs...)
|
||||
}
|
||||
} else {
|
||||
for i := 0; i < len(ctx.AllExpression()); i++ {
|
||||
docs = append(docs, s.pop().(map[string]interface{}))
|
||||
}
|
||||
}
|
||||
|
||||
doc := docs[len(docs)-1]
|
||||
for i := len(docs) - 2; i >= 0; i-- {
|
||||
for k, v := range docs[i] {
|
||||
doc[k] = v
|
||||
}
|
||||
}
|
||||
s.push(doc)
|
||||
case "MERGE_RECURSIVE":
|
||||
var doc map[string]interface{}
|
||||
for i := 0; i < len(ctx.AllExpression()); i++ {
|
||||
err := mergo.Merge(&doc, s.pop().(map[string]interface{}))
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
s.push(doc)
|
||||
// case "PARSE_IDENTIFIER":
|
||||
// case "TRANSLATE":
|
||||
// case "UNSET":
|
||||
// case "UNSET_RECURSIVE":
|
||||
case "VALUES":
|
||||
removeInternal := false
|
||||
if len(ctx.AllExpression()) == 2 {
|
||||
removeInternal = s.pop().(bool)
|
||||
}
|
||||
var values []interface{}
|
||||
for k, v := range s.pop().(map[string]interface{}) {
|
||||
isInternalKey := strings.HasPrefix(k, "_")
|
||||
if !removeInternal || !isInternalKey {
|
||||
values = append(values, v)
|
||||
}
|
||||
}
|
||||
sort.Slice(values, func(i, j int) bool { return lt(values[i], values[j]) })
|
||||
s.push(values)
|
||||
// case "ZIP":
|
||||
|
||||
// Numeric https://www.arangodb.com/docs/stable/aql/functions-numeric.html
|
||||
case "ABS":
|
||||
s.push(math.Abs(s.pop().(float64)))
|
||||
case "ACOS":
|
||||
v := s.pop().(float64)
|
||||
asin := math.Acos(v)
|
||||
if v > 1 || v < -1 {
|
||||
s.push(nil)
|
||||
} else {
|
||||
s.push(asin)
|
||||
}
|
||||
case "ASIN":
|
||||
v := s.pop().(float64)
|
||||
asin := math.Asin(v)
|
||||
if v > 1 || v < -1 {
|
||||
s.push(nil)
|
||||
} else {
|
||||
s.push(asin)
|
||||
}
|
||||
case "ATAN":
|
||||
s.push(math.Atan(s.pop().(float64)))
|
||||
case "ATAN2":
|
||||
s.push(math.Atan2(s.pop().(float64), s.pop().(float64)))
|
||||
case "AVERAGE", "AVG":
|
||||
count := 0
|
||||
sum := float64(0)
|
||||
array := s.pop().([]interface{})
|
||||
for _, element := range array {
|
||||
if element != nil {
|
||||
count += 1
|
||||
sum += toNumber(element)
|
||||
}
|
||||
}
|
||||
if count == 0 {
|
||||
s.push(nil)
|
||||
} else {
|
||||
s.push(sum / float64(count))
|
||||
}
|
||||
case "CEIL":
|
||||
s.push(math.Ceil(s.pop().(float64)))
|
||||
case "COS":
|
||||
s.push(math.Cos(s.pop().(float64)))
|
||||
case "DEGREES":
|
||||
s.push(s.pop().(float64) * 180 / math.Pi)
|
||||
case "EXP":
|
||||
s.push(math.Exp(s.pop().(float64)))
|
||||
case "EXP2":
|
||||
s.push(math.Exp2(s.pop().(float64)))
|
||||
case "FLOOR":
|
||||
s.push(math.Floor(s.pop().(float64)))
|
||||
case "LOG":
|
||||
l := math.Log(s.pop().(float64))
|
||||
if l <= 0 {
|
||||
s.push(nil)
|
||||
} else {
|
||||
s.push(l)
|
||||
}
|
||||
case "LOG2":
|
||||
l := math.Log2(s.pop().(float64))
|
||||
if l <= 0 {
|
||||
s.push(nil)
|
||||
} else {
|
||||
s.push(l)
|
||||
}
|
||||
case "LOG10":
|
||||
l := math.Log10(s.pop().(float64))
|
||||
if l <= 0 {
|
||||
s.push(nil)
|
||||
} else {
|
||||
s.push(l)
|
||||
}
|
||||
case "MAX":
|
||||
var set bool
|
||||
var max float64
|
||||
array := s.pop().([]interface{})
|
||||
for _, element := range array {
|
||||
if element != nil {
|
||||
if !set || toNumber(element) > max {
|
||||
max = toNumber(element)
|
||||
set = true
|
||||
}
|
||||
}
|
||||
}
|
||||
if set {
|
||||
s.push(max)
|
||||
} else {
|
||||
s.push(nil)
|
||||
}
|
||||
case "MEDIAN":
|
||||
array := s.pop().([]interface{})
|
||||
var numbers []float64
|
||||
for _, element := range array {
|
||||
if f, ok := element.(float64); ok {
|
||||
numbers = append(numbers, f)
|
||||
}
|
||||
}
|
||||
|
||||
sort.Float64s(numbers) // sort the numbers
|
||||
|
||||
middlePos := len(numbers) / 2
|
||||
|
||||
switch {
|
||||
case len(numbers) == 0:
|
||||
s.push(nil)
|
||||
case len(numbers)%2 == 1:
|
||||
s.push(numbers[middlePos])
|
||||
default:
|
||||
s.push((numbers[middlePos-1] + numbers[middlePos]) / 2)
|
||||
}
|
||||
case "MIN":
|
||||
var set bool
|
||||
var min float64
|
||||
array := s.pop().([]interface{})
|
||||
for _, element := range array {
|
||||
if element != nil {
|
||||
if !set || toNumber(element) < min {
|
||||
min = toNumber(element)
|
||||
set = true
|
||||
}
|
||||
}
|
||||
}
|
||||
if set {
|
||||
s.push(min)
|
||||
} else {
|
||||
s.push(nil)
|
||||
}
|
||||
// case "PERCENTILE":
|
||||
case "PI":
|
||||
s.push(math.Pi)
|
||||
case "POW":
|
||||
right, left := s.pop(), s.pop()
|
||||
s.push(math.Pow(left.(float64), right.(float64)))
|
||||
case "PRODUCT":
|
||||
product := float64(1)
|
||||
array := s.pop().([]interface{})
|
||||
for _, element := range array {
|
||||
if element != nil {
|
||||
product *= toNumber(element)
|
||||
}
|
||||
}
|
||||
s.push(product)
|
||||
case "RADIANS":
|
||||
s.push(s.pop().(float64) * math.Pi / 180)
|
||||
case "RAND":
|
||||
s.push(rand.Float64())
|
||||
case "RANGE":
|
||||
var array []interface{}
|
||||
var start, end, step float64
|
||||
if len(ctx.AllExpression()) == 2 {
|
||||
right, left := s.pop(), s.pop()
|
||||
start = math.Trunc(left.(float64))
|
||||
end = math.Trunc(right.(float64))
|
||||
step = 1
|
||||
} else {
|
||||
middle, right, left := s.pop(), s.pop(), s.pop()
|
||||
start = left.(float64)
|
||||
end = right.(float64)
|
||||
step = middle.(float64)
|
||||
}
|
||||
for i := start; i <= end; i += step {
|
||||
array = append(array, i)
|
||||
}
|
||||
s.push(array)
|
||||
case "ROUND":
|
||||
x := s.pop().(float64)
|
||||
t := math.Trunc(x)
|
||||
if math.Abs(x-t) == 0.5 {
|
||||
s.push(x + 0.5)
|
||||
} else {
|
||||
s.push(math.Round(x))
|
||||
}
|
||||
case "SIN":
|
||||
s.push(math.Sin(s.pop().(float64)))
|
||||
case "SQRT":
|
||||
s.push(math.Sqrt(s.pop().(float64)))
|
||||
// case "STDDEV_POPULATION":
|
||||
// case "STDDEV_SAMPLE":
|
||||
// case "STDDEV":
|
||||
case "SUM":
|
||||
sum := float64(0)
|
||||
array := s.pop().([]interface{})
|
||||
for _, element := range array {
|
||||
sum += toNumber(element)
|
||||
}
|
||||
s.push(sum)
|
||||
case "TAN":
|
||||
s.push(math.Tan(s.pop().(float64)))
|
||||
// case "VARIANCE_POPULATION", "VARIANCE":
|
||||
// case "VARIANCE_SAMPLE":
|
||||
|
||||
// String https://www.arangodb.com/docs/stable/aql/functions-string.html
|
||||
// case "CHAR_LENGTH":
|
||||
// case "CONCAT":
|
||||
// case "CONCAT_SEPARATOR":
|
||||
// case "CONTAINS":
|
||||
// case "CRC32":
|
||||
// case "ENCODE_URI_COMPONENT":
|
||||
// case "FIND_FIRST":
|
||||
// case "FIND_LAST":
|
||||
// case "FNV64":
|
||||
// case "IPV4_FROM_NUMBER":
|
||||
// case "IPV4_TO_NUMBER":
|
||||
// case "IS_IPV4":
|
||||
// case "JSON_PARSE":
|
||||
// case "JSON_STRINGIFY":
|
||||
// case "LEFT":
|
||||
// case "LENGTH":
|
||||
// case "LEVENSHTEIN_DISTANCE":
|
||||
// case "LIKE":
|
||||
case "LOWER":
|
||||
s.push(strings.ToLower(s.pop().(string)))
|
||||
// case "LTRIM":
|
||||
// case "MD5":
|
||||
// case "NGRAM_POSITIONAL_SIMILARITY":
|
||||
// case "NGRAM_SIMILARITY":
|
||||
// case "RANDOM_TOKEN":
|
||||
// case "REGEX_MATCHES":
|
||||
// case "REGEX_SPLIT":
|
||||
// case "REGEX_TEST":
|
||||
// case "REGEX_REPLACE":
|
||||
// case "REVERSE":
|
||||
// case "RIGHT":
|
||||
// case "RTRIM":
|
||||
// case "SHA1":
|
||||
// case "SHA512":
|
||||
// case "SOUNDEX":
|
||||
// case "SPLIT":
|
||||
// case "STARTS_WITH":
|
||||
// case "SUBSTITUTE":
|
||||
// case "SUBSTRING":
|
||||
// case "TOKENS":
|
||||
// case "TO_BASE64":
|
||||
// case "TO_HEX":
|
||||
// case "TRIM":
|
||||
case "UPPER":
|
||||
s.push(strings.ToUpper(s.pop().(string)))
|
||||
// case "UUID":
|
||||
|
||||
// Type cast https://www.arangodb.com/docs/stable/aql/functions-type-cast.html
|
||||
case "TO_BOOL":
|
||||
s.push(toBool(s.pop()))
|
||||
case "TO_NUMBER":
|
||||
s.push(toNumber(s.pop()))
|
||||
// case "TO_STRING":
|
||||
// case "TO_ARRAY":
|
||||
// case "TO_LIST":
|
||||
|
||||
// case "IS_NULL":
|
||||
// case "IS_BOOL":
|
||||
// case "IS_NUMBER":
|
||||
// case "IS_STRING":
|
||||
// case "IS_ARRAY":
|
||||
// case "IS_LIST":
|
||||
// case "IS_OBJECT":
|
||||
// case "IS_DOCUMENT":
|
||||
// case "IS_DATESTRING":
|
||||
// case "IS_IPV4":
|
||||
// case "IS_KEY":
|
||||
// case "TYPENAME":
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
func unique(array []interface{}) []interface{} {
|
||||
seen := map[interface{}]bool{}
|
||||
var filtered []interface{}
|
||||
for _, e := range array {
|
||||
_, ok := seen[e]
|
||||
if !ok {
|
||||
seen[e] = true
|
||||
filtered = append(filtered, e)
|
||||
}
|
||||
}
|
||||
return filtered
|
||||
}
|
||||
|
||||
func contains(values []interface{}, e interface{}) bool {
|
||||
for _, v := range values {
|
||||
if e == v {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func stringSliceContains(values []string, e string) bool {
|
||||
for _, v := range values {
|
||||
if e == v {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
var functionNames = []string{
|
||||
"APPEND", "COUNT_DISTINCT", "COUNT_UNIQUE", "FIRST", "FLATTEN", "INTERLEAVE", "INTERSECTION", "JACCARD", "LAST",
|
||||
"COUNT", "LENGTH", "MINUS", "NTH", "OUTERSECTION", "POP", "POSITION", "CONTAINS_ARRAY", "PUSH", "REMOVE_NTH",
|
||||
"REPLACE_NTH", "REMOVE_VALUE", "REMOVE_VALUES", "REVERSE", "SHIFT", "SLICE", "SORTED", "SORTED_UNIQUE", "UNION",
|
||||
"UNION_DISTINCT", "UNIQUE", "UNSHIFT", "BIT_AND", "BIT_CONSTRUCT", "BIT_DECONSTRUCT", "BIT_FROM_STRING",
|
||||
"BIT_NEGATE", "BIT_OR", "BIT_POPCOUNT", "BIT_SHIFT_LEFT", "BIT_SHIFT_RIGHT", "BIT_TEST", "BIT_TO_STRING",
|
||||
"BIT_XOR", "DATE_NOW", "DATE_ISO8601", "DATE_TIMESTAMP", "IS_DATESTRING", "DATE_DAYOFWEEK", "DATE_YEAR",
|
||||
"DATE_MONTH", "DATE_DAY", "DATE_HOUR", "DATE_MINUTE", "DATE_SECOND", "DATE_MILLISECOND", "DATE_DAYOFYEAR",
|
||||
"DATE_ISOWEEK", "DATE_LEAPYEAR", "DATE_QUARTER", "DATE_DAYS_IN_MONTH", "DATE_TRUNC", "DATE_ROUND", "DATE_FORMAT",
|
||||
"DATE_ADD", "DATE_SUBTRACT", "DATE_DIFF", "DATE_COMPARE", "ATTRIBUTES", "COUNT", "HAS", "KEEP", "LENGTH",
|
||||
"MATCHES", "MERGE", "MERGE_RECURSIVE", "PARSE_IDENTIFIER", "TRANSLATE", "UNSET", "UNSET_RECURSIVE", "VALUES",
|
||||
"ZIP", "ABS", "ACOS", "ASIN", "ATAN", "ATAN2", "AVERAGE", "AVG", "CEIL", "COS", "DEGREES", "EXP", "EXP2", "FLOOR",
|
||||
"LOG", "LOG2", "LOG10", "MAX", "MEDIAN", "MIN", "PERCENTILE", "PI", "POW", "PRODUCT", "RADIANS", "RAND", "RANGE",
|
||||
"ROUND", "SIN", "SQRT", "STDDEV_POPULATION", "STDDEV_SAMPLE", "STDDEV", "SUM", "TAN", "VARIANCE_POPULATION",
|
||||
"VARIANCE", "VARIANCE_SAMPLE", "CHAR_LENGTH", "CONCAT", "CONCAT_SEPARATOR", "CONTAINS", "CRC32",
|
||||
"ENCODE_URI_COMPONENT", "FIND_FIRST", "FIND_LAST", "FNV64", "IPV4_FROM_NUMBER", "IPV4_TO_NUMBER", "IS_IPV4",
|
||||
"JSON_PARSE", "JSON_STRINGIFY", "LEFT", "LENGTH", "LEVENSHTEIN_DISTANCE", "LIKE", "LOWER", "LTRIM", "MD5",
|
||||
"NGRAM_POSITIONAL_SIMILARITY", "NGRAM_SIMILARITY", "RANDOM_TOKEN", "REGEX_MATCHES", "REGEX_SPLIT", "REGEX_TEST",
|
||||
"REGEX_REPLACE", "REVERSE", "RIGHT", "RTRIM", "SHA1", "SHA512", "SOUNDEX", "SPLIT", "STARTS_WITH", "SUBSTITUTE",
|
||||
"SUBSTRING", "TOKENS", "TO_BASE64", "TO_HEX", "TRIM", "UPPER", "UUID", "TO_BOOL", "TO_NUMBER", "TO_STRING",
|
||||
"TO_ARRAY", "TO_LIST", "IS_NULL", "IS_BOOL", "IS_NUMBER", "IS_STRING", "IS_ARRAY", "IS_LIST", "IS_OBJECT",
|
||||
"IS_DOCUMENT", "IS_DATESTRING", "IS_IPV4", "IS_KEY", "TYPENAME"}
|
||||
380
caql/function_test.go
Normal file
380
caql/function_test.go
Normal file
@@ -0,0 +1,380 @@
|
||||
package caql
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"math"
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestFunctions(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
saql string
|
||||
wantRebuild string
|
||||
wantValue interface{}
|
||||
wantParseErr bool
|
||||
wantRebuildErr bool
|
||||
wantEvalErr bool
|
||||
values string
|
||||
}{
|
||||
// https://www.arangodb.com/docs/3.7/aql/functions-array.html
|
||||
{name: "APPEND", saql: `APPEND([1, 2, 3], [5, 6, 9])`, wantRebuild: `APPEND([1, 2, 3], [5, 6, 9])`, wantValue: jsonParse(`[1, 2, 3, 5, 6, 9]`)},
|
||||
{name: "APPEND", saql: `APPEND([1, 2, 3], [3, 4, 5, 2, 9], true)`, wantRebuild: `APPEND([1, 2, 3], [3, 4, 5, 2, 9], true)`, wantValue: jsonParse(`[1, 2, 3, 4, 5, 9]`)},
|
||||
{name: "COUNT_DISTINCT", saql: `COUNT_DISTINCT([1, 2, 3])`, wantRebuild: `COUNT_DISTINCT([1, 2, 3])`, wantValue: 3},
|
||||
{name: "COUNT_DISTINCT", saql: `COUNT_DISTINCT(["yes", "no", "yes", "sauron", "no", "yes"])`, wantRebuild: `COUNT_DISTINCT(["yes", "no", "yes", "sauron", "no", "yes"])`, wantValue: 3},
|
||||
{name: "FIRST", saql: `FIRST([1, 2, 3])`, wantRebuild: `FIRST([1, 2, 3])`, wantValue: 1},
|
||||
{name: "FIRST", saql: `FIRST([])`, wantRebuild: `FIRST([])`, wantValue: nil},
|
||||
// {name: "FLATTEN", saql: `FLATTEN([1, 2, [3, 4], 5, [6, 7], [8, [9, 10]]])`, wantRebuild: `FLATTEN([1, 2, [3, 4], 5, [6, 7], [8, [9, 10]]])`, wantValue:},
|
||||
// {name: "FLATTEN", saql: `FLATTEN([1, 2, [3, 4], 5, [6, 7], [8, [9, 10]]], 2)`, wantRebuild: `FLATTEN([1, 2, [3, 4], 5, [6, 7], [8, [9, 10]]], 2)`, wantValue:},
|
||||
// {name: "INTERLEAVE", saql: `INTERLEAVE([1, 1, 1], [2, 2, 2], [3, 3, 3])`, wantRebuild: `INTERLEAVE([1, 1, 1], [2, 2, 2], [3, 3, 3])`, wantValue:},
|
||||
// {name: "INTERLEAVE", saql: `INTERLEAVE([1], [2, 2], [3, 3, 3])`, wantRebuild: `INTERLEAVE([1], [2, 2], [3, 3, 3])`, wantValue:},
|
||||
{name: "INTERSECTION", saql: `INTERSECTION([1,2,3,4,5], [2,3,4,5,6], [3,4,5,6,7])`, wantRebuild: `INTERSECTION([1, 2, 3, 4, 5], [2, 3, 4, 5, 6], [3, 4, 5, 6, 7])`, wantValue: jsonParse(`[3, 4, 5]`)},
|
||||
{name: "INTERSECTION", saql: `INTERSECTION([2,4,6], [8,10,12], [14,16,18])`, wantRebuild: `INTERSECTION([2, 4, 6], [8, 10, 12], [14, 16, 18])`, wantValue: jsonParse(`[]`)},
|
||||
// {name: "JACCARD", saql: `JACCARD([1,2,3,4], [3,4,5,6])`, wantRebuild: `JACCARD([1,2,3,4], [3,4,5,6])`, wantValue: 0.3333333333333333},
|
||||
// {name: "JACCARD", saql: `JACCARD([1,1,2,2,2,3], [2,2,3,4])`, wantRebuild: `JACCARD([1,1,2,2,2,3], [2,2,3,4])`, wantValue: 0.5},
|
||||
// {name: "JACCARD", saql: `JACCARD([1,2,3], [])`, wantRebuild: `JACCARD([1, 2, 3], [])`, wantValue: 0},
|
||||
// {name: "JACCARD", saql: `JACCARD([], [])`, wantRebuild: `JACCARD([], [])`, wantValue: 1},
|
||||
{name: "LAST", saql: `LAST([1,2,3,4,5])`, wantRebuild: `LAST([1, 2, 3, 4, 5])`, wantValue: 5},
|
||||
{name: "LENGTH", saql: `LENGTH("🥑")`, wantRebuild: `LENGTH("🥑")`, wantValue: 1},
|
||||
{name: "LENGTH", saql: `LENGTH(1234)`, wantRebuild: `LENGTH(1234)`, wantValue: 4},
|
||||
{name: "LENGTH", saql: `LENGTH([1,2,3,4,5,6,7])`, wantRebuild: `LENGTH([1, 2, 3, 4, 5, 6, 7])`, wantValue: 7},
|
||||
{name: "LENGTH", saql: `LENGTH(false)`, wantRebuild: `LENGTH(false)`, wantValue: 0},
|
||||
{name: "LENGTH", saql: `LENGTH({a:1, b:2, c:3, d:4, e:{f:5,g:6}})`, wantRebuild: `LENGTH({a: 1, b: 2, c: 3, d: 4, e: {f: 5, g: 6}})`, wantValue: 5},
|
||||
{name: "MINUS", saql: `MINUS([1,2,3,4], [3,4,5,6], [5,6,7,8])`, wantRebuild: `MINUS([1, 2, 3, 4], [3, 4, 5, 6], [5, 6, 7, 8])`, wantValue: jsonParse(`[1, 2]`)},
|
||||
{name: "NTH", saql: `NTH(["foo", "bar", "baz"], 2)`, wantRebuild: `NTH(["foo", "bar", "baz"], 2)`, wantValue: "baz"},
|
||||
{name: "NTH", saql: `NTH(["foo", "bar", "baz"], 3)`, wantRebuild: `NTH(["foo", "bar", "baz"], 3)`, wantValue: nil},
|
||||
{name: "NTH", saql: `NTH(["foo", "bar", "baz"], -1)`, wantRebuild: `NTH(["foo", "bar", "baz"], -1)`, wantValue: nil},
|
||||
// {name: "OUTERSECTION", saql: `OUTERSECTION([1, 2, 3], [2, 3, 4], [3, 4, 5])`, wantRebuild: `OUTERSECTION([1, 2, 3], [2, 3, 4], [3, 4, 5])`, wantValue: jsonParse(`[1, 5]`)},
|
||||
{name: "POP", saql: `POP([1, 2, 3, 4])`, wantRebuild: `POP([1, 2, 3, 4])`, wantValue: jsonParse(`[1, 2, 3]`)},
|
||||
{name: "POP", saql: `POP([1])`, wantRebuild: `POP([1])`, wantValue: jsonParse(`[]`)},
|
||||
{name: "POSITION", saql: `POSITION([2,4,6,8], 4)`, wantRebuild: `POSITION([2, 4, 6, 8], 4)`, wantValue: true},
|
||||
{name: "POSITION", saql: `POSITION([2,4,6,8], 4, true)`, wantRebuild: `POSITION([2, 4, 6, 8], 4, true)`, wantValue: 1},
|
||||
{name: "PUSH", saql: `PUSH([1, 2, 3], 4)`, wantRebuild: `PUSH([1, 2, 3], 4)`, wantValue: jsonParse(`[1, 2, 3, 4]`)},
|
||||
{name: "PUSH", saql: `PUSH([1, 2, 2, 3], 2, true)`, wantRebuild: `PUSH([1, 2, 2, 3], 2, true)`, wantValue: jsonParse(`[1, 2, 2, 3]`)},
|
||||
{name: "REMOVE_NTH", saql: `REMOVE_NTH(["a", "b", "c", "d", "e"], 1)`, wantRebuild: `REMOVE_NTH(["a", "b", "c", "d", "e"], 1)`, wantValue: jsonParse(`["a", "c", "d", "e"]`)},
|
||||
{name: "REMOVE_NTH", saql: `REMOVE_NTH(["a", "b", "c", "d", "e"], -2)`, wantRebuild: `REMOVE_NTH(["a", "b", "c", "d", "e"], -2)`, wantValue: jsonParse(`["a", "b", "c", "e"]`)},
|
||||
{name: "REPLACE_NTH", saql: `REPLACE_NTH(["a", "b", "c"], 1 , "z")`, wantRebuild: `REPLACE_NTH(["a", "b", "c"], 1, "z")`, wantValue: jsonParse(`["a", "z", "c"]`)},
|
||||
{name: "REPLACE_NTH", saql: `REPLACE_NTH(["a", "b", "c"], 3 , "z")`, wantRebuild: `REPLACE_NTH(["a", "b", "c"], 3, "z")`, wantValue: jsonParse(`["a", "b", "c", "z"]`)},
|
||||
{name: "REPLACE_NTH", saql: `REPLACE_NTH(["a", "b", "c"], 6, "z", "y")`, wantRebuild: `REPLACE_NTH(["a", "b", "c"], 6, "z", "y")`, wantValue: jsonParse(`["a", "b", "c", "y", "y", "y", "z"]`)},
|
||||
{name: "REPLACE_NTH", saql: `REPLACE_NTH(["a", "b", "c"], -1, "z")`, wantRebuild: `REPLACE_NTH(["a", "b", "c"], -1, "z")`, wantValue: jsonParse(`["a", "b", "z"]`)},
|
||||
{name: "REPLACE_NTH", saql: `REPLACE_NTH(["a", "b", "c"], -9, "z")`, wantRebuild: `REPLACE_NTH(["a", "b", "c"], -9, "z")`, wantValue: jsonParse(`["z", "b", "c"]`)},
|
||||
{name: "REMOVE_VALUE", saql: `REMOVE_VALUE(["a", "b", "b", "a", "c"], "a")`, wantRebuild: `REMOVE_VALUE(["a", "b", "b", "a", "c"], "a")`, wantValue: jsonParse(`["b", "b", "c"]`)},
|
||||
{name: "REMOVE_VALUE", saql: `REMOVE_VALUE(["a", "b", "b", "a", "c"], "a", 1)`, wantRebuild: `REMOVE_VALUE(["a", "b", "b", "a", "c"], "a", 1)`, wantValue: jsonParse(`["b", "b", "a", "c"]`)},
|
||||
{name: "REMOVE_VALUES", saql: `REMOVE_VALUES(["a", "a", "b", "c", "d", "e", "f"], ["a", "f", "d"])`, wantRebuild: `REMOVE_VALUES(["a", "a", "b", "c", "d", "e", "f"], ["a", "f", "d"])`, wantValue: jsonParse(`["b", "c", "e"]`)},
|
||||
{name: "REVERSE", saql: `REVERSE ([2,4,6,8,10])`, wantRebuild: `REVERSE([2, 4, 6, 8, 10])`, wantValue: jsonParse(`[10, 8, 6, 4, 2]`)},
|
||||
{name: "SHIFT", saql: `SHIFT([1, 2, 3, 4])`, wantRebuild: `SHIFT([1, 2, 3, 4])`, wantValue: jsonParse(`[2, 3, 4]`)},
|
||||
{name: "SHIFT", saql: `SHIFT([1])`, wantRebuild: `SHIFT([1])`, wantValue: jsonParse(`[]`)},
|
||||
{name: "SLICE", saql: `SLICE([1, 2, 3, 4, 5], 0, 1)`, wantRebuild: `SLICE([1, 2, 3, 4, 5], 0, 1)`, wantValue: jsonParse(`[1]`)},
|
||||
{name: "SLICE", saql: `SLICE([1, 2, 3, 4, 5], 1, 2)`, wantRebuild: `SLICE([1, 2, 3, 4, 5], 1, 2)`, wantValue: jsonParse(`[2, 3]`)},
|
||||
{name: "SLICE", saql: `SLICE([1, 2, 3, 4, 5], 3)`, wantRebuild: `SLICE([1, 2, 3, 4, 5], 3)`, wantValue: jsonParse(`[4, 5]`)},
|
||||
{name: "SLICE", saql: `SLICE([1, 2, 3, 4, 5], 1, -1)`, wantRebuild: `SLICE([1, 2, 3, 4, 5], 1, -1)`, wantValue: jsonParse(`[2, 3, 4]`)},
|
||||
{name: "SLICE", saql: `SLICE([1, 2, 3, 4, 5], 0, -2)`, wantRebuild: `SLICE([1, 2, 3, 4, 5], 0, -2)`, wantValue: jsonParse(`[1, 2, 3]`)},
|
||||
{name: "SLICE", saql: `SLICE([1, 2, 3, 4, 5], -3, 2)`, wantRebuild: `SLICE([1, 2, 3, 4, 5], -3, 2)`, wantValue: jsonParse(`[3, 4]`)},
|
||||
{name: "SORTED", saql: `SORTED([8,4,2,10,6])`, wantRebuild: `SORTED([8, 4, 2, 10, 6])`, wantValue: jsonParse(`[2, 4, 6, 8, 10]`)},
|
||||
{name: "SORTED_UNIQUE", saql: `SORTED_UNIQUE([8,4,2,10,6,2,8,6,4])`, wantRebuild: `SORTED_UNIQUE([8, 4, 2, 10, 6, 2, 8, 6, 4])`, wantValue: jsonParse(`[2, 4, 6, 8, 10]`)},
|
||||
{name: "UNION", saql: `UNION([1, 2, 3], [1, 2])`, wantRebuild: `UNION([1, 2, 3], [1, 2])`, wantValue: jsonParse(`[1, 1, 2, 2, 3]`)},
|
||||
{name: "UNION_DISTINCT", saql: `UNION_DISTINCT([1, 2, 3], [1, 2])`, wantRebuild: `UNION_DISTINCT([1, 2, 3], [1, 2])`, wantValue: jsonParse(`[1, 2, 3]`)},
|
||||
{name: "UNIQUE", saql: `UNIQUE([1,2,2,3,3,3,4,4,4,4,5,5,5,5,5])`, wantRebuild: `UNIQUE([1, 2, 2, 3, 3, 3, 4, 4, 4, 4, 5, 5, 5, 5, 5])`, wantValue: jsonParse(`[1, 2, 3, 4, 5]`)},
|
||||
{name: "UNSHIFT", saql: `UNSHIFT([1, 2, 3], 4)`, wantRebuild: `UNSHIFT([1, 2, 3], 4)`, wantValue: jsonParse(`[4, 1, 2, 3]`)},
|
||||
{name: "UNSHIFT", saql: `UNSHIFT([1, 2, 3], 2, true)`, wantRebuild: `UNSHIFT([1, 2, 3], 2, true)`, wantValue: jsonParse(`[1, 2, 3]`)},
|
||||
|
||||
// https://www.arangodb.com/docs/3.7/aql/functions-bit.html
|
||||
// {name: "BIT_CONSTRUCT", saql: `BIT_CONSTRUCT([1, 2, 3])`, wantRebuild: `BIT_CONSTRUCT([1, 2, 3])`, wantValue: 14},
|
||||
// {name: "BIT_CONSTRUCT", saql: `BIT_CONSTRUCT([0, 4, 8])`, wantRebuild: `BIT_CONSTRUCT([0, 4, 8])`, wantValue: 273},
|
||||
// {name: "BIT_CONSTRUCT", saql: `BIT_CONSTRUCT([0, 1, 10, 31])`, wantRebuild: `BIT_CONSTRUCT([0, 1, 10, 31])`, wantValue: 2147484675},
|
||||
// {name: "BIT_DECONSTRUCT", saql: `BIT_DECONSTRUCT(14)`, wantRebuild: `BIT_DECONSTRUCT(14) `, wantValue: []interface{}{1, 2, 3}},
|
||||
// {name: "BIT_DECONSTRUCT", saql: `BIT_DECONSTRUCT(273)`, wantRebuild: `BIT_DECONSTRUCT(273)`, wantValue: []interface{}{0, 4, 8}},
|
||||
// {name: "BIT_DECONSTRUCT", saql: `BIT_DECONSTRUCT(2147484675)`, wantRebuild: `BIT_DECONSTRUCT(2147484675)`, wantValue: []interface{}{0, 1, 10, 31}},
|
||||
// {name: "BIT_FROM_STRING", saql: `BIT_FROM_STRING("0111")`, wantRebuild: `BIT_FROM_STRING("0111")`, wantValue: 7},
|
||||
// {name: "BIT_FROM_STRING", saql: `BIT_FROM_STRING("000000000000010")`, wantRebuild: `BIT_FROM_STRING("000000000000010")`, wantValue: 2},
|
||||
// {name: "BIT_FROM_STRING", saql: `BIT_FROM_STRING("11010111011101")`, wantRebuild: `BIT_FROM_STRING("11010111011101")`, wantValue: 13789},
|
||||
// {name: "BIT_FROM_STRING", saql: `BIT_FROM_STRING("100000000000000000000")`, wantRebuild: `BIT_FROM_STRING("100000000000000000000")`, wantValue: 1048756},
|
||||
// {name: "BIT_NEGATE", saql: `BIT_NEGATE(0, 8)`, wantRebuild: `BIT_NEGATE(0, 8)`, wantValue: 255},
|
||||
// {name: "BIT_NEGATE", saql: `BIT_NEGATE(0, 10)`, wantRebuild: `BIT_NEGATE(0, 10)`, wantValue: 1023},
|
||||
// {name: "BIT_NEGATE", saql: `BIT_NEGATE(3, 4)`, wantRebuild: `BIT_NEGATE(3, 4)`, wantValue: 12},
|
||||
// {name: "BIT_NEGATE", saql: `BIT_NEGATE(446359921, 32)`, wantRebuild: `BIT_NEGATE(446359921, 32)`, wantValue: 3848607374},
|
||||
// {name: "BIT_OR", saql: `BIT_OR([1, 4, 8, 16])`, wantRebuild: `BIT_OR([1, 4, 8, 16])`, wantValue: 29},
|
||||
// {name: "BIT_OR", saql: `BIT_OR([3, 7, 63])`, wantRebuild: `BIT_OR([3, 7, 63])`, wantValue: 63},
|
||||
// {name: "BIT_OR", saql: `BIT_OR([255, 127, null, 63])`, wantRebuild: `BIT_OR([255, 127, null, 63])`, wantValue: 255},
|
||||
// {name: "BIT_OR", saql: `BIT_OR(255, 127)`, wantRebuild: `BIT_OR(255, 127)`, wantValue: 255},
|
||||
// {name: "BIT_OR", saql: `BIT_OR("foo")`, wantRebuild: `BIT_OR("foo")`, wantValue: nil},
|
||||
// {name: "BIT_POPCOUNT", saql: `BIT_POPCOUNT(0)`, wantRebuild: `BIT_POPCOUNT(0)`, wantValue: 0},
|
||||
// {name: "BIT_POPCOUNT", saql: `BIT_POPCOUNT(255)`, wantRebuild: `BIT_POPCOUNT(255)`, wantValue: 8},
|
||||
// {name: "BIT_POPCOUNT", saql: `BIT_POPCOUNT(69399252)`, wantRebuild: `BIT_POPCOUNT(69399252)`, wantValue: 12},
|
||||
// {name: "BIT_POPCOUNT", saql: `BIT_POPCOUNT("foo")`, wantRebuild: `BIT_POPCOUNT("foo")`, wantValue: nil},
|
||||
// {name: "BIT_SHIFT_LEFT", saql: `BIT_SHIFT_LEFT(0, 1, 8)`, wantRebuild: `BIT_SHIFT_LEFT(0, 1, 8)`, wantValue: 0},
|
||||
// {name: "BIT_SHIFT_LEFT", saql: `BIT_SHIFT_LEFT(7, 1, 16)`, wantRebuild: `BIT_SHIFT_LEFT(7, 1, 16)`, wantValue: 14},
|
||||
// {name: "BIT_SHIFT_LEFT", saql: `BIT_SHIFT_LEFT(2, 10, 16)`, wantRebuild: `BIT_SHIFT_LEFT(2, 10, 16)`, wantValue: 2048},
|
||||
// {name: "BIT_SHIFT_LEFT", saql: `BIT_SHIFT_LEFT(878836, 16, 32)`, wantRebuild: `BIT_SHIFT_LEFT(878836, 16, 32)`, wantValue: 1760821248},
|
||||
// {name: "BIT_SHIFT_RIGHT", saql: `BIT_SHIFT_RIGHT(0, 1, 8)`, wantRebuild: `BIT_SHIFT_RIGHT(0, 1, 8)`, wantValue: 0},
|
||||
// {name: "BIT_SHIFT_RIGHT", saql: `BIT_SHIFT_RIGHT(33, 1, 16)`, wantRebuild: `BIT_SHIFT_RIGHT(33, 1, 16)`, wantValue: 16},
|
||||
// {name: "BIT_SHIFT_RIGHT", saql: `BIT_SHIFT_RIGHT(65536, 13, 16)`, wantRebuild: `BIT_SHIFT_RIGHT(65536, 13, 16)`, wantValue: 8},
|
||||
// {name: "BIT_SHIFT_RIGHT", saql: `BIT_SHIFT_RIGHT(878836, 4, 32)`, wantRebuild: `BIT_SHIFT_RIGHT(878836, 4, 32)`, wantValue: 54927},
|
||||
// {name: "BIT_TEST", saql: `BIT_TEST(0, 3)`, wantRebuild: `BIT_TEST(0, 3)`, wantValue: false},
|
||||
// {name: "BIT_TEST", saql: `BIT_TEST(255, 0)`, wantRebuild: `BIT_TEST(255, 0)`, wantValue: true},
|
||||
// {name: "BIT_TEST", saql: `BIT_TEST(7, 2)`, wantRebuild: `BIT_TEST(7, 2)`, wantValue: true},
|
||||
// {name: "BIT_TEST", saql: `BIT_TEST(255, 8)`, wantRebuild: `BIT_TEST(255, 8)`, wantValue: false},
|
||||
// {name: "BIT_TO_STRING", saql: `BIT_TO_STRING(7, 4)`, wantRebuild: `BIT_TO_STRING(7, 4)`, wantValue: "0111"},
|
||||
// {name: "BIT_TO_STRING", saql: `BIT_TO_STRING(255, 8)`, wantRebuild: `BIT_TO_STRING(255, 8)`, wantValue: "11111111"},
|
||||
// {name: "BIT_TO_STRING", saql: `BIT_TO_STRING(60, 8)`, wantRebuild: `BIT_TO_STRING(60, 8)`, wantValue: "00011110"},
|
||||
// {name: "BIT_TO_STRING", saql: `BIT_TO_STRING(1048576, 32)`, wantRebuild: `BIT_TO_STRING(1048576, 32)`, wantValue: "00000000000100000000000000000000"},
|
||||
// {name: "BIT_XOR", saql: `BIT_XOR([1, 4, 8, 16])`, wantRebuild: `BIT_XOR([1, 4, 8, 16])`, wantValue: 29},
|
||||
// {name: "BIT_XOR", saql: `BIT_XOR([3, 7, 63])`, wantRebuild: `BIT_XOR([3, 7, 63])`, wantValue: 59},
|
||||
// {name: "BIT_XOR", saql: `BIT_XOR([255, 127, null, 63])`, wantRebuild: `BIT_XOR([255, 127, null, 63])`, wantValue: 191},
|
||||
// {name: "BIT_XOR", saql: `BIT_XOR(255, 257)`, wantRebuild: `BIT_XOR(255, 257)`, wantValue: 510},
|
||||
// {name: "BIT_XOR", saql: `BIT_XOR("foo")`, wantRebuild: `BIT_XOR("foo")`, wantValue: nil},
|
||||
|
||||
// https://www.arangodb.com/docs/3.7/aql/functions-date.html
|
||||
// DATE_TIMESTAMP("2014-05-07T14:19:09.522")
|
||||
// DATE_TIMESTAMP("2014-05-07T14:19:09.522Z")
|
||||
// DATE_TIMESTAMP("2014-05-07 14:19:09.522")
|
||||
// DATE_TIMESTAMP("2014-05-07 14:19:09.522Z")
|
||||
// DATE_TIMESTAMP(2014, 5, 7, 14, 19, 9, 522)
|
||||
// DATE_TIMESTAMP(1399472349522)
|
||||
// DATE_ISO8601("2014-05-07T14:19:09.522Z")
|
||||
// DATE_ISO8601("2014-05-07 14:19:09.522Z")
|
||||
// DATE_ISO8601(2014, 5, 7, 14, 19, 9, 522)
|
||||
// DATE_ISO8601(1399472349522)
|
||||
// {name: "DATE_TIMESTAMP", saql: `DATE_TIMESTAMP(2016, 12, -1)`, wantRebuild: `DATE_TIMESTAMP(2016, 12, -1)`, wantValue: nil},
|
||||
// {name: "DATE_TIMESTAMP", saql: `DATE_TIMESTAMP(2016, 2, 32)`, wantRebuild: `DATE_TIMESTAMP(2016, 2, 32)`, wantValue: 1456963200000},
|
||||
// {name: "DATE_TIMESTAMP", saql: `DATE_TIMESTAMP(1970, 1, 1, 26)`, wantRebuild: `DATE_TIMESTAMP(1970, 1, 1, 26)`, wantValue: 93600000},
|
||||
// {name: "DATE_TRUNC", saql: `DATE_TRUNC('2017-02-03', 'month')`, wantRebuild: `DATE_TRUNC('2017-02-03', 'month')`, wantValue: "2017-02-01T00:00:00.000Z"},
|
||||
// {name: "DATE_TRUNC", saql: `DATE_TRUNC('2017-02-03 04:05:06', 'hours')`, wantRebuild: `DATE_TRUNC('2017-02-03 04:05:06', 'hours')`, wantValue: "2017-02-03 04:00:00.000Z"},
|
||||
// {name: "DATE_ROUND", saql: `DATE_ROUND('2000-04-28T11:11:11.111Z', 1, 'day')`, wantRebuild: `DATE_ROUND('2000-04-28T11:11:11.111Z', 1, 'day')`, wantValue: "2000-04-28T00:00:00.000Z"},
|
||||
// {name: "DATE_ROUND", saql: `DATE_ROUND('2000-04-10T11:39:29Z', 15, 'minutes')`, wantRebuild: `DATE_ROUND('2000-04-10T11:39:29Z', 15, 'minutes')`, wantValue: "2000-04-10T11:30:00.000Z"},
|
||||
// {name: "DATE_FORMAT", saql: `DATE_FORMAT(DATE_NOW(), "%q/%yyyy")`, wantRebuild: `DATE_FORMAT(DATE_NOW(), "%q/%yyyy")`},
|
||||
// {name: "DATE_FORMAT", saql: `DATE_FORMAT(DATE_NOW(), "%dd.%mm.%yyyy %hh:%ii:%ss,%fff")`, wantRebuild: `DATE_FORMAT(DATE_NOW(), "%dd.%mm.%yyyy %hh:%ii:%ss,%fff")`, wantValue: "18.09.2015 15:30:49,374"},
|
||||
// {name: "DATE_FORMAT", saql: `DATE_FORMAT("1969", "Summer of '%yy")`, wantRebuild: `DATE_FORMAT("1969", "Summer of '%yy")`, wantValue: "Summer of '69"},
|
||||
// {name: "DATE_FORMAT", saql: `DATE_FORMAT("2016", "%%l = %l")`, wantRebuild: `DATE_FORMAT("2016", "%%l = %l")`, wantValue: "%l = 1"},
|
||||
// {name: "DATE_FORMAT", saql: `DATE_FORMAT("2016-03-01", "%xxx%")`, wantRebuild: `DATE_FORMAT("2016-03-01", "%xxx%")`, wantValue: "063, trailing % ignored"},
|
||||
// {name: "DATE_ADD", saql: `DATE_ADD(DATE_NOW(), -1, "day")`, wantRebuild: `DATE_ADD(DATE_NOW(), -1, "day")`, wantValue: "yesterday; also see DATE_SUBTRACT()"},
|
||||
// {name: "DATE_ADD", saql: `DATE_ADD(DATE_NOW(), 3, "months")`, wantRebuild: `DATE_ADD(DATE_NOW(), 3, "months")`, wantValue: "in three months"},
|
||||
// {name: "DATE_ADD", saql: `DATE_ADD(DATE_ADD("2015-04-01", 5, "years"), 1, "month")`, wantRebuild: `DATE_ADD(DATE_ADD("2015-04-01", 5, "years"), 1, "month")`, wantValue: "May 1st 2020"},
|
||||
// {name: "DATE_ADD", saql: `DATE_ADD("2015-04-01", 12*5 + 1, "months")`, wantRebuild: `DATE_ADD("2015-04-01", 12*5 + 1, "months")`, wantValue: "also May 1st 2020"},
|
||||
// {name: "DATE_ADD", saql: `DATE_ADD(DATE_TIMESTAMP(DATE_YEAR(DATE_NOW()), 12, 24), -4, "years")`, wantRebuild: `DATE_ADD(DATE_TIMESTAMP(DATE_YEAR(DATE_NOW()), 12, 24), -4, "years")`, wantValue: "Christmas four years ago"},
|
||||
// {name: "DATE_ADD", saql: `DATE_ADD(DATE_ADD("2016-02", "month", 1), -1, "day")`, wantRebuild: `DATE_ADD(DATE_ADD("2016-02", "month", 1), -1, "day")`, wantValue: "last day of February (29th, because 2016 is a leap year!)"},
|
||||
// {name: "DATE_ADD", saql: `DATE_ADD(DATE_NOW(), "P1Y")`, wantRebuild: `DATE_ADD(DATE_NOW(), "P1Y")`},
|
||||
// {name: "DATE_ADD", saql: `DATE_ADD(DATE_NOW(), "P3M2W")`, wantRebuild: `DATE_ADD(DATE_NOW(), "P3M2W")`},
|
||||
// {name: "DATE_ADD", saql: `DATE_ADD(DATE_NOW(), "P5DT26H")`, wantRebuild: `DATE_ADD(DATE_NOW(), "P5DT26H")`},
|
||||
// {name: "DATE_ADD", saql: `DATE_ADD("2000-01-01", "PT4H")`, wantRebuild: `DATE_ADD("2000-01-01", "PT4H")`},
|
||||
// {name: "DATE_ADD", saql: `DATE_ADD("2000-01-01", "PT30M44.4S"`, wantRebuild: `DATE_ADD("2000-01-01", "PT30M44.4S"`},
|
||||
// {name: "DATE_ADD", saql: `DATE_ADD("2000-01-01", "P1Y2M3W4DT5H6M7.89S"`, wantRebuild: `DATE_ADD("2000-01-01", "P1Y2M3W4DT5H6M7.89S"`},
|
||||
// {name: "DATE_SUBTRACT", saql: `DATE_SUBTRACT(DATE_NOW(), 1, "day")`, wantRebuild: `DATE_SUBTRACT(DATE_NOW(), 1, "day")`},
|
||||
// {name: "DATE_SUBTRACT", saql: `DATE_SUBTRACT(DATE_TIMESTAMP(DATE_YEAR(DATE_NOW()), 12, 24), 4, "years")`, wantRebuild: `DATE_SUBTRACT(DATE_TIMESTAMP(DATE_YEAR(DATE_NOW()), 12, 24), 4, "years")`},
|
||||
// {name: "DATE_SUBTRACT", saql: `DATE_SUBTRACT(DATE_ADD("2016-02", "month", 1), 1, "day")`, wantRebuild: `DATE_SUBTRACT(DATE_ADD("2016-02", "month", 1), 1, "day")`},
|
||||
// {name: "DATE_SUBTRACT", saql: `DATE_SUBTRACT(DATE_NOW(), "P4D")`, wantRebuild: `DATE_SUBTRACT(DATE_NOW(), "P4D")`},
|
||||
// {name: "DATE_SUBTRACT", saql: `DATE_SUBTRACT(DATE_NOW(), "PT1H3M")`, wantRebuild: `DATE_SUBTRACT(DATE_NOW(), "PT1H3M")`},
|
||||
// DATE_COMPARE("1985-04-04", DATE_NOW(), "months", "days")
|
||||
// DATE_COMPARE("1984-02-29", DATE_NOW(), "months", "days")
|
||||
// DATE_COMPARE("2001-01-01T15:30:45.678Z", "2001-01-01T08:08:08.008Z", "years", "days")
|
||||
|
||||
// https://www.arangodb.com/docs/3.7/aql/functions-document.html
|
||||
{name: "ATTRIBUTES", saql: `ATTRIBUTES({"foo": "bar", "_key": "123", "_custom": "yes"})`, wantRebuild: `ATTRIBUTES({"foo": "bar", "_key": "123", "_custom": "yes"})`, wantValue: jsonParse(`["_custom", "_key", "foo"]`)},
|
||||
{name: "ATTRIBUTES", saql: `ATTRIBUTES({"foo": "bar", "_key": "123", "_custom": "yes"}, true)`, wantRebuild: `ATTRIBUTES({"foo": "bar", "_key": "123", "_custom": "yes"}, true)`, wantValue: jsonParse(`["foo"]`)},
|
||||
{name: "ATTRIBUTES", saql: `ATTRIBUTES({"foo": "bar", "_key": "123", "_custom": "yes"}, false, true)`, wantRebuild: `ATTRIBUTES({"foo": "bar", "_key": "123", "_custom": "yes"}, false, true)`, wantValue: jsonParse(`["_custom", "_key", "foo"]`)},
|
||||
{name: "HAS", saql: `HAS({name: "Jane"}, "name")`, wantRebuild: `HAS({name: "Jane"}, "name")`, wantValue: true},
|
||||
{name: "HAS", saql: `HAS({name: "Jane"}, "age")`, wantRebuild: `HAS({name: "Jane"}, "age")`, wantValue: false},
|
||||
{name: "HAS", saql: `HAS({name: null}, "name")`, wantRebuild: `HAS({name: null}, "name")`, wantValue: true},
|
||||
// KEEP(doc, "firstname", "name", "likes")
|
||||
// KEEP(doc, ["firstname", "name", "likes"])
|
||||
// MATCHES({name: "jane", age: 27, active: true}, {age: 27, active: true})
|
||||
// MATCHES({"test": 1}, [{"test": 1, "foo": "bar"}, {"foo": 1}, {"test": 1}], true)
|
||||
{name: "MERGE", saql: `MERGE({"user1": {"name": "Jane"}}, {"user2": {"name": "Tom"}})`, wantRebuild: `MERGE({"user1": {"name": "Jane"}}, {"user2": {"name": "Tom"}})`, wantValue: jsonParse(`{"user1": {"name": "Jane"}, "user2": {"name": "Tom"}}`)},
|
||||
{name: "MERGE", saql: `MERGE({"users": {"name": "Jane"}}, {"users": {"name": "Tom"}})`, wantRebuild: `MERGE({"users": {"name": "Jane"}}, {"users": {"name": "Tom"}})`, wantValue: jsonParse(`{"users": {"name": "Tom"}}`)},
|
||||
{name: "MERGE", saql: `MERGE([{foo: "bar"}, {quux: "quetzalcoatl", ruled: true}, {bar: "baz", foo: "done"}])`, wantRebuild: `MERGE([{foo: "bar"}, {quux: "quetzalcoatl", ruled: true}, {bar: "baz", foo: "done"}])`, wantValue: jsonParse(`{"foo": "done", "quux": "quetzalcoatl", "ruled": true, "bar": "baz"}`)},
|
||||
{name: "MERGE_RECURSIVE", saql: `MERGE_RECURSIVE({"user-1": {"name": "Jane", "livesIn": {"city": "LA"}}}, {"user-1": {"age": 42, "livesIn": {"state": "CA"}}})`, wantRebuild: `MERGE_RECURSIVE({"user-1": {"name": "Jane", "livesIn": {"city": "LA"}}}, {"user-1": {"age": 42, "livesIn": {"state": "CA"}}})`, wantValue: jsonParse(`{"user-1": {"name": "Jane", "livesIn": {"city": "LA", "state": "CA"}, "age": 42}}`)},
|
||||
// {name: "TRANSLATE", saql: `TRANSLATE("FR", {US: "United States", UK: "United Kingdom", FR: "France"})`, wantRebuild: `TRANSLATE("FR", {US: "United States", UK: "United Kingdom", FR: "France"})`, wantValue: "France"},
|
||||
// {name: "TRANSLATE", saql: `TRANSLATE(42, {foo: "bar", bar: "baz"})`, wantRebuild: `TRANSLATE(42, {foo: "bar", bar: "baz"})`, wantValue: 42},
|
||||
// {name: "TRANSLATE", saql: `TRANSLATE(42, {foo: "bar", bar: "baz"}, "not found!")`, wantRebuild: `TRANSLATE(42, {foo: "bar", bar: "baz"}, "not found!")`, wantValue: "not found!"},
|
||||
// UNSET(doc, "_id", "_key", "foo", "bar")
|
||||
// UNSET(doc, ["_id", "_key", "foo", "bar"])
|
||||
// UNSET_RECURSIVE(doc, "_id", "_key", "foo", "bar")
|
||||
// UNSET_RECURSIVE(doc, ["_id", "_key", "foo", "bar"])
|
||||
{name: "VALUES", saql: `VALUES({"_key": "users/jane", "name": "Jane", "age": 35})`, wantRebuild: `VALUES({"_key": "users/jane", "name": "Jane", "age": 35})`, wantValue: jsonParse(`[35, "Jane", "users/jane"]`)},
|
||||
{name: "VALUES", saql: `VALUES({"_key": "users/jane", "name": "Jane", "age": 35}, true)`, wantRebuild: `VALUES({"_key": "users/jane", "name": "Jane", "age": 35}, true)`, wantValue: jsonParse(`[35, "Jane"]`)},
|
||||
// {name: "ZIP", saql: `ZIP(["name", "active", "hobbies"], ["some user", true, ["swimming", "riding"]])`, wantRebuild: `ZIP(["name", "active", "hobbies"], ["some user", true, ["swimming", "riding"]])`, wantValue: jsonParse(`{"name": "some user", "active": true, "hobbies": ["swimming", "riding"]}`)},
|
||||
|
||||
// https://www.arangodb.com/docs/3.7/aql/functions-numeric.html
|
||||
{name: "ABS", saql: `ABS(-5)`, wantRebuild: `ABS(-5)`, wantValue: 5},
|
||||
{name: "ABS", saql: `ABS(+5)`, wantRebuild: `ABS(5)`, wantValue: 5},
|
||||
{name: "ABS", saql: `ABS(3.5)`, wantRebuild: `ABS(3.5)`, wantValue: 3.5},
|
||||
{name: "ACOS", saql: `ACOS(-1)`, wantRebuild: `ACOS(-1)`, wantValue: 3.141592653589793},
|
||||
{name: "ACOS", saql: `ACOS(0)`, wantRebuild: `ACOS(0)`, wantValue: 1.5707963267948966},
|
||||
{name: "ACOS", saql: `ACOS(1)`, wantRebuild: `ACOS(1)`, wantValue: 0},
|
||||
{name: "ACOS", saql: `ACOS(2)`, wantRebuild: `ACOS(2)`, wantValue: nil},
|
||||
{name: "ASIN", saql: `ASIN(1)`, wantRebuild: `ASIN(1)`, wantValue: 1.5707963267948966},
|
||||
{name: "ASIN", saql: `ASIN(0)`, wantRebuild: `ASIN(0)`, wantValue: 0},
|
||||
{name: "ASIN", saql: `ASIN(-1)`, wantRebuild: `ASIN(-1)`, wantValue: -1.5707963267948966},
|
||||
{name: "ASIN", saql: `ASIN(2)`, wantRebuild: `ASIN(2)`, wantValue: nil},
|
||||
{name: "ATAN", saql: `ATAN(-1)`, wantRebuild: `ATAN(-1)`, wantValue: -0.7853981633974483},
|
||||
{name: "ATAN", saql: `ATAN(0)`, wantRebuild: `ATAN(0)`, wantValue: 0},
|
||||
{name: "ATAN", saql: `ATAN(10)`, wantRebuild: `ATAN(10)`, wantValue: 1.4711276743037347},
|
||||
{name: "AVERAGE", saql: `AVERAGE([5, 2, 9, 2])`, wantRebuild: `AVERAGE([5, 2, 9, 2])`, wantValue: 4.5},
|
||||
{name: "AVERAGE", saql: `AVERAGE([-3, -5, 2])`, wantRebuild: `AVERAGE([-3, -5, 2])`, wantValue: -2},
|
||||
{name: "AVERAGE", saql: `AVERAGE([999, 80, 4, 4, 4, 3, 3, 3])`, wantRebuild: `AVERAGE([999, 80, 4, 4, 4, 3, 3, 3])`, wantValue: 137.5},
|
||||
{name: "CEIL", saql: `CEIL(2.49)`, wantRebuild: `CEIL(2.49)`, wantValue: 3},
|
||||
{name: "CEIL", saql: `CEIL(2.50)`, wantRebuild: `CEIL(2.50)`, wantValue: 3},
|
||||
{name: "CEIL", saql: `CEIL(-2.50)`, wantRebuild: `CEIL(-2.50)`, wantValue: -2},
|
||||
{name: "CEIL", saql: `CEIL(-2.51)`, wantRebuild: `CEIL(-2.51)`, wantValue: -2},
|
||||
{name: "COS", saql: `COS(1)`, wantRebuild: `COS(1)`, wantValue: 0.5403023058681398},
|
||||
{name: "COS", saql: `COS(0)`, wantRebuild: `COS(0)`, wantValue: 1},
|
||||
{name: "COS", saql: `COS(-3.141592653589783)`, wantRebuild: `COS(-3.141592653589783)`, wantValue: -1},
|
||||
{name: "COS", saql: `COS(RADIANS(45))`, wantRebuild: `COS(RADIANS(45))`, wantValue: 0.7071067811865476},
|
||||
{name: "DEGREES", saql: `DEGREES(0.7853981633974483)`, wantRebuild: `DEGREES(0.7853981633974483)`, wantValue: 45},
|
||||
{name: "DEGREES", saql: `DEGREES(0)`, wantRebuild: `DEGREES(0)`, wantValue: 0},
|
||||
{name: "DEGREES", saql: `DEGREES(3.141592653589793)`, wantRebuild: `DEGREES(3.141592653589793)`, wantValue: 180},
|
||||
{name: "EXP", saql: `EXP(1)`, wantRebuild: `EXP(1)`, wantValue: 2.718281828459045},
|
||||
{name: "EXP", saql: `EXP(10)`, wantRebuild: `EXP(10)`, wantValue: 22026.46579480671},
|
||||
{name: "EXP", saql: `EXP(0)`, wantRebuild: `EXP(0)`, wantValue: 1},
|
||||
{name: "EXP2", saql: `EXP2(16)`, wantRebuild: `EXP2(16)`, wantValue: 65536},
|
||||
{name: "EXP2", saql: `EXP2(1)`, wantRebuild: `EXP2(1)`, wantValue: 2},
|
||||
{name: "EXP2", saql: `EXP2(0)`, wantRebuild: `EXP2(0)`, wantValue: 1},
|
||||
{name: "FLOOR", saql: `FLOOR(2.49)`, wantRebuild: `FLOOR(2.49)`, wantValue: 2},
|
||||
{name: "FLOOR", saql: `FLOOR(2.50)`, wantRebuild: `FLOOR(2.50)`, wantValue: 2},
|
||||
{name: "FLOOR", saql: `FLOOR(-2.50)`, wantRebuild: `FLOOR(-2.50)`, wantValue: -3},
|
||||
{name: "FLOOR", saql: `FLOOR(-2.51)`, wantRebuild: `FLOOR(-2.51)`, wantValue: -3},
|
||||
{name: "LOG", saql: `LOG(2.718281828459045)`, wantRebuild: `LOG(2.718281828459045)`, wantValue: 1},
|
||||
{name: "LOG", saql: `LOG(10)`, wantRebuild: `LOG(10)`, wantValue: 2.302585092994046},
|
||||
{name: "LOG", saql: `LOG(0)`, wantRebuild: `LOG(0)`, wantValue: nil},
|
||||
{name: "LOG2", saql: `LOG2(1024)`, wantRebuild: `LOG2(1024)`, wantValue: 10},
|
||||
{name: "LOG2", saql: `LOG2(8)`, wantRebuild: `LOG2(8)`, wantValue: 3},
|
||||
{name: "LOG2", saql: `LOG2(0)`, wantRebuild: `LOG2(0)`, wantValue: nil},
|
||||
{name: "LOG10", saql: `LOG10(10000)`, wantRebuild: `LOG10(10000)`, wantValue: 4},
|
||||
{name: "LOG10", saql: `LOG10(10)`, wantRebuild: `LOG10(10)`, wantValue: 1},
|
||||
{name: "LOG10", saql: `LOG10(0)`, wantRebuild: `LOG10(0)`, wantValue: nil},
|
||||
{name: "MAX", saql: `MAX([5, 9, -2, null, 1])`, wantRebuild: `MAX([5, 9, -2, null, 1])`, wantValue: 9},
|
||||
{name: "MAX", saql: `MAX([null, null])`, wantRebuild: `MAX([null, null])`, wantValue: nil},
|
||||
{name: "MEDIAN", saql: `MEDIAN([1, 2, 3])`, wantRebuild: `MEDIAN([1, 2, 3])`, wantValue: 2},
|
||||
{name: "MEDIAN", saql: `MEDIAN([1, 2, 3, 4])`, wantRebuild: `MEDIAN([1, 2, 3, 4])`, wantValue: 2.5},
|
||||
{name: "MEDIAN", saql: `MEDIAN([4, 2, 3, 1])`, wantRebuild: `MEDIAN([4, 2, 3, 1])`, wantValue: 2.5},
|
||||
{name: "MEDIAN", saql: `MEDIAN([999, 80, 4, 4, 4, 3, 3, 3])`, wantRebuild: `MEDIAN([999, 80, 4, 4, 4, 3, 3, 3])`, wantValue: 4},
|
||||
{name: "MIN", saql: `MIN([5, 9, -2, null, 1])`, wantRebuild: `MIN([5, 9, -2, null, 1])`, wantValue: -2},
|
||||
{name: "MIN", saql: `MIN([null, null])`, wantRebuild: `MIN([null, null])`, wantValue: nil},
|
||||
// {name: "PERCENTILE", saql: `PERCENTILE([1, 2, 3, 4], 50)`, wantRebuild: `PERCENTILE([1, 2, 3, 4], 50)`, wantValue: 2},
|
||||
// {name: "PERCENTILE", saql: `PERCENTILE([1, 2, 3, 4], 50, "rank")`, wantRebuild: `PERCENTILE([1, 2, 3, 4], 50, "rank")`, wantValue: 2},
|
||||
// {name: "PERCENTILE", saql: `PERCENTILE([1, 2, 3, 4], 50, "interpolation")`, wantRebuild: `PERCENTILE([1, 2, 3, 4], 50, "interpolation")`, wantValue: 2.5},
|
||||
{name: "PI", saql: `PI()`, wantRebuild: `PI()`, wantValue: 3.141592653589793},
|
||||
{name: "POW", saql: `POW(2, 4)`, wantRebuild: `POW(2, 4)`, wantValue: 16},
|
||||
{name: "POW", saql: `POW(5, -1)`, wantRebuild: `POW(5, -1)`, wantValue: 0.2},
|
||||
{name: "POW", saql: `POW(5, 0)`, wantRebuild: `POW(5, 0)`, wantValue: 1},
|
||||
{name: "PRODUCT", saql: `PRODUCT([1, 2, 3, 4])`, wantRebuild: `PRODUCT([1, 2, 3, 4])`, wantValue: 24},
|
||||
{name: "PRODUCT", saql: `PRODUCT([null, -5, 6])`, wantRebuild: `PRODUCT([null, -5, 6])`, wantValue: -30},
|
||||
{name: "PRODUCT", saql: `PRODUCT([])`, wantRebuild: `PRODUCT([])`, wantValue: 1},
|
||||
{name: "RADIANS", saql: `RADIANS(180)`, wantRebuild: `RADIANS(180)`, wantValue: 3.141592653589793},
|
||||
{name: "RADIANS", saql: `RADIANS(90)`, wantRebuild: `RADIANS(90)`, wantValue: 1.5707963267948966},
|
||||
{name: "RADIANS", saql: `RADIANS(0)`, wantRebuild: `RADIANS(0)`, wantValue: 0},
|
||||
// {name: "RAND", saql: `RAND()`, wantRebuild: `RAND()`, wantValue: 0.3503170117504508},
|
||||
// {name: "RAND", saql: `RAND()`, wantRebuild: `RAND()`, wantValue: 0.6138226173882478},
|
||||
{name: "RANGE", saql: `RANGE(1, 4)`, wantRebuild: `RANGE(1, 4)`, wantValue: []interface{}{float64(1), float64(2), float64(3), float64(4)}},
|
||||
{name: "RANGE", saql: `RANGE(1, 4, 2)`, wantRebuild: `RANGE(1, 4, 2)`, wantValue: []interface{}{float64(1), float64(3)}},
|
||||
{name: "RANGE", saql: `RANGE(1, 4, 3)`, wantRebuild: `RANGE(1, 4, 3)`, wantValue: []interface{}{float64(1), float64(4)}},
|
||||
{name: "RANGE", saql: `RANGE(1.5, 2.5)`, wantRebuild: `RANGE(1.5, 2.5)`, wantValue: []interface{}{float64(1), float64(2)}},
|
||||
{name: "RANGE", saql: `RANGE(1.5, 2.5, 1)`, wantRebuild: `RANGE(1.5, 2.5, 1)`, wantValue: []interface{}{1.5, 2.5}},
|
||||
{name: "RANGE", saql: `RANGE(1.5, 2.5, 0.5)`, wantRebuild: `RANGE(1.5, 2.5, 0.5)`, wantValue: []interface{}{1.5, 2.0, 2.5}},
|
||||
{name: "RANGE", saql: `RANGE(-0.75, 1.1, 0.5)`, wantRebuild: `RANGE(-0.75, 1.1, 0.5)`, wantValue: []interface{}{-0.75, -0.25, 0.25, 0.75}},
|
||||
{name: "ROUND", saql: `ROUND(2.49)`, wantRebuild: `ROUND(2.49)`, wantValue: 2},
|
||||
{name: "ROUND", saql: `ROUND(2.50)`, wantRebuild: `ROUND(2.50)`, wantValue: 3},
|
||||
{name: "ROUND", saql: `ROUND(-2.50)`, wantRebuild: `ROUND(-2.50)`, wantValue: -2},
|
||||
{name: "ROUND", saql: `ROUND(-2.51)`, wantRebuild: `ROUND(-2.51)`, wantValue: -3},
|
||||
{name: "SQRT", saql: `SQRT(9)`, wantRebuild: `SQRT(9)`, wantValue: 3},
|
||||
{name: "SQRT", saql: `SQRT(2)`, wantRebuild: `SQRT(2)`, wantValue: 1.4142135623730951},
|
||||
{name: "POW", saql: `POW(4096, 1/4)`, wantRebuild: `POW(4096, 1 / 4)`, wantValue: 8},
|
||||
{name: "POW", saql: `POW(27, 1/3)`, wantRebuild: `POW(27, 1 / 3)`, wantValue: 3},
|
||||
{name: "POW", saql: `POW(9, 1/2)`, wantRebuild: `POW(9, 1 / 2)`, wantValue: 3},
|
||||
// {name: "STDDEV_POPULATION", saql: `STDDEV_POPULATION([1, 3, 6, 5, 2])`, wantRebuild: `STDDEV_POPULATION([1, 3, 6, 5, 2])`, wantValue: 1.854723699099141},
|
||||
// {name: "STDDEV_SAMPLE", saql: `STDDEV_SAMPLE([1, 3, 6, 5, 2])`, wantRebuild: `STDDEV_SAMPLE([1, 3, 6, 5, 2])`, wantValue: 2.0736441353327724},
|
||||
{name: "SUM", saql: `SUM([1, 2, 3, 4])`, wantRebuild: `SUM([1, 2, 3, 4])`, wantValue: 10},
|
||||
{name: "SUM", saql: `SUM([null, -5, 6])`, wantRebuild: `SUM([null, -5, 6])`, wantValue: 1},
|
||||
{name: "SUM", saql: `SUM([])`, wantRebuild: `SUM([])`, wantValue: 0},
|
||||
{name: "TAN", saql: `TAN(10)`, wantRebuild: `TAN(10)`, wantValue: 0.6483608274590866},
|
||||
{name: "TAN", saql: `TAN(5)`, wantRebuild: `TAN(5)`, wantValue: -3.380515006246586},
|
||||
{name: "TAN", saql: `TAN(0)`, wantRebuild: `TAN(0)`, wantValue: 0},
|
||||
// {name: "VARIANCE_POPULATION", saql: `VARIANCE_POPULATION([1, 3, 6, 5, 2])`, wantRebuild: `VARIANCE_POPULATION([1, 3, 6, 5, 2])`, wantValue: 3.4400000000000004},
|
||||
// {name: "VARIANCE_SAMPLE", saql: `VARIANCE_SAMPLE([1, 3, 6, 5, 2])`, wantRebuild: `VARIANCE_SAMPLE([1, 3, 6, 5, 2])`, wantValue: 4.300000000000001},
|
||||
|
||||
// Errors
|
||||
{name: "Function Error 1", saql: "UNKNOWN(value)", wantRebuild: "UNKNOWN(value)", wantRebuildErr: true, wantEvalErr: true, values: `{"value": true}`},
|
||||
{name: "Function Error 2", saql: "ABS(value, value2)", wantRebuild: "ABS(value, value2)", wantEvalErr: true, values: `{"value": true, "value2": false}`},
|
||||
{name: "Function Error 3", saql: `ABS("abs")`, wantRebuild: `ABS("abs")`, wantEvalErr: true},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
parser := &Parser{}
|
||||
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
expr, err := parser.Parse(tt.saql)
|
||||
if (err != nil) != tt.wantParseErr {
|
||||
t.Errorf("Parse() error = %v, wantErr %v", err, tt.wantParseErr)
|
||||
if expr != nil {
|
||||
t.Error(expr.String())
|
||||
}
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
got, err := expr.String()
|
||||
if (err != nil) != tt.wantRebuildErr {
|
||||
t.Error(expr.String())
|
||||
t.Errorf("String() error = %v, wantErr %v", err, tt.wantParseErr)
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if got != tt.wantRebuild {
|
||||
t.Errorf("String() got = %v, want %v", got, tt.wantRebuild)
|
||||
}
|
||||
|
||||
var myJson map[string]interface{}
|
||||
if tt.values != "" {
|
||||
err = json.Unmarshal([]byte(tt.values), &myJson)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
value, err := expr.Eval(myJson)
|
||||
if (err != nil) != tt.wantEvalErr {
|
||||
t.Error(expr.String())
|
||||
t.Errorf("Parse() error = %v, wantErr %v", err, tt.wantParseErr)
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
wantValue := tt.wantValue
|
||||
if i, ok := wantValue.(int); ok {
|
||||
wantValue = float64(i)
|
||||
}
|
||||
|
||||
valueFloat, ok := value.(float64)
|
||||
wantValueFloat, ok2 := wantValue.(float64)
|
||||
if ok && ok2 {
|
||||
if math.Abs(valueFloat-wantValueFloat) > 0.0001 {
|
||||
t.Error(expr.String())
|
||||
t.Errorf("Eval() got = %T %#v, want %T %#v", value, value, wantValue, wantValue)
|
||||
}
|
||||
} else {
|
||||
if !reflect.DeepEqual(value, wantValue) {
|
||||
t.Error(expr.String())
|
||||
t.Errorf("Eval() got = %T %#v, want %T %#v", value, value, wantValue, wantValue)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func jsonParse(s string) interface{} {
|
||||
if s == "" {
|
||||
return nil
|
||||
}
|
||||
var j interface{}
|
||||
err := json.Unmarshal([]byte(s), &j)
|
||||
if err != nil {
|
||||
panic(s + err.Error())
|
||||
}
|
||||
return j
|
||||
}
|
||||
355
caql/interpreter.go
Normal file
355
caql/interpreter.go
Normal file
@@ -0,0 +1,355 @@
|
||||
package caql
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/SecurityBrewery/catalyst/generated/caql/parser"
|
||||
)
|
||||
|
||||
type aqlInterpreter struct {
|
||||
*parser.BaseCAQLParserListener
|
||||
values map[string]interface{}
|
||||
stack []interface{}
|
||||
errs []error
|
||||
}
|
||||
|
||||
// push is a helper function for pushing new node to the listener Stack.
|
||||
func (s *aqlInterpreter) push(i interface{}) {
|
||||
s.stack = append(s.stack, i)
|
||||
}
|
||||
|
||||
// pop is a helper function for poping a node from the listener Stack.
|
||||
func (s *aqlInterpreter) pop() (n interface{}) {
|
||||
// Check that we have nodes in the stack.
|
||||
size := len(s.stack)
|
||||
if size < 1 {
|
||||
s.appendErrors(ErrStack)
|
||||
return
|
||||
}
|
||||
|
||||
// Pop the last value from the Stack.
|
||||
n, s.stack = s.stack[size-1], s.stack[:size-1]
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (s *aqlInterpreter) binaryPop() (interface{}, interface{}) {
|
||||
right, left := s.pop(), s.pop()
|
||||
return left, right
|
||||
}
|
||||
|
||||
// ExitExpression is called when production expression is exited.
|
||||
func (s *aqlInterpreter) ExitExpression(ctx *parser.ExpressionContext) {
|
||||
switch {
|
||||
case ctx.Value_literal() != nil:
|
||||
// pass
|
||||
case ctx.Reference() != nil:
|
||||
// pass
|
||||
case ctx.Operator_unary() != nil:
|
||||
// pass
|
||||
|
||||
case ctx.T_PLUS() != nil:
|
||||
s.push(plus(s.binaryPop()))
|
||||
case ctx.T_MINUS() != nil:
|
||||
s.push(minus(s.binaryPop()))
|
||||
|
||||
case ctx.T_TIMES() != nil:
|
||||
s.push(times(s.binaryPop()))
|
||||
case ctx.T_DIV() != nil:
|
||||
s.push(div(s.binaryPop()))
|
||||
case ctx.T_MOD() != nil:
|
||||
s.push(mod(s.binaryPop()))
|
||||
|
||||
case ctx.T_RANGE() != nil:
|
||||
s.push(aqlrange(s.binaryPop()))
|
||||
|
||||
case ctx.T_LT() != nil && ctx.GetEq_op() == nil:
|
||||
s.push(lt(s.binaryPop()))
|
||||
case ctx.T_GT() != nil && ctx.GetEq_op() == nil:
|
||||
s.push(gt(s.binaryPop()))
|
||||
case ctx.T_LE() != nil && ctx.GetEq_op() == nil:
|
||||
s.push(le(s.binaryPop()))
|
||||
case ctx.T_GE() != nil && ctx.GetEq_op() == nil:
|
||||
s.push(ge(s.binaryPop()))
|
||||
|
||||
case ctx.T_IN() != nil && ctx.GetEq_op() == nil:
|
||||
s.push(maybeNot(ctx, in(s.binaryPop())))
|
||||
|
||||
case ctx.T_EQ() != nil && ctx.GetEq_op() == nil:
|
||||
s.push(eq(s.binaryPop()))
|
||||
case ctx.T_NE() != nil && ctx.GetEq_op() == nil:
|
||||
s.push(ne(s.binaryPop()))
|
||||
|
||||
case ctx.T_ALL() != nil && ctx.GetEq_op() != nil:
|
||||
right, left := s.pop(), s.pop()
|
||||
s.push(all(left.([]interface{}), getOp(ctx.GetEq_op().GetTokenType()), right))
|
||||
case ctx.T_ANY() != nil && ctx.GetEq_op() != nil:
|
||||
right, left := s.pop(), s.pop()
|
||||
s.push(any(left.([]interface{}), getOp(ctx.GetEq_op().GetTokenType()), right))
|
||||
case ctx.T_NONE() != nil && ctx.GetEq_op() != nil:
|
||||
right, left := s.pop(), s.pop()
|
||||
s.push(none(left.([]interface{}), getOp(ctx.GetEq_op().GetTokenType()), right))
|
||||
|
||||
case ctx.T_ALL() != nil && ctx.T_NOT() != nil && ctx.T_IN() != nil:
|
||||
right, left := s.pop(), s.pop()
|
||||
s.push(all(left.([]interface{}), in, right))
|
||||
case ctx.T_ANY() != nil && ctx.T_NOT() != nil && ctx.T_IN() != nil:
|
||||
right, left := s.pop(), s.pop()
|
||||
s.push(any(left.([]interface{}), in, right))
|
||||
case ctx.T_NONE() != nil && ctx.T_NOT() != nil && ctx.T_IN() != nil:
|
||||
right, left := s.pop(), s.pop()
|
||||
s.push(none(left.([]interface{}), in, right))
|
||||
|
||||
case ctx.T_LIKE() != nil:
|
||||
m, err := like(s.binaryPop())
|
||||
s.appendErrors(err)
|
||||
s.push(maybeNot(ctx, m))
|
||||
case ctx.T_REGEX_MATCH() != nil:
|
||||
m, err := regexMatch(s.binaryPop())
|
||||
s.appendErrors(err)
|
||||
s.push(maybeNot(ctx, m))
|
||||
case ctx.T_REGEX_NON_MATCH() != nil:
|
||||
m, err := regexNonMatch(s.binaryPop())
|
||||
s.appendErrors(err)
|
||||
s.push(maybeNot(ctx, m))
|
||||
|
||||
case ctx.T_AND() != nil:
|
||||
s.push(and(s.binaryPop()))
|
||||
case ctx.T_OR() != nil:
|
||||
s.push(or(s.binaryPop()))
|
||||
|
||||
case ctx.T_QUESTION() != nil && len(ctx.AllExpression()) == 3:
|
||||
right, middle, left := s.pop(), s.pop(), s.pop()
|
||||
s.push(ternary(left, middle, right))
|
||||
case ctx.T_QUESTION() != nil && len(ctx.AllExpression()) == 2:
|
||||
right, left := s.pop(), s.pop()
|
||||
s.push(ternary(left, nil, right))
|
||||
|
||||
default:
|
||||
panic("unkown expression")
|
||||
}
|
||||
}
|
||||
|
||||
func (s *aqlInterpreter) appendErrors(err error) {
|
||||
if err != nil {
|
||||
s.errs = append(s.errs, err)
|
||||
}
|
||||
}
|
||||
|
||||
// ExitOperator_unary is called when production operator_unary is exited.
|
||||
func (s *aqlInterpreter) ExitOperator_unary(ctx *parser.Operator_unaryContext) {
|
||||
value := s.pop()
|
||||
switch {
|
||||
case ctx.T_PLUS() != nil:
|
||||
s.push(value.(float64))
|
||||
case ctx.T_MINUS() != nil:
|
||||
s.push(-value.(float64))
|
||||
case ctx.T_NOT() != nil:
|
||||
s.push(!toBool(value))
|
||||
default:
|
||||
panic(fmt.Sprintf("unexpected operation: %s", ctx.GetText()))
|
||||
}
|
||||
}
|
||||
|
||||
// ExitReference is called when production reference is exited.
|
||||
func (s *aqlInterpreter) ExitReference(ctx *parser.ReferenceContext) {
|
||||
switch {
|
||||
case ctx.DOT() != nil:
|
||||
reference := s.pop()
|
||||
|
||||
s.push(reference.(map[string]interface{})[ctx.T_STRING().GetText()])
|
||||
case ctx.T_STRING() != nil:
|
||||
s.push(s.getVar(ctx.T_STRING().GetText()))
|
||||
case ctx.Compound_value() != nil:
|
||||
// pass
|
||||
case ctx.Function_call() != nil:
|
||||
// pass
|
||||
case ctx.T_OPEN() != nil:
|
||||
// pass
|
||||
case ctx.T_ARRAY_OPEN() != nil:
|
||||
key := s.pop()
|
||||
reference := s.pop()
|
||||
|
||||
if f, ok := key.(float64); ok {
|
||||
index := int(f)
|
||||
if index < 0 {
|
||||
index = len(reference.([]interface{})) + index
|
||||
}
|
||||
|
||||
s.push(reference.([]interface{})[index])
|
||||
return
|
||||
}
|
||||
|
||||
s.push(reference.(map[string]interface{})[key.(string)])
|
||||
default:
|
||||
panic(fmt.Sprintf("unexpected value: %s", ctx.GetText()))
|
||||
}
|
||||
}
|
||||
|
||||
// ExitCompound_value is called when production compound_value is exited.
|
||||
func (s *aqlInterpreter) ExitCompound_value(ctx *parser.Compound_valueContext) {
|
||||
// pass
|
||||
}
|
||||
|
||||
// ExitFunction_call is called when production function_call is exited.
|
||||
func (s *aqlInterpreter) ExitFunction_call(ctx *parser.Function_callContext) {
|
||||
s.function(ctx)
|
||||
}
|
||||
|
||||
// ExitValue_literal is called when production value_literal is exited.
|
||||
func (s *aqlInterpreter) ExitValue_literal(ctx *parser.Value_literalContext) {
|
||||
switch {
|
||||
case ctx.T_QUOTED_STRING() != nil:
|
||||
st, err := unquote(ctx.GetText())
|
||||
s.appendErrors(err)
|
||||
s.push(st)
|
||||
case ctx.T_INT() != nil:
|
||||
t := ctx.GetText()
|
||||
|
||||
switch {
|
||||
case strings.HasPrefix(strings.ToLower(t), "0b"):
|
||||
i64, err := strconv.ParseInt(t[2:], 2, 64)
|
||||
s.appendErrors(err)
|
||||
s.push(float64(i64))
|
||||
case strings.HasPrefix(strings.ToLower(t), "0x"):
|
||||
i64, err := strconv.ParseInt(t[2:], 16, 64)
|
||||
s.appendErrors(err)
|
||||
s.push(float64(i64))
|
||||
default:
|
||||
i, err := strconv.Atoi(t)
|
||||
s.appendErrors(err)
|
||||
s.push(float64(i))
|
||||
}
|
||||
case ctx.T_FLOAT() != nil:
|
||||
i, err := strconv.ParseFloat(ctx.GetText(), 64)
|
||||
s.appendErrors(err)
|
||||
s.push(i)
|
||||
case ctx.T_NULL() != nil:
|
||||
s.push(nil)
|
||||
case ctx.T_TRUE() != nil:
|
||||
s.push(true)
|
||||
case ctx.T_FALSE() != nil:
|
||||
s.push(false)
|
||||
default:
|
||||
panic(fmt.Sprintf("unexpected value: %s", ctx.GetText()))
|
||||
}
|
||||
}
|
||||
|
||||
// ExitArray is called when production array is exited.
|
||||
func (s *aqlInterpreter) ExitArray(ctx *parser.ArrayContext) {
|
||||
array := []interface{}{}
|
||||
for range ctx.AllExpression() {
|
||||
// prepend element
|
||||
array = append([]interface{}{s.pop()}, array...)
|
||||
}
|
||||
s.push(array)
|
||||
}
|
||||
|
||||
// ExitObject is called when production object is exited.
|
||||
func (s *aqlInterpreter) ExitObject(ctx *parser.ObjectContext) {
|
||||
object := map[string]interface{}{}
|
||||
for range ctx.AllObject_element() {
|
||||
key, value := s.pop(), s.pop()
|
||||
|
||||
object[key.(string)] = value
|
||||
}
|
||||
s.push(object)
|
||||
}
|
||||
|
||||
// ExitObject_element is called when production object_element is exited.
|
||||
func (s *aqlInterpreter) ExitObject_element(ctx *parser.Object_elementContext) {
|
||||
switch {
|
||||
case ctx.T_STRING() != nil:
|
||||
s.push(ctx.GetText())
|
||||
s.push(s.getVar(ctx.GetText()))
|
||||
case ctx.Object_element_name() != nil, ctx.T_ARRAY_OPEN() != nil:
|
||||
key, value := s.pop(), s.pop()
|
||||
|
||||
s.push(key)
|
||||
s.push(value)
|
||||
default:
|
||||
panic(fmt.Sprintf("unexpected value: %s", ctx.GetText()))
|
||||
}
|
||||
}
|
||||
|
||||
// ExitObject_element_name is called when production object_element_name is exited.
|
||||
func (s *aqlInterpreter) ExitObject_element_name(ctx *parser.Object_element_nameContext) {
|
||||
switch {
|
||||
case ctx.T_STRING() != nil:
|
||||
s.push(ctx.T_STRING().GetText())
|
||||
case ctx.T_QUOTED_STRING() != nil:
|
||||
st, err := unquote(ctx.T_QUOTED_STRING().GetText())
|
||||
if err != nil {
|
||||
s.appendErrors(fmt.Errorf("%w: %s", err, ctx.GetText()))
|
||||
}
|
||||
s.push(st)
|
||||
default:
|
||||
panic(fmt.Sprintf("unexpected value: %s", ctx.GetText()))
|
||||
}
|
||||
}
|
||||
|
||||
func (s *aqlInterpreter) getVar(identifier string) interface{} {
|
||||
v, ok := s.values[identifier]
|
||||
if !ok {
|
||||
s.appendErrors(ErrUndefined)
|
||||
}
|
||||
|
||||
return v
|
||||
}
|
||||
|
||||
func maybeNot(ctx *parser.ExpressionContext, m bool) bool {
|
||||
if ctx.T_NOT() != nil {
|
||||
return !m
|
||||
}
|
||||
return m
|
||||
}
|
||||
|
||||
func getOp(tokenType int) func(left, right interface{}) bool {
|
||||
switch tokenType {
|
||||
case parser.CAQLLexerT_EQ:
|
||||
return eq
|
||||
case parser.CAQLLexerT_NE:
|
||||
return ne
|
||||
case parser.CAQLLexerT_LT:
|
||||
return lt
|
||||
case parser.CAQLLexerT_GT:
|
||||
return gt
|
||||
case parser.CAQLLexerT_LE:
|
||||
return le
|
||||
case parser.CAQLLexerT_GE:
|
||||
return ge
|
||||
case parser.CAQLLexerT_IN:
|
||||
return in
|
||||
default:
|
||||
panic("unkown token type")
|
||||
}
|
||||
}
|
||||
|
||||
func all(slice []interface{}, op func(interface{}, interface{}) bool, expr interface{}) bool {
|
||||
for _, e := range slice {
|
||||
if !op(e, expr) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func any(slice []interface{}, op func(interface{}, interface{}) bool, expr interface{}) bool {
|
||||
for _, e := range slice {
|
||||
if op(e, expr) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func none(slice []interface{}, op func(interface{}, interface{}) bool, expr interface{}) bool {
|
||||
for _, e := range slice {
|
||||
if op(e, expr) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
497
caql/operations.go
Normal file
497
caql/operations.go
Normal file
@@ -0,0 +1,497 @@
|
||||
package caql
|
||||
|
||||
import (
|
||||
"math"
|
||||
"regexp"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Logical operators https://www.arangodb.com/docs/3.7/aql/operators.html#logical-operators
|
||||
|
||||
func or(left, right interface{}) interface{} {
|
||||
if toBool(left) {
|
||||
return left
|
||||
}
|
||||
return right
|
||||
}
|
||||
|
||||
func and(left, right interface{}) interface{} {
|
||||
if !toBool(left) {
|
||||
return left
|
||||
}
|
||||
return right
|
||||
}
|
||||
|
||||
func toBool(i interface{}) bool {
|
||||
switch v := i.(type) {
|
||||
case nil:
|
||||
return false
|
||||
case bool:
|
||||
return v
|
||||
case int:
|
||||
return v != 0
|
||||
case float64:
|
||||
return v != 0
|
||||
case string:
|
||||
return v != ""
|
||||
case []interface{}:
|
||||
return true
|
||||
case map[string]interface{}:
|
||||
return true
|
||||
default:
|
||||
panic("bool conversion failed")
|
||||
}
|
||||
}
|
||||
|
||||
// Arithmetic operators https://www.arangodb.com/docs/3.7/aql/operators.html#arithmetic-operators
|
||||
|
||||
func plus(left, right interface{}) float64 {
|
||||
return toNumber(left) + toNumber(right)
|
||||
}
|
||||
|
||||
func minus(left, right interface{}) float64 {
|
||||
return toNumber(left) - toNumber(right)
|
||||
}
|
||||
|
||||
func times(left, right interface{}) float64 {
|
||||
return round(toNumber(left) * toNumber(right))
|
||||
}
|
||||
|
||||
func round(r float64) float64 {
|
||||
return math.Round(r*100000) / 100000
|
||||
}
|
||||
|
||||
func div(left, right interface{}) float64 {
|
||||
b := toNumber(right)
|
||||
if b == 0 {
|
||||
return 0
|
||||
}
|
||||
return round(toNumber(left) / b)
|
||||
}
|
||||
|
||||
func mod(left, right interface{}) float64 {
|
||||
return math.Mod(toNumber(left), toNumber(right))
|
||||
}
|
||||
|
||||
func toNumber(i interface{}) float64 {
|
||||
switch v := i.(type) {
|
||||
case nil:
|
||||
return 0
|
||||
case bool:
|
||||
if v {
|
||||
return 1
|
||||
}
|
||||
return 0
|
||||
case float64:
|
||||
switch {
|
||||
case math.IsNaN(v):
|
||||
return 0
|
||||
case math.IsInf(v, 0):
|
||||
return 0
|
||||
}
|
||||
return v
|
||||
case string:
|
||||
f, err := strconv.ParseFloat(strings.TrimSpace(v), 64)
|
||||
if err != nil {
|
||||
return 0
|
||||
}
|
||||
return f
|
||||
case []interface{}:
|
||||
if len(v) == 0 {
|
||||
return 0
|
||||
}
|
||||
if len(v) == 1 {
|
||||
return toNumber(v[0])
|
||||
}
|
||||
return 0
|
||||
case map[string]interface{}:
|
||||
return 0
|
||||
default:
|
||||
panic("number conversion error")
|
||||
}
|
||||
}
|
||||
|
||||
// Logical operators https://www.arangodb.com/docs/3.7/aql/operators.html#logical-operators
|
||||
// Order https://www.arangodb.com/docs/3.7/aql/fundamentals-type-value-order.html
|
||||
|
||||
func eq(left, right interface{}) bool {
|
||||
leftV, rightV := typeValue(left), typeValue(right)
|
||||
if leftV != rightV {
|
||||
return false
|
||||
}
|
||||
switch l := left.(type) {
|
||||
case nil:
|
||||
return true
|
||||
case bool, float64, string:
|
||||
return left == right
|
||||
case []interface{}:
|
||||
ra := right.([]interface{})
|
||||
max := len(l)
|
||||
if len(ra) > max {
|
||||
max = len(ra)
|
||||
}
|
||||
for i := 0; i < max; i++ {
|
||||
var li interface{} = nil
|
||||
var rai interface{} = nil
|
||||
if len(l) > i {
|
||||
li = l[i]
|
||||
}
|
||||
if len(ra) > i {
|
||||
rai = ra[i]
|
||||
}
|
||||
|
||||
if !eq(li, rai) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
case map[string]interface{}:
|
||||
ro := right.(map[string]interface{})
|
||||
|
||||
for _, key := range keys(l, ro) {
|
||||
var li interface{} = nil
|
||||
var rai interface{} = nil
|
||||
if lv, ok := l[key]; ok {
|
||||
li = lv
|
||||
}
|
||||
if rv, ok := ro[key]; ok {
|
||||
rai = rv
|
||||
}
|
||||
|
||||
if !eq(li, rai) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
default:
|
||||
panic("unknown type")
|
||||
}
|
||||
}
|
||||
|
||||
func ne(left, right interface{}) bool {
|
||||
return !eq(left, right)
|
||||
}
|
||||
|
||||
func lt(left, right interface{}) bool {
|
||||
leftV, rightV := typeValue(left), typeValue(right)
|
||||
if leftV != rightV {
|
||||
return leftV < rightV
|
||||
}
|
||||
switch l := left.(type) {
|
||||
case nil:
|
||||
return false
|
||||
case bool:
|
||||
return toNumber(l) < toNumber(right)
|
||||
case int:
|
||||
return l < right.(int)
|
||||
case float64:
|
||||
return l < right.(float64)
|
||||
case string:
|
||||
return l < right.(string)
|
||||
case []interface{}:
|
||||
ra := right.([]interface{})
|
||||
max := len(l)
|
||||
if len(ra) > max {
|
||||
max = len(ra)
|
||||
}
|
||||
for i := 0; i < max; i++ {
|
||||
var li interface{} = nil
|
||||
var rai interface{} = nil
|
||||
if len(l) > i {
|
||||
li = l[i]
|
||||
}
|
||||
if len(ra) > i {
|
||||
rai = ra[i]
|
||||
}
|
||||
|
||||
if !eq(li, rai) {
|
||||
return lt(li, rai)
|
||||
}
|
||||
}
|
||||
return false
|
||||
case map[string]interface{}:
|
||||
ro := right.(map[string]interface{})
|
||||
|
||||
for _, key := range keys(l, ro) {
|
||||
var li interface{} = nil
|
||||
var rai interface{} = nil
|
||||
if lv, ok := l[key]; ok {
|
||||
li = lv
|
||||
}
|
||||
if rv, ok := ro[key]; ok {
|
||||
rai = rv
|
||||
}
|
||||
|
||||
if !eq(li, rai) {
|
||||
return lt(li, rai)
|
||||
}
|
||||
}
|
||||
return false
|
||||
default:
|
||||
panic("unknown type")
|
||||
}
|
||||
}
|
||||
|
||||
func keys(l map[string]interface{}, ro map[string]interface{}) []string {
|
||||
var keys []string
|
||||
seen := map[string]bool{}
|
||||
for _, a := range []map[string]interface{}{l, ro} {
|
||||
for k := range a {
|
||||
if _, ok := seen[k]; !ok {
|
||||
seen[k] = true
|
||||
keys = append(keys, k)
|
||||
}
|
||||
}
|
||||
}
|
||||
sort.Strings(keys)
|
||||
return keys
|
||||
}
|
||||
|
||||
func gt(left, right interface{}) bool {
|
||||
leftV, rightV := typeValue(left), typeValue(right)
|
||||
if leftV != rightV {
|
||||
return leftV > rightV
|
||||
}
|
||||
switch l := left.(type) {
|
||||
case nil:
|
||||
return false
|
||||
case bool:
|
||||
return toNumber(l) > toNumber(right)
|
||||
case int:
|
||||
return l > right.(int)
|
||||
case float64:
|
||||
return l > right.(float64)
|
||||
case string:
|
||||
return l > right.(string)
|
||||
case []interface{}:
|
||||
ra := right.([]interface{})
|
||||
max := len(l)
|
||||
if len(ra) > max {
|
||||
max = len(ra)
|
||||
}
|
||||
for i := 0; i < max; i++ {
|
||||
var li interface{} = nil
|
||||
var rai interface{} = nil
|
||||
if len(l) > i {
|
||||
li = l[i]
|
||||
}
|
||||
if len(ra) > i {
|
||||
rai = ra[i]
|
||||
}
|
||||
|
||||
if !eq(li, rai) {
|
||||
return gt(li, rai)
|
||||
}
|
||||
}
|
||||
return false
|
||||
case map[string]interface{}:
|
||||
ro := right.(map[string]interface{})
|
||||
|
||||
for _, key := range keys(l, ro) {
|
||||
var li interface{} = nil
|
||||
var rai interface{} = nil
|
||||
if lv, ok := l[key]; ok {
|
||||
li = lv
|
||||
}
|
||||
if rv, ok := ro[key]; ok {
|
||||
rai = rv
|
||||
}
|
||||
|
||||
if !eq(li, rai) {
|
||||
return gt(li, rai)
|
||||
}
|
||||
}
|
||||
return false
|
||||
default:
|
||||
panic("unknown type")
|
||||
}
|
||||
}
|
||||
|
||||
func le(left, right interface{}) bool {
|
||||
leftV, rightV := typeValue(left), typeValue(right)
|
||||
if leftV != rightV {
|
||||
return leftV <= rightV
|
||||
}
|
||||
switch l := left.(type) {
|
||||
case nil:
|
||||
return false
|
||||
case bool:
|
||||
return toNumber(l) <= toNumber(right)
|
||||
case int:
|
||||
return l <= right.(int)
|
||||
case float64:
|
||||
return l <= right.(float64)
|
||||
case string:
|
||||
return l <= right.(string)
|
||||
case []interface{}:
|
||||
ra := right.([]interface{})
|
||||
max := len(l)
|
||||
if len(ra) > max {
|
||||
max = len(ra)
|
||||
}
|
||||
for i := 0; i < max; i++ {
|
||||
var li interface{} = nil
|
||||
var rai interface{} = nil
|
||||
if len(l) > i {
|
||||
li = l[i]
|
||||
}
|
||||
if len(ra) > i {
|
||||
rai = ra[i]
|
||||
}
|
||||
|
||||
if !eq(li, rai) {
|
||||
return le(li, rai)
|
||||
}
|
||||
}
|
||||
return true
|
||||
case map[string]interface{}:
|
||||
ro := right.(map[string]interface{})
|
||||
|
||||
for _, key := range keys(l, ro) {
|
||||
var li interface{} = nil
|
||||
var rai interface{} = nil
|
||||
if lv, ok := l[key]; ok {
|
||||
li = lv
|
||||
}
|
||||
if rv, ok := ro[key]; ok {
|
||||
rai = rv
|
||||
}
|
||||
|
||||
if !eq(li, rai) {
|
||||
return lt(li, rai)
|
||||
}
|
||||
}
|
||||
return true
|
||||
default:
|
||||
panic("unknown type")
|
||||
}
|
||||
}
|
||||
|
||||
func ge(left, right interface{}) bool {
|
||||
leftV, rightV := typeValue(left), typeValue(right)
|
||||
if leftV != rightV {
|
||||
return leftV >= rightV
|
||||
}
|
||||
switch l := left.(type) {
|
||||
case nil:
|
||||
return false
|
||||
case bool:
|
||||
return toNumber(l) >= toNumber(right)
|
||||
case int:
|
||||
return l >= right.(int)
|
||||
case float64:
|
||||
return l >= right.(float64)
|
||||
case string:
|
||||
return l >= right.(string)
|
||||
case []interface{}:
|
||||
ra := right.([]interface{})
|
||||
max := len(l)
|
||||
if len(ra) > max {
|
||||
max = len(ra)
|
||||
}
|
||||
for i := 0; i < max; i++ {
|
||||
var li interface{} = nil
|
||||
var rai interface{} = nil
|
||||
if len(l) > i {
|
||||
li = l[i]
|
||||
}
|
||||
if len(ra) > i {
|
||||
rai = ra[i]
|
||||
}
|
||||
|
||||
if !eq(li, rai) {
|
||||
return ge(li, rai)
|
||||
}
|
||||
}
|
||||
return true
|
||||
case map[string]interface{}:
|
||||
ro := right.(map[string]interface{})
|
||||
|
||||
for _, key := range keys(l, ro) {
|
||||
var li interface{} = nil
|
||||
var rai interface{} = nil
|
||||
if lv, ok := l[key]; ok {
|
||||
li = lv
|
||||
}
|
||||
if rv, ok := ro[key]; ok {
|
||||
rai = rv
|
||||
}
|
||||
|
||||
if !eq(li, rai) {
|
||||
return gt(li, rai)
|
||||
}
|
||||
}
|
||||
return true
|
||||
default:
|
||||
panic("unknown type")
|
||||
}
|
||||
}
|
||||
|
||||
func in(left, right interface{}) bool {
|
||||
a, ok := right.([]interface{})
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
for _, v := range a {
|
||||
if left == v {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func like(left, right interface{}) (bool, error) {
|
||||
return match(right.(string), left.(string))
|
||||
}
|
||||
|
||||
func regexMatch(left, right interface{}) (bool, error) {
|
||||
return regexp.Match(right.(string), []byte(left.(string)))
|
||||
}
|
||||
|
||||
func regexNonMatch(left, right interface{}) (bool, error) {
|
||||
m, err := regexp.Match(right.(string), []byte(left.(string)))
|
||||
return !m, err
|
||||
}
|
||||
|
||||
func typeValue(v interface{}) int {
|
||||
switch v.(type) {
|
||||
case nil:
|
||||
return 0
|
||||
case bool:
|
||||
return 1
|
||||
case float64, int:
|
||||
return 2
|
||||
case string:
|
||||
return 3
|
||||
case []interface{}:
|
||||
return 4
|
||||
case map[string]interface{}:
|
||||
return 5
|
||||
default:
|
||||
panic("unknown type")
|
||||
}
|
||||
}
|
||||
|
||||
// Ternary operator https://www.arangodb.com/docs/3.7/aql/operators.html#ternary-operator
|
||||
|
||||
func ternary(left, middle, right interface{}) interface{} {
|
||||
if toBool(left) {
|
||||
if middle != nil {
|
||||
return middle
|
||||
}
|
||||
return left
|
||||
}
|
||||
return right
|
||||
}
|
||||
|
||||
// Range operators https://www.arangodb.com/docs/3.7/aql/operators.html#range-operator
|
||||
|
||||
func aqlrange(left, right interface{}) []float64 {
|
||||
var v []float64
|
||||
for i := int(left.(float64)); i <= int(right.(float64)); i++ {
|
||||
v = append(v, float64(i))
|
||||
}
|
||||
return v
|
||||
}
|
||||
120
caql/parser.go
Normal file
120
caql/parser.go
Normal file
@@ -0,0 +1,120 @@
|
||||
package caql
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"strconv"
|
||||
|
||||
"github.com/antlr/antlr4/runtime/Go/antlr"
|
||||
|
||||
"github.com/SecurityBrewery/catalyst/generated/caql/parser"
|
||||
)
|
||||
|
||||
type Parser struct {
|
||||
Searcher Searcher
|
||||
Prefix string
|
||||
}
|
||||
|
||||
func (p *Parser) Parse(aql string) (t *Tree, err error) {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
err = fmt.Errorf("%s", r)
|
||||
}
|
||||
}()
|
||||
// Setup the input
|
||||
inputStream := antlr.NewInputStream(aql)
|
||||
|
||||
errorListener := &errorListener{}
|
||||
|
||||
// Create the Lexer
|
||||
lexer := parser.NewCAQLLexer(inputStream)
|
||||
lexer.RemoveErrorListeners()
|
||||
lexer.AddErrorListener(errorListener)
|
||||
stream := antlr.NewCommonTokenStream(lexer, antlr.TokenDefaultChannel)
|
||||
|
||||
// Create the Parser
|
||||
aqlParser := parser.NewCAQLParser(stream)
|
||||
|
||||
aqlParser.RemoveErrorListeners()
|
||||
aqlParser.AddErrorListener(errorListener)
|
||||
aqlParser.SetErrorHandler(antlr.NewBailErrorStrategy())
|
||||
if errorListener.errs != nil {
|
||||
err = errorListener.errs[0]
|
||||
}
|
||||
|
||||
return &Tree{aqlParser: aqlParser, parseContext: aqlParser.Parse(), searcher: p.Searcher, prefix: p.Prefix}, err
|
||||
}
|
||||
|
||||
type Tree struct {
|
||||
parseContext parser.IParseContext
|
||||
aqlParser *parser.CAQLParser
|
||||
searcher Searcher
|
||||
prefix string
|
||||
}
|
||||
|
||||
func (t *Tree) Eval(values map[string]interface{}) (i interface{}, err error) {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
err = fmt.Errorf("%s", r)
|
||||
}
|
||||
}()
|
||||
interpreter := aqlInterpreter{values: values}
|
||||
|
||||
antlr.ParseTreeWalkerDefault.Walk(&interpreter, t.parseContext)
|
||||
|
||||
if interpreter.errs != nil {
|
||||
return nil, interpreter.errs[0]
|
||||
}
|
||||
return interpreter.stack[0], nil
|
||||
}
|
||||
|
||||
func (t *Tree) String() (s string, err error) {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
err = fmt.Errorf("%s", r)
|
||||
}
|
||||
}()
|
||||
builder := aqlBuilder{searcher: t.searcher, prefix: t.prefix}
|
||||
|
||||
antlr.ParseTreeWalkerDefault.Walk(&builder, t.parseContext)
|
||||
|
||||
return builder.stack[0], err
|
||||
}
|
||||
|
||||
func (t *Tree) BleveString() (s string, err error) {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
err = fmt.Errorf("%s", r)
|
||||
}
|
||||
}()
|
||||
builder := bleveBuilder{}
|
||||
|
||||
antlr.ParseTreeWalkerDefault.Walk(&builder, t.parseContext)
|
||||
|
||||
if builder.err != nil {
|
||||
return "", builder.err
|
||||
}
|
||||
|
||||
return builder.stack[0], err
|
||||
}
|
||||
|
||||
type errorListener struct {
|
||||
*antlr.DefaultErrorListener
|
||||
errs []error
|
||||
}
|
||||
|
||||
func (el *errorListener) SyntaxError(recognizer antlr.Recognizer, offendingSymbol interface{}, line, column int, msg string, e antlr.RecognitionException) {
|
||||
el.errs = append(el.errs, fmt.Errorf("line "+strconv.Itoa(line)+":"+strconv.Itoa(column)+" "+msg))
|
||||
}
|
||||
|
||||
func (el *errorListener) ReportAmbiguity(recognizer antlr.Parser, dfa *antlr.DFA, startIndex, stopIndex int, exact bool, ambigAlts *antlr.BitSet, configs antlr.ATNConfigSet) {
|
||||
el.errs = append(el.errs, errors.New("ReportAmbiguity"))
|
||||
}
|
||||
|
||||
func (el *errorListener) ReportAttemptingFullContext(recognizer antlr.Parser, dfa *antlr.DFA, startIndex, stopIndex int, conflictingAlts *antlr.BitSet, configs antlr.ATNConfigSet) {
|
||||
el.errs = append(el.errs, errors.New("ReportAttemptingFullContext"))
|
||||
}
|
||||
|
||||
func (el *errorListener) ReportContextSensitivity(recognizer antlr.Parser, dfa *antlr.DFA, startIndex, stopIndex, prediction int, configs antlr.ATNConfigSet) {
|
||||
el.errs = append(el.errs, errors.New("ReportContextSensitivity"))
|
||||
}
|
||||
352
caql/rql_test.go
Normal file
352
caql/rql_test.go
Normal file
@@ -0,0 +1,352 @@
|
||||
package caql
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
type MockSearcher struct{}
|
||||
|
||||
func (m MockSearcher) Search(_ string) (ids []string, err error) {
|
||||
return []string{"1", "2", "3"}, nil
|
||||
}
|
||||
|
||||
func TestParseSAQLEval(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
saql string
|
||||
wantRebuild string
|
||||
wantValue interface{}
|
||||
wantParseErr bool
|
||||
wantRebuildErr bool
|
||||
wantEvalErr bool
|
||||
values string
|
||||
}{
|
||||
// Custom
|
||||
{name: "Compare 1", saql: "1 <= 2", wantRebuild: "1 <= 2", wantValue: true},
|
||||
{name: "Compare 2", saql: "1 >= 2", wantRebuild: "1 >= 2", wantValue: false},
|
||||
{name: "Compare 3", saql: "1 == 2", wantRebuild: "1 == 2", wantValue: false},
|
||||
{name: "Compare 4", saql: "1 > 2", wantRebuild: "1 > 2", wantValue: false},
|
||||
{name: "Compare 5", saql: "1 < 2", wantRebuild: "1 < 2", wantValue: true},
|
||||
{name: "Compare 6", saql: "1 != 2", wantRebuild: "1 != 2", wantValue: true},
|
||||
|
||||
{name: "SymbolRef 1", saql: "name", wantRebuild: "name", wantValue: false, values: `{"name": false}`},
|
||||
{name: "SymbolRef 2", saql: "d.name", wantRebuild: "d.name", wantValue: false, values: `{"d": {"name": false}}`},
|
||||
{name: "SymbolRef 3", saql: "name == false", wantRebuild: "name == false", wantValue: true, values: `{"name": false}`},
|
||||
{name: "SymbolRef Error 1", saql: "name, title", wantParseErr: true},
|
||||
{name: "SymbolRef Error 2", saql: "unknown", wantRebuild: "unknown", wantValue: false, wantEvalErr: true, values: `{}`},
|
||||
|
||||
{name: "Misc 1", saql: `active == true && age < 39`, wantRebuild: `active == true AND age < 39`, wantValue: true, values: `{"active": true, "age": 2}`},
|
||||
{name: "Misc 2", saql: `(attr == 10) AND foo == 'bar' OR NOT baz`, wantRebuild: `(attr == 10) AND foo == "bar" OR NOT baz`, wantValue: false, values: `{"attr": 2, "foo": "bar", "baz": true}`},
|
||||
{name: "Misc 3", saql: `attr == 10 AND (foo == 'bar' OR foo == 'baz')`, wantRebuild: `attr == 10 AND (foo == "bar" OR foo == "baz")`, wantValue: false, values: `{"attr": 2, "foo": "bar", "baz": true}`},
|
||||
{name: "Misc 4", saql: `5 > 1 AND "a" != "b"`, wantRebuild: `5 > 1 AND "a" != "b"`, wantValue: true},
|
||||
|
||||
{name: "LIKE 1", saql: `"foo" LIKE "%f%"`, wantRebuild: `"foo" LIKE "%f%"`, wantValue: true},
|
||||
{name: "LIKE 2", saql: `"foo" NOT LIKE "%f%"`, wantRebuild: `"foo" NOT LIKE "%f%"`, wantValue: false},
|
||||
{name: "LIKE 3", saql: `NOT "foo" LIKE "%f%"`, wantRebuild: `NOT "foo" LIKE "%f%"`, wantValue: false},
|
||||
|
||||
{name: "Summand 1", saql: "1 + 2", wantRebuild: "1 + 2", wantValue: 3},
|
||||
{name: "Summand 2", saql: "1 - 2", wantRebuild: "1 - 2", wantValue: -1},
|
||||
|
||||
{name: "Factor 1", saql: "1 * 2", wantRebuild: "1 * 2", wantValue: 2},
|
||||
{name: "Factor 2", saql: "1 / 2", wantRebuild: "1 / 2", wantValue: 0.5},
|
||||
{name: "Factor 3", saql: "1.0 / 2.0", wantRebuild: "1.0 / 2.0", wantValue: 0.5},
|
||||
{name: "Factor 4", saql: "1 % 2", wantRebuild: "1 % 2", wantValue: 1},
|
||||
|
||||
{name: "Term 1", saql: "(1 + 2) * 2", wantRebuild: "(1 + 2) * 2", wantValue: 6},
|
||||
{name: "Term 2", saql: "2 * (1 + 2)", wantRebuild: "2 * (1 + 2)", wantValue: 6},
|
||||
|
||||
// https://www.arangodb.com/docs/3.7/aql/fundamentals-data-types.html
|
||||
{name: "Null 1", saql: `null`, wantRebuild: "null"},
|
||||
{name: "Bool 1", saql: `true`, wantRebuild: "true", wantValue: true},
|
||||
{name: "Bool 2", saql: `false`, wantRebuild: "false", wantValue: false},
|
||||
{name: "Numeric 1", saql: "1", wantRebuild: "1", wantValue: 1},
|
||||
{name: "Numeric 2", saql: "+1", wantRebuild: "1", wantValue: 1},
|
||||
{name: "Numeric 3", saql: "42", wantRebuild: "42", wantValue: 42},
|
||||
{name: "Numeric 4", saql: "-1", wantRebuild: "-1", wantValue: -1},
|
||||
{name: "Numeric 5", saql: "-42", wantRebuild: "-42", wantValue: -42},
|
||||
{name: "Numeric 6", saql: "1.23", wantRebuild: "1.23", wantValue: 1.23},
|
||||
{name: "Numeric 7", saql: "-99.99", wantRebuild: "-99.99", wantValue: -99.99},
|
||||
{name: "Numeric 8", saql: "0.5", wantRebuild: "0.5", wantValue: 0.5},
|
||||
{name: "Numeric 9", saql: ".5", wantRebuild: ".5", wantValue: 0.5},
|
||||
{name: "Numeric 10", saql: "-4.87e103", wantRebuild: "-4.87e103", wantValue: -4.87e+103},
|
||||
{name: "Numeric 11", saql: "0b10", wantRebuild: "0b10", wantValue: 2},
|
||||
{name: "Numeric 12", saql: "0x10", wantRebuild: "0x10", wantValue: 16},
|
||||
{name: "Numeric Error 1", saql: "1.", wantParseErr: true},
|
||||
{name: "Numeric Error 2", saql: "01.23", wantParseErr: true},
|
||||
{name: "Numeric Error 3", saql: "00.23", wantParseErr: true},
|
||||
{name: "Numeric Error 4", saql: "00", wantParseErr: true},
|
||||
|
||||
// {name: "String 1", saql: `"yikes!"`, wantRebuild: `"yikes!"`, wantValue: "yikes!"},
|
||||
// {name: "String 2", saql: `"don't know"`, wantRebuild: `"don't know"`, wantValue: "don't know"},
|
||||
// {name: "String 3", saql: `"this is a \"quoted\" word"`, wantRebuild: `"this is a \"quoted\" word"`, wantValue: "this is a \"quoted\" word"},
|
||||
// {name: "String 4", saql: `"this is a longer string."`, wantRebuild: `"this is a longer string."`, wantValue: "this is a longer string."},
|
||||
// {name: "String 5", saql: `"the path separator on Windows is \\"`, wantRebuild: `"the path separator on Windows is \\"`, wantValue: "the path separator on Windows is \\"},
|
||||
// {name: "String 6", saql: `'yikes!'`, wantRebuild: `"yikes!"`, wantValue: "yikes!"},
|
||||
// {name: "String 7", saql: `'don\'t know'`, wantRebuild: `"don't know"`, wantValue: "don't know"},
|
||||
// {name: "String 8", saql: `'this is a "quoted" word'`, wantRebuild: `"this is a \"quoted\" word"`, wantValue: "this is a \"quoted\" word"},
|
||||
// {name: "String 9", saql: `'this is a longer string.'`, wantRebuild: `"this is a longer string."`, wantValue: "this is a longer string."},
|
||||
// {name: "String 10", saql: `'the path separator on Windows is \\'`, wantRebuild: `"the path separator on Windows is \\"`, wantValue: `the path separator on Windows is \`},
|
||||
|
||||
{name: "Array 1", saql: "[]", wantRebuild: "[]", wantValue: []interface{}{}},
|
||||
{name: "Array 2", saql: `[true]`, wantRebuild: `[true]`, wantValue: []interface{}{true}},
|
||||
{name: "Array 3", saql: `[1, 2, 3]`, wantRebuild: `[1, 2, 3]`, wantValue: []interface{}{float64(1), float64(2), float64(3)}},
|
||||
{
|
||||
name: "Array 4", saql: `[-99, "yikes!", [false, ["no"], []], 1]`, wantRebuild: `[-99, "yikes!", [false, ["no"], []], 1]`,
|
||||
wantValue: []interface{}{-99.0, "yikes!", []interface{}{false, []interface{}{"no"}, []interface{}{}}, float64(1)},
|
||||
},
|
||||
{name: "Array 5", saql: `[["fox", "marshal"]]`, wantRebuild: `[["fox", "marshal"]]`, wantValue: []interface{}{[]interface{}{"fox", "marshal"}}},
|
||||
{name: "Array 6", saql: `[1, 2, 3,]`, wantRebuild: `[1, 2, 3]`, wantValue: []interface{}{float64(1), float64(2), float64(3)}},
|
||||
|
||||
{name: "Array Error 1", saql: "(1,2,3)", wantParseErr: true},
|
||||
{name: "Array Access 1", saql: "u.friends[0]", wantRebuild: "u.friends[0]", wantValue: 7, values: `{"u": {"friends": [7,8,9]}}`},
|
||||
{name: "Array Access 2", saql: "u.friends[2]", wantRebuild: "u.friends[2]", wantValue: 9, values: `{"u": {"friends": [7,8,9]}}`},
|
||||
{name: "Array Access 3", saql: "u.friends[-1]", wantRebuild: "u.friends[-1]", wantValue: 9, values: `{"u": {"friends": [7,8,9]}}`},
|
||||
{name: "Array Access 4", saql: "u.friends[-2]", wantRebuild: "u.friends[-2]", wantValue: 8, values: `{"u": {"friends": [7,8,9]}}`},
|
||||
|
||||
{name: "Object 1", saql: "{}", wantRebuild: "{}", wantValue: map[string]interface{}{}},
|
||||
{name: "Object 2", saql: `{a: 1}`, wantRebuild: "{a: 1}", wantValue: map[string]interface{}{"a": float64(1)}},
|
||||
{name: "Object 3", saql: `{'a': 1}`, wantRebuild: `{'a': 1}`, wantValue: map[string]interface{}{"a": float64(1)}},
|
||||
{name: "Object 4", saql: `{"a": 1}`, wantRebuild: `{"a": 1}`, wantValue: map[string]interface{}{"a": float64(1)}},
|
||||
{name: "Object 5", saql: `{'return': 1}`, wantRebuild: `{'return': 1}`, wantValue: map[string]interface{}{"return": float64(1)}},
|
||||
{name: "Object 6", saql: `{"return": 1}`, wantRebuild: `{"return": 1}`, wantValue: map[string]interface{}{"return": float64(1)}},
|
||||
{name: "Object 9", saql: `{a: 1,}`, wantRebuild: "{a: 1}", wantValue: map[string]interface{}{"a": float64(1)}},
|
||||
{name: "Object 10", saql: `{"a": 1,}`, wantRebuild: `{"a": 1}`, wantValue: map[string]interface{}{"a": float64(1)}},
|
||||
// {"Object 8", "{`return`: 1}", `{"return": 1}`, true},
|
||||
// {"Object 7", "{´return´: 1}", `{"return": 1}`, true},
|
||||
{name: "Object Error 1: return is a keyword", saql: `{like: 1}`, wantParseErr: true},
|
||||
|
||||
{name: "Object Access 1", saql: "u.address.city.name", wantRebuild: "u.address.city.name", wantValue: "Munich", values: `{"u": {"address": {"city": {"name": "Munich"}}}}`},
|
||||
{name: "Object Access 2", saql: "u.friends[0].name.first", wantRebuild: "u.friends[0].name.first", wantValue: "Kevin", values: `{"u": {"friends": [{"name": {"first": "Kevin"}}]}}`},
|
||||
{name: "Object Access 3", saql: `u["address"]["city"]["name"]`, wantRebuild: `u["address"]["city"]["name"]`, wantValue: "Munich", values: `{"u": {"address": {"city": {"name": "Munich"}}}}`},
|
||||
{name: "Object Access 4", saql: `u["friends"][0]["name"]["first"]`, wantRebuild: `u["friends"][0]["name"]["first"]`, wantValue: "Kevin", values: `{"u": {"friends": [{"name": {"first": "Kevin"}}]}}`},
|
||||
{name: "Object Access 5", saql: "u._key", wantRebuild: "u._key", wantValue: false, values: `{"u": {"_key": false}}`},
|
||||
|
||||
// This query language does not support binds
|
||||
// https://www.arangodb.com/docs/3.7/aql/fundamentals-bind-parameters.html
|
||||
// {name: "Bind 1", saql: "u.id == @id && u.name == @name", wantRebuild: `u.id == @id AND u.name == @name`, wantValue: true},
|
||||
// {name: "Bind 2", saql: "u.id == CONCAT('prefix', @id, 'suffix') && u.name == @name", wantRebuild: `u.id == CONCAT('prefix', @id, 'suffix') AND u.name == @name`, wantValue: false},
|
||||
// {name: "Bind 3", saql: "doc.@attr.@subattr", wantRebuild: `doc.@attr.@subattr`, wantValue: true, values: `{"doc": {"@attr": {"@subattr": true}}}`},
|
||||
// {name: "Bind 4", saql: "doc[@attr][@subattr]", wantRebuild: `doc[@attr][@subattr]`, wantValue: true, values: `{"doc": {"@attr": {"@subattr": true}}}`},
|
||||
|
||||
// https://www.arangodb.com/docs/3.7/aql/fundamentals-type-value-order.html
|
||||
{name: "Compare 7", saql: `null < false`, wantRebuild: `null < false`, wantValue: true},
|
||||
{name: "Compare 8", saql: `null < true`, wantRebuild: `null < true`, wantValue: true},
|
||||
{name: "Compare 9", saql: `null < 1`, wantRebuild: `null < 1`, wantValue: true},
|
||||
{name: "Compare 10", saql: `null < ''`, wantRebuild: `null < ""`, wantValue: true},
|
||||
{name: "Compare 11", saql: `null < ' '`, wantRebuild: `null < " "`, wantValue: true},
|
||||
{name: "Compare 12", saql: `null < '3'`, wantRebuild: `null < "3"`, wantValue: true},
|
||||
{name: "Compare 13", saql: `null < 'abc'`, wantRebuild: `null < "abc"`, wantValue: true},
|
||||
{name: "Compare 14", saql: `null < []`, wantRebuild: `null < []`, wantValue: true},
|
||||
{name: "Compare 15", saql: `null < {}`, wantRebuild: `null < {}`, wantValue: true},
|
||||
{name: "Compare 16", saql: `false < true`, wantRebuild: `false < true`, wantValue: true},
|
||||
{name: "Compare 17", saql: `false < 5`, wantRebuild: `false < 5`, wantValue: true},
|
||||
{name: "Compare 18", saql: `false < ''`, wantRebuild: `false < ""`, wantValue: true},
|
||||
{name: "Compare 19", saql: `false < ' '`, wantRebuild: `false < " "`, wantValue: true},
|
||||
{name: "Compare 20", saql: `false < '7'`, wantRebuild: `false < "7"`, wantValue: true},
|
||||
{name: "Compare 21", saql: `false < 'abc'`, wantRebuild: `false < "abc"`, wantValue: true},
|
||||
{name: "Compare 22", saql: `false < []`, wantRebuild: `false < []`, wantValue: true},
|
||||
{name: "Compare 23", saql: `false < {}`, wantRebuild: `false < {}`, wantValue: true},
|
||||
{name: "Compare 24", saql: `true < 9`, wantRebuild: `true < 9`, wantValue: true},
|
||||
{name: "Compare 25", saql: `true < ''`, wantRebuild: `true < ""`, wantValue: true},
|
||||
{name: "Compare 26", saql: `true < ' '`, wantRebuild: `true < " "`, wantValue: true},
|
||||
{name: "Compare 27", saql: `true < '11'`, wantRebuild: `true < "11"`, wantValue: true},
|
||||
{name: "Compare 28", saql: `true < 'abc'`, wantRebuild: `true < "abc"`, wantValue: true},
|
||||
{name: "Compare 29", saql: `true < []`, wantRebuild: `true < []`, wantValue: true},
|
||||
{name: "Compare 30", saql: `true < {}`, wantRebuild: `true < {}`, wantValue: true},
|
||||
{name: "Compare 31", saql: `13 < ''`, wantRebuild: `13 < ""`, wantValue: true},
|
||||
{name: "Compare 32", saql: `15 < ' '`, wantRebuild: `15 < " "`, wantValue: true},
|
||||
{name: "Compare 33", saql: `17 < '18'`, wantRebuild: `17 < "18"`, wantValue: true},
|
||||
{name: "Compare 34", saql: `21 < 'abc'`, wantRebuild: `21 < "abc"`, wantValue: true},
|
||||
{name: "Compare 35", saql: `23 < []`, wantRebuild: `23 < []`, wantValue: true},
|
||||
{name: "Compare 36", saql: `25 < {}`, wantRebuild: `25 < {}`, wantValue: true},
|
||||
{name: "Compare 37", saql: `'' < ' '`, wantRebuild: `"" < " "`, wantValue: true},
|
||||
{name: "Compare 38", saql: `'' < '27'`, wantRebuild: `"" < "27"`, wantValue: true},
|
||||
{name: "Compare 39", saql: `'' < 'abc'`, wantRebuild: `"" < "abc"`, wantValue: true},
|
||||
{name: "Compare 40", saql: `'' < []`, wantRebuild: `"" < []`, wantValue: true},
|
||||
{name: "Compare 41", saql: `'' < {}`, wantRebuild: `"" < {}`, wantValue: true},
|
||||
{name: "Compare 42", saql: `[] < {}`, wantRebuild: `[] < {}`, wantValue: true},
|
||||
{name: "Compare 43", saql: `[] < [29]`, wantRebuild: `[] < [29]`, wantValue: true},
|
||||
{name: "Compare 44", saql: `[1] < [2]`, wantRebuild: `[1] < [2]`, wantValue: true},
|
||||
{name: "Compare 45", saql: `[1, 2] < [2]`, wantRebuild: `[1, 2] < [2]`, wantValue: true},
|
||||
{name: "Compare 46", saql: `[99, 99] < [100]`, wantRebuild: `[99, 99] < [100]`, wantValue: true},
|
||||
{name: "Compare 47", saql: `[false] < [true]`, wantRebuild: `[false] < [true]`, wantValue: true},
|
||||
{name: "Compare 48", saql: `[false, 1] < [false, '']`, wantRebuild: `[false, 1] < [false, ""]`, wantValue: true},
|
||||
{name: "Compare 49", saql: `{} < {"a": 1}`, wantRebuild: `{} < {"a": 1}`, wantValue: true},
|
||||
{name: "Compare 50", saql: `{} == {"a": null}`, wantRebuild: `{} == {"a": null}`, wantValue: true},
|
||||
{name: "Compare 51", saql: `{"a": 1} < {"a": 2}`, wantRebuild: `{"a": 1} < {"a": 2}`, wantValue: true},
|
||||
{name: "Compare 52", saql: `{"b": 1} < {"a": 0}`, wantRebuild: `{"b": 1} < {"a": 0}`, wantValue: true},
|
||||
{name: "Compare 53", saql: `{"a": {"c": true}} < {"a": {"c": 0}}`, wantRebuild: `{"a": {"c": true}} < {"a": {"c": 0}}`, wantValue: true},
|
||||
{name: "Compare 54", saql: `{"a": {"c": true, "a": 0}} < {"a": {"c": false, "a": 1}}`, wantRebuild: `{"a": {"c": true, "a": 0}} < {"a": {"c": false, "a": 1}}`, wantValue: true},
|
||||
{name: "Compare 55", saql: `{"a": 1, "b": 2} == {"b": 2, "a": 1}`, wantRebuild: `{"a": 1, "b": 2} == {"b": 2, "a": 1}`, wantValue: true},
|
||||
|
||||
// https://www.arangodb.com/docs/3.7/aql/operators.html
|
||||
{name: "Compare 56", saql: `0 == null`, wantRebuild: `0 == null`, wantValue: false},
|
||||
{name: "Compare 57", saql: `1 > 0`, wantRebuild: `1 > 0`, wantValue: true},
|
||||
{name: "Compare 58", saql: `true != null`, wantRebuild: `true != null`, wantValue: true},
|
||||
{name: "Compare 59", saql: `45 <= "yikes!"`, wantRebuild: `45 <= "yikes!"`, wantValue: true},
|
||||
{name: "Compare 60", saql: `65 != "65"`, wantRebuild: `65 != "65"`, wantValue: true},
|
||||
{name: "Compare 61", saql: `65 == 65`, wantRebuild: `65 == 65`, wantValue: true},
|
||||
{name: "Compare 62", saql: `1.23 > 1.32`, wantRebuild: `1.23 > 1.32`, wantValue: false},
|
||||
{name: "Compare 63", saql: `1.5 IN [2, 3, 1.5]`, wantRebuild: `1.5 IN [2, 3, 1.5]`, wantValue: true},
|
||||
{name: "Compare 64", saql: `"foo" IN null`, wantRebuild: `"foo" IN null`, wantValue: false},
|
||||
{name: "Compare 65", saql: `42 NOT IN [17, 40, 50]`, wantRebuild: `42 NOT IN [17, 40, 50]`, wantValue: true},
|
||||
{name: "Compare 66", saql: `"abc" == "abc"`, wantRebuild: `"abc" == "abc"`, wantValue: true},
|
||||
{name: "Compare 67", saql: `"abc" == "ABC"`, wantRebuild: `"abc" == "ABC"`, wantValue: false},
|
||||
{name: "Compare 68", saql: `"foo" LIKE "f%"`, wantRebuild: `"foo" LIKE "f%"`, wantValue: true},
|
||||
{name: "Compare 69", saql: `"foo" NOT LIKE "f%"`, wantRebuild: `"foo" NOT LIKE "f%"`, wantValue: false},
|
||||
{name: "Compare 70", saql: `"foo" =~ "^f[o].$"`, wantRebuild: `"foo" =~ "^f[o].$"`, wantValue: true},
|
||||
{name: "Compare 71", saql: `"foo" !~ "[a-z]+bar$"`, wantRebuild: `"foo" !~ "[a-z]+bar$"`, wantValue: true},
|
||||
|
||||
{name: "Compare 72", saql: `"abc" LIKE "a%"`, wantRebuild: `"abc" LIKE "a%"`, wantValue: true},
|
||||
{name: "Compare 73", saql: `"abc" LIKE "_bc"`, wantRebuild: `"abc" LIKE "_bc"`, wantValue: true},
|
||||
{name: "Compare 74", saql: `"a_b_foo" LIKE "a\\_b\\_foo"`, wantRebuild: `"a_b_foo" LIKE "a\\_b\\_foo"`, wantValue: true},
|
||||
|
||||
// https://www.arangodb.com/docs/3.7/aql/operators.html#array-comparison-operators
|
||||
{name: "Compare Array 1", saql: `[1, 2, 3] ALL IN [2, 3, 4]`, wantRebuild: `[1, 2, 3] ALL IN [2, 3, 4]`, wantValue: false},
|
||||
{name: "Compare Array 2", saql: `[1, 2, 3] ALL IN [1, 2, 3]`, wantRebuild: `[1, 2, 3] ALL IN [1, 2, 3]`, wantValue: true},
|
||||
{name: "Compare Array 3", saql: `[1, 2, 3] NONE IN [3]`, wantRebuild: `[1, 2, 3] NONE IN [3]`, wantValue: false},
|
||||
{name: "Compare Array 4", saql: `[1, 2, 3] NONE IN [23, 42]`, wantRebuild: `[1, 2, 3] NONE IN [23, 42]`, wantValue: true},
|
||||
{name: "Compare Array 5", saql: `[1, 2, 3] ANY IN [4, 5, 6]`, wantRebuild: `[1, 2, 3] ANY IN [4, 5, 6]`, wantValue: false},
|
||||
{name: "Compare Array 6", saql: `[1, 2, 3] ANY IN [1, 42]`, wantRebuild: `[1, 2, 3] ANY IN [1, 42]`, wantValue: true},
|
||||
{name: "Compare Array 7", saql: `[1, 2, 3] ANY == 2`, wantRebuild: `[1, 2, 3] ANY == 2`, wantValue: true},
|
||||
{name: "Compare Array 8", saql: `[1, 2, 3] ANY == 4`, wantRebuild: `[1, 2, 3] ANY == 4`, wantValue: false},
|
||||
{name: "Compare Array 9", saql: `[1, 2, 3] ANY > 0`, wantRebuild: `[1, 2, 3] ANY > 0`, wantValue: true},
|
||||
{name: "Compare Array 10", saql: `[1, 2, 3] ANY <= 1`, wantRebuild: `[1, 2, 3] ANY <= 1`, wantValue: true},
|
||||
{name: "Compare Array 11", saql: `[1, 2, 3] NONE < 99`, wantRebuild: `[1, 2, 3] NONE < 99`, wantValue: false},
|
||||
{name: "Compare Array 12", saql: `[1, 2, 3] NONE > 10`, wantRebuild: `[1, 2, 3] NONE > 10`, wantValue: true},
|
||||
{name: "Compare Array 13", saql: `[1, 2, 3] ALL > 2`, wantRebuild: `[1, 2, 3] ALL > 2`, wantValue: false},
|
||||
{name: "Compare Array 14", saql: `[1, 2, 3] ALL > 0`, wantRebuild: `[1, 2, 3] ALL > 0`, wantValue: true},
|
||||
{name: "Compare Array 15", saql: `[1, 2, 3] ALL >= 3`, wantRebuild: `[1, 2, 3] ALL >= 3`, wantValue: false},
|
||||
{name: "Compare Array 16", saql: `["foo", "bar"] ALL != "moo"`, wantRebuild: `["foo", "bar"] ALL != "moo"`, wantValue: true},
|
||||
{name: "Compare Array 17", saql: `["foo", "bar"] NONE == "bar"`, wantRebuild: `["foo", "bar"] NONE == "bar"`, wantValue: false},
|
||||
{name: "Compare Array 18", saql: `["foo", "bar"] ANY == "foo"`, wantRebuild: `["foo", "bar"] ANY == "foo"`, wantValue: true},
|
||||
|
||||
// https://www.arangodb.com/docs/3.7/aql/operators.html#logical-operators
|
||||
{name: "Logical 1", saql: "active == true OR age < 39", wantRebuild: "active == true OR age < 39", wantValue: true, values: `{"active": true, "age": 4}`},
|
||||
{name: "Logical 2", saql: "active == true || age < 39", wantRebuild: "active == true OR age < 39", wantValue: true, values: `{"active": true, "age": 4}`},
|
||||
{name: "Logical 3", saql: "active == true AND age < 39", wantRebuild: "active == true AND age < 39", wantValue: true, values: `{"active": true, "age": 4}`},
|
||||
{name: "Logical 4", saql: "active == true && age < 39", wantRebuild: "active == true AND age < 39", wantValue: true, values: `{"active": true, "age": 4}`},
|
||||
{name: "Logical 5", saql: "!active", wantRebuild: "NOT active", wantValue: false, values: `{"active": true}`},
|
||||
{name: "Logical 6", saql: "NOT active", wantRebuild: "NOT active", wantValue: false, values: `{"active": true}`},
|
||||
{name: "Logical 7", saql: "not active", wantRebuild: "NOT active", wantValue: false, values: `{"active": true}`},
|
||||
{name: "Logical 8", saql: "NOT NOT active", wantRebuild: "NOT NOT active", wantValue: true, values: `{"active": true}`},
|
||||
|
||||
{name: "Logical 9", saql: `u.age > 15 && u.address.city != ""`, wantRebuild: `u.age > 15 AND u.address.city != ""`, wantValue: false, values: `{"u": {"age": 2, "address": {"city": "Munich"}}}`},
|
||||
{name: "Logical 10", saql: `true || false`, wantRebuild: `true OR false`, wantValue: true},
|
||||
{name: "Logical 11", saql: `NOT u.isInvalid`, wantRebuild: `NOT u.isInvalid`, wantValue: false, values: `{"u": {"isInvalid": true}}`},
|
||||
{name: "Logical 12", saql: `1 || ! 0`, wantRebuild: `1 OR NOT 0`, wantValue: 1},
|
||||
|
||||
{name: "Logical 13", saql: `25 > 1 && 42 != 7`, wantRebuild: `25 > 1 AND 42 != 7`, wantValue: true},
|
||||
{name: "Logical 14", saql: `22 IN [23, 42] || 23 NOT IN [22, 7]`, wantRebuild: `22 IN [23, 42] OR 23 NOT IN [22, 7]`, wantValue: true},
|
||||
{name: "Logical 15", saql: `25 != 25`, wantRebuild: `25 != 25`, wantValue: false},
|
||||
|
||||
{name: "Logical 16", saql: `1 || 7`, wantRebuild: `1 OR 7`, wantValue: 1},
|
||||
// {name: "Logical 17", saql: `null || "foo"`, wantRebuild: `null OR "foo"`, wantValue: "foo"},
|
||||
{name: "Logical 17", saql: `null || "foo"`, wantRebuild: `null OR d._key IN ["1","2","3"]`, wantValue: "foo", values: `{"d": {"_key": "1"}}`}, // eval != rebuild
|
||||
{name: "Logical 18", saql: `null && true`, wantRebuild: `null AND true`, wantValue: nil},
|
||||
{name: "Logical 19", saql: `true && 23`, wantRebuild: `true AND 23`, wantValue: 23},
|
||||
|
||||
{name: "Logical 20", saql: "true == (6 < 8)", wantRebuild: "true == (6 < 8)", wantValue: true},
|
||||
{name: "Logical 21", saql: "true == 6 < 8", wantRebuild: "true == 6 < 8", wantValue: true}, // does not work in go
|
||||
|
||||
// https://www.arangodb.com/docs/3.7/aql/operators.html#arithmetic-operators
|
||||
{name: "Arithmetic 1", saql: `1 + 1`, wantRebuild: `1 + 1`, wantValue: 2},
|
||||
{name: "Arithmetic 2", saql: `33 - 99`, wantRebuild: `33 - 99`, wantValue: -66},
|
||||
{name: "Arithmetic 3", saql: `12.4 * 4.5`, wantRebuild: `12.4 * 4.5`, wantValue: 55.8},
|
||||
{name: "Arithmetic 4", saql: `13.0 / 0.1`, wantRebuild: `13.0 / 0.1`, wantValue: 130.0},
|
||||
{name: "Arithmetic 5", saql: `23 % 7`, wantRebuild: `23 % 7`, wantValue: 2},
|
||||
{name: "Arithmetic 6", saql: `-15`, wantRebuild: `-15`, wantValue: -15},
|
||||
{name: "Arithmetic 7", saql: `+9.99`, wantRebuild: `9.99`, wantValue: 9.99},
|
||||
|
||||
{name: "Arithmetic 8", saql: `1 + "a"`, wantRebuild: `1 + "a"`, wantValue: 1},
|
||||
{name: "Arithmetic 9", saql: `1 + "99"`, wantRebuild: `1 + "99"`, wantValue: 100},
|
||||
{name: "Arithmetic 10", saql: `1 + null`, wantRebuild: `1 + null`, wantValue: 1},
|
||||
{name: "Arithmetic 11", saql: `null + 1`, wantRebuild: `null + 1`, wantValue: 1},
|
||||
{name: "Arithmetic 12", saql: `3 + []`, wantRebuild: `3 + []`, wantValue: 3},
|
||||
{name: "Arithmetic 13", saql: `24 + [2]`, wantRebuild: `24 + [2]`, wantValue: 26},
|
||||
{name: "Arithmetic 14", saql: `24 + [2, 4]`, wantRebuild: `24 + [2, 4]`, wantValue: 24},
|
||||
{name: "Arithmetic 15", saql: `25 - null`, wantRebuild: `25 - null`, wantValue: 25},
|
||||
{name: "Arithmetic 16", saql: `17 - true`, wantRebuild: `17 - true`, wantValue: 16},
|
||||
{name: "Arithmetic 17", saql: `23 * {}`, wantRebuild: `23 * {}`, wantValue: 0},
|
||||
{name: "Arithmetic 18", saql: `5 * [7]`, wantRebuild: `5 * [7]`, wantValue: 35},
|
||||
{name: "Arithmetic 19", saql: `24 / "12"`, wantRebuild: `24 / "12"`, wantValue: 2},
|
||||
{name: "Arithmetic Error 1: Divison by zero", saql: `1 / 0`, wantRebuild: `1 / 0`, wantValue: 0},
|
||||
|
||||
// https://www.arangodb.com/docs/3.7/aql/operators.html#ternary-operator
|
||||
{name: "Ternary 1", saql: `u.age > 15 || u.active == true ? u.userId : null`, wantRebuild: `u.age > 15 OR u.active == true ? u.userId : null`, wantValue: 45, values: `{"u": {"active": true, "age": 2, "userId": 45}}`},
|
||||
{name: "Ternary 2", saql: `u.value ? : 'value is null, 0 or not present'`, wantRebuild: `u.value ? : "value is null, 0 or not present"`, wantValue: "value is null, 0 or not present", values: `{"u": {"value": 0}}`},
|
||||
|
||||
// https://www.arangodb.com/docs/3.7/aql/operators.html#range-operator
|
||||
{name: "Range 1", saql: `2010..2013`, wantRebuild: `2010..2013`, wantValue: []float64{2010, 2011, 2012, 2013}},
|
||||
// {"Array operators 1", `u.friends[*].name`, `u.friends[*].name`, false},
|
||||
|
||||
// Security
|
||||
{name: "Security 1", saql: `doc.value == 1 || true REMOVE doc IN collection //`, wantParseErr: true},
|
||||
{name: "Security 2", saql: `doc.value == 1 || true INSERT {foo: "bar"} IN collection //`, wantParseErr: true},
|
||||
|
||||
// https://www.arangodb.com/docs/3.7/aql/operators.html#operator-precedence
|
||||
{name: "Precendence", saql: `2 > 15 && "a" != ""`, wantRebuild: `2 > 15 AND "a" != ""`, wantValue: false},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
parser := &Parser{
|
||||
Searcher: &MockSearcher{},
|
||||
}
|
||||
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
expr, err := parser.Parse(tt.saql)
|
||||
if (err != nil) != tt.wantParseErr {
|
||||
t.Errorf("Parse() error = %v, wantErr %v", err, tt.wantParseErr)
|
||||
if expr != nil {
|
||||
t.Error(expr.String())
|
||||
}
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
got, err := expr.String()
|
||||
if (err != nil) != tt.wantRebuildErr {
|
||||
t.Error(expr.String())
|
||||
t.Errorf("String() error = %v, wantErr %v", err, tt.wantParseErr)
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if got != tt.wantRebuild {
|
||||
t.Errorf("String() got = %v, want %v", got, tt.wantRebuild)
|
||||
}
|
||||
|
||||
var myJson map[string]interface{}
|
||||
if tt.values != "" {
|
||||
err = json.Unmarshal([]byte(tt.values), &myJson)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
value, err := expr.Eval(myJson)
|
||||
if (err != nil) != tt.wantEvalErr {
|
||||
t.Error(expr.String())
|
||||
t.Errorf("Parse() error = %v, wantErr %v", err, tt.wantParseErr)
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
wantValue := tt.wantValue
|
||||
if i, ok := wantValue.(int); ok {
|
||||
wantValue = float64(i)
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(value, wantValue) {
|
||||
t.Error(expr.String())
|
||||
t.Errorf("Eval() got = %T %#v, want %T %#v", value, value, wantValue, wantValue)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
154
caql/set.go
Normal file
154
caql/set.go
Normal file
@@ -0,0 +1,154 @@
|
||||
// Adapted from https://github.com/badgerodon/collections under the MIT License
|
||||
// Original License:
|
||||
//
|
||||
// Copyright (c) 2012 Caleb Doxsey
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
// this software and associated documentation files (the "Software"), to deal in
|
||||
// the Software without restriction, including without limitation the rights to
|
||||
// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||
// the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
// subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in all
|
||||
// copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
package caql
|
||||
|
||||
import "sort"
|
||||
|
||||
type (
|
||||
Set struct {
|
||||
hash map[interface{}]nothing
|
||||
}
|
||||
|
||||
nothing struct{}
|
||||
)
|
||||
|
||||
// Create a new set
|
||||
func New(initial ...interface{}) *Set {
|
||||
s := &Set{make(map[interface{}]nothing)}
|
||||
|
||||
for _, v := range initial {
|
||||
s.Insert(v)
|
||||
}
|
||||
|
||||
return s
|
||||
}
|
||||
|
||||
// Find the difference between two sets
|
||||
func (s *Set) Difference(set *Set) *Set {
|
||||
n := make(map[interface{}]nothing)
|
||||
|
||||
for k := range s.hash {
|
||||
if _, exists := set.hash[k]; !exists {
|
||||
n[k] = nothing{}
|
||||
}
|
||||
}
|
||||
|
||||
return &Set{n}
|
||||
}
|
||||
|
||||
// Call f for each item in the set
|
||||
func (s *Set) Do(f func(interface{})) {
|
||||
for k := range s.hash {
|
||||
f(k)
|
||||
}
|
||||
}
|
||||
|
||||
// Test to see whether or not the element is in the set
|
||||
func (s *Set) Has(element interface{}) bool {
|
||||
_, exists := s.hash[element]
|
||||
return exists
|
||||
}
|
||||
|
||||
// Add an element to the set
|
||||
func (s *Set) Insert(element interface{}) {
|
||||
s.hash[element] = nothing{}
|
||||
}
|
||||
|
||||
// Find the intersection of two sets
|
||||
func (s *Set) Intersection(set *Set) *Set {
|
||||
n := make(map[interface{}]nothing)
|
||||
|
||||
for k := range s.hash {
|
||||
if _, exists := set.hash[k]; exists {
|
||||
n[k] = nothing{}
|
||||
}
|
||||
}
|
||||
|
||||
return &Set{n}
|
||||
}
|
||||
|
||||
// Return the number of items in the set
|
||||
func (s *Set) Len() int {
|
||||
return len(s.hash)
|
||||
}
|
||||
|
||||
// Test whether or not this set is a proper subset of "set"
|
||||
func (s *Set) ProperSubsetOf(set *Set) bool {
|
||||
return s.SubsetOf(set) && s.Len() < set.Len()
|
||||
}
|
||||
|
||||
// Remove an element from the set
|
||||
func (s *Set) Remove(element interface{}) {
|
||||
delete(s.hash, element)
|
||||
}
|
||||
|
||||
func (s *Set) Minus(set *Set) *Set {
|
||||
n := make(map[interface{}]nothing)
|
||||
for k := range s.hash {
|
||||
n[k] = nothing{}
|
||||
}
|
||||
|
||||
for _, v := range set.Values() {
|
||||
delete(n, v)
|
||||
}
|
||||
|
||||
return &Set{n}
|
||||
}
|
||||
|
||||
// Test whether or not this set is a subset of "set"
|
||||
func (s *Set) SubsetOf(set *Set) bool {
|
||||
if s.Len() > set.Len() {
|
||||
return false
|
||||
}
|
||||
for k := range s.hash {
|
||||
if _, exists := set.hash[k]; !exists {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// Find the union of two sets
|
||||
func (s *Set) Union(set *Set) *Set {
|
||||
n := make(map[interface{}]nothing)
|
||||
|
||||
for k := range s.hash {
|
||||
n[k] = nothing{}
|
||||
}
|
||||
for k := range set.hash {
|
||||
n[k] = nothing{}
|
||||
}
|
||||
|
||||
return &Set{n}
|
||||
}
|
||||
|
||||
func (s *Set) Values() []interface{} {
|
||||
values := []interface{}{}
|
||||
|
||||
for k := range s.hash {
|
||||
values = append(values, k)
|
||||
}
|
||||
sort.Slice(values, func(i, j int) bool { return lt(values[i], values[j]) })
|
||||
|
||||
return values
|
||||
}
|
||||
96
caql/set_test.go
Normal file
96
caql/set_test.go
Normal file
@@ -0,0 +1,96 @@
|
||||
// Adapted from https://github.com/badgerodon/collections under the MIT License
|
||||
// Original License:
|
||||
//
|
||||
// Copyright (c) 2012 Caleb Doxsey
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
// this software and associated documentation files (the "Software"), to deal in
|
||||
// the Software without restriction, including without limitation the rights to
|
||||
// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||
// the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
// subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in all
|
||||
// copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
package caql
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func Test(t *testing.T) {
|
||||
s := New()
|
||||
|
||||
s.Insert(5)
|
||||
|
||||
if s.Len() != 1 {
|
||||
t.Errorf("Length should be 1")
|
||||
}
|
||||
|
||||
if !s.Has(5) {
|
||||
t.Errorf("Membership test failed")
|
||||
}
|
||||
|
||||
s.Remove(5)
|
||||
|
||||
if s.Len() != 0 {
|
||||
t.Errorf("Length should be 0")
|
||||
}
|
||||
|
||||
if s.Has(5) {
|
||||
t.Errorf("The set should be empty")
|
||||
}
|
||||
|
||||
// Difference
|
||||
s1 := New(1, 2, 3, 4, 5, 6)
|
||||
s2 := New(4, 5, 6)
|
||||
s3 := s1.Difference(s2)
|
||||
|
||||
if s3.Len() != 3 {
|
||||
t.Errorf("Length should be 3")
|
||||
}
|
||||
|
||||
if !(s3.Has(1) && s3.Has(2) && s3.Has(3)) {
|
||||
t.Errorf("Set should only contain 1, 2, 3")
|
||||
}
|
||||
|
||||
// Intersection
|
||||
s3 = s1.Intersection(s2)
|
||||
if s3.Len() != 3 {
|
||||
t.Errorf("Length should be 3 after intersection")
|
||||
}
|
||||
|
||||
if !(s3.Has(4) && s3.Has(5) && s3.Has(6)) {
|
||||
t.Errorf("Set should contain 4, 5, 6")
|
||||
}
|
||||
|
||||
// Union
|
||||
s4 := New(7, 8, 9)
|
||||
s3 = s2.Union(s4)
|
||||
|
||||
if s3.Len() != 6 {
|
||||
t.Errorf("Length should be 6 after union")
|
||||
}
|
||||
|
||||
if !(s3.Has(7)) {
|
||||
t.Errorf("Set should contain 4, 5, 6, 7, 8, 9")
|
||||
}
|
||||
|
||||
// Subset
|
||||
if !s1.SubsetOf(s1) {
|
||||
t.Errorf("set should be a subset of itself")
|
||||
}
|
||||
// Proper Subset
|
||||
if s1.ProperSubsetOf(s1) {
|
||||
t.Errorf("set should not be a subset of itself")
|
||||
}
|
||||
|
||||
}
|
||||
79
caql/unquote.go
Normal file
79
caql/unquote.go
Normal file
@@ -0,0 +1,79 @@
|
||||
// Adapted from https://github.com/golang/go
|
||||
// Original License:
|
||||
//
|
||||
// Copyright 2009 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the https://go.dev/LICENSE file.
|
||||
|
||||
package caql
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"strings"
|
||||
"unicode/utf8"
|
||||
)
|
||||
|
||||
// unquote interprets s as a single-quoted, double-quoted,
|
||||
// or backquoted string literal, returning the string value
|
||||
// that s quotes.
|
||||
func unquote(s string) (string, error) {
|
||||
n := len(s)
|
||||
if n < 2 {
|
||||
return "", strconv.ErrSyntax
|
||||
}
|
||||
quote := s[0]
|
||||
if quote != s[n-1] {
|
||||
return "", strconv.ErrSyntax
|
||||
}
|
||||
s = s[1 : n-1]
|
||||
|
||||
if quote == '`' {
|
||||
if strings.ContainsRune(s, '`') {
|
||||
return "", strconv.ErrSyntax
|
||||
}
|
||||
if strings.ContainsRune(s, '\r') {
|
||||
// -1 because we know there is at least one \r to remove.
|
||||
buf := make([]byte, 0, len(s)-1)
|
||||
for i := 0; i < len(s); i++ {
|
||||
if s[i] != '\r' {
|
||||
buf = append(buf, s[i])
|
||||
}
|
||||
}
|
||||
return string(buf), nil
|
||||
}
|
||||
return s, nil
|
||||
}
|
||||
if quote != '"' && quote != '\'' {
|
||||
return "", strconv.ErrSyntax
|
||||
}
|
||||
if strings.ContainsRune(s, '\n') {
|
||||
return "", strconv.ErrSyntax
|
||||
}
|
||||
|
||||
// Is it trivial? Avoid allocation.
|
||||
if !strings.ContainsRune(s, '\\') && !strings.ContainsRune(s, rune(quote)) {
|
||||
switch quote {
|
||||
case '"', '\'':
|
||||
if utf8.ValidString(s) {
|
||||
return s, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var runeTmp [utf8.UTFMax]byte
|
||||
buf := make([]byte, 0, 3*len(s)/2) // Try to avoid more allocations.
|
||||
for len(s) > 0 {
|
||||
c, multibyte, ss, err := strconv.UnquoteChar(s, quote)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
s = ss
|
||||
if c < utf8.RuneSelf || !multibyte {
|
||||
buf = append(buf, byte(c))
|
||||
} else {
|
||||
n := utf8.EncodeRune(runeTmp[:], c)
|
||||
buf = append(buf, runeTmp[:n]...)
|
||||
}
|
||||
}
|
||||
return string(buf), nil
|
||||
}
|
||||
125
caql/unquote_test.go
Normal file
125
caql/unquote_test.go
Normal file
@@ -0,0 +1,125 @@
|
||||
// Adapted from https://github.com/golang/go
|
||||
// Original License:
|
||||
//
|
||||
// Copyright 2009 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the https://go.dev/LICENSE file.
|
||||
|
||||
package caql
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"testing"
|
||||
)
|
||||
|
||||
type quoteTest struct {
|
||||
in string
|
||||
out string
|
||||
ascii string
|
||||
graphic string
|
||||
}
|
||||
|
||||
var quotetests = []quoteTest{
|
||||
{in: "\a\b\f\r\n\t\v", out: `"\a\b\f\r\n\t\v"`, ascii: `"\a\b\f\r\n\t\v"`, graphic: `"\a\b\f\r\n\t\v"`},
|
||||
{"\\", `"\\"`, `"\\"`, `"\\"`},
|
||||
{"abc\xffdef", `"abc\xffdef"`, `"abc\xffdef"`, `"abc\xffdef"`},
|
||||
{"\u263a", `"☺"`, `"\u263a"`, `"☺"`},
|
||||
{"\U0010ffff", `"\U0010ffff"`, `"\U0010ffff"`, `"\U0010ffff"`},
|
||||
{"\x04", `"\x04"`, `"\x04"`, `"\x04"`},
|
||||
// Some non-printable but graphic runes. Final column is double-quoted.
|
||||
{"!\u00a0!\u2000!\u3000!", `"!\u00a0!\u2000!\u3000!"`, `"!\u00a0!\u2000!\u3000!"`, "\"!\u00a0!\u2000!\u3000!\""},
|
||||
}
|
||||
|
||||
type unQuoteTest struct {
|
||||
in string
|
||||
out string
|
||||
}
|
||||
|
||||
var unquotetests = []unQuoteTest{
|
||||
{`""`, ""},
|
||||
{`"a"`, "a"},
|
||||
{`"abc"`, "abc"},
|
||||
{`"☺"`, "☺"},
|
||||
{`"hello world"`, "hello world"},
|
||||
{`"\xFF"`, "\xFF"},
|
||||
{`"\377"`, "\377"},
|
||||
{`"\u1234"`, "\u1234"},
|
||||
{`"\U00010111"`, "\U00010111"},
|
||||
{`"\U0001011111"`, "\U0001011111"},
|
||||
{`"\a\b\f\n\r\t\v\\\""`, "\a\b\f\n\r\t\v\\\""},
|
||||
{`"'"`, "'"},
|
||||
|
||||
{`'a'`, "a"},
|
||||
{`'☹'`, "☹"},
|
||||
{`'\a'`, "\a"},
|
||||
{`'\x10'`, "\x10"},
|
||||
{`'\377'`, "\377"},
|
||||
{`'\u1234'`, "\u1234"},
|
||||
{`'\U00010111'`, "\U00010111"},
|
||||
{`'\t'`, "\t"},
|
||||
{`' '`, " "},
|
||||
{`'\''`, "'"},
|
||||
{`'"'`, "\""},
|
||||
|
||||
{"``", ``},
|
||||
{"`a`", `a`},
|
||||
{"`abc`", `abc`},
|
||||
{"`☺`", `☺`},
|
||||
{"`hello world`", `hello world`},
|
||||
{"`\\xFF`", `\xFF`},
|
||||
{"`\\377`", `\377`},
|
||||
{"`\\`", `\`},
|
||||
{"`\n`", "\n"},
|
||||
{"` `", ` `},
|
||||
{"` `", ` `},
|
||||
{"`a\rb`", "ab"},
|
||||
}
|
||||
|
||||
var misquoted = []string{
|
||||
``,
|
||||
`"`,
|
||||
`"a`,
|
||||
`"'`,
|
||||
`b"`,
|
||||
`"\"`,
|
||||
`"\9"`,
|
||||
`"\19"`,
|
||||
`"\129"`,
|
||||
`'\'`,
|
||||
`'\9'`,
|
||||
`'\19'`,
|
||||
`'\129'`,
|
||||
// `'ab'`,
|
||||
`"\x1!"`,
|
||||
`"\U12345678"`,
|
||||
`"\z"`,
|
||||
"`",
|
||||
"`xxx",
|
||||
"`\"",
|
||||
`"\'"`,
|
||||
`'\"'`,
|
||||
"\"\n\"",
|
||||
"\"\\n\n\"",
|
||||
"'\n'",
|
||||
}
|
||||
|
||||
func TestUnquote(t *testing.T) {
|
||||
for _, tt := range unquotetests {
|
||||
if out, err := unquote(tt.in); err != nil || out != tt.out {
|
||||
t.Errorf("unquote(%#q) = %q, %v want %q, nil", tt.in, out, err, tt.out)
|
||||
}
|
||||
}
|
||||
|
||||
// run the quote tests too, backward
|
||||
for _, tt := range quotetests {
|
||||
if in, err := unquote(tt.out); in != tt.in {
|
||||
t.Errorf("unquote(%#q) = %q, %v, want %q, nil", tt.out, in, err, tt.in)
|
||||
}
|
||||
}
|
||||
|
||||
for _, s := range misquoted {
|
||||
if out, err := unquote(s); out != "" || err != strconv.ErrSyntax {
|
||||
t.Errorf("unquote(%#q) = %q, %v want %q, %v", s, out, err, "", strconv.ErrSyntax)
|
||||
}
|
||||
}
|
||||
}
|
||||
155
caql/wildcard.go
Normal file
155
caql/wildcard.go
Normal file
@@ -0,0 +1,155 @@
|
||||
// Adapted from https://github.com/golang/go
|
||||
// Original License:
|
||||
//
|
||||
// Copyright 2010 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the https://go.dev/LICENSE file.
|
||||
|
||||
package caql
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"strings"
|
||||
"unicode/utf8"
|
||||
)
|
||||
|
||||
// ErrBadPattern indicates a pattern was malformed.
|
||||
var ErrBadPattern = errors.New("syntax error in pattern")
|
||||
|
||||
// match reports whether name matches the shell pattern.
|
||||
// The pattern syntax is:
|
||||
//
|
||||
// pattern:
|
||||
// { term }
|
||||
// term:
|
||||
// '%' matches any sequence of non-/ characters
|
||||
// '_' matches any single non-/ character
|
||||
// c matches character c (c != '%', '_', '\\')
|
||||
// '\\' c matches character c
|
||||
//
|
||||
// match requires pattern to match all of name, not just a substring.
|
||||
// The only possible returned error is ErrBadPattern, when pattern
|
||||
// is malformed.
|
||||
//
|
||||
func match(pattern, name string) (matched bool, err error) {
|
||||
Pattern:
|
||||
for len(pattern) > 0 {
|
||||
var star bool
|
||||
var chunk string
|
||||
star, chunk, pattern = scanChunk(pattern)
|
||||
if star && chunk == "" {
|
||||
// Trailing * matches rest of string unless it has a /.
|
||||
return !strings.ContainsRune(name, '/'), nil
|
||||
}
|
||||
// Look for match at current position.
|
||||
t, ok, err := matchChunk(chunk, name)
|
||||
// if we're the last chunk, make sure we've exhausted the name
|
||||
// otherwise we'll give a false result even if we could still match
|
||||
// using the star
|
||||
if ok && (len(t) == 0 || len(pattern) > 0) {
|
||||
name = t
|
||||
continue
|
||||
}
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
if star {
|
||||
// Look for match skipping i+1 bytes.
|
||||
// Cannot skip /.
|
||||
for i := 0; i < len(name) && name[i] != '/'; i++ {
|
||||
t, ok, err := matchChunk(chunk, name[i+1:])
|
||||
if ok {
|
||||
// if we're the last chunk, make sure we exhausted the name
|
||||
if len(pattern) == 0 && len(t) > 0 {
|
||||
continue
|
||||
}
|
||||
name = t
|
||||
continue Pattern
|
||||
}
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
}
|
||||
}
|
||||
// Before returning false with no error,
|
||||
// check that the remainder of the pattern is syntactically valid.
|
||||
for len(pattern) > 0 {
|
||||
_, chunk, pattern = scanChunk(pattern)
|
||||
if _, _, err := matchChunk(chunk, ""); err != nil {
|
||||
return false, err
|
||||
}
|
||||
}
|
||||
return false, nil
|
||||
}
|
||||
return len(name) == 0, nil
|
||||
}
|
||||
|
||||
// scanChunk gets the next segment of pattern, which is a non-star string
|
||||
// possibly preceded by a star.
|
||||
func scanChunk(pattern string) (star bool, chunk, rest string) {
|
||||
for len(pattern) > 0 && pattern[0] == '%' {
|
||||
pattern = pattern[1:]
|
||||
star = true
|
||||
}
|
||||
var i int
|
||||
Scan:
|
||||
for i = 0; i < len(pattern); i++ {
|
||||
switch pattern[i] {
|
||||
case '\\':
|
||||
// error check handled in matchChunk: bad pattern.
|
||||
if i+1 < len(pattern) {
|
||||
i++
|
||||
}
|
||||
case '%':
|
||||
break Scan
|
||||
}
|
||||
}
|
||||
return star, pattern[0:i], pattern[i:]
|
||||
}
|
||||
|
||||
// matchChunk checks whether chunk matches the beginning of s.
|
||||
// If so, it returns the remainder of s (after the match).
|
||||
// Chunk is all single-character operators: literals, char classes, and ?.
|
||||
func matchChunk(chunk, s string) (rest string, ok bool, err error) {
|
||||
// failed records whether the match has failed.
|
||||
// After the match fails, the loop continues on processing chunk,
|
||||
// checking that the pattern is well-formed but no longer reading s.
|
||||
failed := false
|
||||
for len(chunk) > 0 {
|
||||
if !failed && len(s) == 0 {
|
||||
failed = true
|
||||
}
|
||||
switch chunk[0] {
|
||||
|
||||
case '_':
|
||||
if !failed {
|
||||
if s[0] == '/' {
|
||||
failed = true
|
||||
}
|
||||
_, n := utf8.DecodeRuneInString(s)
|
||||
s = s[n:]
|
||||
}
|
||||
chunk = chunk[1:]
|
||||
|
||||
case '\\':
|
||||
chunk = chunk[1:]
|
||||
if len(chunk) == 0 {
|
||||
return "", false, ErrBadPattern
|
||||
}
|
||||
fallthrough
|
||||
|
||||
default:
|
||||
if !failed {
|
||||
if chunk[0] != s[0] {
|
||||
failed = true
|
||||
}
|
||||
s = s[1:]
|
||||
}
|
||||
chunk = chunk[1:]
|
||||
}
|
||||
}
|
||||
if failed {
|
||||
return "", false, nil
|
||||
}
|
||||
return s, true, nil
|
||||
}
|
||||
50
caql/wildcard_test.go
Normal file
50
caql/wildcard_test.go
Normal file
@@ -0,0 +1,50 @@
|
||||
// Adapted from https://github.com/golang/go
|
||||
// Original License:
|
||||
//
|
||||
// Copyright 2009 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the https://go.dev/LICENSE file.
|
||||
|
||||
package caql
|
||||
|
||||
import "testing"
|
||||
|
||||
type MatchTest struct {
|
||||
pattern, s string
|
||||
match bool
|
||||
err error
|
||||
}
|
||||
|
||||
var matchTests = []MatchTest{
|
||||
{"abc", "abc", true, nil},
|
||||
{"%", "abc", true, nil},
|
||||
{"%c", "abc", true, nil},
|
||||
{"a%", "a", true, nil},
|
||||
{"a%", "abc", true, nil},
|
||||
{"a%", "ab/c", false, nil},
|
||||
{"a%/b", "abc/b", true, nil},
|
||||
{"a%/b", "a/c/b", false, nil},
|
||||
{"a%b%c%d%e%/f", "axbxcxdxe/f", true, nil},
|
||||
{"a%b%c%d%e%/f", "axbxcxdxexxx/f", true, nil},
|
||||
{"a%b%c%d%e%/f", "axbxcxdxe/xxx/f", false, nil},
|
||||
{"a%b%c%d%e%/f", "axbxcxdxexxx/fff", false, nil},
|
||||
{"a%b_c%x", "abxbbxdbxebxczzx", true, nil},
|
||||
{"a%b_c%x", "abxbbxdbxebxczzy", false, nil},
|
||||
{"a\\%b", "a%b", true, nil},
|
||||
{"a\\%b", "ab", false, nil},
|
||||
{"a_b", "a☺b", true, nil},
|
||||
{"a___b", "a☺b", false, nil},
|
||||
{"a_b", "a/b", false, nil},
|
||||
{"a%b", "a/b", false, nil},
|
||||
{"\\", "a", false, ErrBadPattern},
|
||||
{"%x", "xxx", true, nil},
|
||||
}
|
||||
|
||||
func TestMatch(t *testing.T) {
|
||||
for _, tt := range matchTests {
|
||||
ok, err := match(tt.pattern, tt.s)
|
||||
if ok != tt.match || err != tt.err {
|
||||
t.Errorf("match(%#q, %#q) = %v, %v want %v, %v", tt.pattern, tt.s, ok, err, tt.match, tt.err)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user