jaegertracing/jaeger · error · ErrFilterUnsupported

%w: it evaluates a conjunction of predicates only

Error message

%w: it evaluates a conjunction of predicates only

What it means

flatConjuncts flattens an AND conjunction for ToLegacyShape conversion. Every argument of an OpAnd call must itself be a Call; a non-Call argument (e.g. a bare comparison constant or malformed node) cannot be expressed in the legacy conjunctive shape and yields ErrFilterUnsupported with 'conjunction of predicates only'.

Source

Thrown at internal/storage/v2/api/tracestore/shape.go:148

			return query, err
		}
	}
	return rewritten, nil
}

// flatConjuncts returns the leaf predicates of a conjunction. A nested `and` means the same
// as a flat one, so it is flattened rather than refused; `or` and `not` have no legacy form,
// so they are refused as the operators they are.
func flatConjuncts(filter *expression.Call) ([]*expression.Call, error) {
	switch filter.Op {
	case expression.OpOr, expression.OpNot:
		return nil, errUnsupportedOperator(filter.Op)
	case expression.OpAnd:
		conjuncts := make([]*expression.Call, 0, len(filter.Args))
		for _, arg := range filter.Args {
			nested, ok := arg.(*expression.Call)
			if !ok {
				return nil, fmt.Errorf("%w: it evaluates a conjunction of predicates only", ErrFilterUnsupported)
			}
			inner, err := flatConjuncts(nested)
			if err != nil {
				return nil, err
			}
			conjuncts = append(conjuncts, inner...)
		}
		return conjuncts, nil
	default:
		return []*expression.Call{filter}, nil
	}
}

// applyAsLegacyField writes one predicate into the legacy field that carries it. A legacy field
// holds a key and one value, so a predicate that reads no single value off the span — a
// quantifier over the events, an operator applied to a call — has nowhere to go.
func applyAsLegacyField(query *TraceQueryParams, predicate *expression.Call) error {
	if len(predicate.Args) != 2 {

View on GitHub (pinned to 806f444784)

Solutions

  1. Ensure every AND argument is a predicate Call (comparison or nested AND).
  2. Rebuild the expression using the expression package constructors instead of hand-built nodes.
  3. Catch this and fall back to the filter-capabilities path instead of legacy shape conversion.

Example fix

// before
expression.Call{Op: expression.OpAnd, Args: []expression.Expression{pred, someConstant}}
// after
expression.Call{Op: expression.OpAnd, Args: []expression.Expression{pred1, pred2}}
Defensive patterns

Strategy: validation

Validate before calling

func isPredicate(e expression.Expression) bool {
  _, ok := e.(*expression.Call)
  return ok
}
// assert every AND arg is a Call before calling ToLegacyShape

Type guard

func isCallNode(e expression.Expression) (*expression.Call, bool) {
  c, ok := e.(*expression.Call)
  return c, ok
}

Try / catch

shape, err := tracestore.ToLegacyShape(filter)
if err != nil {
  if errors.Is(err, tracestore.ErrFilterUnsupported) {
    return queryViaFilterPath(filter) // skip legacy conversion
  }
  return err
}

Prevention

When it happens

Trigger: Calling ToLegacyShape on a filter where an OpAnd node has a non-Call argument — i.e. a malformed or non-predicate expression nested directly under AND.

Common situations: Programmatically constructed expression trees with wrong node types; filters built by external tools that don't enforce the predicate-only AND contract.

Related errors


AI-assisted analysis of jaegertracing/jaeger@806f444784 (2026-09-01). Data as JSON: /api/errors/7edfb22135cb0d38. Report an issue: GitHub.