Files
traces/pkg/traceql/ast_conditions.go
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

158 lines
4.3 KiB
Go

package traceql
func (r RootExpr) extractConditions(request *FetchSpansRequest) {
r.Pipeline.extractConditions(request)
if r.MetricsPipeline != nil {
r.MetricsPipeline.extractConditions(request)
}
}
func (f SpansetFilter) extractConditions(request *FetchSpansRequest) {
f.Expression.extractConditions(request)
// For empty spansets { } ensure there is something that matches all spans.
// Use start time which would have been selected as part of the second pass
// metadata, and is still fairly efficient to pull back.
if s, ok := f.Expression.(Static); ok {
if b, ok := s.Bool(); !ok || !b {
return
}
for _, c := range request.Conditions {
if c.Attribute.Intrinsic != IntrinsicNone && c.Op == OpNone {
// A different match-all intrinsic is already present.
return
}
}
request.appendCondition(Condition{
Attribute: NewIntrinsic(IntrinsicSpanStartTime),
Op: OpNone,
})
}
}
// extractConditions on Select puts its conditions into the SecondPassConditions
func (o SelectOperation) extractConditions(request *FetchSpansRequest) {
selectR := &FetchSpansRequest{}
for _, expr := range o.attrs {
expr.extractConditions(selectR)
}
// copy any conditions to the normal request's SecondPassConditions
request.SecondPassConditions = append(request.SecondPassConditions, selectR.Conditions...)
}
func (o *BinaryOperation) extractConditions(request *FetchSpansRequest) {
// TODO we can further optimise this by attempting to execute every FieldExpression, if they only contain statics it should resolve
switch l := o.LHS.(type) {
case Attribute:
switch r := o.RHS.(type) {
case Static:
switch {
case (o.RHS.(Static).Type == TypeNil && o.Op == OpNotEqual) || !o.Op.isBoolean(): // the fetch layer can't build predicates on operators that are not boolean
request.appendCondition(Condition{
Attribute: l,
Op: OpNone,
Operands: nil,
})
case r.Type == TypeBoolean && (o.Op == OpOr || o.Op == OpAnd):
request.appendCondition(Condition{
Attribute: l,
Op: OpNone,
Operands: nil,
})
default:
request.appendCondition(Condition{
Attribute: l,
Op: o.Op,
Operands: []Static{r},
})
}
case Attribute:
// Both sides are attributes, just fetch both
request.appendCondition(Condition{
Attribute: o.LHS.(Attribute),
Op: OpNone,
Operands: nil,
})
request.appendCondition(Condition{
Attribute: o.RHS.(Attribute),
Op: OpNone,
Operands: nil,
})
default:
// Just fetch LHS and try to do something smarter with RHS
request.appendCondition(Condition{
Attribute: o.LHS.(Attribute),
Op: OpNone,
Operands: nil,
})
o.RHS.extractConditions(request)
}
case Static:
switch r := o.RHS.(type) {
case Static:
return // if both are Static, don't need to send any conditions
case Attribute:
switch {
case (o.LHS.(Static).Type == TypeNil && o.Op == OpNotEqual) || !o.Op.isBoolean(): // the fetch layer can't build predicates on operators that are not boolean
request.appendCondition(Condition{
Attribute: r,
Op: OpNone,
Operands: nil,
})
case l.Type == TypeBoolean && (o.Op == OpOr || o.Op == OpAnd):
request.appendCondition(Condition{
Attribute: r,
Op: OpNone,
Operands: nil,
})
default:
request.appendCondition(Condition{
Attribute: r,
Op: o.Op,
Operands: []Static{l},
})
}
default:
o.RHS.extractConditions(request)
}
default:
o.LHS.extractConditions(request)
o.RHS.extractConditions(request)
request.AllConditions = request.AllConditions && (o.Op != OpOr)
}
}
func (o UnaryOperation) extractConditions(request *FetchSpansRequest) {
// TODO when Op is Not we should just either negate all inner Operands or just fetch the columns with OpNone
switch expr := o.Expression.(type) {
case Attribute:
switch o.Op {
case OpExists, OpNotExists:
request.appendCondition(Condition{
Attribute: expr,
Op: o.Op,
Operands: nil,
})
return
default:
expr.extractConditions(request)
}
default:
expr.extractConditions(request)
}
}
func (s Static) extractConditions(*FetchSpansRequest) {
}
func (a Attribute) extractConditions(request *FetchSpansRequest) {
request.appendCondition(Condition{
Attribute: a,
Op: OpNone,
Operands: nil,
})
}