From af271092c3a3a56471422b1394540105479b0248 Mon Sep 17 00:00:00 2001 From: Pijus Kamandulis Date: Tue, 6 Oct 2026 14:09:20 +0300 Subject: [PATCH] Support string values in MIN and MAX aggregates (#19) Return the numeric extreme when a group contains any number, fall back to alphabetic string comparison when it does not, and keep nil when neither type is present. Co-authored-by: Cursor Agent --- .../memory_executor/aggregate_functions.go | 93 +++++++++++-------- .../aggregate_functions_test.go | 88 ++++++++++++++++++ 2 files changed, 142 insertions(+), 39 deletions(-) diff --git a/query_executors/memory_executor/aggregate_functions.go b/query_executors/memory_executor/aggregate_functions.go index 20bd2a0..48efab9 100644 --- a/query_executors/memory_executor/aggregate_functions.go +++ b/query_executors/memory_executor/aggregate_functions.go @@ -1,7 +1,7 @@ package memoryexecutor import ( - "math" + "strings" "github.com/pikami/cosmium/parsers" ) @@ -44,57 +44,72 @@ func (r rowContext) aggregate_Count(arguments []interface{}) interface{} { } func (r rowContext) aggregate_Max(arguments []interface{}) interface{} { - selectExpression := arguments[0].(parsers.SelectItem) - max := 0.0 - count := 0 - - for _, item := range r.grouppedRows { - value := item.resolveSelectItem(selectExpression) - if numericValue, ok := value.(float64); ok { - if numericValue > max { - max = numericValue - } - count++ - } else if numericValue, ok := value.(int); ok { - if float64(numericValue) > max { - max = float64(numericValue) - } - count++ - } - } - - if count > 0 { - return max - } else { - return nil - } + return r.aggregateMinMax(arguments, true) } func (r rowContext) aggregate_Min(arguments []interface{}) interface{} { + return r.aggregateMinMax(arguments, false) +} + +func (r rowContext) aggregateMinMax(arguments []interface{}, isMax bool) interface{} { selectExpression := arguments[0].(parsers.SelectItem) - min := math.MaxFloat64 - count := 0 + var extremeNumber float64 + hasNumber := false + var extremeString string + hasString := false for _, item := range r.grouppedRows { value := item.resolveSelectItem(selectExpression) - if numericValue, ok := value.(float64); ok { - if numericValue < min { - min = numericValue + + if numericValue, ok := aggregateNumber(value); ok { + if !hasNumber || numberIsExtreme(numericValue, extremeNumber, isMax) { + extremeNumber = numericValue + hasNumber = true } - count++ - } else if numericValue, ok := value.(int); ok { - if float64(numericValue) < min { - min = float64(numericValue) + continue + } + + if strValue, ok := value.(string); ok { + if !hasString || stringIsExtreme(strValue, extremeString, isMax) { + extremeString = strValue + hasString = true } - count++ } } - if count > 0 { - return min - } else { - return nil + if hasNumber { + return extremeNumber } + if hasString { + return extremeString + } + return nil +} + +func aggregateNumber(value interface{}) (float64, bool) { + switch numericValue := value.(type) { + case float64: + return numericValue, true + case int: + return float64(numericValue), true + default: + return 0, false + } +} + +func numberIsExtreme(candidate, current float64, isMax bool) bool { + if isMax { + return candidate > current + } + return candidate < current +} + +func stringIsExtreme(candidate, current string, isMax bool) bool { + comparison := strings.Compare(candidate, current) + if isMax { + return comparison > 0 + } + return comparison < 0 } func (r rowContext) aggregate_Sum(arguments []interface{}) interface{} { diff --git a/query_executors/memory_executor/aggregate_functions_test.go b/query_executors/memory_executor/aggregate_functions_test.go index 5d3e198..4f48655 100644 --- a/query_executors/memory_executor/aggregate_functions_test.go +++ b/query_executors/memory_executor/aggregate_functions_test.go @@ -176,6 +176,94 @@ func Test_Execute_AggregateFunctions(t *testing.T) { ) }) + mixedTypeData := []memoryexecutor.RowType{ + map[string]interface{}{"key": "numbers", "value": 10}, + map[string]interface{}{"key": "numbers", "value": "zebra"}, + map[string]interface{}{"key": "numbers", "value": 2}, + map[string]interface{}{"key": "strings", "value": "mango"}, + map[string]interface{}{"key": "strings", "value": "apple"}, + map[string]interface{}{"key": "strings", "value": true}, + map[string]interface{}{"key": "empty", "value": "b"}, + map[string]interface{}{"key": "empty", "value": ""}, + map[string]interface{}{"key": "single", "value": "zebra"}, + map[string]interface{}{"key": "single", "value": 5}, + map[string]interface{}{"key": "single", "value": "apple"}, + map[string]interface{}{"key": "other", "value": true}, + map[string]interface{}{"key": "other", "flag": false}, + } + + t.Run("Should execute function MAX() on strings and mixed types", func(t *testing.T) { + testQueryExecute( + t, + parsers.SelectStmt{ + SelectItems: []parsers.SelectItem{ + {Path: []string{"c", "key"}}, + { + Alias: "max", + Type: parsers.SelectItemTypeFunctionCall, + Value: parsers.FunctionCall{ + Type: parsers.FunctionCallAggregateMax, + Arguments: []interface{}{ + parsers.SelectItem{ + Path: []string{"c", "value"}, + Type: parsers.SelectItemTypeField, + }, + }, + }, + }, + }, + GroupBy: []parsers.SelectItem{ + {Path: []string{"c", "key"}}, + }, + Table: parsers.Table{SelectItem: testutils.SelectItem_Path("c")}, + }, + mixedTypeData, + []memoryexecutor.RowType{ + map[string]interface{}{"key": "numbers", "max": 10.0}, + map[string]interface{}{"key": "strings", "max": "mango"}, + map[string]interface{}{"key": "empty", "max": "b"}, + map[string]interface{}{"key": "single", "max": 5.0}, + map[string]interface{}{"key": "other", "max": nil}, + }, + ) + }) + + t.Run("Should execute function MIN() on strings and mixed types", func(t *testing.T) { + testQueryExecute( + t, + parsers.SelectStmt{ + SelectItems: []parsers.SelectItem{ + {Path: []string{"c", "key"}}, + { + Alias: "min", + Type: parsers.SelectItemTypeFunctionCall, + Value: parsers.FunctionCall{ + Type: parsers.FunctionCallAggregateMin, + Arguments: []interface{}{ + parsers.SelectItem{ + Path: []string{"c", "value"}, + Type: parsers.SelectItemTypeField, + }, + }, + }, + }, + }, + GroupBy: []parsers.SelectItem{ + {Path: []string{"c", "key"}}, + }, + Table: parsers.Table{SelectItem: testutils.SelectItem_Path("c")}, + }, + mixedTypeData, + []memoryexecutor.RowType{ + map[string]interface{}{"key": "numbers", "min": 2.0}, + map[string]interface{}{"key": "strings", "min": "apple"}, + map[string]interface{}{"key": "empty", "min": ""}, + map[string]interface{}{"key": "single", "min": 5.0}, + map[string]interface{}{"key": "other", "min": nil}, + }, + ) + }) + t.Run("Should execute function SUM()", func(t *testing.T) { testQueryExecute( t,