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