diff --git a/bintree.go b/bintree.go index a36b2b0..eee2bcd 100644 --- a/bintree.go +++ b/bintree.go @@ -347,9 +347,9 @@ func (state *binTreeState) checkNode(index int) { return } else if defNode.NodeType == nodeTypeNot { if state.data[defNode.Left] == binTreeStateTrue { - state.MarkNode(index, !true) + state.MarkNode(index, false) } else if state.data[defNode.Left] == binTreeStateFalse { - state.MarkNode(index, !false) + state.MarkNode(index, true) } return } else if defNode.NodeType == nodeTypeLoop { @@ -374,6 +374,7 @@ func (state *binTreeState) MarkNode(index int, value bool) { } else { state.data[index] = binTreeStateFalse } + state.resolveRecursive(index) // We are done if we are the root node diff --git a/fastMatcher.go b/fastMatcher.go index 2997b54..1323380 100644 --- a/fastMatcher.go +++ b/fastMatcher.go @@ -265,27 +265,39 @@ func (m *FastMatcher) matchOp(op *OpNode, litVal *FastVal) error { } var opRes bool + var validOp bool + var compareOut int switch op.Op { case OpTypeEquals: - opRes = lhsVal.Equals(rhsVal) + opRes, validOp = lhsVal.Equals(rhsVal) case OpTypeLessThan: - opRes = lhsVal.Compare(rhsVal) < 0 + compareOut, validOp = lhsVal.Compare(rhsVal) + opRes = compareOut < 0 case OpTypeLessEquals: - opRes = lhsVal.Compare(rhsVal) <= 0 + compareOut, validOp = lhsVal.Compare(rhsVal) + opRes = compareOut <= 0 case OpTypeGreaterThan: - opRes = lhsVal.Compare(rhsVal) > 0 + compareOut, validOp = lhsVal.Compare(rhsVal) + opRes = compareOut > 0 case OpTypeGreaterEquals: - opRes = lhsVal.Compare(rhsVal) >= 0 + compareOut, validOp = lhsVal.Compare(rhsVal) + opRes = compareOut >= 0 case OpTypeMatches: - opRes = lhsVal.Matches(rhsVal) + opRes, validOp = lhsVal.Matches(rhsVal) case OpTypeExists: opRes = true + validOp = true default: panic("invalid op type") } // Mark the result of this operation - m.buckets.MarkNode(bucketIdx, opRes) + if !validOp { + // TODO FIX + m.buckets.MarkNode(bucketIdx, false) + } else { + m.buckets.MarkNode(bucketIdx, opRes) + } // Check if running this values ops has resolved the entirety // of the expression, if so we can leave immediately. @@ -498,7 +510,11 @@ func (m *FastMatcher) matchLoop(token tokenType, tokenData []byte, loop *LoopNod m.buckets.SetStallIndex(previousStallIndex) // Apply the overall loop result to the binary tree - m.buckets.MarkNode(loopBucketIdx, loopState) + if loopState { + m.buckets.MarkNode(loopBucketIdx, true) + } else { + m.buckets.MarkNode(loopBucketIdx, false) + } return nil } diff --git a/fastMatcher_test.go b/fastMatcher_test.go index 6686ebd..ab08417 100644 --- a/fastMatcher_test.go +++ b/fastMatcher_test.go @@ -158,9 +158,9 @@ func TestMatcherNotTrueEquals(t *testing.T) { } func TestMatcherMissingNotEquals(t *testing.T) { - // This tests a specific case where a missing value should actually - // result in a truthy result in the expression. Due to the nature - // of our bintree implementation, this needs special handling. + // This tests a specific case where both missing value and false booleans + // will return truthy results, since the "true" value here translates to + // a true boolean runJSONExprMatchTest(t, ` ["not", ["equals", @@ -169,7 +169,6 @@ func TestMatcherMissingNotEquals(t *testing.T) { ] ] `, []string{ - "5b47eb0950e9076fc0aecd52", "5b47eb093771f06ced629663", "5b47eb09ffac5a6ce37042e7", "5b47eb095c3ad73b9925f7f8", @@ -177,6 +176,8 @@ func TestMatcherMissingNotEquals(t *testing.T) { "5b47eb09996a4154c35b2f98", "5b47eb091f57571d3c3b1aa1", "5b47eb098eee4b4c4330ec64", + "5b47eb0936ff92a567a0307e", + "5b47eb096b1d911c0b9492fb", }) } @@ -214,22 +215,6 @@ func TestMatcherNotExists(t *testing.T) { }) } -func TestMatcherDisparateTypeEquals(t *testing.T) { - // TODO(brett19): Should probably discuss whether type-cast equals - // actually makes sense... This validates that these something like: - // (true == "thisShouldBeABoolean") === true - // which may not actually make a whole lot of sense... - runJSONExprMatchTest(t, ` - ["equals", - ["field", "sometimesValue"], - ["value", "thisShouldBeABoolean"] - ] - `, []string{ - "5b47eb0936ff92a567a0307e", - "5b47eb096b1d911c0b9492fb", - }) -} - func TestMatcherSometimesMissingBoolEquals(t *testing.T) { runJSONExprMatchTest(t, ` ["equals", diff --git a/fastval.go b/fastval.go index 741cdf3..5e7953c 100644 --- a/fastval.go +++ b/fastval.go @@ -39,6 +39,13 @@ const ( TimeValue ) +// When users try to match a string to a bool, the bool is converted to a JSON string +// These are constants +var TrueValueBytes = []byte("true") +var FalseValueBytes = []byte("false") + +var toJsonStringBuffer []byte + type FastVal struct { dataType ValueType data interface{} @@ -166,107 +173,147 @@ func (val FastVal) GetTime() *time.Time { return val.data.(*time.Time) } -func (val FastVal) AsInt() int64 { +func (val FastVal) AsInt() (int64, bool) { switch val.dataType { case IntValue: - return val.GetInt() + return val.GetInt(), true case UintValue: - return int64(val.GetUint()) + return int64(val.GetUint()), true case FloatValue: - return int64(val.GetFloat()) + return int64(val.GetFloat()), true + case JsonStringValue: + fallthrough case JsonIntValue: - parsedVal, _ := strconv.ParseInt(string(val.sliceData), 10, 64) - return parsedVal + parsedVal, err := strconv.ParseInt(string(val.sliceData), 10, 64) + return parsedVal, err == nil case JsonUintValue: - parsedVal, _ := strconv.ParseUint(string(val.sliceData), 10, 64) - return int64(parsedVal) + parsedVal, err := strconv.ParseUint(string(val.sliceData), 10, 64) + return int64(parsedVal), err == nil case JsonFloatValue: - parsedVal, _ := strconv.ParseFloat(string(val.sliceData), 64) - return int64(parsedVal) + parsedVal, err := strconv.ParseFloat(string(val.sliceData), 64) + return int64(parsedVal), err == nil case TrueValue: - return 1 + return 1, true case FalseValue: - return 0 + return 0, true case NullValue: - return math.MinInt64 + return 0, false } - return 0 + return 0, false } -func (val FastVal) AsUint() uint64 { +func (val FastVal) AsUint() (uint64, bool) { switch val.dataType { case IntValue: - return uint64(val.GetInt()) + return uint64(val.GetInt()), true case UintValue: - return val.GetUint() + return val.GetUint(), true case FloatValue: - return uint64(val.GetFloat()) + return uint64(val.GetFloat()), true case JsonIntValue: - parsedVal, _ := strconv.ParseInt(string(val.sliceData), 10, 64) - return uint64(parsedVal) + parsedVal, err := strconv.ParseInt(string(val.sliceData), 10, 64) + return uint64(parsedVal), err == nil + case JsonStringValue: + fallthrough case JsonUintValue: - parsedVal, _ := strconv.ParseUint(string(val.sliceData), 10, 64) - return parsedVal + parsedVal, err := strconv.ParseUint(string(val.sliceData), 10, 64) + return parsedVal, err == nil case JsonFloatValue: - parsedVal, _ := strconv.ParseFloat(string(val.sliceData), 64) - return uint64(parsedVal) + parsedVal, err := strconv.ParseFloat(string(val.sliceData), 64) + return uint64(parsedVal), err == nil case TrueValue: - return 1 + return 1, true case FalseValue: - return 0 + return 0, true case NullValue: - return math.MaxUint64 + return uint64(0), false } - return 0 + return 0, false } -func (val FastVal) AsFloat() float64 { +func (val FastVal) AsFloat() (float64, bool) { switch val.dataType { case IntValue: - return float64(val.GetInt()) + return float64(val.GetInt()), true case UintValue: - return float64(val.GetUint()) + return float64(val.GetUint()), true case FloatValue: - return val.GetFloat() + return val.GetFloat(), true case JsonIntValue: - parsedVal, _ := strconv.ParseInt(string(val.sliceData), 10, 64) - return float64(parsedVal) + parsedVal, err := strconv.ParseInt(string(val.sliceData), 10, 64) + return float64(parsedVal), err == nil case JsonUintValue: - parsedVal, _ := strconv.ParseUint(string(val.sliceData), 10, 64) - return float64(parsedVal) + parsedVal, err := strconv.ParseUint(string(val.sliceData), 10, 64) + return float64(parsedVal), err == nil + case JsonStringValue: + fallthrough case JsonFloatValue: - parsedVal, _ := strconv.ParseFloat(string(val.sliceData), 64) - return parsedVal + parsedVal, err := strconv.ParseFloat(string(val.sliceData), 64) + return parsedVal, err == nil case TrueValue: - return 1.0 + return 1.0, true case FalseValue: - return 0.0 + return 0.0, true case NullValue: - return math.MaxFloat64 + return 0.0, false } - return 0.0 + return 0.0, false } -func (val FastVal) AsBoolean() bool { - return val.AsInt() != 0 && val.AsInt() != math.MinInt64 +var JsonStringTrueRegexp *regexp.Regexp = regexp.MustCompile("^[T|t][R|r][U|u][E|e]$") +var JsonStringFalseRegexp *regexp.Regexp = regexp.MustCompile("^[F|f][A|a][L|l][S|s][E|e]$") + +func (val FastVal) AsBoolean() (bool, bool) { + switch val.dataType { + case JsonStringValue: + if JsonStringTrueRegexp.Match(val.sliceData) { + return true, true + } else if JsonStringFalseRegexp.Match(val.sliceData) { + return false, true + } else { + return false, false + } + case IntValue: + return val.GetInt() != 0, true + case UintValue: + return val.GetUint() != 0, true + case FloatValue: + return val.GetFloat() != 0.0, true + case JsonIntValue: + parsedVal, err := strconv.ParseInt(string(val.sliceData), 10, 64) + return parsedVal != 0, err == nil + case JsonUintValue: + parsedVal, err := strconv.ParseUint(string(val.sliceData), 10, 64) + return parsedVal != 0, err == nil + case JsonFloatValue: + parsedVal, err := strconv.ParseFloat(string(val.sliceData), 64) + return parsedVal != 0.0, err == nil + case TrueValue: + return true, true + case FalseValue: + return false, true + default: + // Undefined + return true, false + } } -func (val FastVal) AsRegex() FastValRegexIface { +func (val FastVal) AsRegex() (FastValRegexIface, bool) { switch val.dataType { case RegexValue: - return val.data.(*regexp.Regexp) + return val.data.(*regexp.Regexp), true case PcreValue: - return val.data.(PcreWrapperInterface) + return val.data.(PcreWrapperInterface), true } - return nil + return nil, false } -func (val FastVal) AsTime() *time.Time { +func (val FastVal) AsTime() (*time.Time, bool) { switch val.dataType { case TimeValue: - return val.data.(*time.Time) + return val.data.(*time.Time), true } - return nil + return nil, false } func (val FastVal) ToBinString() (FastVal, error) { @@ -283,7 +330,34 @@ func (val FastVal) ToBinString() (FastVal, error) { return val, errors.New("invalid type coercion") } +// The following reuse an internal buffer so this should be hidden from outside callers +// Internally, this must be called only once per comparison, and should be used for implicit comversion +// (i.e. no double implicit conversion to string from the following 3 types) +func (val FastVal) toJsonStringInternal() (FastVal, error) { + val, err := val.ToJsonString() + + if err != nil { + switch val.dataType { + case UintValue: + toJsonStringBuffer = toJsonStringBuffer[:0] + toJsonStringBuffer = strconv.AppendUint(toJsonStringBuffer, val.GetUint(), 10) + return NewJsonStringFastVal(toJsonStringBuffer), nil + case IntValue: + toJsonStringBuffer = toJsonStringBuffer[:0] + toJsonStringBuffer = strconv.AppendInt(toJsonStringBuffer, val.GetInt(), 10) + return NewJsonStringFastVal(toJsonStringBuffer), nil + case FloatValue: + toJsonStringBuffer = toJsonStringBuffer[:0] + toJsonStringBuffer = strconv.AppendFloat(toJsonStringBuffer, val.GetFloat(), 'E', -1, 64) + return NewJsonStringFastVal(toJsonStringBuffer), nil + } + } + + return val, err +} + func (val FastVal) ToJsonString() (FastVal, error) { + invalidErr := errors.New("invalid type coercion") switch val.dataType { case StringValue: // TODO: Improve AsJsonString allocations @@ -295,9 +369,14 @@ func (val FastVal) ToJsonString() (FastVal, error) { return NewJsonStringFastVal(quotedBytes[1 : len(quotedBytes)-1]), nil case JsonStringValue: return val, nil + case TrueValue: + return NewJsonStringFastVal(TrueValueBytes), nil + case FalseValue: + return NewJsonStringFastVal(FalseValueBytes), nil + case NullValue: + return NewInvalidFastVal(), invalidErr } - - return val, errors.New("invalid type coercion") + return val, invalidErr } func (val FastVal) floatToIntOverflows() bool { @@ -312,42 +391,56 @@ func (val FastVal) floatToIntOverflows() bool { } } -func (val FastVal) compareInt(other FastVal) int { +func (val FastVal) compareNull(other FastVal) (int, bool) { + if val.IsNull() && other.IsNull() { + return 0, true + } else if val.IsNull() && !other.IsNull() { + return -1, true + } else if !val.IsNull() && other.IsNull() { + return 1, true + } else { + // Shouldn't be possible + return 0, false + } +} + +func (val FastVal) compareInt(other FastVal) (int, bool) { if other.dataType == FloatValue && other.floatToIntOverflows() { return val.compareFloat(other) } - intVal := val.AsInt() - intOval := other.AsInt() + intVal, valid := val.AsInt() + intOval, valid2 := other.AsInt() if intVal < intOval { - return -1 + return -1, valid && valid2 } else if intVal > intOval { - return 1 + return 1, valid && valid2 } else { - return 0 + return 0, valid && valid2 } } -func (val FastVal) compareUint(other FastVal) int { - uintVal := val.AsUint() - uintOval := other.AsUint() +func (val FastVal) compareUint(other FastVal) (int, bool) { + uintVal, valid := val.AsUint() + uintOval, valid2 := other.AsUint() + if uintVal < uintOval { - return -1 + return -1, valid && valid2 } else if uintVal > uintOval { - return 1 + return 1, valid && valid2 } else { - return 0 + return 0, valid && valid2 } } -func (val FastVal) compareFloat(other FastVal) int { +func (val FastVal) compareFloat(other FastVal) (int, bool) { // TODO(brett19): EPISLON probably should be defined better than this // possibly even 0 if we want to force exact matching for floats... EPSILON := 0.0000001 - floatVal := val.AsFloat() - floatOval := other.AsFloat() + floatVal, valid := val.AsFloat() + floatOval, valid2 := other.AsFloat() if math.IsNaN(floatVal) || math.IsNaN(floatOval) { // Comparing Not-A-Number @@ -355,75 +448,82 @@ func (val FastVal) compareFloat(other FastVal) int { // In the meantime - because we have to return something, just let imaginary numbers be < real numbers if math.IsNaN(floatVal) && math.IsNaN(floatOval) { // In go, two NaN's are not the same - thus this should never return 0, so return -1 by default - return -1 + return 0, false } else if math.IsNaN(floatVal) && !math.IsNaN(floatOval) { - return -1 + return -1, false } else if !math.IsNaN(floatVal) && math.IsNaN(floatOval) { - return 1 + return 1, false } } // Perform epsilon comparison first if math.Abs(floatVal-floatOval) < EPSILON { - return 0 + return 0, valid && valid2 } // Traditional comparison if floatVal < floatOval { - return -1 + return -1, valid && valid2 } else if floatVal > floatOval { - return 1 + return 1, valid && valid2 } else { - return 0 + return 0, valid && valid2 } } -func (val FastVal) compareBoolean(other FastVal) int { - // We cheat here and use int comparison mode, since integer conversions - // of the boolean datatypes are consistent - return val.compareInt(other) +func (val FastVal) compareBoolean(other FastVal) (int, bool) { + valBool, valid := val.AsBoolean() + otherBool, valid2 := other.AsBoolean() + if !valid || !valid2 { + return 0, false + } + + if valBool == otherBool { + return 0, true + } else if valBool && !otherBool { + return 1, true + } else { + return -1, true + } } -func (val FastVal) compareStrings(other FastVal) int { +func (val FastVal) compareStrings(other FastVal) (int, bool) { // TODO: Improve string comparisons to avoid casting or converting - escVal, _ := val.ToJsonString() - escOval, _ := other.ToJsonString() - return strings.Compare(string(escVal.sliceData), string(escOval.sliceData)) + escVal, err := val.toJsonStringInternal() + escOval, err1 := other.toJsonStringInternal() + + result := strings.Compare(string(escVal.sliceData), string(escOval.sliceData)) + return result, err == nil && err1 == nil } -func (val FastVal) compareTime(other FastVal) int { - thisTime := val.AsTime() - otherTime := other.AsTime() +func (val FastVal) compareTime(other FastVal) (int, bool) { + thisTime, valid := val.AsTime() + otherTime, valid2 := other.AsTime() - // Consider nil value as the smaller/earlier than non-nil values - if thisTime == nil && otherTime == nil { - return 0 - } else if otherTime == nil && thisTime != nil { - return 1 - } else if otherTime != nil && thisTime == nil { - return -1 + if thisTime == nil || otherTime == nil { + return 0, false } if thisTime.Equal(*otherTime) { - return 0 + return 0, valid && valid2 } else if thisTime.After(*otherTime) { - return 1 + return 1, valid && valid2 } else { - return -1 + return -1, valid && valid2 } } -func (val FastVal) compareArray(other FastVal) int { +func (val FastVal) compareArray(other FastVal) (int, bool) { // TODO - need a better way but for now treat them the same return val.compareObjArrData(other) } -func (val FastVal) compareObject(other FastVal) int { +func (val FastVal) compareObject(other FastVal) (int, bool) { // TODO - need a better way but for now treat them the same return val.compareObjArrData(other) } -func (val FastVal) compareObjArrData(other FastVal) int { +func (val FastVal) compareObjArrData(other FastVal) (int, bool) { var same bool // Do not use reflect switch val.dataType { @@ -431,29 +531,46 @@ func (val FastVal) compareObjArrData(other FastVal) int { fallthrough case ObjectValue: if len(val.sliceData) > len(other.sliceData) { - return 1 + return 1, true } else if len(val.sliceData) < len(other.sliceData) { - return -1 + return -1, true } else { same = true for i := range val.sliceData { if val.sliceData[i] > other.sliceData[i] { - return 1 + return 1, true } else if val.sliceData[i] < other.sliceData[i] { - return -1 + return -1, true } } } } if same { - return 0 + return 0, true } else { - return -1 + return -1, true } } +// This is really using other as the baseline for calling compare, +// and then reversing the result +// This is so that comparisons between different data types are bidirectionally consistent +func (val FastVal) reverseCompare(other FastVal) (int, bool) { + result, valid := other.Compare(val) + return result * -1, valid +} + +// Compares based on the LHS literal. If unable, then attempt casting to the RHS +func (val FastVal) Compare(other FastVal) (int, bool) { + ret, valid := val.compareInternal(other) + if !valid { + ret, valid = val.reverseCompare(other) + } + return ret, valid +} + // Returns compared val and boolean indicating if the comparison is valid -func (val FastVal) Compare(other FastVal) int { +func (val FastVal) compareInternal(other FastVal) (int, bool) { switch val.dataType { case IntValue: return val.compareInt(other) @@ -483,27 +600,44 @@ func (val FastVal) Compare(other FastVal) int { return val.compareArray(other) case ObjectValue: return val.compareObject(other) + case NullValue: + return val.compareNull(other) } if val.dataType < other.dataType { - return -1 + return -1, true } else if val.dataType > other.dataType { - return 1 + return 1, true } else { - return 0 + return 0, true } } -func (val FastVal) Equals(other FastVal) bool { - return val.Compare(other) == 0 +func (val FastVal) Equals(other FastVal) (bool, bool) { + result, valid := val.compareInternal(other) + if !valid { + // For equality, if invalid comparison, force a valid inequality + return false, true + } else { + return result == 0, true + } } -func (val FastVal) matchStrings(other FastVal) bool { - escVal, _ := val.ToJsonString() - return other.AsRegex().Match(escVal.sliceData) +func (val FastVal) matchStrings(other FastVal) (bool, bool) { + escVal, err := val.toJsonStringInternal() + if err != nil { + return false, false + } + + regex, valid := other.AsRegex() + if !valid { + return false, valid + } else { + return regex.Match(escVal.sliceData), true + } } -func (val FastVal) Matches(other FastVal) bool { +func (val FastVal) Matches(other FastVal) (bool, bool) { switch val.dataType { case StringValue: return val.matchStrings(other) @@ -512,7 +646,7 @@ func (val FastVal) Matches(other FastVal) bool { case JsonStringValue: return val.matchStrings(other) default: - return false + return false, false } } diff --git a/fastval_math.go b/fastval_math.go index 845967f..0fced99 100644 --- a/fastval_math.go +++ b/fastval_math.go @@ -23,7 +23,10 @@ var mathRadiansFunc func(float64) float64 = func(deg float64) float64 { func FastValMathRound(val FastVal) FastVal { if val.IsFloat() { - originalValue := val.AsFloat() + originalValue, valid := val.AsFloat() + if !valid { + return NewInvalidFastVal() + } roundedValue := floatMathRound(originalValue) return NewFloatFastVal(roundedValue) } else if val.IsInt() || val.IsUInt() { @@ -39,10 +42,14 @@ func FastValMathAbs(val FastVal) FastVal { // Not gonna do abs on an uint return val } else { + floatVal, valid := val.AsFloat() + if !valid { + return NewInvalidFastVal() + } if val.IsFloat() { - return NewFloatFastVal(math.Abs(val.AsFloat())) + return NewFloatFastVal(math.Abs(floatVal)) } else if val.IsInt() { - return NewIntFastVal(int64(math.Abs(val.AsFloat()))) + return NewIntFastVal(int64(math.Abs(floatVal))) } } @@ -79,35 +86,41 @@ func fastValNegate(a float64) float64 { } func genericFastValIntOp(val FastVal, op intToIntOp) FastVal { - if val.IsNumeric() { - return NewIntFastVal(op(val.AsInt())) + intVal, valid := val.AsInt() + if valid && val.IsNumeric() { + return NewIntFastVal(op(intVal)) } return NewInvalidFastVal() } func genericFastVal2IntsOp(val, val1 FastVal, op int2ToIntOp) FastVal { - if !val.IsNumeric() || !val1.IsNumeric() { + valInt, valid := val.AsInt() + val1Int, valid2 := val1.AsInt() + if !val.IsNumeric() || !val1.IsNumeric() || !valid || !valid2 { return NewInvalidFastVal() } - return NewIntFastVal(op(val.AsInt(), val1.AsInt())) + return NewIntFastVal(op(valInt, val1Int)) } func genericFastValFloatOp(val FastVal, op floatToFloatOp) FastVal { - if val.IsNumeric() { - return NewFloatFastVal(op(val.AsFloat())) + valFloat, valid := val.AsFloat() + if valid && val.IsNumeric() { + return NewFloatFastVal(op(valFloat)) } return NewInvalidFastVal() } func genericFastVal2FloatsOp(val, val1 FastVal, op float2ToFloatOp) FastVal { - if !val.IsNumeric() || !val1.IsNumeric() { + valFloat, valid := val.AsFloat() + val1Float, valid2 := val1.AsFloat() + if !val.IsNumeric() || !val1.IsNumeric() || !valid || !valid2 { return NewInvalidFastVal() } - return NewFloatFastVal(op(val.AsFloat(), val1.AsFloat())) + return NewFloatFastVal(op(valFloat, val1Float)) } func FastValMathSqrt(val FastVal) FastVal { diff --git a/filterExprParser_test.go b/filterExprParser_test.go index 87023d2..3e9fb65 100644 --- a/filterExprParser_test.go +++ b/filterExprParser_test.go @@ -290,7 +290,7 @@ OR assert.Equal("field2", fe.FilterExpr.Expr[0].Expr[0].Expr.Operand.RHS.FieldWMath.Type1.Field.String()) fe = &FilterExpression{} - err = parser.ParseString("fieldpath.path IS NOT NULL", fe) + err = parser.ParseString("fieldpath.path IS NOT NULL AND fieldpath.path2 IS NOT NULL", fe) assert.Nil(err) assert.Equal("fieldpath", fe.FilterExpr.Expr[0].Expr[0].Expr.Operand.LHS.FieldWMath.Type1.Field.Path[0].String()) assert.Equal("path", fe.FilterExpr.Expr[0].Expr[0].Expr.Operand.LHS.FieldWMath.Type1.Field.Path[1].String()) @@ -302,7 +302,8 @@ OR m := NewFastMatcher(matchDef) userData := map[string]interface{}{ "fieldpath": map[string]interface{}{ - "path": 0, + "path": 0, + "path2": "string", }, } udMarsh, _ := json.Marshal(userData) @@ -369,8 +370,16 @@ OR "field1": -2, "field2": 2e30, }, - "oneVar": true, - "oneList": []uint16{1, 2, 3, 4}, + "oneVar": true, + "oneList": []uint16{1, 2, 3, 4}, + "nullVal": nil, + "int": 12, + "negInt": -12, + "zeroint": 0, + "float": 0.12, + "bool": true, + "string": "a temporary string", + "nullString": "null", } udMarsh, _ = json.Marshal(userData) match, err = m.Match(udMarsh) @@ -420,6 +429,66 @@ OR match, err = m.Match(udMarsh) assert.True(match) + // Mixed-mode Comparisons + // Null comparison + fe = &FilterExpression{} + err = parser.ParseString("nullVal < 10 AND 10 > nullVal AND negInt > nullVal AND nullVal < negInt AND nullVal < int AND int > nullVal AND NOT nullVal IS NOT NULL AND nullVal IS NULL AND \"string\" > `nullVal` AND NOT \"abc\" < `nullVal` AND nullVal != \"aString\" AND nullVal < \"string\"", fe) + assert.Nil(err) + expr, err = fe.OutputExpression() + assert.Nil(err) + matchDef = trans.Transform([]Expression{expr}) + assert.NotNil(matchDef) + m = NewFastMatcher(matchDef) + match, err = m.Match(udMarsh) + assert.True(match) + + fe = &FilterExpression{} + err = parser.ParseString("string IS NOT NULL AND nullString IS NOT NULL", fe) + assert.Nil(err) + expr, err = fe.OutputExpression() + assert.Nil(err) + matchDef = trans.Transform([]Expression{expr}) + assert.NotNil(matchDef) + m = NewFastMatcher(matchDef) + match, err = m.Match(udMarsh) + assert.True(match) + + // int and string comparison + fe = &FilterExpression{} + err = parser.ParseString("int == \"12\" AND float > \"0.1\" AND NOT float == \"invalidFloatStr\" AND string < \"b\" AND string > 123 AND string != -24435", fe) + assert.Nil(err) + expr, err = fe.OutputExpression() + assert.Nil(err) + matchDef = trans.Transform([]Expression{expr}) + assert.NotNil(matchDef) + m = NewFastMatcher(matchDef) + match, err = m.Match(udMarsh) + assert.True(match) + + // Boolean and int comparisons + fe = &FilterExpression{} + err = parser.ParseString("bool == 1.0 AND NOT 24 < bool AND bool == \"trUE\" AND bool == -145 AND int > false AND zeroint == false", fe) + assert.Nil(err) + expr, err = fe.OutputExpression() + assert.Nil(err) + matchDef = trans.Transform([]Expression{expr}) + assert.NotNil(matchDef) + m = NewFastMatcher(matchDef) + match, err = m.Match(udMarsh) + assert.True(match) + + // Reverse compare - casting a string to int in this case isn't possible + fe = &FilterExpression{} + err = parser.ParseString("int > \"0a\"", fe) + assert.Nil(err) + expr, err = fe.OutputExpression() + assert.Nil(err) + matchDef = trans.Transform([]Expression{expr}) + assert.NotNil(matchDef) + m = NewFastMatcher(matchDef) + match, err = m.Match(udMarsh) + assert.True(match) + fe = &FilterExpression{} err = parser.ParseString("`onePath.Only` <> \"value\" OR `onePath.Only` <> \"value2\"", fe) assert.Nil(err) @@ -903,6 +972,17 @@ OR match, err = m.Match(udMarsh) assert.True(match) + fe = &FilterExpression{} + err = parser.ParseString("ASIN(int)==3.05682983181e+307", fe) + assert.Nil(err) + expr, err = fe.OutputExpression() + assert.Nil(err) + matchDef = trans.Transform([]Expression{expr}) + m = NewFastMatcher(matchDef) + assert.NotNil(matchDef) + match, err = m.Match(udMarsh) + assert.False(match) + fe = &FilterExpression{} err = parser.ParseString("ATAN(int)<>3.05682983181e+307", fe) assert.Nil(err) @@ -936,6 +1016,17 @@ OR match, err = m.Match(udMarsh) assert.True(match) + fe = &FilterExpression{} + err = parser.ParseString("ACOS(int2)<3.05682983181e+307", fe) + assert.Nil(err) + expr, err = fe.OutputExpression() + assert.Nil(err) + matchDef = trans.Transform([]Expression{expr}) + m = NewFastMatcher(matchDef) + assert.NotNil(matchDef) + match, err = m.Match(udMarsh) + assert.True(match) + fe = &FilterExpression{} err = parser.ParseString("ABS(-achievements[2]*10) > 0", fe) assert.Nil(err)