diff --git a/query/graphql/planner/sum.go b/query/graphql/planner/sum.go index d764f4af30..641808f8cd 100644 --- a/query/graphql/planner/sum.go +++ b/query/graphql/planner/sum.go @@ -153,7 +153,34 @@ func (n *sumNode) Source() planNode { return n.plan } // Explain method returns a map containing all attributes of this node that // are to be explained, subscribes / opts-in this node to be an explainablePlanNode. func (n *sumNode) Explain() (map[string]interface{}, error) { - return map[string]interface{}{}, nil + sourceExplanations := make([]map[string]interface{}, len(n.aggregateMapping)) + + for i, source := range n.aggregateMapping { + explainerMap := map[string]interface{}{} + + // Add the filter attribute if it exists. + if source.Filter == nil || source.Filter.ExternalConditions == nil { + explainerMap[filterLabel] = nil + } else { + explainerMap[filterLabel] = source.Filter.ExternalConditions + } + + // Add the main field name. + explainerMap["fieldName"] = source.Field.Name + + // Add the child field name if it exists. + if source.ChildTarget.HasValue { + explainerMap["childFieldName"] = source.ChildTarget.Name + } else { + explainerMap["childFieldName"] = nil + } + + sourceExplanations[i] = explainerMap + } + + return map[string]interface{}{ + "sources": sourceExplanations, + }, nil } func (n *sumNode) Next() (bool, error) { diff --git a/tests/integration/query/explain/utils.go b/tests/integration/query/explain/utils.go index 679c5e679b..b8f1768fa5 100644 --- a/tests/integration/query/explain/utils.go +++ b/tests/integration/query/explain/utils.go @@ -22,11 +22,14 @@ var bookAuthorGQLSchema = (` type article { name: String author: author + pages: Int } type book { name: String author: author + pages: Int + chapterPages: [Int] } type author { diff --git a/tests/integration/query/explain/with_sum_test.go b/tests/integration/query/explain/with_sum_test.go new file mode 100644 index 0000000000..325bc9eda5 --- /dev/null +++ b/tests/integration/query/explain/with_sum_test.go @@ -0,0 +1,509 @@ +// 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 test_explain + +import ( + "testing" + + testUtils "github.com/sourcenetwork/defradb/tests/integration" +) + +func TestExplainQuerySumOfRelatedOneToManyField(t *testing.T) { + test := testUtils.QueryTestCase{ + Description: "Explain a simple sum query of a One-to-Many realted sub-type.", + Query: `query @explain { + author { + name + _key + TotalPages: _sum( + books: {field: pages} + ) + } + }`, + + Docs: map[int][]string{ + // books + 1: { + `{ + "name": "Painted House", + "author_id": "bae-25fafcc7-f251-58c1-9495-ead73e676fb8", + "pages": 22 + }`, + `{ + "name": "A Time for Mercy", + "author_id": "bae-25fafcc7-f251-58c1-9495-ead73e676fb8", + "pages": 101 + }`, + `{ + "name": "Theif Lord", + "author_id": "bae-3dddb519-3612-5e43-86e5-49d6295d4f84", + "pages": 321 + }`, + }, + + // authors + 2: { + // _key: "bae-25fafcc7-f251-58c1-9495-ead73e676fb8" + `{ + "name": "John Grisham", + "age": 65, + "verified": true, + "contact_id": "bae-1fe427b8-ab8d-56c3-9df2-826a6ce86fed" + }`, + // _key: "bae-3dddb519-3612-5e43-86e5-49d6295d4f84" + `{ + "name": "Cornelia Funke", + "age": 62, + "verified": false, + "contact_id": "bae-c0960a29-b704-5c37-9c2e-59e1249e4559" + }`, + }, + }, + + Results: []dataMap{ + { + "explain": dataMap{ + "selectTopNode": dataMap{ + "sumNode": dataMap{ + "sources": []dataMap{ + { + "fieldName": "books", + "childFieldName": "pages", + "filter": nil, + }, + }, + "selectNode": dataMap{ + "filter": nil, + "typeIndexJoin": dataMap{ + "joinType": "typeJoinMany", + "rootName": "author", + "root": dataMap{ + "scanNode": dataMap{ + "collectionID": "3", + "collectionName": "author", + "filter": nil, + "spans": []dataMap{ + { + "start": "/3", + "end": "/4", + }, + }, + }, + }, + "subTypeName": "books", + "subType": dataMap{ + "selectTopNode": dataMap{ + "selectNode": dataMap{ + "filter": nil, + "scanNode": dataMap{ + "collectionID": "2", + "collectionName": "book", + "filter": nil, + "spans": []dataMap{ + { + "start": "/2", + "end": "/3", + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + } + + executeTestCase(t, test) +} + +func TestExplainQuerySumOfRelatedOneToManyFieldWithFilter(t *testing.T) { + test := testUtils.QueryTestCase{ + Description: "Explain a simple sum query of a One-to-Many realted sub-type with a filter.", + Query: `query @explain { + author { + name + TotalPages: _sum( + articles: { + field: pages, + filter: { + name: { + _eq: "To my dear readers" + } + } + } + ) + } + }`, + + Docs: map[int][]string{ + // articles + 0: { + `{ + "name": "After Guantánamo, Another Injustice", + "author_id": "bae-25fafcc7-f251-58c1-9495-ead73e676fb8", + "pages": 2 + }`, + `{ + "name": "To my dear readers", + "author_id": "bae-3dddb519-3612-5e43-86e5-49d6295d4f84", + "pages": 11 + }`, + `{ + "name": "Twinklestar's Favourite Xmas Cookie", + "author_id": "bae-3dddb519-3612-5e43-86e5-49d6295d4f84", + "pages": 31 + }`, + }, + + // authors + 2: { + // _key: "bae-25fafcc7-f251-58c1-9495-ead73e676fb8" + `{ + "name": "John Grisham", + "age": 65, + "verified": true, + "contact_id": "bae-1fe427b8-ab8d-56c3-9df2-826a6ce86fed" + }`, + // _key: "bae-3dddb519-3612-5e43-86e5-49d6295d4f84" + `{ + "name": "Cornelia Funke", + "age": 62, + "verified": false, + "contact_id": "bae-c0960a29-b704-5c37-9c2e-59e1249e4559" + }`, + }, + }, + + Results: []dataMap{ + { + "explain": dataMap{ + "selectTopNode": dataMap{ + "sumNode": dataMap{ + "sources": []dataMap{ + { + "fieldName": "articles", + "childFieldName": "pages", + "filter": dataMap{ + "name": dataMap{ + "$eq": "To my dear readers", + }, + }, + }, + }, + "selectNode": dataMap{ + "filter": nil, + "typeIndexJoin": dataMap{ + "joinType": "typeJoinMany", + "rootName": "author", + "root": dataMap{ + "scanNode": dataMap{ + "collectionID": "3", + "collectionName": "author", + "filter": nil, + "spans": []dataMap{ + { + "start": "/3", + "end": "/4", + }, + }, + }, + }, + "subTypeName": "articles", + "subType": dataMap{ + "selectTopNode": dataMap{ + "selectNode": dataMap{ + "filter": nil, + "scanNode": dataMap{ + "collectionID": "1", + "collectionName": "article", + "filter": dataMap{ + "name": dataMap{ + "$eq": "To my dear readers", + }, + }, + "spans": []dataMap{ + { + "start": "/1", + "end": "/2", + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + } + + executeTestCase(t, test) +} + +func TestExplainQuerySumOfInlineArrayField_ShouldHaveEmptyChildField(t *testing.T) { + test := testUtils.QueryTestCase{ + Description: "Explain a simple sum query on an inline array field (childFieldName is nil).", + Query: `query @explain { + book { + name + NotSureWhySomeoneWouldSumTheChapterPagesButHereItIs: _sum(chapterPages: {}) + } + }`, + + Docs: map[int][]string{ + // books + 1: { + `{ + "name": "Painted House", + "author_id": "bae-25fafcc7-f251-58c1-9495-ead73e676fb8", + "pages": 77, + "chapterPages": [1, 22, 33, 44, 55, 66] + }`, // sum of chapterPages == 221 + + `{ + "name": "A Time for Mercy", + "author_id": "bae-25fafcc7-f251-58c1-9495-ead73e676fb8", + "pages": 55, + "chapterPages": [1, 22] + }`, // sum of chapterPages == 23 + + `{ + "name": "Theif Lord", + "author_id": "bae-3dddb519-3612-5e43-86e5-49d6295d4f84", + "pages": 321, + "chapterPages": [10, 50, 100, 200, 300] + }`, // sum of chapterPages == 660 + }, + }, + + Results: []dataMap{ + { + "explain": dataMap{ + "selectTopNode": dataMap{ + "sumNode": dataMap{ + "sources": []dataMap{ + { + "fieldName": "chapterPages", + "childFieldName": nil, + "filter": nil, + }, + }, + "selectNode": dataMap{ + "filter": nil, + "scanNode": dataMap{ + "collectionID": "2", + "collectionName": "book", + "filter": nil, + "spans": []dataMap{ + { + "start": "/2", + "end": "/3", + }, + }, + }, + }, + }, + }, + }, + }, + }, + } + + executeTestCase(t, test) +} + +func TestExplainQuerySumOfRelatedOneToManyFieldWithManySources(t *testing.T) { + test := testUtils.QueryTestCase{ + Description: "Explain a simple sum query of a One-to-Many realted sub-type with many sources.", + Query: `query @explain { + author { + name + TotalPages: _sum( + books: {field: pages}, + articles: {field: pages} + ) + } + }`, + + Docs: map[int][]string{ + // articles + 0: { + `{ + "name": "After Guantánamo, Another Injustice", + "author_id": "bae-25fafcc7-f251-58c1-9495-ead73e676fb8", + "pages": 2 + }`, + `{ + "name": "To my dear readers", + "author_id": "bae-3dddb519-3612-5e43-86e5-49d6295d4f84", + "pages": 11 + }`, + `{ + "name": "Twinklestar's Favourite Xmas Cookie", + "author_id": "bae-3dddb519-3612-5e43-86e5-49d6295d4f84", + "pages": 31 + }`, + }, + + // books + 1: { + `{ + "name": "Painted House", + "author_id": "bae-25fafcc7-f251-58c1-9495-ead73e676fb8", + "pages": 22 + }`, + `{ + "name": "A Time for Mercy", + "author_id": "bae-25fafcc7-f251-58c1-9495-ead73e676fb8", + "pages": 101 + }`, + `{ + "name": "Theif Lord", + "author_id": "bae-3dddb519-3612-5e43-86e5-49d6295d4f84", + "pages": 321 + }`, + }, + + // authors + 2: { + // _key: "bae-25fafcc7-f251-58c1-9495-ead73e676fb8" + `{ + "name": "John Grisham", + "age": 65, + "verified": true, + "contact_id": "bae-1fe427b8-ab8d-56c3-9df2-826a6ce86fed" + }`, + // _key: "bae-3dddb519-3612-5e43-86e5-49d6295d4f84" + `{ + "name": "Cornelia Funke", + "age": 62, + "verified": false, + "contact_id": "bae-c0960a29-b704-5c37-9c2e-59e1249e4559" + }`, + }, + }, + + Results: []dataMap{ + { + "explain": dataMap{ + "selectTopNode": dataMap{ + "sumNode": dataMap{ + "sources": []dataMap{ + { + "childFieldName": "pages", + "fieldName": "books", + "filter": nil, + }, + + { + "childFieldName": "pages", + "fieldName": "articles", + "filter": nil, + }, + }, + "selectNode": dataMap{ + "filter": nil, + "parallelNode": []dataMap{ + { + "typeIndexJoin": dataMap{ + "joinType": "typeJoinMany", + "rootName": "author", + "root": dataMap{ + "scanNode": dataMap{ + "collectionID": "3", + "collectionName": "author", + "filter": nil, + "spans": []dataMap{ + { + "start": "/3", + "end": "/4", + }, + }, + }, + }, + "subTypeName": "books", + "subType": dataMap{ + "selectTopNode": dataMap{ + "selectNode": dataMap{ + "filter": nil, + "scanNode": dataMap{ + "collectionID": "2", + "collectionName": "book", + "filter": nil, + "spans": []dataMap{ + { + "start": "/2", + "end": "/3", + }, + }, + }, + }, + }, + }, + }, + }, + { + "typeIndexJoin": dataMap{ + "joinType": "typeJoinMany", + "rootName": "author", + "root": dataMap{ + "scanNode": dataMap{ + "collectionID": "3", + "collectionName": "author", + "filter": nil, + "spans": []dataMap{ + { + "start": "/3", + "end": "/4", + }, + }, + }, + }, + "subTypeName": "articles", + "subType": dataMap{ + "selectTopNode": dataMap{ + "selectNode": dataMap{ + "filter": nil, + "scanNode": dataMap{ + "collectionID": "1", + "collectionName": "article", + "filter": nil, + "spans": []dataMap{ + { + "start": "/1", + "end": "/2", + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + } + + executeTestCase(t, test) +}