Files
cc86e98b04 Rewrite conditions on same attribute to equivalent array operation (#6353)
* Add operators IN, NOT IN, MATCH ANY, and MATCH NONE

* Implement Static.append()

* Print string arrays with '`' instead of '"'

Example ["aa", "bb"] -> [`aa`, `bb`]

* Rewrite binary operations to array equivalent

The rewriter handles the following cases:
- { .a =  "a" || .a =  "b" } => { .a IN  ["a", "b"] }
- { .a != "a" && .a != "b" } => { .a NOT IN ["a", "b"] }
- { .a =~ "a" || .a =~ "b" } => { .a MATCH ANY ["a", "b"] }
- { .a !~ "a" && .a !~ "b" } => { .a MATCH NONE ["a", "b"] }

* Add test cases with arrays to TestBinOp

* traceql.Parse() applies AST optimizations by default

* Rename predicate_test.go to predicates_test.go

* Add array predicates to parquetquery

* BREAKING CHANGE: more precise regex validation

BinaryOperation.validate() considered everythin a valid regex where
FieldExpression.EncodeToString(false) happened to return a valid regex.
With this change the RHS of a regex operation is required to be a string
or string array

* vp4: handle predicates with arrays correctly

* vp5: handle predicates with arrays correctly

* vp3: handle predicates with arrays correctly

* Add test cases for optimized queries

* Update query test examples with optimizable queries

* vp3: fix handling of array types

* CHANGELOG.md

* Pass array operators down to fetch layer

* Read to first virtual row after seeking on a low definition level

This helps avoid additional spans/op in queries with only
resource attributes

---------

Co-authored-by: Martin Disibio <[email protected]>
2026-02-22 00:55:23 -08:00

935 lines
24 KiB
Go

package traceql
import (
"errors"
"fmt"
"math"
"strings"
"github.com/grafana/tempo/pkg/regexp"
)
var errSpansetOperationMultiple = errors.New("spanset operators are not supported for multiple spansets per trace. consider using coalesce()")
func (g GroupOperation) evaluate(ss []*Spanset) ([]*Spanset, error) {
result := make([]*Spanset, 0, len(ss))
groups := g.groupBuffer
// Iterate over each spanset in the input slice
for _, spanset := range ss {
// clear out the groups
for k := range groups {
delete(groups, k)
}
// Iterate over each span in the spanset
for _, span := range spanset.Spans {
// Execute the FieldExpression for the span
result, err := g.Expression.execute(span)
if err != nil {
return nil, err
}
// Check if the result already has a group in the map
group, ok := groups[result.MapKey()]
if !ok {
// If not, create a new group and add it to the map
group = &Spanset{}
// copy all existing attributes forward
group.Attributes = append(group.Attributes, spanset.Attributes...)
group.AddAttribute(g.String(), result)
groups[result.MapKey()] = group
}
// Add the current spanset to the group
group.Spans = append(group.Spans, span)
}
// add all groups created by this spanset to the result
for _, group := range groups {
result = append(result, group)
}
}
return result, nil
}
// CoalesceOperation undoes grouping. It takes spansets and recombines them into
// one by trace id. Since all spansets are guaranteed to be from the same traceid
// due to the structure of the engine we can cheat and just recombine all spansets
// in ss into one without checking.
func (CoalesceOperation) evaluate(ss []*Spanset) ([]*Spanset, error) {
l := 0
for _, spanset := range ss {
l += len(spanset.Spans)
}
result := &Spanset{
Spans: make([]Span, 0, l),
}
for _, spanset := range ss {
result.Spans = append(result.Spans, spanset.Spans...)
}
return []*Spanset{result}, nil
}
func (o SpansetOperation) evaluate(input []*Spanset) (output []*Spanset, err error) {
for i := range input {
curr := input[i : i+1]
lhs, err := o.LHS.evaluate(curr)
if err != nil {
return nil, err
}
rhs, err := o.RHS.evaluate(curr)
if err != nil {
return nil, err
}
var relFn func(s Span, l, r []Span) []Span
switch o.Op {
case OpSpansetAnd:
if len(lhs) > 0 && len(rhs) > 0 {
output = addSpanset(input[i], uniqueSpans(lhs, rhs), output)
}
case OpSpansetUnion:
if len(lhs) > 0 || len(rhs) > 0 {
output = addSpanset(input[i], uniqueSpans(lhs, rhs), output)
}
// relationship operators all set relFn which is used by below code
// to perform the operation
case OpSpansetNotDescendant: // !>>
fallthrough
case OpSpansetNotAncestor: // !<<
fallthrough
case OpSpansetAncestor: // <<
fallthrough
case OpSpansetDescendant: // >>
fallthrough
case OpSpansetUnionAncestor: // &<<
fallthrough
case OpSpansetUnionDescendant: // &>>
falseForAll := o.Op == OpSpansetNotDescendant || o.Op == OpSpansetNotAncestor
invert := o.Op == OpSpansetAncestor || o.Op == OpSpansetNotAncestor || o.Op == OpSpansetUnionAncestor
union := o.Op == OpSpansetUnionAncestor || o.Op == OpSpansetUnionDescendant
relFn = func(s Span, l, r []Span) []Span {
return s.DescendantOf(l, r, falseForAll, invert, union, o.matchingSpansBuffer)
}
case OpSpansetNotChild: // !>
fallthrough
case OpSpansetChild: // >
fallthrough
case OpSpansetNotParent: // !<
fallthrough
case OpSpansetParent: // <
fallthrough
case OpSpansetUnionParent: // &<
fallthrough
case OpSpansetUnionChild: // &>
falseForAll := o.Op == OpSpansetNotParent || o.Op == OpSpansetNotChild
invert := o.Op == OpSpansetParent || o.Op == OpSpansetNotParent || o.Op == OpSpansetUnionParent
union := o.Op == OpSpansetUnionParent || o.Op == OpSpansetUnionChild
relFn = func(s Span, l, r []Span) []Span {
return s.ChildOf(l, r, falseForAll, invert, union, o.matchingSpansBuffer)
}
case OpSpansetNotSibling: // !~
fallthrough
case OpSpansetSibling: // ~
fallthrough
case OpSpansetUnionSibling: // &~
falseForAll := o.Op == OpSpansetNotSibling
union := o.Op == OpSpansetUnionSibling
relFn = func(s Span, l, r []Span) []Span {
return s.SiblingOf(l, r, falseForAll, union, o.matchingSpansBuffer)
}
default:
return nil, fmt.Errorf("spanset operation (%v) not supported", o.Op)
}
// if relFn was set up above we are doing a relationship operation.
if relFn != nil {
o.matchingSpansBuffer, err = o.joinSpansets(lhs, rhs, relFn) // o.matchingSpansBuffer is passed into the functions above and is stored here
if err != nil {
return nil, err
}
output = addSpanset(input[i], o.matchingSpansBuffer, output)
}
}
return output, nil
}
// joinSpansets compares all pairwise combinations of the inputs and returns the right-hand side
// where the eval callback returns true. For now the behavior is only defined when there is exactly one
// spanset on both sides and will return an error if multiple spansets are present.
func (o *SpansetOperation) joinSpansets(lhs, rhs []*Spanset, eval func(s Span, l, r []Span) []Span) ([]Span, error) {
if len(rhs) < 1 {
return nil, nil
}
if len(lhs) > 1 || len(rhs) > 1 {
return nil, errSpansetOperationMultiple
}
// if rhs side is empty then no spans match and we can bail out here
if len(rhs[0].Spans) == 0 {
return nil, nil
}
var lspans []Span
if len(lhs) >= 1 {
lspans = lhs[0].Spans
}
return eval(rhs[0].Spans[0], lspans, rhs[0].Spans), nil
}
// addSpanset is a helper function that adds a new spanset to the output. it clones
// the input to prevent modifying the original spanset.
func addSpanset(input *Spanset, matching []Span, output []*Spanset) []*Spanset {
if len(matching) == 0 {
return output
}
matchingSpanset := input.clone()
matchingSpanset.Spans = matching
return append(output, matchingSpanset)
}
// SelectOperation evaluate is a no-op b/c the fetch layer has already decorated the spans with the requested attributes
func (o SelectOperation) evaluate(input []*Spanset) (output []*Spanset, err error) {
return input, nil
}
func (f ScalarFilter) evaluate(input []*Spanset) (output []*Spanset, err error) {
// TODO we solve this gap where pipeline elements and scalar binary
// operations meet in a generic way. For now we only support well-defined
// case: aggregate binop static
switch l := f.LHS.(type) {
case Aggregate:
switch r := f.RHS.(type) {
case Static:
input, err = l.evaluate(input)
if err != nil {
return nil, err
}
for _, ss := range input {
res, err := binOp(f.Op, ss.Scalar, r)
if err != nil {
return nil, fmt.Errorf("scalar filter (%v) failed: %v", f, err)
}
if res {
output = append(output, ss)
}
}
default:
return nil, fmt.Errorf("scalar filter lhs (%v) not supported", f.LHS)
}
default:
return nil, fmt.Errorf("scalar filter lhs (%v) not supported", f.LHS)
}
return output, nil
}
func (a Aggregate) evaluate(input []*Spanset) ([]*Spanset, error) {
var output []*Spanset
for _, ss := range input {
switch a.op {
case aggregateCount:
cpy := ss.clone()
cpy.Scalar = NewStaticInt(len(ss.Spans))
cpy.AddAttribute(a.String(), cpy.Scalar)
output = append(output, cpy)
case aggregateAvg:
var sum *Static
count := 0
for _, s := range ss.Spans {
val, err := a.e.execute(s)
if err != nil {
return nil, err
}
if val.IsNil() {
continue
}
if sum == nil {
sum = &val
} else {
sum.sumInto(&val)
}
count++
}
if sum == nil {
continue
}
cpy := ss.clone()
cpy.Scalar = sum.divideBy(float64(count))
cpy.AddAttribute(a.String(), cpy.Scalar)
output = append(output, cpy)
case aggregateMax:
var maxS *Static
for _, s := range ss.Spans {
val, err := a.e.execute(s)
if err != nil {
return nil, err
}
if val.IsNil() {
continue
}
if maxS == nil || val.compare(maxS) > 0 {
maxS = &val
}
}
if maxS == nil {
continue
}
cpy := ss.clone()
cpy.Scalar = *maxS
cpy.AddAttribute(a.String(), cpy.Scalar)
output = append(output, cpy)
case aggregateMin:
var minS *Static
for _, s := range ss.Spans {
val, err := a.e.execute(s)
if err != nil {
return nil, err
}
if val.IsNil() {
continue
}
if minS == nil || val.compare(minS) == -1 {
minS = &val
}
}
if minS == nil {
continue
}
cpy := ss.clone()
cpy.Scalar = *minS
cpy.AddAttribute(a.String(), cpy.Scalar)
output = append(output, cpy)
case aggregateSum:
var sum *Static
for _, s := range ss.Spans {
val, err := a.e.execute(s)
if err != nil {
return nil, err
}
if val.IsNil() {
continue
}
if sum == nil {
sum = &val
} else {
sum.sumInto(&val)
}
}
if sum == nil {
continue
}
cpy := ss.clone()
cpy.Scalar = *sum
cpy.AddAttribute(a.String(), cpy.Scalar)
output = append(output, cpy)
default:
return nil, fmt.Errorf("aggregate operation (%v) not supported", a.op)
}
}
return output, nil
}
func (o *BinaryOperation) execute(span Span) (Static, error) {
recording := o.b.Recording
if recording {
o.b.Start()
}
lhs, err := o.LHS.execute(span)
if err != nil {
return NewStaticNil(), err
}
if recording {
o.b.Finish(leftBranch)
}
// Look for cases where we don't even need to evaluate the RHS
// But wait until we have enough samples so we can optimize
if !recording {
if lhsB, ok := lhs.Bool(); ok {
if o.Op == OpAnd && !lhsB {
// x && y
// x is false so we don't need to evaluate y
return StaticFalse, nil
}
if o.Op == OpOr && lhsB {
// x || y
// x is true so we don't need to evaluate y
return StaticTrue, nil
}
}
}
if recording {
o.b.Start()
}
rhs, err := o.RHS.execute(span)
if err != nil {
return NewStaticNil(), err
}
if recording {
o.b.Finish(rightBranch)
}
lhsT := lhs.Type
rhsT := rhs.Type
// ensure the resolved types are still valid
if !lhsT.isMatchingOperand(rhsT) {
return StaticFalse, nil
}
if !o.Op.binaryTypesValid(lhsT, rhsT) {
return StaticFalse, nil
}
switch {
case lhsT == TypeString && rhsT == TypeString:
// if both types are strings, execute string operations ...
lhsS := lhs.EncodeToString(false)
rhsS := rhs.EncodeToString(false)
switch o.Op {
case OpGreater:
return NewStaticBool(strings.Compare(lhsS, rhsS) > 0), nil
case OpGreaterEqual:
return NewStaticBool(strings.Compare(lhsS, rhsS) >= 0), nil
case OpLess:
return NewStaticBool(strings.Compare(lhsS, rhsS) < 0), nil
case OpLessEqual:
return NewStaticBool(strings.Compare(lhsS, rhsS) <= 0), nil
case OpRegex, OpNotRegex:
shouldMatch := o.Op == OpRegex
if len(o.compiledExpressions) == 0 {
exp, err := regexp.NewRegexp([]string{rhsS}, shouldMatch)
if err != nil {
return NewStaticNil(), err
}
o.compiledExpressions = append(o.compiledExpressions, exp)
}
if len(o.compiledExpressions) != 1 {
return NewStaticNil(), errors.New("unexpected numbers of pre-compiled regexp")
}
matched := o.compiledExpressions[0].MatchString(lhsS)
return NewStaticBool(matched), nil
}
// if not executed, fall through to the catch-all below
case lhsT == TypeInt && rhsT == TypeInt:
// if both types are ints, execute int operations
lhsN, _ := lhs.Int()
rhsN, _ := rhs.Int()
switch o.Op {
case OpAdd:
return NewStaticInt(lhsN + rhsN), nil
case OpSub:
return NewStaticInt(lhsN - rhsN), nil
case OpDiv:
return NewStaticInt(lhsN / rhsN), nil
case OpMod:
return NewStaticInt(lhsN % rhsN), nil
case OpMult:
return NewStaticInt(lhsN * rhsN), nil
case OpGreater:
return NewStaticBool(lhsN > rhsN), nil
case OpGreaterEqual:
return NewStaticBool(lhsN >= rhsN), nil
case OpLess:
return NewStaticBool(lhsN < rhsN), nil
case OpLessEqual:
return NewStaticBool(lhsN <= rhsN), nil
case OpPower:
return NewStaticInt(intPow(rhsN, lhsN)), nil
}
// if not executed, fall through to the catch-all below
case lhsT == TypeBoolean && rhsT == TypeBoolean:
// if both types are bools, execute boolean operations
lhsB, _ := lhs.Bool()
rhsB, _ := rhs.Bool()
if recording {
switch o.Op {
case OpAnd:
if !lhsB {
// Record cost of wasted rhs execution
o.b.Penalize(rightBranch)
}
if !rhsB {
// Record cost of wasted lhs execution
o.b.Penalize(leftBranch)
}
case OpOr:
if rhsB {
// Record cost of wasted lhs execution
o.b.Penalize(rightBranch)
}
if lhsB {
// Record cost of wasted rhs execution
o.b.Penalize(leftBranch)
}
}
if done := o.b.Sampled(); done {
if o.b.OptimalBranch() == rightBranch {
// RHS is the optimal starting branch,
// so swap the elements now.
o.LHS, o.RHS = o.RHS, o.LHS
}
}
}
switch o.Op {
case OpAnd:
return NewStaticBool(lhsB && rhsB), nil
case OpOr:
return NewStaticBool(lhsB || rhsB), nil
}
// if not executed, fall through to the catch-all below
case lhsT.isMatchingArrayElement(rhsT):
// if operands matching arrays / array elements, execute array operations
// we only support boolean op in the arrays
if !o.Op.isBoolean() {
return NewStaticNil(), errors.ErrUnsupported
}
var (
// the operator to apply to each element of the array
elemOp Operator
// the array and scalar side of the operation
array Static
scalar Static
flipOperands bool
)
if lhs.Type.isArray() && rhs.Type.isArray() {
return NewStaticNil(), errors.New("array operators must consist of a scalar and an array operand")
}
// determine which side is the array and which is the scalar
if o.Op.isArrayOp() {
elemOp = o.Op.toElementOp()
array = rhs
scalar = lhs
// array operations like IN, NOT IN, REGEX_MATCH_ANY, REGEX_MATCH_NONE are not symmetric
if !array.Type.isArray() {
return NewStaticNil(), errors.New("array operators require array on the right side")
}
} else {
elemOp = o.Op
array = rhs
scalar = lhs
if scalar.Type.isArray() {
elemOp = getFlippedOp(o.Op)
array, scalar = scalar, array
// for regex operations we assume the RHS is a regex: must flip back operands later
if elemOp == OpRegex || elemOp == OpNotRegex {
flipOperands = true
}
}
}
// operators like OpNotEqual and OpNotRegex have the semantics of 'not in' / 'match none': must check all elements
matchAll := elemOp == OpNotEqual || elemOp == OpNotRegex
// apply operation to each element of the array
var elemCount, matchCount int
for i, elem := range array.Elements() {
elemCount++
l := scalar
r := elem
if flipOperands {
l, r = r, l
}
var exp []*regexp.Regexp
if len(o.compiledExpressions) > i {
exp = o.compiledExpressions[i : i+1]
}
res, exp, err := binOpExecuteScalar(elemOp, l, r, exp)
if err != nil {
return NewStaticNil(), err
}
if len(o.compiledExpressions) == i {
o.compiledExpressions = append(o.compiledExpressions, exp...)
}
match, ok := res.Bool()
if ok && match {
matchCount++
if !matchAll {
break
}
}
}
var result Static
if matchAll {
result = NewStaticBool(matchCount == elemCount)
} else {
result = NewStaticBool(matchCount > 0)
}
return result, nil
}
// if no operation was executed, execute float operations as a catch-all
var result Static
switch o.Op {
case OpAdd:
result = NewStaticFloat(lhs.Float() + rhs.Float())
case OpSub:
result = NewStaticFloat(lhs.Float() - rhs.Float())
case OpDiv:
result = NewStaticFloat(lhs.Float() / rhs.Float())
case OpMod:
result = NewStaticFloat(math.Mod(lhs.Float(), rhs.Float()))
case OpMult:
result = NewStaticFloat(lhs.Float() * rhs.Float())
case OpGreater:
result = NewStaticBool(lhs.Float() > rhs.Float())
case OpGreaterEqual:
result = NewStaticBool(lhs.Float() >= rhs.Float())
case OpLess:
result = NewStaticBool(lhs.Float() < rhs.Float())
case OpLessEqual:
result = NewStaticBool(lhs.Float() <= rhs.Float())
case OpPower:
result = NewStaticFloat(math.Pow(lhs.Float(), rhs.Float()))
case OpEqual:
result = NewStaticBool(lhs.Equals(&rhs))
case OpNotEqual:
result = NewStaticBool(lhs.NotEquals(&rhs))
default:
return NewStaticNil(), errors.New("unexpected operator " + o.Op.String())
}
return result, nil
}
// binOpExecuteScalar executes binary operations on scalar values only. This is used for comparing array elements
// and duplicates large parts of BinaryOperation.execute().
// NOTE: The code duplication with BinaryOperation.execute() IS intentional as the inlining has shown to be relevant
// for query performance.
func binOpExecuteScalar(op Operator, lhs, rhs Static, expressions []*regexp.Regexp) (Static, []*regexp.Regexp, error) {
lhsT := lhs.Type
rhsT := rhs.Type
// Fast validation - we know these are scalar types from array iteration
if !lhsT.isMatchingOperand(rhsT) || !op.binaryTypesValid(lhsT, rhsT) {
return StaticFalse, expressions, nil
}
switch {
case lhsT == TypeString && rhsT == TypeString:
// String operations
lhsS := lhs.EncodeToString(false)
rhsS := rhs.EncodeToString(false)
switch op {
case OpGreater:
return NewStaticBool(strings.Compare(lhsS, rhsS) > 0), expressions, nil
case OpGreaterEqual:
return NewStaticBool(strings.Compare(lhsS, rhsS) >= 0), expressions, nil
case OpLess:
return NewStaticBool(strings.Compare(lhsS, rhsS) < 0), expressions, nil
case OpLessEqual:
return NewStaticBool(strings.Compare(lhsS, rhsS) <= 0), expressions, nil
case OpRegex, OpNotRegex:
shouldMatch := op == OpRegex
if len(expressions) == 0 {
exp, err := regexp.NewRegexp([]string{rhsS}, shouldMatch)
if err != nil {
return NewStaticNil(), nil, err
}
expressions = append(expressions, exp)
}
if len(expressions) != 1 {
return NewStaticNil(), expressions, errors.New("unexpected numbers of pre-compiled regexp")
}
matched := expressions[0].MatchString(lhsS)
return NewStaticBool(matched), expressions, nil
}
case lhsT == TypeInt && rhsT == TypeInt:
// Integer operations
lhsN, _ := lhs.Int()
rhsN, _ := rhs.Int()
switch op {
case OpAdd:
return NewStaticInt(lhsN + rhsN), expressions, nil
case OpSub:
return NewStaticInt(lhsN - rhsN), expressions, nil
case OpDiv:
return NewStaticInt(lhsN / rhsN), expressions, nil
case OpMod:
return NewStaticInt(lhsN % rhsN), expressions, nil
case OpMult:
return NewStaticInt(lhsN * rhsN), expressions, nil
case OpGreater:
return NewStaticBool(lhsN > rhsN), expressions, nil
case OpGreaterEqual:
return NewStaticBool(lhsN >= rhsN), expressions, nil
case OpLess:
return NewStaticBool(lhsN < rhsN), expressions, nil
case OpLessEqual:
return NewStaticBool(lhsN <= rhsN), expressions, nil
case OpPower:
return NewStaticInt(intPow(rhsN, lhsN)), expressions, nil
}
case lhsT == TypeBoolean && rhsT == TypeBoolean:
// Boolean operations
lhsB, _ := lhs.Bool()
rhsB, _ := rhs.Bool()
switch op {
case OpAnd:
return NewStaticBool(lhsB && rhsB), expressions, nil
case OpOr:
return NewStaticBool(lhsB || rhsB), expressions, nil
}
}
// Float catch-all
switch op {
case OpAdd:
return NewStaticFloat(lhs.Float() + rhs.Float()), expressions, nil
case OpSub:
return NewStaticFloat(lhs.Float() - rhs.Float()), expressions, nil
case OpDiv:
return NewStaticFloat(lhs.Float() / rhs.Float()), expressions, nil
case OpMod:
return NewStaticFloat(math.Mod(lhs.Float(), rhs.Float())), expressions, nil
case OpMult:
return NewStaticFloat(lhs.Float() * rhs.Float()), expressions, nil
case OpGreater:
return NewStaticBool(lhs.Float() > rhs.Float()), expressions, nil
case OpGreaterEqual:
return NewStaticBool(lhs.Float() >= rhs.Float()), expressions, nil
case OpLess:
return NewStaticBool(lhs.Float() < rhs.Float()), expressions, nil
case OpLessEqual:
return NewStaticBool(lhs.Float() <= rhs.Float()), expressions, nil
case OpPower:
return NewStaticFloat(math.Pow(lhs.Float(), rhs.Float())), expressions, nil
case OpEqual:
return NewStaticBool(lhs.Equals(&rhs)), expressions, nil
case OpNotEqual:
return NewStaticBool(lhs.NotEquals(&rhs)), expressions, nil
default:
return NewStaticNil(), nil, errors.New("unexpected operator " + op.String())
}
}
// getFlippedOp will return the flipped op, used when flipping the LHS and RHS of a BinaryOperation
func getFlippedOp(op Operator) Operator {
switch op {
case OpGreater:
return OpLess
case OpGreaterEqual:
return OpLessEqual
case OpLess:
return OpGreater
case OpLessEqual:
return OpGreaterEqual
default:
return op
}
}
// why does this and the above exist?
func binOp(op Operator, lhs, rhs Static) (bool, error) {
lhsT := lhs.impliedType()
rhsT := rhs.impliedType()
if !lhsT.isMatchingOperand(rhsT) {
return false, nil
}
if !op.binaryTypesValid(lhsT, rhsT) {
return false, nil
}
if lhsT == TypeBoolean && rhsT == TypeBoolean {
lhsB, _ := lhs.Bool()
rhsB, _ := rhs.Bool()
switch op {
case OpAnd:
return lhsB && rhsB, nil
case OpOr:
return lhsB || rhsB, nil
}
}
switch op {
case OpGreater:
return lhs.Float() > rhs.Float(), nil
case OpGreaterEqual:
return lhs.Float() >= rhs.Float(), nil
case OpLess:
return lhs.Float() < rhs.Float(), nil
case OpLessEqual:
return lhs.Float() <= rhs.Float(), nil
case OpEqual:
return lhs.Equals(&rhs), nil
case OpNotEqual:
return lhs.NotEquals(&rhs), nil
}
return false, errors.New("unexpected operator " + op.String())
}
func (o UnaryOperation) execute(span Span) (Static, error) {
static, err := o.Expression.execute(span)
if err != nil {
return NewStaticNil(), err
}
if o.Op == OpNot {
if static.Type != TypeBoolean {
return NewStaticNil(), fmt.Errorf("expression (%v) expected a boolean, but got %v", o, static.Type)
}
b, _ := static.Bool()
return NewStaticBool(!b), nil
}
if o.Op == OpSub {
if !static.Type.isNumeric() {
return NewStaticNil(), fmt.Errorf("expression (%v) expected a numeric, but got %v", o, static.Type)
}
switch static.Type {
case TypeInt:
n, _ := static.Int()
return NewStaticInt(-1 * n), nil
case TypeFloat:
return NewStaticFloat(-1 * static.Float()), nil
case TypeDuration:
d, _ := static.Duration()
return NewStaticDuration(-1 * d), nil
}
}
if o.Op == OpExists {
return NewStaticBool(static.Type != TypeNil), nil
}
if o.Op == OpNotExists {
staticNilString := NewStaticString("nil")
return NewStaticBool(static.Equals(&staticNilString)), nil
}
return NewStaticNil(), fmt.Errorf("UnaryOperation has invalid operator %v", o.Op)
}
func (s Static) execute(Span) (Static, error) {
return s, nil
}
func (a Attribute) execute(span Span) (Static, error) {
static, ok := span.AttributeFor(a)
if ok {
return static, nil
}
return StaticNil, nil
}
func uniqueSpans(ss1 []*Spanset, ss2 []*Spanset) []Span {
ss1Count := 0
ss2Count := 0
for _, ss1 := range ss1 {
ss1Count += len(ss1.Spans)
}
for _, ss2 := range ss2 {
ss2Count += len(ss2.Spans)
}
output := make([]Span, 0, ss1Count+ss2Count)
ssCount := ss2Count
ssSmaller := ss2
ssLarger := ss1
if ss1Count < ss2Count {
ssCount = ss1Count
ssSmaller = ss1
ssLarger = ss2
}
// make the map with ssSmaller
spans := make(map[Span]struct{}, ssCount)
for _, ss := range ssSmaller {
for _, span := range ss.Spans {
spans[span] = struct{}{}
output = append(output, span)
}
}
// only add the spans from ssLarger that aren't in the map
for _, ss := range ssLarger {
for _, span := range ss.Spans {
if _, ok := spans[span]; !ok {
output = append(output, span)
}
}
}
return output
}
func intPow(m, n int) int {
result := 1
for i := 0; i < n; i++ {
result *= m
}
return result
}