Skip to content

Commit

Permalink
fix: Handle limit and offset in sub groups (#440)
Browse files Browse the repository at this point in the history
  • Loading branch information
AndrewSisley committed May 16, 2022
1 parent d36ffc8 commit 5e3a3b8
Show file tree
Hide file tree
Showing 4 changed files with 358 additions and 0 deletions.
99 changes: 99 additions & 0 deletions db/tests/query/simple/with_group_limit_offset_test.go
@@ -0,0 +1,99 @@
// Copyright 2022 Democratized Data Foundation
//
// Use of this software is governed by the Business Source License
// included in the file licenses/BSL.txt.
//
// As of the Change Date specified in that file, in accordance with
// the Business Source License, use of this software will be governed
// by the Apache License, Version 2.0, included in the file
// licenses/APL.txt.

package simple

import (
"testing"

testUtils "github.com/sourcenetwork/defradb/db/tests"
)

func TestQuerySimpleWithGroupByNumberWithGroupLimitAndOffset(t *testing.T) {
test := testUtils.QueryTestCase{
Description: "Simple query with group by number, no children, rendered, limited and offset group",
Query: `query {
users(groupBy: [Age]) {
Age
_group(limit: 1, offset: 1) {
Name
}
}
}`,
Docs: map[int][]string{
0: {
(`{
"Name": "John",
"Age": 32
}`),
(`{
"Name": "Bob",
"Age": 32
}`),
(`{
"Name": "Alice",
"Age": 19
}`)},
},
Results: []map[string]interface{}{
{
"Age": uint64(32),
"_group": []map[string]interface{}{
{
"Name": "John",
},
},
},
{
"Age": uint64(19),
"_group": []map[string]interface{}{},
},
},
}

executeTestCase(t, test)
}

func TestQuerySimpleWithGroupByNumberWithLimitAndOffsetAndWithGroupLimitAndOffset(t *testing.T) {
test := testUtils.QueryTestCase{
Description: "Simple query with group by number with limit and offset, no children, rendered, limited and offset group",
Query: `query {
users(groupBy: [Age], limit: 1, offset: 1) {
Age
_group(limit: 1, offset: 1) {
Name
}
}
}`,
Docs: map[int][]string{
0: {
(`{
"Name": "John",
"Age": 32
}`),
(`{
"Name": "Bob",
"Age": 32
}`),
(`{
"Name": "Alice",
"Age": 19
}`)},
},
Results: []map[string]interface{}{
{
"Age": uint64(19),
"_group": []map[string]interface{}{},
},
},
}

executeTestCase(t, test)
}
228 changes: 228 additions & 0 deletions db/tests/query/simple/with_group_limit_test.go
@@ -0,0 +1,228 @@
// Copyright 2022 Democratized Data Foundation
//
// Use of this software is governed by the Business Source License
// included in the file licenses/BSL.txt.
//
// As of the Change Date specified in that file, in accordance with
// the Business Source License, use of this software will be governed
// by the Apache License, Version 2.0, included in the file
// licenses/APL.txt.

package simple

import (
"testing"

testUtils "github.com/sourcenetwork/defradb/db/tests"
)

func TestQuerySimpleWithGroupByNumberWithGroupLimit(t *testing.T) {
test := testUtils.QueryTestCase{
Description: "Simple query with group by number, no children, rendered, limited group",
Query: `query {
users(groupBy: [Age]) {
Age
_group(limit: 1) {
Name
}
}
}`,
Docs: map[int][]string{
0: {
(`{
"Name": "John",
"Age": 32
}`),
(`{
"Name": "Bob",
"Age": 32
}`),
(`{
"Name": "Alice",
"Age": 19
}`)},
},
Results: []map[string]interface{}{
{
"Age": uint64(32),
"_group": []map[string]interface{}{
{
"Name": "Bob",
},
},
},
{
"Age": uint64(19),
"_group": []map[string]interface{}{
{
"Name": "Alice",
},
},
},
},
}

executeTestCase(t, test)
}

func TestQuerySimpleWithGroupByNumberWithMultipleGroupsWithDifferentLimits(t *testing.T) {
test := testUtils.QueryTestCase{
Description: "Simple query with group by number, no children, multiple rendered, limited groups",
Query: `query {
users(groupBy: [Age]) {
Age
G1: _group(limit: 1) {
Name
}
G2: _group(limit: 2) {
Name
}
}
}`,
Docs: map[int][]string{
0: {
(`{
"Name": "John",
"Age": 32
}`),
(`{
"Name": "Bob",
"Age": 32
}`),
(`{
"Name": "Alice",
"Age": 19
}`)},
},
Results: []map[string]interface{}{
{
"Age": uint64(32),
"G1": []map[string]interface{}{
{
"Name": "Bob",
},
},
"G2": []map[string]interface{}{
{
"Name": "Bob",
},
{
"Name": "John",
},
},
},
{
"Age": uint64(19),
"G1": []map[string]interface{}{
{
"Name": "Alice",
},
},
"G2": []map[string]interface{}{
{
"Name": "Alice",
},
},
},
},
}

executeTestCase(t, test)
}

func TestQuerySimpleWithGroupByNumberWithLimitAndGroupWithHigherLimit(t *testing.T) {
test := testUtils.QueryTestCase{
Description: "Simple query with group by number and limit, no children, rendered, limited group",
Query: `query {
users(groupBy: [Age], limit: 1) {
Age
_group(limit: 2) {
Name
}
}
}`,
Docs: map[int][]string{
0: {
(`{
"Name": "John",
"Age": 32
}`),
(`{
"Name": "Bob",
"Age": 32
}`),
(`{
"Name": "Alice",
"Age": 19
}`)},
},
Results: []map[string]interface{}{
{
"Age": uint64(32),
"_group": []map[string]interface{}{
{
"Name": "Bob",
},
{
"Name": "John",
},
},
},
},
}

executeTestCase(t, test)
}

func TestQuerySimpleWithGroupByNumberWithLimitAndGroupWithLowerLimit(t *testing.T) {
test := testUtils.QueryTestCase{
Description: "Simple query with group by number and limit, no children, rendered, limited group",
Query: `query {
users(groupBy: [Age], limit: 2) {
Age
_group(limit: 1) {
Name
}
}
}`,
Docs: map[int][]string{
0: {
(`{
"Name": "John",
"Age": 32
}`),
(`{
"Name": "Bob",
"Age": 32
}`),
(`{
"Name": "Alice",
"Age": 19
}`),
(`{
"Name": "Alice",
"Age": 42
}`)},
},
Results: []map[string]interface{}{
{
"Age": uint64(32),
"_group": []map[string]interface{}{
{
"Name": "Bob",
},
},
},
{
"Age": uint64(42),
"_group": []map[string]interface{}{
{
"Name": "Alice",
},
},
},
},
}

executeTestCase(t, test)
}
24 changes: 24 additions & 0 deletions query/graphql/planner/group.go
Expand Up @@ -121,6 +121,30 @@ func (n *groupNode) Next() (bool, error) {
}

n.values = values.values

for _, group := range n.values {
for _, childSelect := range n.childSelects {
subSelect, hasSubSelect := group[childSelect.Name]
if !hasSubSelect {
continue
}

childDocs := subSelect.([]map[string]interface{})
if childSelect.Limit != nil {
l := int64(len(childDocs))

// We must hide all child documents before the offset
for i := int64(0); i < childSelect.Limit.Offset && i < l; i++ {
childDocs[i][parser.HiddenFieldName] = struct{}{}
}

// We must hide all child documents after the offset plus limit
for i := childSelect.Limit.Limit + childSelect.Limit.Offset; i < l; i++ {
childDocs[i][parser.HiddenFieldName] = struct{}{}
}
}
}
}
}

if n.currentIndex < len(n.values) {
Expand Down
7 changes: 7 additions & 0 deletions query/graphql/planner/planner.go
Expand Up @@ -339,6 +339,13 @@ func (p *Planner) expandLimitPlan(plan *selectTopNode, parentPlan *selectTopNode
return nil
}

// Limits get more complicated with groups and have to be handled internally, so we ensure
// any limit plan is disabled here
if parentPlan != nil && parentPlan.group != nil && len(parentPlan.group.childSelects) != 0 {
plan.limit = nil
return nil
}

// if this is a child node, and the parent select has an aggregate then we need to
// replace the hard limit with a render limit to allow the full set of child records
// to be aggregated
Expand Down

0 comments on commit 5e3a3b8

Please sign in to comment.