diff --git a/db/tests/query/simple/with_group_limit_offset_test.go b/db/tests/query/simple/with_group_limit_offset_test.go new file mode 100644 index 0000000000..ea3adc66a2 --- /dev/null +++ b/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) +} diff --git a/db/tests/query/simple/with_group_limit_test.go b/db/tests/query/simple/with_group_limit_test.go new file mode 100644 index 0000000000..5a59f95675 --- /dev/null +++ b/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) +} diff --git a/query/graphql/planner/group.go b/query/graphql/planner/group.go index 22a49734a4..f1581b4cf1 100644 --- a/query/graphql/planner/group.go +++ b/query/graphql/planner/group.go @@ -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) { diff --git a/query/graphql/planner/planner.go b/query/graphql/planner/planner.go index 87c1cc8c19..07d9a223b9 100644 --- a/query/graphql/planner/planner.go +++ b/query/graphql/planner/planner.go @@ -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