From c3c6495539bd743e3ffac9652d155e491c8ca0b2 Mon Sep 17 00:00:00 2001 From: Shahzad Lone Date: Tue, 5 Jul 2022 11:11:29 -0400 Subject: [PATCH] feat: Add ability to explain `sortNode` attribute(s). (#558) - Relevant issue(s): Resolves #481. Fixes #584 ([2] case). - Description Adds the attributes for `sortNode` to be included in the returned explain graph response. So far, we are only introducing 1 attribute, which represents a list containing all the `orderings` of each field requested to be sorted, with its corresponding direction. - Request: ``` query @explain { author(order: {age: ASC}) { name age verified } } ``` - Response: ``` { "explain": { "selectTopNode": { "sortNode": { "selectNode": { "filter": null, "scanNode": { "filter": null, "collectionID": "3", "collectionName": "author", "spans": []{ { "start": "/3", "end": "/4", } } } } "orderings": []{ { "direction": "ASC", "fields": [ "age" ], } } } } } } ``` --- core/doc.go | 24 + query/graphql/mapper/targetable.go | 7 + query/graphql/planner/planner.go | 2 +- query/graphql/planner/sort.go | 39 +- .../query/explain/with_sort_test.go | 496 ++++++++++++++++++ 5 files changed, 563 insertions(+), 5 deletions(-) create mode 100644 tests/integration/query/explain/with_sort_test.go diff --git a/core/doc.go b/core/doc.go index 0513c5d25d..4a0db75b76 100644 --- a/core/doc.go +++ b/core/doc.go @@ -222,3 +222,27 @@ func (m *DocumentMapping) SetChildAt(index int, childMapping DocumentMapping) { newMappings[index] = childMapping m.ChildMappings = newMappings } + +// TryToFindNameFromIndex returns the corresponding name of the given index. +// +// Additionally, will also return true if the index was found, and false otherwise. +func (mapping *DocumentMapping) TryToFindNameFromIndex(targetIndex int) (string, bool) { + // Try to find the name of this index in the IndexesByName. + for name, indexes := range mapping.IndexesByName { + for _, index := range indexes { + if index == targetIndex { + return name, true + } + } + } + + // Try to find the name of this index in the ChildMappings. + for _, childMapping := range mapping.ChildMappings { + name, found := childMapping.TryToFindNameFromIndex(targetIndex) + if found { + return name, true + } + } + + return "", false +} diff --git a/query/graphql/mapper/targetable.go b/query/graphql/mapper/targetable.go index 0de68643e0..3d0b679a22 100644 --- a/query/graphql/mapper/targetable.go +++ b/query/graphql/mapper/targetable.go @@ -106,6 +106,13 @@ type OrderCondition struct { Direction SortDirection } +func (oc OrderCondition) IsEmpty() bool { + if oc.Direction == "" && len(oc.FieldIndexes) == 0 { + return true + } + return false +} + type OrderBy struct { Conditions []OrderCondition } diff --git a/query/graphql/planner/planner.go b/query/graphql/planner/planner.go index ddd12f8795..c32f861b06 100644 --- a/query/graphql/planner/planner.go +++ b/query/graphql/planner/planner.go @@ -455,7 +455,7 @@ func (p *Planner) explainRequest( explainGraph, err := buildExplainGraph(plan) if err != nil { - return nil, err + return nil, multiErr(err, plan.Close()) } topExplainGraph := []map[string]interface{}{ diff --git a/query/graphql/planner/sort.go b/query/graphql/planner/sort.go index d938645731..a28630901c 100644 --- a/query/graphql/planner/sort.go +++ b/query/graphql/planner/sort.go @@ -11,6 +11,8 @@ package planner import ( + "fmt" + "github.com/sourcenetwork/defradb/core" "github.com/sourcenetwork/defradb/query/graphql/mapper" ) @@ -54,6 +56,7 @@ type sortNode struct { // that sorts, then provides the values // sorted sortStrategy sortingStrategy + // indicates if our underlying sortStrategy is still // consuming and sorting data. needSort bool @@ -97,7 +100,38 @@ func (n *sortNode) Value() core.Doc { // 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 *sortNode) Explain() (map[string]interface{}, error) { - return map[string]interface{}{}, nil + orderings := []map[string]interface{}{} + + for _, element := range n.ordering { + // Skip all empty elements. + if element.IsEmpty() { + continue + } + + // Build the list containing the corresponding names of all the indexes. + fieldNames := []string{} + for _, fieldIndex := range element.FieldIndexes { + // Try to find the name of this index. + fieldName, found := n.documentMapping.TryToFindNameFromIndex(fieldIndex) + if !found { + return nil, fmt.Errorf("No corresponding name was found for index=%d", fieldIndex) + } + + fieldNames = append(fieldNames, fieldName) + } + + // Put it all together for this order element. + orderings = append(orderings, + map[string]interface{}{ + "fields": fieldNames, + "direction": string(element.Direction), + }, + ) + } + + return map[string]interface{}{ + "orderings": orderings, + }, nil } func (n *sortNode) Next() (bool, error) { @@ -124,9 +158,6 @@ func (n *sortNode) Next() (bool, error) { if err := n.sortStrategy.Add(n.plan.Value()); err != nil { return false, err } - - // finalize, assign valueIter = sortStrategy - // break } next, err := n.valueIter.Next() diff --git a/tests/integration/query/explain/with_sort_test.go b/tests/integration/query/explain/with_sort_test.go new file mode 100644 index 0000000000..42768e40bd --- /dev/null +++ b/tests/integration/query/explain/with_sort_test.go @@ -0,0 +1,496 @@ +// 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 TestExplainAscendingOrderQueryOnParent(t *testing.T) { + test := testUtils.QueryTestCase{ + Description: "Explain An Ascending Order Query On Parent Field.", + + Query: `query @explain { + author(order: {age: ASC}) { + name + age + } + }`, + + Docs: map[int][]string{ + // authors + 2: { + // _key: bae-41598f0c-19bc-5da6-813b-e80f14a10df3 + `{ + "name": "John Grisham", + "age": 65, + "verified": true + }`, + // _key: bae-b769708d-f552-5c3d-a402-ccfd7ac7fb04 + `{ + "name": "Cornelia Funke", + "age": 62, + "verified": false + }`, + }, + }, + + Results: []dataMap{ + { + "explain": dataMap{ + "selectTopNode": dataMap{ + "sortNode": dataMap{ + "selectNode": dataMap{ + "filter": nil, + "scanNode": dataMap{ + "filter": nil, + "collectionID": "3", + "collectionName": "author", + "spans": []dataMap{ + { + "start": "/3", + "end": "/4", + }, + }, + }, + }, + "orderings": []dataMap{ + { + "direction": "ASC", + "fields": []string{ + "age", + }, + }, + }, + }, + }, + }, + }, + }, + } + + executeTestCase(t, test) +} + +func TestExplainQueryWithMultiOrderFieldsOnParent(t *testing.T) { + test := testUtils.QueryTestCase{ + Description: "Explain Query With Multiple Order Fields on the Parent.", + + Query: `query @explain { + author(order: {name: ASC, age: DESC}) { + name + age + } + }`, + + Docs: map[int][]string{ + // authors + 2: { + // _key: bae-41598f0c-19bc-5da6-813b-e80f14a10df3 + `{ + "name": "John Grisham", + "age": 65, + "verified": true + }`, + // _key: bae-b769708d-f552-5c3d-a402-ccfd7ac7fb04 + `{ + "name": "Cornelia Funke", + "age": 62, + "verified": false + }`, + }, + }, + + Results: []dataMap{ + { + "explain": dataMap{ + "selectTopNode": dataMap{ + "sortNode": dataMap{ + "selectNode": dataMap{ + "filter": nil, + "scanNode": dataMap{ + "filter": nil, + "collectionID": "3", + "collectionName": "author", + "spans": []dataMap{ + { + "start": "/3", + "end": "/4", + }, + }, + }, + }, + "orderings": []dataMap{ + { + "direction": "ASC", + "fields": []string{ + "name", + }, + }, + { + "direction": "DESC", + "fields": []string{ + "age", + }, + }, + }, + }, + }, + }, + }, + }, + } + + executeTestCase(t, test) +} + +func TestExplainQueryWithOrderFieldOnChild(t *testing.T) { + test := testUtils.QueryTestCase{ + Description: "Explain Query With Order Field On A Child.", + + Query: `query @explain { + author { + name + articles(order: {name: DESC}) { + name + } + } + }`, + + Docs: map[int][]string{ + // articles + 0: { + `{ + "name": "After Guantánamo, Another Injustice", + "author_id": "bae-41598f0c-19bc-5da6-813b-e80f14a10df3" + }`, + `{ + "name": "To my dear readers", + "author_id": "bae-b769708d-f552-5c3d-a402-ccfd7ac7fb04" + }`, + `{ + "name": "Twinklestar's Favourite Xmas Cookie", + "author_id": "bae-b769708d-f552-5c3d-a402-ccfd7ac7fb04" + }`, + }, + + // authors + 2: { + // _key: bae-41598f0c-19bc-5da6-813b-e80f14a10df3 + `{ + "name": "John Grisham", + "age": 65, + "verified": true + }`, + // _key: bae-b769708d-f552-5c3d-a402-ccfd7ac7fb04 + `{ + "name": "Cornelia Funke", + "age": 62, + "verified": false + }`, + }, + }, + + Results: []dataMap{ + { + "explain": dataMap{ + "selectTopNode": dataMap{ + "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{ + "sortNode": dataMap{ + "orderings": []dataMap{ + { + "direction": "DESC", + "fields": []string{ + "name", + }, + }, + }, + "selectNode": dataMap{ + "filter": nil, + "scanNode": dataMap{ + "collectionID": "1", + "collectionName": "article", + "filter": nil, + "spans": []dataMap{ + { + "start": "/1", + "end": "/2", + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + } + + executeTestCase(t, test) +} + +func TestExplainQueryWithOrderOnBothTheParentAndChild(t *testing.T) { + test := testUtils.QueryTestCase{ + Description: "Explain A Query With Order On Parent and An Order on Child.", + + Query: `query @explain { + author(order: {name: ASC}) { + name + articles(order: {name: DESC}) { + name + } + } + }`, + + Docs: map[int][]string{ + // articles + 0: { + `{ + "name": "After Guantánamo, Another Injustice", + "author_id": "bae-41598f0c-19bc-5da6-813b-e80f14a10df3" + }`, + `{ + "name": "To my dear readers", + "author_id": "bae-b769708d-f552-5c3d-a402-ccfd7ac7fb04" + }`, + `{ + "name": "Twinklestar's Favourite Xmas Cookie", + "author_id": "bae-b769708d-f552-5c3d-a402-ccfd7ac7fb04" + }`, + }, + + // authors + 2: { + // _key: bae-41598f0c-19bc-5da6-813b-e80f14a10df3 + `{ + "name": "John Grisham", + "age": 65, + "verified": true + }`, + // _key: bae-b769708d-f552-5c3d-a402-ccfd7ac7fb04 + `{ + "name": "Cornelia Funke", + "age": 62, + "verified": false + }`, + }, + }, + + Results: []dataMap{ + { + "explain": dataMap{ + "selectTopNode": dataMap{ + "sortNode": dataMap{ + "orderings": []dataMap{ + { + "direction": "ASC", + "fields": []string{ + "name", + }, + }, + }, + "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{ + "sortNode": dataMap{ + "orderings": []dataMap{ + { + "direction": "DESC", + "fields": []string{ + "name", + }, + }, + }, + "selectNode": dataMap{ + "filter": nil, + "scanNode": dataMap{ + "collectionID": "1", + "collectionName": "article", + "filter": nil, + "spans": []dataMap{ + { + "start": "/1", + "end": "/2", + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + } + + executeTestCase(t, test) +} + +func TestExplainQueryWhereParentIsOrderedByChild(t *testing.T) { + test := testUtils.QueryTestCase{ + Description: "Explain Query Where The Parent Is Ordered By It's Child.", + + Query: `query @explain { + author( + order: { + articles: {name: ASC} + } + ) { + articles { + name + } + } + }`, + + Docs: map[int][]string{ + // articles + 0: { + `{ + "name": "After Guantánamo, Another Injustice", + "author_id": "bae-41598f0c-19bc-5da6-813b-e80f14a10df3" + }`, + `{ + "name": "To my dear readers", + "author_id": "bae-b769708d-f552-5c3d-a402-ccfd7ac7fb04" + }`, + `{ + "name": "Twinklestar's Favourite Xmas Cookie", + "author_id": "bae-b769708d-f552-5c3d-a402-ccfd7ac7fb04" + }`, + }, + + // authors + 2: { + // _key: bae-41598f0c-19bc-5da6-813b-e80f14a10df3 + `{ + "name": "John Grisham", + "age": 65, + "verified": true + }`, + // _key: bae-b769708d-f552-5c3d-a402-ccfd7ac7fb04 + `{ + "name": "Cornelia Funke", + "age": 62, + "verified": false + }`, + }, + }, + + Results: []dataMap{ + { + "explain": dataMap{ + "selectTopNode": dataMap{ + "sortNode": dataMap{ + "orderings": []dataMap{ + { + "direction": "ASC", + "fields": []string{ + "articles", + "name", + }, + }, + }, + "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": nil, + "spans": []dataMap{ + { + "start": "/1", + "end": "/2", + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + } + + executeTestCase(t, test) +}