diff --git a/.gitignore b/.gitignore index 824e47ac5b6..a730dddcf13 100644 --- a/.gitignore +++ b/.gitignore @@ -26,3 +26,6 @@ integration/e2e/deployments/e2e_integration_test[0-9]* /tmp gh-token.txt .cache +.devcontainer +testdata +k6 diff --git a/tempodb/encoding/vparquet2/block_traceql.go b/tempodb/encoding/vparquet2/block_traceql.go index afbafa11e9d..49dd427b6dc 100644 --- a/tempodb/encoding/vparquet2/block_traceql.go +++ b/tempodb/encoding/vparquet2/block_traceql.go @@ -1759,6 +1759,45 @@ func createIntPredicate(op traceql.Operator, operands traceql.Operands) (parquet } } +// createIntPredicateFromFloat adapts float comparisons for integer fields. +// If the float has no fractional part, it's treated as an integer directly. +// Otherwise, depending on the operator, the comparison boundary is shifted up or down +// to the nearest integer, or a trivial true/false predicate is returned as needed. +func createIntPredicateFromFloat(op traceql.Operator, operands traceql.Operands) (parquetquery.Predicate, error) { + if op == traceql.OpNone { + return nil, nil + } + + f := operands[0].Float() + + // Treat it as an integer if the fractional part of the float is zero + if _, frac := math.Modf(f); frac == 0 { + operands := traceql.Operands{traceql.NewStaticInt(int(f))} + return createIntPredicate(op, operands) + } + + switch op { + case traceql.OpEqual: + // No reason to search for an exact match among integers if the float has a fractional part + return nil, nil + case traceql.OpNotEqual: + // An integer always differs from a float with a fraction + return parquetquery.NewCallbackPredicate(func() bool { return true }), nil + case traceql.OpGreater, traceql.OpGreaterEqual: + // Shift to the next integer for a float with a fraction + // { .attr > 123.4 } -> { .attr >= 124 } + // { .attr >= 123.4 } -> { .attr >= 124 } + return createIntPredicate(traceql.OpGreaterEqual, traceql.Operands{traceql.NewStaticInt(int(f) + 1)}) + case traceql.OpLess, traceql.OpLessEqual: + // Use the integer floor for a float with a fraction + // { .attr < 123.4 } -> { .attr <= 123 } + // { .attr <= 123.4 } -> { .attr <= 123 } + return createIntPredicate(traceql.OpLessEqual, traceql.Operands{traceql.NewStaticInt(int(f))}) + default: + return nil, fmt.Errorf("operator not supported for integers(from float): %+v", op) + } +} + func createFloatPredicate(op traceql.Operator, operands traceql.Operands) (parquetquery.Predicate, error) { if op == traceql.OpNone { return nil, nil @@ -1846,13 +1885,33 @@ func createAttributeIterator(makeIter makeIterFn, conditions []traceql.Condition attrStringPreds = append(attrStringPreds, pred) case traceql.TypeInt: + // Create a predicate specifically for integer comparisons pred, err := createIntPredicate(cond.Op, cond.Operands) if err != nil { return nil, fmt.Errorf("creating attribute predicate: %w", err) } attrIntPreds = append(attrIntPreds, pred) + // If the operand can also be interpreted as a float, create an additional predicate + if i, ok := cond.Operands[0].Int(); ok { + // Convert the integer operand to a float to handle cross-type comparisons + operands := traceql.Operands{traceql.NewStaticFloat(float64(i))} + pred, err := createFloatPredicate(cond.Op, operands) + if err != nil { + return nil, fmt.Errorf("creating attribute predicate: %w", err) + } + attrFltPreds = append(attrFltPreds, pred) + } + case traceql.TypeFloat: + // Attempt to create a predicate for integer comparisons, if applicable + if pred, err := createIntPredicateFromFloat(cond.Op, cond.Operands); err != nil { + return nil, fmt.Errorf("creating attribute predicate: %w", err) + } else if pred != nil { + attrIntPreds = append(attrIntPreds, pred) + } + + // Create a predicate specifically for float comparisons pred, err := createFloatPredicate(cond.Op, cond.Operands) if err != nil { return nil, fmt.Errorf("creating attribute predicate: %w", err) diff --git a/tempodb/encoding/vparquet2/block_traceql_test.go b/tempodb/encoding/vparquet2/block_traceql_test.go index 92fc146c929..2aa400d4c80 100644 --- a/tempodb/encoding/vparquet2/block_traceql_test.go +++ b/tempodb/encoding/vparquet2/block_traceql_test.go @@ -209,6 +209,31 @@ func TestBackendBlockSearchTraceQL(t *testing.T) { parse(t, `{resource.`+LabelServiceName+` <= 124}`), }, }, + // Cross-type comparisons + traceql.MustExtractFetchSpansRequestWithMetadata(`{.crossint > 122.9}`), + traceql.MustExtractFetchSpansRequestWithMetadata(`{.crossint >= 122.9}`), + traceql.MustExtractFetchSpansRequestWithMetadata(`{.crossint <= 123.0}`), + traceql.MustExtractFetchSpansRequestWithMetadata(`{.crossint = 123.0}`), + traceql.MustExtractFetchSpansRequestWithMetadata(`{.crossint != 123.1}`), + traceql.MustExtractFetchSpansRequestWithMetadata(`{.crossint >= 123.0}`), + traceql.MustExtractFetchSpansRequestWithMetadata(`{.crossint < 123.1}`), + traceql.MustExtractFetchSpansRequestWithMetadata(`{.crossint <= 123.1}`), + traceql.MustExtractFetchSpansRequestWithMetadata(`{.crossfloat_nofrag > 455}`), + traceql.MustExtractFetchSpansRequestWithMetadata(`{.crossfloat_nofrag >= 455}`), + traceql.MustExtractFetchSpansRequestWithMetadata(`{.crossfloat_nofrag <= 456}`), + traceql.MustExtractFetchSpansRequestWithMetadata(`{.crossfloat_nofrag = 456}`), + traceql.MustExtractFetchSpansRequestWithMetadata(`{.crossfloat_nofrag != 457}`), + traceql.MustExtractFetchSpansRequestWithMetadata(`{.crossfloat_nofrag >= 456}`), + traceql.MustExtractFetchSpansRequestWithMetadata(`{.crossfloat_nofrag <= 457}`), + traceql.MustExtractFetchSpansRequestWithMetadata(`{.crossfloat_nofrag < 457}`), + traceql.MustExtractFetchSpansRequestWithMetadata(`{.crossfloat_frag != 455}`), + traceql.MustExtractFetchSpansRequestWithMetadata(`{.crossfloat_frag > 455}`), + traceql.MustExtractFetchSpansRequestWithMetadata(`{.crossfloat_frag >= 455}`), + traceql.MustExtractFetchSpansRequestWithMetadata(`{.crossfloat_frag > 456}`), + traceql.MustExtractFetchSpansRequestWithMetadata(`{.crossfloat_frag >= 456}`), + traceql.MustExtractFetchSpansRequestWithMetadata(`{.crossfloat_frag <= 457}`), + traceql.MustExtractFetchSpansRequestWithMetadata(`{.crossfloat_frag < 457}`), + traceql.MustExtractFetchSpansRequestWithMetadata(`{.crossfloat_frag != 457}`), } for _, req := range searchesThatMatch { @@ -316,6 +341,31 @@ func TestBackendBlockSearchTraceQL(t *testing.T) { parse(t, `{`+LabelDuration+` = 100s }`), // Match }, }, + // Cross-type comparisons + traceql.MustExtractFetchSpansRequestWithMetadata(`{.crossint < 122.9}`), + traceql.MustExtractFetchSpansRequestWithMetadata(`{.crossint = 122.9}`), + traceql.MustExtractFetchSpansRequestWithMetadata(`{.crossint <= 122.9}`), + traceql.MustExtractFetchSpansRequestWithMetadata(`{.crossint < 123.0}`), + traceql.MustExtractFetchSpansRequestWithMetadata(`{.crossint != 123.0}`), + traceql.MustExtractFetchSpansRequestWithMetadata(`{.crossint > 123.0}`), + traceql.MustExtractFetchSpansRequestWithMetadata(`{.crossint >= 123.1}`), + traceql.MustExtractFetchSpansRequestWithMetadata(`{.crossint = 123.1}`), + traceql.MustExtractFetchSpansRequestWithMetadata(`{.crossint > 123.1}`), + traceql.MustExtractFetchSpansRequestWithMetadata(`{.crossfloat_nofrag < 455}`), + traceql.MustExtractFetchSpansRequestWithMetadata(`{.crossfloat_nofrag = 455}`), + traceql.MustExtractFetchSpansRequestWithMetadata(`{.crossfloat_nofrag <= 455}`), + traceql.MustExtractFetchSpansRequestWithMetadata(`{.crossfloat_nofrag < 456}`), + traceql.MustExtractFetchSpansRequestWithMetadata(`{.crossfloat_nofrag != 456}`), + traceql.MustExtractFetchSpansRequestWithMetadata(`{.crossfloat_nofrag > 456}`), + traceql.MustExtractFetchSpansRequestWithMetadata(`{.crossfloat_nofrag >= 457}`), + traceql.MustExtractFetchSpansRequestWithMetadata(`{.crossfloat_nofrag = 457}`), + traceql.MustExtractFetchSpansRequestWithMetadata(`{.crossfloat_nofrag > 457}`), + traceql.MustExtractFetchSpansRequestWithMetadata(`{.crossfloat_frag < 456}`), + traceql.MustExtractFetchSpansRequestWithMetadata(`{.crossfloat_frag = 456}`), + traceql.MustExtractFetchSpansRequestWithMetadata(`{.crossfloat_frag <= 456}`), + traceql.MustExtractFetchSpansRequestWithMetadata(`{.crossfloat_frag >= 457}`), + traceql.MustExtractFetchSpansRequestWithMetadata(`{.crossfloat_frag = 457}`), + traceql.MustExtractFetchSpansRequestWithMetadata(`{.crossfloat_frag > 457}`), } for _, req := range searchesThatDontMatch { @@ -431,7 +481,10 @@ func fullyPopulatedTestTrace(id common.ID) *Trace { {Key: "bar", ValueInt: intPtr(123)}, {Key: "float", ValueDouble: fltPtr(456.78)}, {Key: "bool", ValueBool: boolPtr(false)}, - + // For cross-type comparisons + {Key: "crossint", ValueInt: intPtr(123)}, + {Key: "crossfloat_nofrag", ValueDouble: fltPtr(456.0)}, + {Key: "crossfloat_frag", ValueDouble: fltPtr(456.78)}, // Edge-cases {Key: LabelName, Value: strPtr("Bob")}, // Conflicts with intrinsic but still looked up by .name {Key: LabelServiceName, Value: strPtr("spanservicename")}, // Overrides resource-level dedicated column diff --git a/tempodb/encoding/vparquet3/block_traceql.go b/tempodb/encoding/vparquet3/block_traceql.go index 209c757c644..b3b24435a60 100644 --- a/tempodb/encoding/vparquet3/block_traceql.go +++ b/tempodb/encoding/vparquet3/block_traceql.go @@ -2063,6 +2063,45 @@ func createIntPredicate(op traceql.Operator, operands traceql.Operands) (parquet } } +// createIntPredicateFromFloat adapts float comparisons for integer fields. +// If the float has no fractional part, it's treated as an integer directly. +// Otherwise, depending on the operator, the comparison boundary is shifted up or down +// to the nearest integer, or a trivial true/false predicate is returned as needed. +func createIntPredicateFromFloat(op traceql.Operator, operands traceql.Operands) (parquetquery.Predicate, error) { + if op == traceql.OpNone { + return nil, nil + } + + f := operands[0].Float() + + // Treat it as an integer if the fractional part of the float is zero + if _, frac := math.Modf(f); frac == 0 { + operands := traceql.Operands{traceql.NewStaticInt(int(f))} + return createIntPredicate(op, operands) + } + + switch op { + case traceql.OpEqual: + // No reason to search for an exact match among integers if the float has a fractional part + return nil, nil + case traceql.OpNotEqual: + // An integer always differs from a float with a fraction + return parquetquery.NewCallbackPredicate(func() bool { return true }), nil + case traceql.OpGreater, traceql.OpGreaterEqual: + // Shift to the next integer for a float with a fraction + // { .attr > 123.4 } -> { .attr >= 124 } + // { .attr >= 123.4 } -> { .attr >= 124 } + return createIntPredicate(traceql.OpGreaterEqual, traceql.Operands{traceql.NewStaticInt(int(f) + 1)}) + case traceql.OpLess, traceql.OpLessEqual: + // Use the integer floor for a float with a fraction + // { .attr < 123.4 } -> { .attr <= 123 } + // { .attr <= 123.4 } -> { .attr <= 123 } + return createIntPredicate(traceql.OpLessEqual, traceql.Operands{traceql.NewStaticInt(int(f))}) + default: + return nil, fmt.Errorf("operator not supported for integers(from float): %+v", op) + } +} + func createFloatPredicate(op traceql.Operator, operands traceql.Operands) (parquetquery.Predicate, error) { if op == traceql.OpNone { return nil, nil @@ -2164,13 +2203,33 @@ func createAttributeIterator(makeIter makeIterFn, conditions []traceql.Condition attrStringPreds = append(attrStringPreds, pred) case traceql.TypeInt: + // Create a predicate specifically for integer comparisons pred, err := createIntPredicate(cond.Op, cond.Operands) if err != nil { return nil, fmt.Errorf("creating attribute predicate: %w", err) } attrIntPreds = append(attrIntPreds, pred) + // If the operand can also be interpreted as a float, create an additional predicate + if i, ok := cond.Operands[0].Int(); ok { + // Convert the integer operand to a float to handle cross-type comparisons + operands := traceql.Operands{traceql.NewStaticFloat(float64(i))} + pred, err := createFloatPredicate(cond.Op, operands) + if err != nil { + return nil, fmt.Errorf("creating attribute predicate: %w", err) + } + attrFltPreds = append(attrFltPreds, pred) + } + case traceql.TypeFloat: + // Attempt to create a predicate for integer comparisons, if applicable + if pred, err := createIntPredicateFromFloat(cond.Op, cond.Operands); err != nil { + return nil, fmt.Errorf("creating attribute predicate: %w", err) + } else if pred != nil { + attrIntPreds = append(attrIntPreds, pred) + } + + // Create a predicate specifically for float comparisons pred, err := createFloatPredicate(cond.Op, cond.Operands) if err != nil { return nil, fmt.Errorf("creating attribute predicate: %w", err) diff --git a/tempodb/encoding/vparquet3/block_traceql_test.go b/tempodb/encoding/vparquet3/block_traceql_test.go index 8ab94aff874..e70da7b531a 100644 --- a/tempodb/encoding/vparquet3/block_traceql_test.go +++ b/tempodb/encoding/vparquet3/block_traceql_test.go @@ -222,6 +222,31 @@ func TestBackendBlockSearchTraceQL(t *testing.T) { }, }, }, + // Cross-type comparisons + {".crossint > 122.9", traceql.MustExtractFetchSpansRequestWithMetadata(`{.crossint > 122.9}`)}, + {".crossint >= 122.9", traceql.MustExtractFetchSpansRequestWithMetadata(`{.crossint >= 122.9}`)}, + {".crossint <= 123.0", traceql.MustExtractFetchSpansRequestWithMetadata(`{.crossint <= 123.0}`)}, + {".crossint = 123.0", traceql.MustExtractFetchSpansRequestWithMetadata(`{.crossint = 123.0}`)}, + {".crossint != 123.1", traceql.MustExtractFetchSpansRequestWithMetadata(`{.crossint != 123.1}`)}, + {".crossint >= 123.0", traceql.MustExtractFetchSpansRequestWithMetadata(`{.crossint >= 123.0}`)}, + {".crossint < 123.1", traceql.MustExtractFetchSpansRequestWithMetadata(`{.crossint < 123.1}`)}, + {".crossint <= 123.1", traceql.MustExtractFetchSpansRequestWithMetadata(`{.crossint <= 123.1}`)}, + {".crossfloat_nofrag > 455", traceql.MustExtractFetchSpansRequestWithMetadata(`{.crossfloat_nofrag > 455}`)}, + {".crossfloat_nofrag >= 455", traceql.MustExtractFetchSpansRequestWithMetadata(`{.crossfloat_nofrag >= 455}`)}, + {".crossfloat_nofrag <= 456", traceql.MustExtractFetchSpansRequestWithMetadata(`{.crossfloat_nofrag <= 456}`)}, + {".crossfloat_nofrag = 456", traceql.MustExtractFetchSpansRequestWithMetadata(`{.crossfloat_nofrag = 456}`)}, + {".crossfloat_nofrag != 457", traceql.MustExtractFetchSpansRequestWithMetadata(`{.crossfloat_nofrag != 457}`)}, + {".crossfloat_nofrag >= 456", traceql.MustExtractFetchSpansRequestWithMetadata(`{.crossfloat_nofrag >= 456}`)}, + {".crossfloat_nofrag <= 457", traceql.MustExtractFetchSpansRequestWithMetadata(`{.crossfloat_nofrag <= 457}`)}, + {".crossfloat_nofrag < 457", traceql.MustExtractFetchSpansRequestWithMetadata(`{.crossfloat_nofrag < 457}`)}, + {".crossfloat_frag != 455", traceql.MustExtractFetchSpansRequestWithMetadata(`{.crossfloat_frag != 455}`)}, + {".crossfloat_frag > 455", traceql.MustExtractFetchSpansRequestWithMetadata(`{.crossfloat_frag > 455}`)}, + {".crossfloat_frag >= 455", traceql.MustExtractFetchSpansRequestWithMetadata(`{.crossfloat_frag >= 455}`)}, + {".crossfloat_frag > 456", traceql.MustExtractFetchSpansRequestWithMetadata(`{.crossfloat_frag > 456}`)}, + {".crossfloat_frag >= 456", traceql.MustExtractFetchSpansRequestWithMetadata(`{.crossfloat_frag >= 456}`)}, + {".crossfloat_frag <= 457", traceql.MustExtractFetchSpansRequestWithMetadata(`{.crossfloat_frag <= 457}`)}, + {".crossfloat_frag < 457", traceql.MustExtractFetchSpansRequestWithMetadata(`{.crossfloat_frag < 457}`)}, + {".crossfloat_frag != 457", traceql.MustExtractFetchSpansRequestWithMetadata(`{.crossfloat_frag != 457}`)}, } for _, tc := range searchesThatMatch { @@ -348,6 +373,31 @@ func TestBackendBlockSearchTraceQL(t *testing.T) { }, }, }, + // Cross-type comparisons + {".crossint < 122.9", traceql.MustExtractFetchSpansRequestWithMetadata(`{.crossint < 122.9}`)}, + {".crossint = 122.9", traceql.MustExtractFetchSpansRequestWithMetadata(`{.crossint = 122.9}`)}, + {".crossint <= 122.9", traceql.MustExtractFetchSpansRequestWithMetadata(`{.crossint <= 122.9}`)}, + {".crossint < 123.0", traceql.MustExtractFetchSpansRequestWithMetadata(`{.crossint < 123.0}`)}, + {".crossint != 123.0", traceql.MustExtractFetchSpansRequestWithMetadata(`{.crossint != 123.0}`)}, + {".crossint > 123.0", traceql.MustExtractFetchSpansRequestWithMetadata(`{.crossint > 123.0}`)}, + {".crossint >= 123.1", traceql.MustExtractFetchSpansRequestWithMetadata(`{.crossint >= 123.1}`)}, + {".crossint = 123.1", traceql.MustExtractFetchSpansRequestWithMetadata(`{.crossint = 123.1}`)}, + {".crossint > 123.1", traceql.MustExtractFetchSpansRequestWithMetadata(`{.crossint > 123.1}`)}, + {".crossfloat_nofrag < 455", traceql.MustExtractFetchSpansRequestWithMetadata(`{.crossfloat_nofrag < 455}`)}, + {".crossfloat_nofrag = 455", traceql.MustExtractFetchSpansRequestWithMetadata(`{.crossfloat_nofrag = 455}`)}, + {".crossfloat_nofrag <= 455", traceql.MustExtractFetchSpansRequestWithMetadata(`{.crossfloat_nofrag <= 455}`)}, + {".crossfloat_nofrag < 456", traceql.MustExtractFetchSpansRequestWithMetadata(`{.crossfloat_nofrag < 456}`)}, + {".crossfloat_nofrag != 456", traceql.MustExtractFetchSpansRequestWithMetadata(`{.crossfloat_nofrag != 456}`)}, + {".crossfloat_nofrag > 456", traceql.MustExtractFetchSpansRequestWithMetadata(`{.crossfloat_nofrag > 456}`)}, + {".crossfloat_nofrag >= 457", traceql.MustExtractFetchSpansRequestWithMetadata(`{.crossfloat_nofrag >= 457}`)}, + {".crossfloat_nofrag = 457", traceql.MustExtractFetchSpansRequestWithMetadata(`{.crossfloat_nofrag = 457}`)}, + {".crossfloat_nofrag > 457", traceql.MustExtractFetchSpansRequestWithMetadata(`{.crossfloat_nofrag > 457}`)}, + {".crossfloat_frag < 456", traceql.MustExtractFetchSpansRequestWithMetadata(`{.crossfloat_frag < 456}`)}, + {".crossfloat_frag = 456", traceql.MustExtractFetchSpansRequestWithMetadata(`{.crossfloat_frag = 456}`)}, + {".crossfloat_frag <= 456", traceql.MustExtractFetchSpansRequestWithMetadata(`{.crossfloat_frag <= 456}`)}, + {".crossfloat_frag >= 457", traceql.MustExtractFetchSpansRequestWithMetadata(`{.crossfloat_frag >= 457}`)}, + {".crossfloat_frag = 457", traceql.MustExtractFetchSpansRequestWithMetadata(`{.crossfloat_frag = 457}`)}, + {".crossfloat_frag > 457", traceql.MustExtractFetchSpansRequestWithMetadata(`{.crossfloat_frag > 457}`)}, } for _, tc := range searchesThatDontMatch { @@ -475,7 +525,10 @@ func fullyPopulatedTestTrace(id common.ID) *Trace { {Key: "bar", ValueInt: intPtr(123)}, {Key: "float", ValueDouble: fltPtr(456.78)}, {Key: "bool", ValueBool: boolPtr(false)}, - + // For cross-type comparisons + {Key: "crossint", ValueInt: intPtr(123)}, + {Key: "crossfloat_nofrag", ValueDouble: fltPtr(456.0)}, + {Key: "crossfloat_frag", ValueDouble: fltPtr(456.78)}, // Edge-cases {Key: LabelName, Value: strPtr("Bob")}, // Conflicts with intrinsic but still looked up by .name {Key: LabelServiceName, Value: strPtr("spanservicename")}, // Overrides resource-level dedicated column diff --git a/tempodb/encoding/vparquet4/block_traceql.go b/tempodb/encoding/vparquet4/block_traceql.go index f67fc272e36..1049fef9dd8 100644 --- a/tempodb/encoding/vparquet4/block_traceql.go +++ b/tempodb/encoding/vparquet4/block_traceql.go @@ -2537,6 +2537,45 @@ func createIntPredicate(op traceql.Operator, operands traceql.Operands) (parquet } } +// createIntPredicateFromFloat adapts float comparisons for integer fields. +// If the float has no fractional part, it's treated as an integer directly. +// Otherwise, depending on the operator, the comparison boundary is shifted up or down +// to the nearest integer, or a trivial true/false predicate is returned as needed. +func createIntPredicateFromFloat(op traceql.Operator, operands traceql.Operands) (parquetquery.Predicate, error) { + if op == traceql.OpNone { + return nil, nil + } + + f := operands[0].Float() + + // Treat it as an integer if the fractional part of the float is zero + if _, frac := math.Modf(f); frac == 0 { + operands := traceql.Operands{traceql.NewStaticInt(int(f))} + return createIntPredicate(op, operands) + } + + switch op { + case traceql.OpEqual: + // No reason to search for an exact match among integers if the float has a fractional part + return nil, nil + case traceql.OpNotEqual: + // An integer always differs from a float with a fraction + return parquetquery.NewCallbackPredicate(func() bool { return true }), nil + case traceql.OpGreater, traceql.OpGreaterEqual: + // Shift to the next integer for a float with a fraction + // { .attr > 123.4 } -> { .attr >= 124 } + // { .attr >= 123.4 } -> { .attr >= 124 } + return createIntPredicate(traceql.OpGreaterEqual, traceql.Operands{traceql.NewStaticInt(int(f) + 1)}) + case traceql.OpLess, traceql.OpLessEqual: + // Use the integer floor for a float with a fraction + // { .attr < 123.4 } -> { .attr <= 123 } + // { .attr <= 123.4 } -> { .attr <= 123 } + return createIntPredicate(traceql.OpLessEqual, traceql.Operands{traceql.NewStaticInt(int(f))}) + default: + return nil, fmt.Errorf("operator not supported for integers(from float): %+v", op) + } +} + func createFloatPredicate(op traceql.Operator, operands traceql.Operands) (parquetquery.Predicate, error) { if op == traceql.OpNone { return nil, nil @@ -2637,13 +2676,33 @@ func createAttributeIterator(makeIter makeIterFn, conditions []traceql.Condition attrStringPreds = append(attrStringPreds, pred) case traceql.TypeInt: + // Create a predicate specifically for integer comparisons pred, err := createIntPredicate(cond.Op, cond.Operands) if err != nil { return nil, fmt.Errorf("creating attribute predicate: %w", err) } attrIntPreds = append(attrIntPreds, pred) + // If the operand can also be interpreted as a float, create an additional predicate + if i, ok := cond.Operands[0].Int(); ok { + // Convert the integer operand to a float to handle cross-type comparisons + operands := traceql.Operands{traceql.NewStaticFloat(float64(i))} + pred, err := createFloatPredicate(cond.Op, operands) + if err != nil { + return nil, fmt.Errorf("creating attribute predicate: %w", err) + } + attrFltPreds = append(attrFltPreds, pred) + } + case traceql.TypeFloat: + // Attempt to create a predicate for integer comparisons, if applicable + if pred, err := createIntPredicateFromFloat(cond.Op, cond.Operands); err != nil { + return nil, fmt.Errorf("creating attribute predicate: %w", err) + } else if pred != nil { + attrIntPreds = append(attrIntPreds, pred) + } + + // Create a predicate specifically for float comparisons pred, err := createFloatPredicate(cond.Op, cond.Operands) if err != nil { return nil, fmt.Errorf("creating attribute predicate: %w", err) diff --git a/tempodb/encoding/vparquet4/block_traceql_test.go b/tempodb/encoding/vparquet4/block_traceql_test.go index b5c0701ebe5..ac45fdf4a7b 100644 --- a/tempodb/encoding/vparquet4/block_traceql_test.go +++ b/tempodb/encoding/vparquet4/block_traceql_test.go @@ -244,6 +244,31 @@ func TestBackendBlockSearchTraceQL(t *testing.T) { }, }, }, + // Cross-type comparisons + {".crossint > 122.9", traceql.MustExtractFetchSpansRequestWithMetadata(`{.crossint > 122.9}`)}, + {".crossint >= 122.9", traceql.MustExtractFetchSpansRequestWithMetadata(`{.crossint >= 122.9}`)}, + {".crossint <= 123.0", traceql.MustExtractFetchSpansRequestWithMetadata(`{.crossint <= 123.0}`)}, + {".crossint = 123.0", traceql.MustExtractFetchSpansRequestWithMetadata(`{.crossint = 123.0}`)}, + {".crossint != 123.1", traceql.MustExtractFetchSpansRequestWithMetadata(`{.crossint != 123.1}`)}, + {".crossint >= 123.0", traceql.MustExtractFetchSpansRequestWithMetadata(`{.crossint >= 123.0}`)}, + {".crossint < 123.1", traceql.MustExtractFetchSpansRequestWithMetadata(`{.crossint < 123.1}`)}, + {".crossint <= 123.1", traceql.MustExtractFetchSpansRequestWithMetadata(`{.crossint <= 123.1}`)}, + {".crossfloat_nofrag > 455", traceql.MustExtractFetchSpansRequestWithMetadata(`{.crossfloat_nofrag > 455}`)}, + {".crossfloat_nofrag >= 455", traceql.MustExtractFetchSpansRequestWithMetadata(`{.crossfloat_nofrag >= 455}`)}, + {".crossfloat_nofrag <= 456", traceql.MustExtractFetchSpansRequestWithMetadata(`{.crossfloat_nofrag <= 456}`)}, + {".crossfloat_nofrag = 456", traceql.MustExtractFetchSpansRequestWithMetadata(`{.crossfloat_nofrag = 456}`)}, + {".crossfloat_nofrag != 457", traceql.MustExtractFetchSpansRequestWithMetadata(`{.crossfloat_nofrag != 457}`)}, + {".crossfloat_nofrag >= 456", traceql.MustExtractFetchSpansRequestWithMetadata(`{.crossfloat_nofrag >= 456}`)}, + {".crossfloat_nofrag <= 457", traceql.MustExtractFetchSpansRequestWithMetadata(`{.crossfloat_nofrag <= 457}`)}, + {".crossfloat_nofrag < 457", traceql.MustExtractFetchSpansRequestWithMetadata(`{.crossfloat_nofrag < 457}`)}, + {".crossfloat_frag != 455", traceql.MustExtractFetchSpansRequestWithMetadata(`{.crossfloat_frag != 455}`)}, + {".crossfloat_frag > 455", traceql.MustExtractFetchSpansRequestWithMetadata(`{.crossfloat_frag > 455}`)}, + {".crossfloat_frag >= 455", traceql.MustExtractFetchSpansRequestWithMetadata(`{.crossfloat_frag >= 455}`)}, + {".crossfloat_frag > 456", traceql.MustExtractFetchSpansRequestWithMetadata(`{.crossfloat_frag > 456}`)}, + {".crossfloat_frag >= 456", traceql.MustExtractFetchSpansRequestWithMetadata(`{.crossfloat_frag >= 456}`)}, + {".crossfloat_frag <= 457", traceql.MustExtractFetchSpansRequestWithMetadata(`{.crossfloat_frag <= 457}`)}, + {".crossfloat_frag < 457", traceql.MustExtractFetchSpansRequestWithMetadata(`{.crossfloat_frag < 457}`)}, + {".crossfloat_frag != 457", traceql.MustExtractFetchSpansRequestWithMetadata(`{.crossfloat_frag != 457}`)}, } for _, tc := range searchesThatMatch { @@ -373,6 +398,31 @@ func TestBackendBlockSearchTraceQL(t *testing.T) { }, }, }, + // Cross-type comparisons + {".crossint < 122.9", traceql.MustExtractFetchSpansRequestWithMetadata(`{.crossint < 122.9}`)}, + {".crossint = 122.9", traceql.MustExtractFetchSpansRequestWithMetadata(`{.crossint = 122.9}`)}, + {".crossint <= 122.9", traceql.MustExtractFetchSpansRequestWithMetadata(`{.crossint <= 122.9}`)}, + {".crossint < 123.0", traceql.MustExtractFetchSpansRequestWithMetadata(`{.crossint < 123.0}`)}, + {".crossint != 123.0", traceql.MustExtractFetchSpansRequestWithMetadata(`{.crossint != 123.0}`)}, + {".crossint > 123.0", traceql.MustExtractFetchSpansRequestWithMetadata(`{.crossint > 123.0}`)}, + {".crossint >= 123.1", traceql.MustExtractFetchSpansRequestWithMetadata(`{.crossint >= 123.1}`)}, + {".crossint = 123.1", traceql.MustExtractFetchSpansRequestWithMetadata(`{.crossint = 123.1}`)}, + {".crossint > 123.1", traceql.MustExtractFetchSpansRequestWithMetadata(`{.crossint > 123.1}`)}, + {".crossfloat_nofrag < 455", traceql.MustExtractFetchSpansRequestWithMetadata(`{.crossfloat_nofrag < 455}`)}, + {".crossfloat_nofrag = 455", traceql.MustExtractFetchSpansRequestWithMetadata(`{.crossfloat_nofrag = 455}`)}, + {".crossfloat_nofrag <= 455", traceql.MustExtractFetchSpansRequestWithMetadata(`{.crossfloat_nofrag <= 455}`)}, + {".crossfloat_nofrag < 456", traceql.MustExtractFetchSpansRequestWithMetadata(`{.crossfloat_nofrag < 456}`)}, + {".crossfloat_nofrag != 456", traceql.MustExtractFetchSpansRequestWithMetadata(`{.crossfloat_nofrag != 456}`)}, + {".crossfloat_nofrag > 456", traceql.MustExtractFetchSpansRequestWithMetadata(`{.crossfloat_nofrag > 456}`)}, + {".crossfloat_nofrag >= 457", traceql.MustExtractFetchSpansRequestWithMetadata(`{.crossfloat_nofrag >= 457}`)}, + {".crossfloat_nofrag = 457", traceql.MustExtractFetchSpansRequestWithMetadata(`{.crossfloat_nofrag = 457}`)}, + {".crossfloat_nofrag > 457", traceql.MustExtractFetchSpansRequestWithMetadata(`{.crossfloat_nofrag > 457}`)}, + {".crossfloat_frag < 456", traceql.MustExtractFetchSpansRequestWithMetadata(`{.crossfloat_frag < 456}`)}, + {".crossfloat_frag = 456", traceql.MustExtractFetchSpansRequestWithMetadata(`{.crossfloat_frag = 456}`)}, + {".crossfloat_frag <= 456", traceql.MustExtractFetchSpansRequestWithMetadata(`{.crossfloat_frag <= 456}`)}, + {".crossfloat_frag >= 457", traceql.MustExtractFetchSpansRequestWithMetadata(`{.crossfloat_frag >= 457}`)}, + {".crossfloat_frag = 457", traceql.MustExtractFetchSpansRequestWithMetadata(`{.crossfloat_frag = 457}`)}, + {".crossfloat_frag > 457", traceql.MustExtractFetchSpansRequestWithMetadata(`{.crossfloat_frag > 457}`)}, } for _, tc := range searchesThatDontMatch { @@ -573,6 +623,11 @@ func fullyPopulatedTestTrace(id common.ID) *Trace { attr("foo", "def"), attr("bar", 123), attr("float", 456.78), + // For cross-type comparisons + attr("crossint", 123), + attr("crossfloat_nofrag", 456.0), + attr("crossfloat_frag", 456.78), + // attr("bool", false), attr("str-array", []string{"value-one", "value-two"}), attr("int-array", []int64{111, 222, 333, 444}), diff --git a/tempodb/tempodb_search_test.go b/tempodb/tempodb_search_test.go index e3f57642621..d12ec21bbf1 100644 --- a/tempodb/tempodb_search_test.go +++ b/tempodb/tempodb_search_test.go @@ -56,6 +56,7 @@ func TestSearchCompleteBlock(t *testing.T) { nestedSet, tagValuesRunner, tagNamesRunner, + traceQLCrossType, ) }) if vers == vparquet4.VersionString { @@ -134,6 +135,100 @@ func traceQLRunner(t *testing.T, _ *tempopb.Trace, wantMeta *tempopb.TraceSearch } } +func traceQLCrossType(t *testing.T, _ *tempopb.Trace, wantMeta *tempopb.TraceSearchMetadata, searchesThatMatch, searchesThatDontMatch []*tempopb.SearchRequest, meta *backend.BlockMeta, r Reader, _ common.BackendBlock) { + ctx := context.Background() + e := traceql.NewEngine() + + quotedAttributesThatMatch := []*tempopb.SearchRequest{ + {Query: `{ .floatAttr > 123.0 }`}, + {Query: `{ .floatAttr >= 123.0 }`}, + {Query: `{ .floatAttr <= 123.4 }`}, + {Query: `{ .floatAttr = 123.4 }`}, + {Query: `{ .floatAttr >= 123.4 }`}, + {Query: `{ .floatAttr <= 123.9 }`}, + {Query: `{ .floatAttr < 123.9 }`}, + {Query: `{ .intAttr > 122 }`}, + {Query: `{ .intAttr >= 122 }`}, + {Query: `{ .intAttr <= 123 }`}, + {Query: `{ .intAttr = 123 }`}, + {Query: `{ .intAttr >= 123 }`}, + {Query: `{ .intAttr <= 124 }`}, + {Query: `{ .intAttr < 124 }`}, + {Query: `{ .floatAttr > 123 }`}, + {Query: `{ .floatAttr >= 123 }`}, + {Query: `{ .floatAttr <= 124 }`}, + {Query: `{ .floatAttr < 124 }`}, + {Query: `{ .intAttr > 122.9 }`}, + {Query: `{ .intAttr >= 122.9 }`}, + {Query: `{ .intAttr <= 123.0 }`}, + {Query: `{ .intAttr = 123.0 }`}, + {Query: `{ .intAttr >= 123.0 }`}, + {Query: `{ .intAttr <= 123.1 }`}, + {Query: `{ .intAttr < 123.1 }`}, + {Query: `{ .intAttr != 123.1 }`}, + } + + searchesThatMatch = append(searchesThatMatch, quotedAttributesThatMatch...) + for _, req := range searchesThatMatch { + fetcher := traceql.NewSpansetFetcherWrapper(func(ctx context.Context, req traceql.FetchSpansRequest) (traceql.FetchSpansResponse, error) { + return r.Fetch(ctx, meta, req, common.DefaultSearchOptions()) + }) + + res, err := e.ExecuteSearch(ctx, req, fetcher) + if errors.Is(err, common.ErrUnsupported) { + continue + } + + require.NoError(t, err, "search request: %+v", req) + actual := actualForExpectedMeta(wantMeta, res) + require.NotNil(t, actual, "search request: %v", req) + actual.SpanSet = nil // todo: add the matching spansets to wantmeta + actual.SpanSets = nil + actual.ServiceStats = nil + require.Equal(t, wantMeta, actual, "search request: %v", req) + } + + quotedAttributesThaDonttMatch := []*tempopb.SearchRequest{ + {Query: `{ .floatAttr < 123.0 }`}, + {Query: `{ .floatAttr <= 123.0 }`}, + {Query: `{ .floatAttr < 123.4 }`}, + {Query: `{ .floatAttr != 123.4 }`}, + {Query: `{ .floatAttr > 123.4 }`}, + {Query: `{ .floatAttr >= 123.9 }`}, + {Query: `{ .floatAttr > 123.9 }`}, + {Query: `{ .intAttr < 122 }`}, + {Query: `{ .intAttr <= 122 }`}, + {Query: `{ .intAttr < 123 }`}, + {Query: `{ .intAttr != 123 }`}, + {Query: `{ .intAttr > 123 }`}, + {Query: `{ .intAttr >= 124 }`}, + {Query: `{ .intAttr > 124 }`}, + {Query: `{ .floatAttr < 123 }`}, + {Query: `{ .floatAttr <= 123 }`}, + {Query: `{ .floatAttr = 123 }`}, + {Query: `{ .floatAttr >= 124 }`}, + {Query: `{ .floatAttr > 124 }`}, + {Query: `{ .intAttr < 122.9 }`}, + {Query: `{ .intAttr <= 122.9 }`}, + {Query: `{ .intAttr < 123.0 }`}, + {Query: `{ .intAttr != 123.0 }`}, + {Query: `{ .intAttr > 123.0 }`}, + {Query: `{ .intAttr >= 123.1 }`}, + {Query: `{ .intAttr > 123.1 }`}, + } + + searchesThatDontMatch = append(searchesThatDontMatch, quotedAttributesThaDonttMatch...) + for _, req := range searchesThatDontMatch { + fetcher := traceql.NewSpansetFetcherWrapper(func(ctx context.Context, req traceql.FetchSpansRequest) (traceql.FetchSpansResponse, error) { + return r.Fetch(ctx, meta, req, common.DefaultSearchOptions()) + }) + + res, err := e.ExecuteSearch(ctx, req, fetcher) + require.NoError(t, err, "search request: %+v", req) + require.Nil(t, actualForExpectedMeta(wantMeta, res), "search request: %v", req) + } +} + func advancedTraceQLRunner(t *testing.T, wantTr *tempopb.Trace, wantMeta *tempopb.TraceSearchMetadata, _, _ []*tempopb.SearchRequest, meta *backend.BlockMeta, r Reader, _ common.BackendBlock) { ctx := context.Background() e := traceql.NewEngine() @@ -1438,7 +1533,7 @@ func tagNamesRunner(t *testing.T, _ *tempopb.Trace, _ *tempopb.TraceSearchMetada query: "{ resource.cluster = `MyCluster` }", expected: map[string][]string{ "span": {"child", "foo", "http.method", "http.status_code", "http.url", "span-dedicated.01", "span-dedicated.02"}, - "resource": {"bat", "{ } ( ) = ~ ! < > & | ^", "cluster", "container", "k8s.cluster.name", "k8s.container.name", "k8s.namespace.name", "k8s.pod.name", "namespace", "pod", "res-dedicated.01", "res-dedicated.02", "service.name"}, + "resource": {"bat", "{ } ( ) = ~ ! < > & | ^", "intAttr", "floatAttr", "cluster", "container", "k8s.cluster.name", "k8s.container.name", "k8s.namespace.name", "k8s.pod.name", "namespace", "pod", "res-dedicated.01", "res-dedicated.02", "service.name"}, }, }, { @@ -1447,7 +1542,7 @@ func tagNamesRunner(t *testing.T, _ *tempopb.Trace, _ *tempopb.TraceSearchMetada query: "{ span.foo = `Bar` }", expected: map[string][]string{ "span": {"child", "parent", "{ } ( ) = ~ ! < > & | ^", "foo", "http.method", "http.status_code", "http.url", "span-dedicated.01", "span-dedicated.02"}, - "resource": {"bat", "{ } ( ) = ~ ! < > & | ^", "cluster", "container", "k8s.cluster.name", "k8s.container.name", "k8s.namespace.name", "k8s.pod.name", "namespace", "pod", "res-dedicated.01", "res-dedicated.02", "service.name"}, + "resource": {"bat", "{ } ( ) = ~ ! < > & | ^", "intAttr", "floatAttr", "cluster", "container", "k8s.cluster.name", "k8s.container.name", "k8s.namespace.name", "k8s.pod.name", "namespace", "pod", "res-dedicated.01", "res-dedicated.02", "service.name"}, }, }, } @@ -1807,6 +1902,13 @@ func intKV(k string, v int) *v1_common.KeyValue { } } +func float64KV(k string, v float64) *v1_common.KeyValue { + return &v1_common.KeyValue{ + Key: k, + Value: &v1_common.AnyValue{Value: &v1_common.AnyValue_DoubleValue{DoubleValue: v}}, + } +} + func boolKV(k string) *v1_common.KeyValue { return &v1_common.KeyValue{ Key: k, @@ -1902,6 +2004,8 @@ func makeExpectedTrace() ( stringKV("res-dedicated.01", "res-1a"), stringKV("res-dedicated.02", "res-2a"), stringKV(attributeWithTerminalChars, "foobar"), + intKV("intAttr", 123), + float64KV("floatAttr", 123.4), }, }, ScopeSpans: []*v1.ScopeSpans{