signoz/pkg/querybuilder/having_rewriter_test.go
2025-06-18 15:30:29 +05:30

282 lines
8.0 KiB
Go

package querybuilder
import (
"testing"
"github.com/SigNoz/signoz/pkg/types/metrictypes"
qbtypes "github.com/SigNoz/signoz/pkg/types/querybuildertypes/querybuildertypesv5"
)
func TestHavingExpressionRewriter_RewriteForTraces(t *testing.T) {
tests := []struct {
name string
expression string
aggregations []qbtypes.TraceAggregation
expected string
}{
{
name: "single aggregation with __result",
expression: "__result > 100",
aggregations: []qbtypes.TraceAggregation{
{Expression: "count()", Alias: ""},
},
expected: "__result_0 > 100",
},
{
name: "single aggregation with alias",
expression: "total_count > 100 AND total_count < 1000",
aggregations: []qbtypes.TraceAggregation{
{Expression: "count()", Alias: "total_count"},
},
expected: "__result_0 > 100 AND __result_0 < 1000",
},
{
name: "multiple aggregations with aliases",
expression: "error_count > 10 OR success_count > 100",
aggregations: []qbtypes.TraceAggregation{
{Expression: "countIf(status = 'error')", Alias: "error_count"},
{Expression: "countIf(status = 'success')", Alias: "success_count"},
},
expected: "__result_0 > 10 OR __result_1 > 100",
},
{
name: "expression reference",
expression: "count() > 50",
aggregations: []qbtypes.TraceAggregation{
{Expression: "count()", Alias: ""},
},
expected: "__result_0 > 50",
},
{
name: "__result{number} format",
expression: "__result0 > 10 AND __result1 < 100",
aggregations: []qbtypes.TraceAggregation{
{Expression: "count()", Alias: ""},
{Expression: "sum(duration)", Alias: ""},
},
expected: "__result_0 > 10 AND __result_1 < 100",
},
{
name: "complex expression with parentheses",
expression: "(total > 100 AND errors < 10) OR (total < 50 AND errors = 0)",
aggregations: []qbtypes.TraceAggregation{
{Expression: "count()", Alias: "total"},
{Expression: "countIf(error = true)", Alias: "errors"},
},
expected: "(__result_0 > 100 AND __result_1 < 10) OR (__result_0 < 50 AND __result_1 = 0)",
},
{
name: "with quoted strings",
expression: "status = 'active' AND count > 100",
aggregations: []qbtypes.TraceAggregation{
{Expression: "status", Alias: "status"},
{Expression: "count()", Alias: "count"},
},
expected: "__result_0 = 'active' AND __result_1 > 100",
},
{
name: "avoid partial replacements",
expression: "count_distinct > 10 AND count > 100",
aggregations: []qbtypes.TraceAggregation{
{Expression: "count_distinct(user_id)", Alias: "count_distinct"},
{Expression: "count()", Alias: "count"},
},
expected: "__result_0 > 10 AND __result_1 > 100",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
rewriter := NewHavingExpressionRewriter()
result := rewriter.RewriteForTraces(tt.expression, tt.aggregations)
if result != tt.expected {
t.Errorf("Expected: %s, Got: %s", tt.expected, result)
}
})
}
}
func TestHavingExpressionRewriter_RewriteForLogs(t *testing.T) {
tests := []struct {
name string
expression string
aggregations []qbtypes.LogAggregation
expected string
}{
{
name: "single aggregation with __result",
expression: "__result > 1000",
aggregations: []qbtypes.LogAggregation{
{Expression: "count()", Alias: ""},
},
expected: "__result_0 > 1000",
},
{
name: "multiple aggregations with aliases and expressions",
expression: "total_logs > 1000 AND avg(size) < 1024",
aggregations: []qbtypes.LogAggregation{
{Expression: "count()", Alias: "total_logs"},
{Expression: "avg(size)", Alias: ""},
},
expected: "__result_0 > 1000 AND __result_1 < 1024",
},
{
name: "complex boolean expression",
expression: "(error_logs > 100 AND error_logs < 1000) OR warning_logs > 5000",
aggregations: []qbtypes.LogAggregation{
{Expression: "countIf(level = 'error')", Alias: "error_logs"},
{Expression: "countIf(level = 'warning')", Alias: "warning_logs"},
},
expected: "(__result_0 > 100 AND __result_0 < 1000) OR __result_1 > 5000",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
rewriter := NewHavingExpressionRewriter()
result := rewriter.RewriteForLogs(tt.expression, tt.aggregations)
if result != tt.expected {
t.Errorf("Expected: %s, Got: %s", tt.expected, result)
}
})
}
}
func TestHavingExpressionRewriter_RewriteForMetrics(t *testing.T) {
tests := []struct {
name string
expression string
aggregations []qbtypes.MetricAggregation
expected string
}{
{
name: "metric with space aggregation",
expression: "avg(cpu_usage) > 80",
aggregations: []qbtypes.MetricAggregation{
{
MetricName: "cpu_usage",
SpaceAggregation: metrictypes.SpaceAggregationAvg,
},
},
expected: "value > 80",
},
{
name: "metric with time aggregation",
expression: "rate(requests) > 1000",
aggregations: []qbtypes.MetricAggregation{
{
MetricName: "requests",
TimeAggregation: metrictypes.TimeAggregationRate,
},
},
expected: "value > 1000",
},
{
name: "metric with both aggregations",
expression: "sum(rate(requests)) > 5000",
aggregations: []qbtypes.MetricAggregation{
{
MetricName: "requests",
TimeAggregation: metrictypes.TimeAggregationRate,
SpaceAggregation: metrictypes.SpaceAggregationSum,
},
},
expected: "value > 5000",
},
{
name: "metric with __result",
expression: "__result < 100",
aggregations: []qbtypes.MetricAggregation{
{
MetricName: "memory_usage",
SpaceAggregation: metrictypes.SpaceAggregationMax,
},
},
expected: "value < 100",
},
{
name: "metric name without aggregation",
expression: "temperature > 30",
aggregations: []qbtypes.MetricAggregation{
{
MetricName: "temperature",
},
},
expected: "value > 30",
},
{
name: "complex expression with parentheses",
expression: "(avg(cpu_usage) > 80 AND avg(cpu_usage) < 95) OR __result > 99",
aggregations: []qbtypes.MetricAggregation{
{
MetricName: "cpu_usage",
SpaceAggregation: metrictypes.SpaceAggregationAvg,
},
},
expected: "(value > 80 AND value < 95) OR value > 99",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
rewriter := NewHavingExpressionRewriter()
result := rewriter.RewriteForMetrics(tt.expression, tt.aggregations)
if result != tt.expected {
t.Errorf("Expected: %s, Got: %s", tt.expected, result)
}
})
}
}
func TestHavingExpressionRewriter_EdgeCases(t *testing.T) {
tests := []struct {
name string
expression string
aggregations []qbtypes.TraceAggregation
expected string
}{
{
name: "empty expression",
expression: "",
aggregations: []qbtypes.TraceAggregation{},
expected: "",
},
{
name: "no matching columns",
expression: "unknown_column > 100",
aggregations: []qbtypes.TraceAggregation{
{Expression: "count()", Alias: "total"},
},
expected: "unknown_column > 100",
},
{
name: "expression within quoted string",
expression: "status = 'count() > 100' AND total > 100",
aggregations: []qbtypes.TraceAggregation{
{Expression: "status", Alias: "status"},
{Expression: "count()", Alias: "total"},
},
expected: "__result_0 = 'count() > 100' AND __result_1 > 100",
},
{
name: "double quotes",
expression: `name = "test" AND count > 10`,
aggregations: []qbtypes.TraceAggregation{
{Expression: "name", Alias: "name"},
{Expression: "count()", Alias: "count"},
},
expected: `__result_0 = "test" AND __result_1 > 10`,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
rewriter := NewHavingExpressionRewriter()
result := rewriter.RewriteForTraces(tt.expression, tt.aggregations)
if result != tt.expected {
t.Errorf("Expected: %s, Got: %s", tt.expected, result)
}
})
}
}