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 <[email protected]>
This commit is contained in:
Pijus Kamandulis
2026-10-06 14:09:20 +03:00
committed by GitHub
co-authored by Cursor Agent
parent dca5851795
commit af271092c3
2 changed files with 142 additions and 39 deletions
@@ -1,7 +1,7 @@
package memoryexecutor package memoryexecutor
import ( import (
"math" "strings"
"github.com/pikami/cosmium/parsers" "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{} { func (r rowContext) aggregate_Max(arguments []interface{}) interface{} {
selectExpression := arguments[0].(parsers.SelectItem) return r.aggregateMinMax(arguments, true)
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
}
} }
func (r rowContext) aggregate_Min(arguments []interface{}) interface{} { 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) selectExpression := arguments[0].(parsers.SelectItem)
min := math.MaxFloat64 var extremeNumber float64
count := 0 hasNumber := false
var extremeString string
hasString := false
for _, item := range r.grouppedRows { for _, item := range r.grouppedRows {
value := item.resolveSelectItem(selectExpression) value := item.resolveSelectItem(selectExpression)
if numericValue, ok := value.(float64); ok {
if numericValue < min { if numericValue, ok := aggregateNumber(value); ok {
min = numericValue if !hasNumber || numberIsExtreme(numericValue, extremeNumber, isMax) {
extremeNumber = numericValue
hasNumber = true
} }
count++ continue
} else if numericValue, ok := value.(int); ok { }
if float64(numericValue) < min {
min = float64(numericValue) if strValue, ok := value.(string); ok {
if !hasString || stringIsExtreme(strValue, extremeString, isMax) {
extremeString = strValue
hasString = true
} }
count++
} }
} }
if count > 0 { if hasNumber {
return min return extremeNumber
} else { }
if hasString {
return extremeString
}
return nil 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{} { func (r rowContext) aggregate_Sum(arguments []interface{}) interface{} {
@@ -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) { t.Run("Should execute function SUM()", func(t *testing.T) {
testQueryExecute( testQueryExecute(
t, t,