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

320 lines
7.3 KiB
Go

package traceql
import (
"fmt"
"regexp"
)
// unsupportedError is returned for traceql features that are not yet supported.
type unsupportedError struct {
feature string
}
func newUnsupportedError(feature string) *unsupportedError {
return &unsupportedError{feature: feature}
}
func (e *unsupportedError) Error() string {
return e.feature + " not yet supported"
}
func (r RootExpr) validate() error {
err := r.Pipeline.validate()
if err != nil {
return err
}
if r.MetricsPipeline != nil {
err := r.MetricsPipeline.validate()
if err != nil {
return err
}
}
if r.MetricsSecondStage != nil {
err := r.MetricsSecondStage.validate()
if err != nil {
return err
}
}
// extra validation to disallow compare() with second stage functions
// for example: `{} | compare({status=error}) | topk(10)` doesn't make sense
if r.MetricsPipeline != nil && r.MetricsSecondStage != nil {
// cast and check if the first stage is a compare operation
if _, ok := r.MetricsPipeline.(*MetricsCompare); ok {
return fmt.Errorf("`compare()` cannot be used with second stage functions")
}
}
return nil
}
func (p Pipeline) validate() error {
for _, p := range p.Elements {
err := p.validate()
if err != nil {
return err
}
}
return nil
}
func (o GroupOperation) validate() error {
// todo: once grouping is supported the below validation will apply
if !o.Expression.referencesSpan() {
return fmt.Errorf("grouping field expressions must reference the span: %s", o.String())
}
return o.Expression.validate()
}
func (o CoalesceOperation) validate() error {
return nil
}
func (o SelectOperation) validate() error {
for _, e := range o.attrs {
if err := e.validate(); err != nil {
return err
}
}
return nil
}
func (o ScalarOperation) validate() error {
if err := o.LHS.validate(); err != nil {
return err
}
if err := o.RHS.validate(); err != nil {
return err
}
if o.Op == OpNotExists || o.Op == OpExists {
return fmt.Errorf("illegal operation for the given type: %s", o.String())
}
lhsT := o.LHS.impliedType()
rhsT := o.RHS.impliedType()
if !lhsT.isMatchingOperand(rhsT) {
return fmt.Errorf("binary operations must operate on the same type: %s", o.String())
}
if !o.Op.binaryTypesValid(lhsT, rhsT) {
return fmt.Errorf("illegal operation for the given types: %s", o.String())
}
return nil
}
func (a Aggregate) validate() error {
if a.e == nil {
return nil
}
if err := a.e.validate(); err != nil {
return err
}
// aggregate field expressions require a type of a number or attribute
t := a.e.impliedType()
if t != TypeAttribute && !t.isNumeric() {
return fmt.Errorf("aggregate field expressions must resolve to a number type: %s", a.String())
}
if !a.e.referencesSpan() {
return fmt.Errorf("aggregate field expressions must reference the span: %s", a.String())
}
switch a.op {
case aggregateCount, aggregateAvg, aggregateMin, aggregateMax, aggregateSum:
default:
return newUnsupportedError(fmt.Sprintf("aggregate operation (%v)", a.op))
}
return nil
}
func (o SpansetOperation) validate() error {
if err := o.LHS.validate(); err != nil {
return err
}
if o.Op == OpNotExists || o.Op == OpExists {
return fmt.Errorf("illegal operation for the given type: %s", o.String())
}
return o.RHS.validate()
}
func (f SpansetFilter) validate() error {
if err := f.Expression.validate(); err != nil {
return err
}
t := f.Expression.impliedType()
if t != TypeAttribute && t != TypeBoolean {
return fmt.Errorf("span filter field expressions must resolve to a boolean: %s", f.String())
}
return nil
}
func (f ScalarFilter) validate() error {
if err := f.LHS.validate(); err != nil {
return err
}
if err := f.RHS.validate(); err != nil {
return err
}
lhsT := f.LHS.impliedType()
rhsT := f.RHS.impliedType()
if !lhsT.isMatchingOperand(rhsT) {
return fmt.Errorf("binary operations must operate on the same type: %s", f.String())
}
if !f.Op.binaryTypesValid(lhsT, rhsT) {
return fmt.Errorf("illegal operation for the given types: %s", f.String())
}
// Only supported expression types
switch f.LHS.(type) {
case Aggregate:
default:
return newUnsupportedError("scalar filter lhs of type (%v)")
}
switch f.RHS.(type) {
case Static:
default:
return newUnsupportedError("scalar filter rhs of type (%v)")
}
return nil
}
func (o *BinaryOperation) validate() error {
if err := o.LHS.validate(); err != nil {
return err
}
if err := o.RHS.validate(); err != nil {
return err
}
lhsT := o.LHS.impliedType()
rhsT := o.RHS.impliedType()
if o.Op == OpNotExists || o.Op == OpExists {
return fmt.Errorf("illegal operation for the given type: %s", o.String())
}
if !lhsT.isMatchingOperand(rhsT) {
return fmt.Errorf("binary operations must operate on the same type: %s", o.String())
}
if !o.Op.binaryTypesValid(lhsT, rhsT) {
return fmt.Errorf("illegal operation for the given types: %s", o.String())
}
// if this is a regex operator confirm the RHS is a valid regex
if o.Op == OpRegex || o.Op == OpNotRegex {
static, ok := o.RHS.(Static)
if !ok {
return fmt.Errorf("invalid type for %s or %s: %s", OpRegex, OpNotRegex, o.RHS.String())
}
switch rhsT {
case TypeString:
_, err := regexp.Compile(static.EncodeToString(false))
if err != nil {
return fmt.Errorf("invalid regex: %s", o.RHS.String())
}
case TypeStringArray:
strs, _ := static.StringArray()
for _, str := range strs {
_, err := regexp.Compile(str)
if err != nil {
return fmt.Errorf("invalid regex: %s", o.RHS.String())
}
}
default:
return fmt.Errorf("invalid type for %s or %s: %s", OpRegex, OpNotRegex, o.RHS.String())
}
}
// this condition may not be possible to hit since it's not parseable.
// however, if we did somehow end up this situation, it would be good to return
// a reasonable error
switch o.Op {
case OpSpansetChild,
OpSpansetParent,
OpSpansetDescendant,
OpSpansetAncestor,
OpSpansetSibling,
OpSpansetNotChild,
OpSpansetNotParent,
OpSpansetNotSibling,
OpSpansetNotAncestor,
OpSpansetNotDescendant,
OpSpansetUnionChild,
OpSpansetUnionParent,
OpSpansetUnionSibling,
OpSpansetUnionAncestor,
OpSpansetUnionDescendant:
return newUnsupportedError(fmt.Sprintf("binary operation (%v)", o.Op))
}
return nil
}
func (o UnaryOperation) validate() error {
if err := o.Expression.validate(); err != nil {
return err
}
// Disallow intrinsic=nil
if o.Op == OpNotExists {
if attr, ok := o.Expression.(Attribute); ok {
switch {
case attr.Intrinsic != IntrinsicNone:
return fmt.Errorf("%s=nil is not valid because intrinsics cannot be nil", attr.String())
case attr == NewScopedAttribute(AttributeScopeResource, false, "service.name"):
return fmt.Errorf("%s=nil is not valid because resource.service.name cannot be nil", attr.String())
}
}
}
t := o.Expression.impliedType()
if t == TypeAttribute {
return nil
}
if !o.Op.unaryTypesValid(t) {
return fmt.Errorf("illegal operation for the given type: %s", o.String())
}
return nil
}
func (s Static) validate() error {
return nil
}
func (a Attribute) validate() error {
if a.Parent {
return newUnsupportedError("parent")
}
switch a.Intrinsic {
case IntrinsicParent:
return newUnsupportedError(fmt.Sprintf("intrinsic (%v)", a.Intrinsic))
}
return nil
}
func (h *Hints) validate() error {
return nil
}