mirror of
https://github.com/hanzoai/traces.git
synced 2026-10-30 18:20:41 +00:00
* 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]>
320 lines
7.3 KiB
Go
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
|
|
}
|