mirror of https://github.com/pikami/cosmium.git
Implement 'GROUP BY' statement
This commit is contained in:
parent
18edb925bf
commit
b72bba86c8
|
@ -8,6 +8,7 @@ type SelectStmt struct {
|
|||
Count int
|
||||
Parameters map[string]interface{}
|
||||
OrderExpressions []OrderExpression
|
||||
GroupBy []SelectItem
|
||||
}
|
||||
|
||||
type Table struct {
|
||||
|
|
|
@ -63,6 +63,24 @@ func Test_Parse(t *testing.T) {
|
|||
)
|
||||
})
|
||||
|
||||
t.Run("Should parse SELECT with GROUP BY", func(t *testing.T) {
|
||||
testQueryParse(
|
||||
t,
|
||||
`SELECT c.id, c["pk"] FROM c GROUP BY c.id, c.pk`,
|
||||
parsers.SelectStmt{
|
||||
SelectItems: []parsers.SelectItem{
|
||||
{Path: []string{"c", "id"}},
|
||||
{Path: []string{"c", "pk"}},
|
||||
},
|
||||
Table: parsers.Table{Value: "c"},
|
||||
GroupBy: []parsers.SelectItem{
|
||||
{Path: []string{"c", "id"}},
|
||||
{Path: []string{"c", "pk"}},
|
||||
},
|
||||
},
|
||||
)
|
||||
})
|
||||
|
||||
t.Run("Should parse IN function", func(t *testing.T) {
|
||||
testQueryParse(
|
||||
t,
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -6,7 +6,7 @@ import "github.com/pikami/cosmium/parsers"
|
|||
func makeSelectStmt(
|
||||
columns, table,
|
||||
whereClause interface{}, distinctClause interface{},
|
||||
count interface{}, orderList interface{},
|
||||
count interface{}, groupByClause interface{}, orderList interface{},
|
||||
) (parsers.SelectStmt, error) {
|
||||
selectStmt := parsers.SelectStmt{
|
||||
SelectItems: columns.([]parsers.SelectItem),
|
||||
|
@ -30,6 +30,10 @@ func makeSelectStmt(
|
|||
selectStmt.OrderExpressions = orderExpressions
|
||||
}
|
||||
|
||||
if groupByClause != nil {
|
||||
selectStmt.GroupBy = groupByClause.([]parsers.SelectItem)
|
||||
}
|
||||
|
||||
return selectStmt, nil
|
||||
}
|
||||
|
||||
|
@ -149,8 +153,10 @@ SelectStmt <- Select ws
|
|||
topClause:TopClause? ws columns:Selection ws
|
||||
From ws table:TableName ws
|
||||
whereClause:(ws Where ws condition:Condition { return condition, nil })?
|
||||
groupByClause:(ws GroupBy ws columns:ColumnList { return columns, nil })?
|
||||
orderByClause:OrderByClause? {
|
||||
return makeSelectStmt(columns, table, whereClause, distinctClause, topClause, orderByClause)
|
||||
return makeSelectStmt(columns, table, whereClause,
|
||||
distinctClause, topClause, groupByClause, orderByClause)
|
||||
}
|
||||
|
||||
DistinctClause <- "DISTINCT"i
|
||||
|
@ -285,6 +291,8 @@ And <- "AND"i
|
|||
|
||||
Or <- "OR"i
|
||||
|
||||
GroupBy <- "GROUP"i ws "BY"i
|
||||
|
||||
OrderBy <- "ORDER"i ws "BY"i
|
||||
|
||||
ComparisonOperator <- ("=" / "!=" / "<" / "<=" / ">" / ">=") {
|
||||
|
|
|
@ -36,12 +36,20 @@ func Execute(query parsers.SelectStmt, data []RowType) []RowType {
|
|||
ctx.orderBy(query.OrderExpressions, result)
|
||||
}
|
||||
|
||||
// Apply select
|
||||
selectedData := make([]RowType, 0)
|
||||
for _, row := range result {
|
||||
selectedData = append(selectedData, ctx.selectRow(query.SelectItems, row))
|
||||
// Apply group
|
||||
isGroupSelect := query.GroupBy != nil && len(query.GroupBy) > 0
|
||||
if isGroupSelect {
|
||||
result = ctx.groupBy(query, result)
|
||||
}
|
||||
|
||||
// Apply select
|
||||
if !isGroupSelect {
|
||||
selectedData := make([]RowType, 0)
|
||||
for _, row := range result {
|
||||
selectedData = append(selectedData, ctx.selectRow(query.SelectItems, row))
|
||||
}
|
||||
result = selectedData
|
||||
}
|
||||
result = selectedData
|
||||
|
||||
// Apply distinct
|
||||
if query.Distinct {
|
||||
|
@ -182,6 +190,11 @@ func (c memoryExecutorContext) getFieldValue(field parsers.SelectItem, row RowTy
|
|||
return typedValue.Value
|
||||
}
|
||||
|
||||
rowValue := row
|
||||
if array, isArray := row.([]RowType); isArray {
|
||||
rowValue = array[0]
|
||||
}
|
||||
|
||||
if field.Type == parsers.SelectItemTypeFunctionCall {
|
||||
var typedValue parsers.FunctionCall
|
||||
var ok bool
|
||||
|
@ -192,82 +205,83 @@ func (c memoryExecutorContext) getFieldValue(field parsers.SelectItem, row RowTy
|
|||
|
||||
switch typedValue.Type {
|
||||
case parsers.FunctionCallStringEquals:
|
||||
return c.strings_StringEquals(typedValue.Arguments, row)
|
||||
return c.strings_StringEquals(typedValue.Arguments, rowValue)
|
||||
case parsers.FunctionCallContains:
|
||||
return c.strings_Contains(typedValue.Arguments, row)
|
||||
return c.strings_Contains(typedValue.Arguments, rowValue)
|
||||
case parsers.FunctionCallEndsWith:
|
||||
return c.strings_EndsWith(typedValue.Arguments, row)
|
||||
return c.strings_EndsWith(typedValue.Arguments, rowValue)
|
||||
case parsers.FunctionCallStartsWith:
|
||||
return c.strings_StartsWith(typedValue.Arguments, row)
|
||||
return c.strings_StartsWith(typedValue.Arguments, rowValue)
|
||||
case parsers.FunctionCallConcat:
|
||||
return c.strings_Concat(typedValue.Arguments, row)
|
||||
return c.strings_Concat(typedValue.Arguments, rowValue)
|
||||
case parsers.FunctionCallIndexOf:
|
||||
return c.strings_IndexOf(typedValue.Arguments, row)
|
||||
return c.strings_IndexOf(typedValue.Arguments, rowValue)
|
||||
case parsers.FunctionCallToString:
|
||||
return c.strings_ToString(typedValue.Arguments, row)
|
||||
return c.strings_ToString(typedValue.Arguments, rowValue)
|
||||
case parsers.FunctionCallUpper:
|
||||
return c.strings_Upper(typedValue.Arguments, row)
|
||||
return c.strings_Upper(typedValue.Arguments, rowValue)
|
||||
case parsers.FunctionCallLower:
|
||||
return c.strings_Lower(typedValue.Arguments, row)
|
||||
return c.strings_Lower(typedValue.Arguments, rowValue)
|
||||
case parsers.FunctionCallLeft:
|
||||
return c.strings_Left(typedValue.Arguments, row)
|
||||
return c.strings_Left(typedValue.Arguments, rowValue)
|
||||
case parsers.FunctionCallLength:
|
||||
return c.strings_Length(typedValue.Arguments, row)
|
||||
return c.strings_Length(typedValue.Arguments, rowValue)
|
||||
case parsers.FunctionCallLTrim:
|
||||
return c.strings_LTrim(typedValue.Arguments, row)
|
||||
return c.strings_LTrim(typedValue.Arguments, rowValue)
|
||||
case parsers.FunctionCallReplace:
|
||||
return c.strings_Replace(typedValue.Arguments, row)
|
||||
return c.strings_Replace(typedValue.Arguments, rowValue)
|
||||
case parsers.FunctionCallReplicate:
|
||||
return c.strings_Replicate(typedValue.Arguments, row)
|
||||
return c.strings_Replicate(typedValue.Arguments, rowValue)
|
||||
case parsers.FunctionCallReverse:
|
||||
return c.strings_Reverse(typedValue.Arguments, row)
|
||||
return c.strings_Reverse(typedValue.Arguments, rowValue)
|
||||
case parsers.FunctionCallRight:
|
||||
return c.strings_Right(typedValue.Arguments, row)
|
||||
return c.strings_Right(typedValue.Arguments, rowValue)
|
||||
case parsers.FunctionCallRTrim:
|
||||
return c.strings_RTrim(typedValue.Arguments, row)
|
||||
return c.strings_RTrim(typedValue.Arguments, rowValue)
|
||||
case parsers.FunctionCallSubstring:
|
||||
return c.strings_Substring(typedValue.Arguments, row)
|
||||
return c.strings_Substring(typedValue.Arguments, rowValue)
|
||||
case parsers.FunctionCallTrim:
|
||||
return c.strings_Trim(typedValue.Arguments, row)
|
||||
return c.strings_Trim(typedValue.Arguments, rowValue)
|
||||
|
||||
case parsers.FunctionCallIsDefined:
|
||||
return c.typeChecking_IsDefined(typedValue.Arguments, row)
|
||||
return c.typeChecking_IsDefined(typedValue.Arguments, rowValue)
|
||||
case parsers.FunctionCallIsArray:
|
||||
return c.typeChecking_IsArray(typedValue.Arguments, row)
|
||||
return c.typeChecking_IsArray(typedValue.Arguments, rowValue)
|
||||
case parsers.FunctionCallIsBool:
|
||||
return c.typeChecking_IsBool(typedValue.Arguments, row)
|
||||
return c.typeChecking_IsBool(typedValue.Arguments, rowValue)
|
||||
case parsers.FunctionCallIsFiniteNumber:
|
||||
return c.typeChecking_IsFiniteNumber(typedValue.Arguments, row)
|
||||
return c.typeChecking_IsFiniteNumber(typedValue.Arguments, rowValue)
|
||||
case parsers.FunctionCallIsInteger:
|
||||
return c.typeChecking_IsInteger(typedValue.Arguments, row)
|
||||
return c.typeChecking_IsInteger(typedValue.Arguments, rowValue)
|
||||
case parsers.FunctionCallIsNull:
|
||||
return c.typeChecking_IsNull(typedValue.Arguments, row)
|
||||
return c.typeChecking_IsNull(typedValue.Arguments, rowValue)
|
||||
case parsers.FunctionCallIsNumber:
|
||||
return c.typeChecking_IsNumber(typedValue.Arguments, row)
|
||||
return c.typeChecking_IsNumber(typedValue.Arguments, rowValue)
|
||||
case parsers.FunctionCallIsObject:
|
||||
return c.typeChecking_IsObject(typedValue.Arguments, row)
|
||||
return c.typeChecking_IsObject(typedValue.Arguments, rowValue)
|
||||
case parsers.FunctionCallIsPrimitive:
|
||||
return c.typeChecking_IsPrimitive(typedValue.Arguments, row)
|
||||
return c.typeChecking_IsPrimitive(typedValue.Arguments, rowValue)
|
||||
case parsers.FunctionCallIsString:
|
||||
return c.typeChecking_IsString(typedValue.Arguments, row)
|
||||
return c.typeChecking_IsString(typedValue.Arguments, rowValue)
|
||||
|
||||
case parsers.FunctionCallArrayConcat:
|
||||
return c.array_Concat(typedValue.Arguments, row)
|
||||
return c.array_Concat(typedValue.Arguments, rowValue)
|
||||
case parsers.FunctionCallArrayLength:
|
||||
return c.array_Length(typedValue.Arguments, row)
|
||||
return c.array_Length(typedValue.Arguments, rowValue)
|
||||
case parsers.FunctionCallArraySlice:
|
||||
return c.array_Slice(typedValue.Arguments, row)
|
||||
return c.array_Slice(typedValue.Arguments, rowValue)
|
||||
case parsers.FunctionCallSetIntersect:
|
||||
return c.set_Intersect(typedValue.Arguments, row)
|
||||
return c.set_Intersect(typedValue.Arguments, rowValue)
|
||||
case parsers.FunctionCallSetUnion:
|
||||
return c.set_Union(typedValue.Arguments, row)
|
||||
return c.set_Union(typedValue.Arguments, rowValue)
|
||||
|
||||
case parsers.FunctionCallIn:
|
||||
return c.misc_In(typedValue.Arguments, row)
|
||||
return c.misc_In(typedValue.Arguments, rowValue)
|
||||
}
|
||||
}
|
||||
|
||||
value := row
|
||||
value := rowValue
|
||||
|
||||
if len(field.Path) > 1 {
|
||||
for _, pathSegment := range field.Path[1:] {
|
||||
if nestedValue, ok := value.(map[string]interface{}); ok {
|
||||
|
@ -314,6 +328,47 @@ func (c memoryExecutorContext) orderBy(orderBy []parsers.OrderExpression, data [
|
|||
sort.SliceStable(data, less)
|
||||
}
|
||||
|
||||
func (c memoryExecutorContext) groupBy(selectStmt parsers.SelectStmt, data []RowType) []RowType {
|
||||
groupedRows := make(map[string][]RowType)
|
||||
groupedKeys := make([]string, 0)
|
||||
|
||||
// Group rows by group by columns
|
||||
for _, row := range data {
|
||||
key := c.generateGroupKey(selectStmt.GroupBy, row)
|
||||
if _, ok := groupedRows[key]; !ok {
|
||||
groupedKeys = append(groupedKeys, key)
|
||||
}
|
||||
groupedRows[key] = append(groupedRows[key], row)
|
||||
}
|
||||
|
||||
// Aggregate each group
|
||||
aggregatedRows := make([]RowType, 0)
|
||||
for _, key := range groupedKeys {
|
||||
groupRows := groupedRows[key]
|
||||
aggregatedRow := c.aggregateGroup(selectStmt, groupRows)
|
||||
aggregatedRows = append(aggregatedRows, aggregatedRow)
|
||||
}
|
||||
|
||||
return aggregatedRows
|
||||
}
|
||||
|
||||
func (c memoryExecutorContext) generateGroupKey(groupByFields []parsers.SelectItem, row RowType) string {
|
||||
var keyBuilder strings.Builder
|
||||
for _, column := range groupByFields {
|
||||
fieldValue := c.getFieldValue(column, row)
|
||||
keyBuilder.WriteString(fmt.Sprintf("%v", fieldValue))
|
||||
keyBuilder.WriteString(":")
|
||||
}
|
||||
|
||||
return keyBuilder.String()
|
||||
}
|
||||
|
||||
func (c memoryExecutorContext) aggregateGroup(selectStmt parsers.SelectStmt, groupRows []RowType) RowType {
|
||||
aggregatedRow := c.selectRow(selectStmt.SelectItems, groupRows)
|
||||
|
||||
return aggregatedRow
|
||||
}
|
||||
|
||||
func compareValues(val1, val2 interface{}) int {
|
||||
if reflect.TypeOf(val1) != reflect.TypeOf(val2) {
|
||||
return 1
|
||||
|
|
|
@ -59,6 +59,26 @@ func Test_Execute(t *testing.T) {
|
|||
)
|
||||
})
|
||||
|
||||
t.Run("Should execute SELECT with GROUP BY", func(t *testing.T) {
|
||||
testQueryExecute(
|
||||
t,
|
||||
parsers.SelectStmt{
|
||||
SelectItems: []parsers.SelectItem{
|
||||
{Path: []string{"c", "pk"}},
|
||||
},
|
||||
Table: parsers.Table{Value: "c"},
|
||||
GroupBy: []parsers.SelectItem{
|
||||
{Path: []string{"c", "pk"}},
|
||||
},
|
||||
},
|
||||
mockData,
|
||||
[]memoryexecutor.RowType{
|
||||
map[string]interface{}{"pk": 123},
|
||||
map[string]interface{}{"pk": 456},
|
||||
},
|
||||
)
|
||||
})
|
||||
|
||||
t.Run("Should execute IN function", func(t *testing.T) {
|
||||
testQueryExecute(
|
||||
t,
|
||||
|
|
Loading…
Reference in New Issue