diff --git a/pkg/types/querybuildertypes/querybuildertypesv5/formula.go b/pkg/types/querybuildertypes/querybuildertypesv5/formula.go index bd010fb56fc..49a3c7a2123 100644 --- a/pkg/types/querybuildertypes/querybuildertypesv5/formula.go +++ b/pkg/types/querybuildertypes/querybuildertypesv5/formula.go @@ -191,13 +191,13 @@ func parseAggregationReference(variable string) (aggregationRef, error) { // Simple query reference like "A" - defaults to first aggregation (index 0) defaultIndex := 0 return aggregationRef{ - QueryName: parts[0], + QueryName: strings.ToUpper(parts[0]), Index: &defaultIndex, }, nil } if len(parts) == 2 { - queryName := parts[0] + queryName := strings.ToUpper(parts[0]) reference := parts[1] // Try to parse as index @@ -277,10 +277,15 @@ func (fe *FormulaEvaluator) buildSeriesLookup(timeSeriesData map[string]*TimeSer seriesMetadata: make(map[string]*TimeSeries), } + normalizedSeriesMap := make(map[string]*TimeSeriesData, len(timeSeriesData)) + for name, ts := range timeSeriesData { + normalizedSeriesMap[strings.ToUpper(name)] = ts + } + for variable, aggRef := range fe.aggRefs { // We are only interested in the time series data for the queries that are // involved in the formula expression. - data, exists := timeSeriesData[aggRef.QueryName] + data, exists := normalizedSeriesMap[aggRef.QueryName] if !exists { continue } diff --git a/pkg/types/querybuildertypes/querybuildertypesv5/formula_test.go b/pkg/types/querybuildertypes/querybuildertypesv5/formula_test.go index bcae592f079..59958b0eabd 100644 --- a/pkg/types/querybuildertypes/querybuildertypesv5/formula_test.go +++ b/pkg/types/querybuildertypes/querybuildertypesv5/formula_test.go @@ -864,6 +864,132 @@ func TestComplexExpression(t *testing.T) { } } +func TestCaseInsensitiveQueryNames(t *testing.T) { + tests := []struct { + name string + expression string + tsData map[string]*TimeSeriesData + expected float64 + }{ + { + name: "lowercase query names", + expression: "a / b", + tsData: map[string]*TimeSeriesData{ + "A": createFormulaTestTimeSeriesData("A", []*TimeSeries{ + { + Labels: createLabels(map[string]string{}), + Values: createValues(map[int64]float64{1: 10}), + }, + }), + "B": createFormulaTestTimeSeriesData("B", []*TimeSeries{ + { + Labels: createLabels(map[string]string{}), + Values: createValues(map[int64]float64{1: 2}), + }, + }), + }, + expected: 5.0, + }, + { + name: "mixed case query names", + expression: "A / b", + tsData: map[string]*TimeSeriesData{ + "A": createFormulaTestTimeSeriesData("A", []*TimeSeries{ + { + Labels: createLabels(map[string]string{}), + Values: createValues(map[int64]float64{1: 10}), + }, + }), + "B": createFormulaTestTimeSeriesData("B", []*TimeSeries{ + { + Labels: createLabels(map[string]string{}), + Values: createValues(map[int64]float64{1: 2}), + }, + }), + }, + expected: 5.0, + }, + { + name: "uppercase query names with lowercase data keys", + expression: "A / B", + tsData: map[string]*TimeSeriesData{ + "a": createFormulaTestTimeSeriesData("a", []*TimeSeries{ + { + Labels: createLabels(map[string]string{}), + Values: createValues(map[int64]float64{1: 10}), + }, + }), + "b": createFormulaTestTimeSeriesData("b", []*TimeSeries{ + { + Labels: createLabels(map[string]string{}), + Values: createValues(map[int64]float64{1: 2}), + }, + }), + }, + expected: 5.0, + }, + { + name: "all lowercase", + expression: "a/b", + tsData: map[string]*TimeSeriesData{ + "a": createFormulaTestTimeSeriesData("a", []*TimeSeries{ + { + Labels: createLabels(map[string]string{}), + Values: createValues(map[int64]float64{1: 100}), + }, + }), + "b": createFormulaTestTimeSeriesData("b", []*TimeSeries{ + { + Labels: createLabels(map[string]string{}), + Values: createValues(map[int64]float64{1: 10}), + }, + }), + }, + expected: 10.0, + }, + { + name: "complex expression with mixed case", + expression: "a + B * c", + tsData: map[string]*TimeSeriesData{ + "A": createFormulaTestTimeSeriesData("A", []*TimeSeries{ + { + Labels: createLabels(map[string]string{}), + Values: createValues(map[int64]float64{1: 5}), + }, + }), + "b": createFormulaTestTimeSeriesData("b", []*TimeSeries{ + { + Labels: createLabels(map[string]string{}), + Values: createValues(map[int64]float64{1: 3}), + }, + }), + "C": createFormulaTestTimeSeriesData("C", []*TimeSeries{ + { + Labels: createLabels(map[string]string{}), + Values: createValues(map[int64]float64{1: 2}), + }, + }), + }, + expected: 11.0, // 5 + 3 * 2 = 11 + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + evaluator, err := NewFormulaEvaluator(tt.expression, map[string]bool{"a": true, "A": true, "b": true, "B": true, "c": true, "C": true}) + require.NoError(t, err) + + result, err := evaluator.EvaluateFormula(tt.tsData) + require.NoError(t, err) + require.NotNil(t, result) + + assert.Equal(t, 1, len(result), "should have exactly one result series") + assert.Equal(t, 1, len(result[0].Values), "should have exactly one value") + assert.Equal(t, tt.expected, result[0].Values[0].Value, "calculated value should match expected") + }) + } +} + func TestAbsValueExpression(t *testing.T) { tsData := map[string]*TimeSeriesData{ "A": createFormulaTestTimeSeriesData("A", []*TimeSeries{