Files
traces/pkg/traceql/test_examples.yaml
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

366 lines
12 KiB
YAML

# valid queries parse successfully and return nil when calling .validate()
valid:
# spanset filters
- '{ true }'
- '{ !true }'
- '{ true && false }'
- '{ true || false }'
- '{ 1 = 2 }'
- '{ 1 != 2 }'
- '{ 1 > 2 }'
- '{ 1 >= 2 }'
- '{ 1 < 2 }'
- '{ 1 <= 2 }'
- '{ -1 = 2 }'
- '{ "test" =~ "test" }'
- '{ "test" !~ "test" }'
- '{ "test" = "test" }'
- '{ "test" != "test" }'
- '{ .a }'
- '{ !.a }'
- '{ .a && false }'
- '{ .a || true }'
- '{ .a = 2 }'
- '{ .a != 2 }'
- '{ .a > 2 }'
- '{ .a >= 2 }'
- '{ .a < 2 }'
- '{ .a <= 2 }'
- '{ -.a = 2 }'
- '{ .a =~ "test" }'
- '{ .a !~ "test" }'
- '{ .a = "test" }'
- '{ .a != "test" }'
- '{ resource.a != 3 }'
- '{ span.a != 3 }'
- '{ !("test" != .c || ((true && .b) || 3 < .a)) }'
- '{ status = ok }'
- '{ status = unset }'
- '{ status = error }'
- '{ status != error }'
- '{ statusMessage = "status message" }'
- '{ statusMessage = "" }'
- '{ statusMessage =~ ".*" }'
- '{ statusMessage != "" }'
- '{ kind = internal }'
- '{ kind = client }'
- '{ kind = consumer }'
- '{ traceDuration > 1s }'
- '{ rootServiceName = "foo" }'
- '{ rootName != "foo" }'
- '{ duration > 1s }'
- '{ 1 < 1h }'
- '{ 1 <= 1.1 }'
- '{ event.foo = "bar" }'
- '{ link.foo = "bar" }'
- '{ instrumentation.foo = "bar" }'
- '{ span.foo != 3.0 }'
# scoped intrinsics
- '{ trace:duration > 2s }'
- '{ trace:rootName = "a" }'
- '{ trace:rootService = "a" }'
- '{ trace:id = "aasdf4ergf" }'
- '{ span:duration > 2s }'
- '{ span:status = error }'
- '{ span:kind = client }'
- '{ span:statusMessage = "status message" }'
- '{ span:id = "61626364656667" }'
- '{ span:parentID = "84737586494" }'
- '{ event:name = "exception" }'
- '{ event:timeSinceStart > 1s }'
- '{ link:traceID = "f1f1f1f1f1f1ff1f1f1f1f1f1f1f1f1" }'
- '{ link:spanID = "f1f1f1f1f1f1f1f1" }'
- '{ instrumentation:name = "grpc" }'
- '{ instrumentation:version = "v3.34" }'
# binary operations
- '{ 1 + 1 = 2 }'
- '{ 1 - 1 = 2 }'
- '{ 1 * 1 = 2 }'
- '{ 1 / 1 = 2 }'
- '{ 1 ^ 1 = 2 }'
- '{ .a + 1 = 2 }'
- '{ .a - 1 = 2 }'
- '{ .a * 1 = 2 }'
- '{ .a / 1 = 2 }'
- '{ .a ^ 1 = 2 }'
- '{ duration > 1s * 2s }'
- '{ 1 * 1h = 1 }' # combining float, int and duration can make sense, but can also be weird. we just accept it all
- '{ 1 / 1.1 = 1 }'
- '{ .http.status >= "200" }'
# spanset expressions
- '{ true } && { true }'
- '{ true } || { true }'
- '{ true } >> { true }'
- '{ true } << { true }'
- '{ true } > { true }'
- '{ true } < { true }'
- '{ true } ~ { true }'
- '{ true } !> { true }'
- '{ true } !< { true }'
- '{ true } !~ { true }'
- '{ true } !>> { true }'
- '{ true } !<< { true }'
- '{ true } &> { true }'
- '{ true } &< { true }'
- '{ true } &~ { true }'
- '{ true } &>> { true }'
- '{ true } &<< { true }'
- '({ true } | count() > 1 | { false }) >> ({ true } | count() > 1 | { false })'
- '({ true } | count() > 1 | { false }) > ({ true } | count() > 1 | { false })'
- '({ true } | count() > 1 | { false }) ~ ({ true } | count() > 1 | { false })'
# can be optimized
- '{.attr="a" || .attr="b"}'
- '{.attr=1 || .attr=2}'
- '{.attr=1.1 || .attr=2.2}'
- '{.attr!="a" && .attr!="b"}'
- '{.attr!=1 && .attr!=2}'
- '{.attr!=1.1 && .attr!=1.1}'
- '{.attr=~"a.*" || .attr=~"b.*"}'
- '{.attr!~"a.*" && .attr!~"b.*"}'
# scalar filters
- 'avg(.field) > 1'
- 'max(duration) >= 1s'
- 'max(duration) > 1' # same note as above for int, float and duration
- '{ true } | max(duration) = 1h'
- '{ true } | min(duration) = 1h'
- '{ true } | sum(duration) = 1h'
- '{ true } | max(.a) = 1'
- '{ true } | max(span.a) = 1'
- '{ true } | max(resource.a) = 1'
- '{ true } | max(1 + .a) = 1'
- '{ true } | max((1 + .a) * 2) = 1'
- 'max(duration) > 3s | { status = error || .http.status = 500 }'
# select
- 'select(.a)'
- '{} | select(.a,.b,.c)'
# pipelines
- '{ true } | { .a }'
- '{ true } | count() = 1'
- '{ true } | avg(duration) = 1h'
- 'count() = 1 | { true }'
- '{ true } | count() = 1 | { true }'
# pipeline expressions
- '({ true } | count() > 1 | { false }) && ({ true } | count() > 1 | { false })'
- '({ true } | count() > 1 | { false }) || ({ true } | count() > 1 | { false })'
# coalesce - will be valid when supported
- '{ true } | coalesce()'
- '{ true } | by(1 + .a) | coalesce()'
# by - will be valid when supported
- '{ true } | by(.a)'
- '{ true } | by(1 + .a)'
- 'by(.a) | { true }'
- '{ true } | by(name) | count() > 2'
- '{ true } | by(.field) | avg(.b) = 2'
- '{ true } | by(3 * .field - 2) | max(duration) < 1s'
# metrics
- '{} | rate()'
- '{} | count_over_time() by (name) with(sample=0.1)'
- '{} | min_over_time(duration) by (span.http.path)'
- '{} | max_over_time(duration) by (span.http.path)'
- '{} | avg_over_time(duration) by (span.http.path)'
- '{} | sum_over_time(duration) by (span.http.path)'
- '{} | quantile_over_time(duration, 0, 0.9, 1) by (span.http.path)'
# second stage on metrics
- '{} | rate() | topk(10)'
- '{} | rate() by (span.client_ip) | topk(10)'
- '{} | count_over_time() by (name) | topk(10) with(sample=0.1)'
- '{} | quantile_over_time(duration, 0, 0.9, 1) by (span.http.path) | topk(10)'
- '{} | avg_over_time(duration) by (span:name) | topk(10)'
- '{} | rate() by (span.client_ip) | bottomk(10)'
- '{} | rate() | bottomk(10)'
- '{} | count_over_time() by (name) | bottomk(10) with(sample=0.1)'
- '{} | quantile_over_time(duration, 0, 0.9, 1) by (span.http.path) | bottomk(10)'
- '{} | avg_over_time(duration) by (span:name) | bottomk(10)'
# undocumented - nested set
- '{ nestedSetLeft > 3 }'
- '{ } >> { kind = server } | select(nestedSetLeft, nestedSetRight, nestedSetParent)'
# nil comparisons
- '{ nil != nil }'
- '{ nil = nil }'
- '{ .foo != nil }'
- '{ name != nil }'
# parse_fails throw an error when parsing
parse_fails:
- 'true'
- '[ true ]'
- '( true )'
# spanset filters
- '{ . }'
- '{ < }'
- '{ .a < }'
- '{ .a < 3'
- '{ (.a < 3 }'
- '{ attribute = 4 }' # custom attribute not prefixed with ., span., resource. or parent.
- '{ .attribute == 4 }' # invalid operator
- '{ span. }'
# spanset expressions
- '{ true } + { true }'
- '{ true } - { true }'
- '{ true } * { true }'
- '{ true } / { true }'
- '{ true } ^ { true }'
- '{ true } = { true }' # an interesting operator. possible future addition
- '{ true } <= { true }'
- '{ true } >= { true }'
# scalar expressions must evaluate to a number
- 'max(name) = "foo"'
- 'avg("foo") = "bar"'
- 'max(status) = ok'
- 'max(kind) = consumer'
- 'max(duration) < ok'
- 'min(1) = "foo"'
- 'min(parent) = nil'
- 'avg(span:childCount) > "foo"'
# scalar filters
- 'avg(.field) + 1' # scalar filters must resolve to boolean
- 'sum(3) - 2'
- 'min(span:childCount) && 2'
# select
- 'select(.a'
- 'select()'
- 'select(1 + "string")' # Don't support arbitrary field expressions
# pipelines
- 'coalesce() | { true }' # pipelines can't start with coalesce
- 'count() > 3 && { true }' # scalar filters have to be in pipeline
- '{ true } | count()' # naked scalar pipelines not allowed
- '{ true } | notAnAggregate() = 1'
- '{ true } | count = 1'
- '{ true } | max() = 1'
- '{ true } | by()'
# pipeline expressions
- '({ true }) + (count()) = 1'
- '({ true }) && (count())'
- '({ true } | count()) && ({ true } | count()) = 1'
- '({ true }) + ({ true }) = 1'
- '({ true } | count()) + ({ true } | count())'
# todo: improve the following
- '(by(namespace) | count()) > 2 * 2' # scalar expressions are currently not allowed in scalar pipelines
- '(by(namespace) | count()) * 2 > 2'
- '2 < (by(namespace) | count())' # static value needs to be on the RHS to remove conflicts with scalar expressions
# mismatched scoped intrinsics
- '{ trace:status = error }'
- '{ trace:kind = server }'
- '{ trace:name = "a" }'
- '{ trace:rootServiceName = "a" }' # should be trace:rootService only
- '{ span:rootName = "bar" }'
# invalid metrics second stage functions
- '{} | topk(10)'
- '{} | topk(10) with(sample=0.1)'
- '{} | bottomk(10)'
- '{} | bottomk(10) with(sample=0.1)'
- '{} | rate() | topk(-1)'
- '{} | rate() | bottomk(-1)'
# validate_fails parse correctly and return an error **besides unsupported** when calling .validate()
validate_fails:
# span expressions must evaluate to a boolean
- '{ status }'
- '{ kind }'
- '{ ok }'
- '{ 1.1 }'
- '{ 1h }'
- '{ "foo" }'
- '{ 1 + 1 }'
# binary operators - incorrect types
- '{ 1 + "foo" = 1 }'
- '{ 1 - true = 1 }'
- '{ 1 / ok = 1 }'
- '{ 1 ^ name = 1 }'
- '{ 1 = "foo" }'
- '{ 1 != true }'
- '{ 1 > ok }'
- '{ 1 = name }'
- '{ 1 =~ 2}'
- '{ 1 !~ 2}'
- '{ 1 && "foo" }'
- '{ 1 || ok }'
- '{ true || 1.1 }'
- '{ status > ok }'
- '{ kind < consumer }'
- '{ traceDuration > "test" }'
- '{ rootServiceName = 1 }'
- '{ rootName != 3.2 }'
- '{ "foo" = span:childCount }' # string comparison with int is invalid
- '{ span:status = nil }' # Intrinsics cannot be nil
- '{ nil = span:status }' # Intrinsics cannot be nil
- '{ resource.service.name = nil }' # special attribute resource.service.name cannot be nil
- '{ nil = resource.service.name }' # special attribute resource.service.name cannot be nil
# invalid regexes
- '{ span.foo =~ "[" }'
- '{ span.foo !~ "[" }'
# unary operators - incorrect types
- '{ -true }'
- '{ -"foo" = "bar" }'
- '{ -ok = status }'
- '{ -name = "foo" }'
- '{ !"foo" = "bar" }'
- '{ !ok = status }'
- '{ !consumer = kind }'
- '{ !name = "foo" }'
- '{ !1 = 1 }'
- '{ !1h = 1 }'
- '{ !1.1 = 1.1 }'
# scalar expressions must evaluate to a number
- 'min(1 = 3) = 1'
# scalar expressions must reference the span
- 'sum(3) = 2'
- 'sum(3) = min(14)'
- 'min(2h) < max(duration)'
- 'min(3) = max(duration)'
- 'min(1) = max(2) + 3'
- 'min(1.1 - 3) > 1'
- 'max(1h + 2h) > 1'
# by - will *not* be valid when supported - group expressions must reference the span
- '{ true } | by(1)'
- '{ true } | by("foo")'
# nested set params are ints
- '{ nestedSetLeft = "foo" }'
- '{ nestedSetRight = false }'
- '{ nestedSetParent > "foo" }'
# invalid metrics second stage functions
- '{} | rate() | topk(0)'
- '{} | rate() | topk(0) with(sample=0.1)'
- '{} | rate() | bottomk(0)'
- '{} | rate() | bottomk(0) with(sample=0.1)'
# compare function with second stage functions is not valid
- '{} | compare({status=error}) | topk(10)'
- '{} | compare({status=error}) | bottomk(10)'
# unsupported parse correctly and return an unsupported error when calling .validate()
unsupported:
# complex scalar filters - will be valid when supported
- 'min(.field) < max(duration)'
- 'sum(.field) = min(.field)'
- 'min(.field) + max(.field) > 1'
- 'min(.field) + max(span:childCount) > max(duration) - min(.field)'
- 'min(span:childCount) < 2 / 6'
- 'max(1 - (2 + .field)) < avg(3 * duration ^ 2)'
# aggregates - will be valid when supported
- '{ true } | max(parent.a) = 1'
- '{ .http.status = 200 } | max(.field) - min(.field) > 3'
# parent - will be valid when supported
- '{ parent.a != 3 }'
- '{ parent.resource.a && true }'
- '{ parent.span.a > 3 }'
- '{ parent.duration = 1h }'
- '{ (-(3 / 2) * .test - parent.blerg + .other)^3 = 2 }'
# parent - will not be valid when supported
- '{ parent }'
- '{ 1 % parent = 1 }'
- '{ 1 >= parent }'
# spanset pipelines + scalar filters - will be valid when supported
- '{ true } | count() + count() = 1'
- '({ true } | count()) + ({ true } | count()) = 1'
- '({ true } | count()) - ({ true } | count()) <= 1'
- '({ true } | count()) / ({ true } | count()) > ({ true } | count()) / ({ true } | count())'
- '({ true } | count()) * ({ true } | count()) < ({ true } | count()) / ({ true } | count())'
- '({ .http.status = 200 } | count()) + ({ name = `foo` } | avg(duration)) = 2'
- '({ .a } | count()) > ({ .b } | count())'
# other scalar filters. no idea if these should be supported
- '3 = 2' # naked scalar filter, technically allowed
- 'avg(.field) > 1 - 3' # scalar expressions in scalar filters are currently not allowed. possible future addition
# parsed and the ast is dumped to stdout. this is a debugging tool
dump: