mirror of
https://github.com/pikami/cosmium.git
synced 2026-10-10 03:56:11 +01:00
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:
co-authored by
Cursor Agent
parent
dca5851795
commit
af271092c3
@@ -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,
|
||||||
|
|||||||
Reference in New Issue
Block a user