diff --git a/client/request/explain.go b/client/request/explain.go index e0f8a481cb..ee8cb2b388 100644 --- a/client/request/explain.go +++ b/client/request/explain.go @@ -15,5 +15,6 @@ type ExplainType string // Types of explain requests. const ( - SimpleExplain ExplainType = "simple" + SimpleExplain ExplainType = "simple" + ExecuteExplain ExplainType = "execute" ) diff --git a/planner/average.go b/planner/average.go index d10136ae77..aacadab2ca 100644 --- a/planner/average.go +++ b/planner/average.go @@ -26,6 +26,13 @@ type averageNode struct { sumFieldIndex int countFieldIndex int virtualFieldIndex int + + execInfo averageExecInfo +} + +type averageExecInfo struct { + // Total number of times averageNode was executed. + iterations uint64 } func (p *Planner) Average( @@ -64,6 +71,8 @@ func (n *averageNode) Close() error { return n.plan.Close() } func (n *averageNode) Source() planNode { return n.plan } func (n *averageNode) Next() (bool, error) { + n.execInfo.iterations++ + hasNext, err := n.plan.Next() if err != nil || !hasNext { return hasNext, err @@ -100,6 +109,17 @@ func (n *averageNode) SetPlan(p planNode) { n.plan = p } // 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 *averageNode) Explain() (map[string]any, error) { - return map[string]any{}, nil +func (n *averageNode) Explain(explainType request.ExplainType) (map[string]any, error) { + switch explainType { + case request.SimpleExplain: + return map[string]any{}, nil + + case request.ExecuteExplain: + return map[string]any{ + "iterations": n.execInfo.iterations, + }, nil + + default: + return nil, ErrUnknownExplainRequestType + } } diff --git a/planner/commit.go b/planner/commit.go index 32bb788af9..72a7c29056 100644 --- a/planner/commit.go +++ b/planner/commit.go @@ -37,6 +37,13 @@ type dagScanNode struct { fetcher fetcher.HeadFetcher spans core.Spans commitSelect *mapper.CommitSelect + + execInfo dagScanExecInfo +} + +type dagScanExecInfo struct { + // Total number of times dag scan was issued. + iterations uint64 } func (p *Planner) DAGScan(commitSelect *mapper.CommitSelect) *dagScanNode { @@ -118,23 +125,21 @@ func (n *dagScanNode) Close() error { func (n *dagScanNode) Source() planNode { return nil } -// 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 *dagScanNode) Explain() (map[string]any, error) { - explainerMap := map[string]any{} +func (n *dagScanNode) simpleExplain() (map[string]any, error) { + simpleExplainMap := map[string]any{} // Add the field attribute to the explanation if it exists. if n.commitSelect.FieldName.HasValue() { - explainerMap["field"] = n.commitSelect.FieldName.Value() + simpleExplainMap["field"] = n.commitSelect.FieldName.Value() } else { - explainerMap["field"] = nil + simpleExplainMap["field"] = nil } // Add the cid attribute to the explanation if it exists. if n.commitSelect.Cid.HasValue() { - explainerMap["cid"] = n.commitSelect.Cid.Value() + simpleExplainMap["cid"] = n.commitSelect.Cid.Value() } else { - explainerMap["cid"] = nil + simpleExplainMap["cid"] = nil } // Build the explanation of the spans attribute. @@ -152,12 +157,31 @@ func (n *dagScanNode) Explain() (map[string]any, error) { } } // Add the built spans attribute, if it was valid. - explainerMap[spansLabel] = spansExplainer + simpleExplainMap[spansLabel] = spansExplainer - return explainerMap, nil + return simpleExplainMap, nil +} + +// 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 *dagScanNode) Explain(explainType request.ExplainType) (map[string]any, error) { + switch explainType { + case request.SimpleExplain: + return n.simpleExplain() + + case request.ExecuteExplain: + return map[string]any{ + "iterations": n.execInfo.iterations, + }, nil + + default: + return nil, ErrUnknownExplainRequestType + } } func (n *dagScanNode) Next() (bool, error) { + n.execInfo.iterations++ + var currentCid *cid.Cid store := n.planner.txn.DAGstore() diff --git a/planner/count.go b/planner/count.go index cf3acd3c38..28222f11c7 100644 --- a/planner/count.go +++ b/planner/count.go @@ -20,6 +20,7 @@ import ( "github.com/sourcenetwork/immutable" "github.com/sourcenetwork/immutable/enumerable" + "github.com/sourcenetwork/defradb/client/request" "github.com/sourcenetwork/defradb/core" "github.com/sourcenetwork/defradb/planner/mapper" ) @@ -33,6 +34,13 @@ type countNode struct { virtualFieldIndex int aggregateMapping []mapper.AggregateTarget + + execInfo countExecInfo +} + +type countExecInfo struct { + // Total number of times countNode was executed. + iterations uint64 } func (p *Planner) Count(field *mapper.Aggregate, host *mapper.Select) (*countNode, error) { @@ -60,25 +68,23 @@ func (n *countNode) Close() error { return n.plan.Close() } func (n *countNode) 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 *countNode) Explain() (map[string]any, error) { +func (n *countNode) simpleExplain() (map[string]any, error) { sourceExplanations := make([]map[string]any, len(n.aggregateMapping)) for i, source := range n.aggregateMapping { - explainerMap := map[string]any{} + simpleExplainMap := map[string]any{} // Add the filter attribute if it exists. if source.Filter == nil || source.Filter.ExternalConditions == nil { - explainerMap[filterLabel] = nil + simpleExplainMap[filterLabel] = nil } else { - explainerMap[filterLabel] = source.Filter.ExternalConditions + simpleExplainMap[filterLabel] = source.Filter.ExternalConditions } // Add the main field name. - explainerMap[fieldNameLabel] = source.Field.Name + simpleExplainMap[fieldNameLabel] = source.Field.Name - sourceExplanations[i] = explainerMap + sourceExplanations[i] = simpleExplainMap } return map[string]any{ @@ -86,7 +92,26 @@ func (n *countNode) Explain() (map[string]any, error) { }, nil } +// 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 *countNode) Explain(explainType request.ExplainType) (map[string]any, error) { + switch explainType { + case request.SimpleExplain: + return n.simpleExplain() + + case request.ExecuteExplain: + return map[string]any{ + "iterations": n.execInfo.iterations, + }, nil + + default: + return nil, ErrUnknownExplainRequestType + } +} + func (n *countNode) Next() (bool, error) { + n.execInfo.iterations++ + hasValue, err := n.plan.Next() if err != nil || !hasValue { return hasValue, err diff --git a/planner/create.go b/planner/create.go index f5cf874748..291c723300 100644 --- a/planner/create.go +++ b/planner/create.go @@ -14,6 +14,7 @@ import ( "encoding/json" "github.com/sourcenetwork/defradb/client" + "github.com/sourcenetwork/defradb/client/request" "github.com/sourcenetwork/defradb/core" "github.com/sourcenetwork/defradb/db/base" "github.com/sourcenetwork/defradb/planner/mapper" @@ -44,6 +45,13 @@ type createNode struct { returned bool results planNode + + execInfo createExecInfo +} + +type createExecInfo struct { + // Total number of times createNode was executed. + iterations uint64 } func (n *createNode) Kind() string { return "createNode" } @@ -62,6 +70,8 @@ func (n *createNode) Start() error { // Next only returns once. func (n *createNode) Next() (bool, error) { + n.execInfo.iterations++ + if n.err != nil { return false, n.err } @@ -121,9 +131,7 @@ func (n *createNode) Close() error { func (n *createNode) Source() planNode { return n.results } -// 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 *createNode) Explain() (map[string]any, error) { +func (n *createNode) simpleExplain() (map[string]any, error) { data := map[string]any{} err := json.Unmarshal([]byte(n.newDocStr), &data) if err != nil { @@ -135,6 +143,23 @@ func (n *createNode) Explain() (map[string]any, error) { }, nil } +// 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 *createNode) Explain(explainType request.ExplainType) (map[string]any, error) { + switch explainType { + case request.SimpleExplain: + return n.simpleExplain() + + case request.ExecuteExplain: + return map[string]any{ + "iterations": n.execInfo.iterations, + }, nil + + default: + return nil, ErrUnknownExplainRequestType + } +} + func (p *Planner) CreateDoc(parsed *mapper.Mutation) (planNode, error) { results, err := p.Select(&parsed.Select) if err != nil { diff --git a/planner/delete.go b/planner/delete.go index cb300ebdbf..ef79463302 100644 --- a/planner/delete.go +++ b/planner/delete.go @@ -12,6 +12,7 @@ package planner import ( "github.com/sourcenetwork/defradb/client" + "github.com/sourcenetwork/defradb/client/request" "github.com/sourcenetwork/defradb/core" "github.com/sourcenetwork/defradb/planner/mapper" ) @@ -27,9 +28,18 @@ type deleteNode struct { filter *mapper.Filter ids []string + + execInfo deleteExecInfo +} + +type deleteExecInfo struct { + // Total number of times deleteNode was executed. + iterations uint64 } func (n *deleteNode) Next() (bool, error) { + n.execInfo.iterations++ + next, err := n.source.Next() if err != nil { return false, err @@ -74,22 +84,37 @@ func (n *deleteNode) Source() planNode { return n.source } -// 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 *deleteNode) Explain() (map[string]any, error) { - explainerMap := map[string]any{} +func (n *deleteNode) simpleExplain() (map[string]any, error) { + simpleExplainMap := map[string]any{} // Add the document id(s) that request wants to delete. - explainerMap[idsLabel] = n.ids + simpleExplainMap[idsLabel] = n.ids // Add the filter attribute if it exists, otherwise have it nil. if n.filter == nil || n.filter.ExternalConditions == nil { - explainerMap[filterLabel] = nil + simpleExplainMap[filterLabel] = nil } else { - explainerMap[filterLabel] = n.filter.ExternalConditions + simpleExplainMap[filterLabel] = n.filter.ExternalConditions } - return explainerMap, nil + return simpleExplainMap, nil +} + +// 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 *deleteNode) Explain(explainType request.ExplainType) (map[string]any, error) { + switch explainType { + case request.SimpleExplain: + return n.simpleExplain() + + case request.ExecuteExplain: + return map[string]any{ + "iterations": n.execInfo.iterations, + }, nil + + default: + return nil, ErrUnknownExplainRequestType + } } func (p *Planner) DeleteDocs(parsed *mapper.Mutation) (planNode, error) { diff --git a/planner/errors.go b/planner/errors.go index 720f3b2844..4a2f5d371e 100644 --- a/planner/errors.go +++ b/planner/errors.go @@ -13,8 +13,9 @@ package planner import "github.com/sourcenetwork/defradb/errors" const ( - errUnknownDependency string = "given field does not exist" - errFailedToClosePlan string = "failed to close the plan" + errUnknownDependency string = "given field does not exist" + errFailedToClosePlan string = "failed to close the plan" + errFailedToCollectExecExplainInfo string = "failed to collect execution explain information" ) var ( @@ -30,6 +31,7 @@ var ( ErrMissingChildValue = errors.New("expected child value, however none was yielded") ErrUnknownRelationType = errors.New("failed sub selection, unknown relation type") ErrUnknownExplainRequestType = errors.New("can not explain request of unknown type") + ErrFailedToCollectExecExplainInfo = errors.New(errFailedToCollectExecExplainInfo) ErrUnknownDependency = errors.New(errUnknownDependency) ) @@ -40,3 +42,7 @@ func NewErrUnknownDependency(name string) error { func NewErrFailedToClosePlan(inner error, location string) error { return errors.Wrap(errFailedToClosePlan, inner, errors.NewKV("Location", location)) } + +func NewErrFailedToCollectExecExplainInfo(inner error) error { + return errors.Wrap(errFailedToCollectExecExplainInfo, inner) +} diff --git a/planner/explain.go b/planner/explain.go index 37f0500724..f4494fcf72 100644 --- a/planner/explain.go +++ b/planner/explain.go @@ -11,12 +11,23 @@ package planner import ( + "context" + "strconv" + "github.com/iancoleman/strcase" + + "github.com/sourcenetwork/defradb/client/request" ) type explainablePlanNode interface { planNode - Explain() (map[string]any, error) + + // Explain returns explain datapoints that are scoped to this node. + // + // It is possible that no datapoint is gathered for a certain node. + // + // Explain with type execute should NOT be called before the `Next()` has been called. + Explain(explainType request.ExplainType) (map[string]any, error) } // Compile time check for all planNodes that should be explainable (satisfy explainablePlanNode). @@ -110,7 +121,7 @@ func buildSimpleExplainGraph(source planNode) (map[string]any, error) { // For typeIndexJoin restructure the graphs to show both `root` and `subType` at the same level. case *typeIndexJoin: // Get the non-restructured explain graph. - indexJoinGraph, err := node.Explain() + indexJoinGraph, err := node.Explain(request.SimpleExplain) if err != nil { return nil, err } @@ -131,7 +142,7 @@ func buildSimpleExplainGraph(source planNode) (map[string]any, error) { // If this node has subscribed to the optable-interface that makes a node explainable. case explainablePlanNode: // Start building the explain graph. - explainGraphBuilder, err := node.Explain() + explainGraphBuilder, err := node.Explain(request.SimpleExplain) if err != nil { return nil, err } @@ -168,3 +179,176 @@ func buildSimpleExplainGraph(source planNode) (map[string]any, error) { return explainGraph, nil } + +// collectExecuteExplainInfo structures and returns the already collected information +// when the request was executed with the explain option. +// +// Note: Can only be called once the entire plan has been executed. +func collectExecuteExplainInfo(executedPlan planNode) (map[string]any, error) { + excuteExplainInfo := map[string]any{} + + if executedPlan == nil { + return excuteExplainInfo, nil + } + + switch executedNode := executedPlan.(type) { + case MultiNode: + multiChildExplainGraph := []map[string]any{} + for _, childSource := range executedNode.Children() { + childExplainGraph, err := collectExecuteExplainInfo(childSource) + if err != nil { + return nil, err + } + multiChildExplainGraph = append(multiChildExplainGraph, childExplainGraph) + } + explainNodeLabelTitle := strcase.ToLowerCamel(executedNode.Kind()) + excuteExplainInfo[explainNodeLabelTitle] = multiChildExplainGraph + + case explainablePlanNode: + excuteExplainBuilder, err := executedNode.Explain(request.ExecuteExplain) + if err != nil { + return nil, err + } + + if excuteExplainBuilder == nil { + excuteExplainBuilder = map[string]any{} + } + + if next := executedNode.Source(); next != nil && next.Kind() != topLevelNodeKind { + nextExplainGraph, err := collectExecuteExplainInfo(next) + if err != nil { + return nil, err + } + for key, value := range nextExplainGraph { + excuteExplainBuilder[key] = value + } + } + explainNodeLabelTitle := strcase.ToLowerCamel(executedNode.Kind()) + excuteExplainInfo[explainNodeLabelTitle] = excuteExplainBuilder + + default: + var err error + excuteExplainInfo, err = collectExecuteExplainInfo(executedPlan.Source()) + if err != nil { + return nil, err + } + } + + return excuteExplainInfo, nil +} + +// executeAndExplainRequest executes the plan graph gathering the information/datapoints +// during the execution. Then once the execution is complete returns the collected datapoints. +// +// Note: This function only fails if the collection of the datapoints goes wrong, otherwise +// even if plan execution fails this function would return the collected datapoints. +func (p *Planner) executeAndExplainRequest( + ctx context.Context, + plan planNode, +) ([]map[string]any, error) { + executionSuccess := false + planExecutions := uint64(0) + + if err := plan.Start(); err != nil { + return []map[string]any{ + { + request.ExplainLabel: map[string]any{ + "executionSuccess": executionSuccess, + "executionErrors": []string{"plan failed to start"}, + "planExecutions": planExecutions, + }, + }, + }, nil + } + + next, err := plan.Next() + planExecutions++ + if err != nil { + return []map[string]any{ + { + request.ExplainLabel: map[string]any{ + "executionSuccess": executionSuccess, + "executionErrors": []string{ + "failure at plan execution count: " + strconv.FormatUint(planExecutions, 10), + err.Error(), + }, + "planExecutions": planExecutions, + }, + }, + }, nil + } + + docs := []map[string]any{} + docMap := plan.DocumentMap() + + for next { + copy := docMap.ToMap(plan.Value()) + docs = append(docs, copy) + + next, err = plan.Next() + planExecutions++ + + if err != nil { + return []map[string]any{ + { + request.ExplainLabel: map[string]any{ + "executionSuccess": executionSuccess, + "executionErrors": []string{ + "failure at plan execution count: " + strconv.FormatUint(planExecutions, 10), + err.Error(), + }, + "planExecutions": planExecutions, + "sizeOfResultSoFar": len(docs), + }, + }, + }, nil + } + } + executionSuccess = true + + executeExplain, err := collectExecuteExplainInfo(plan) + if err != nil { + return nil, NewErrFailedToCollectExecExplainInfo(err) + } + + executeExplain["executionSuccess"] = executionSuccess + executeExplain["planExecutions"] = planExecutions + executeExplain["sizeOfResult"] = len(docs) + + return []map[string]any{ + { + request.ExplainLabel: executeExplain, + }, + }, err +} + +// explainRequest explains the given request plan according to the type of explain request. +func (p *Planner) explainRequest( + ctx context.Context, + plan planNode, + explainType request.ExplainType, +) ([]map[string]any, error) { + switch explainType { + case request.SimpleExplain: + // walks through the plan graph, and outputs the concrete planNodes that should + // be executed, maintaining their order in the plan graph (does not actually execute them). + explainGraph, err := buildSimpleExplainGraph(plan) + if err != nil { + return nil, err + } + + explainResult := []map[string]any{ + { + request.ExplainLabel: explainGraph, + }, + } + + return explainResult, nil + + case request.ExecuteExplain: + return p.executeAndExplainRequest(ctx, plan) + + default: + return nil, ErrUnknownExplainRequestType + } +} diff --git a/planner/group.go b/planner/group.go index 9a6b19da86..1405998545 100644 --- a/planner/group.go +++ b/planner/group.go @@ -36,6 +36,25 @@ type groupNode struct { values []core.Doc currentIndex int + + execInfo groupExecInfo +} + +type groupExecInfo struct { + // Total number of times groupNode was executed. + iterations uint64 + + // Total number of groups. + groups uint64 + + // Total number of child selections (hidden and visible). + childSelections uint64 + + // Total number of child selections hidden before offset. + hiddenBeforeOffset uint64 + + // Total number of child selections hidden after offset and limit. + hiddenAfterLimit uint64 } // Creates a new group node. @@ -127,6 +146,8 @@ func (n *groupNode) Close() error { func (n *groupNode) Source() planNode { return n.dataSources[0].Source() } func (n *groupNode) Next() (bool, error) { + n.execInfo.iterations++ + if n.values == nil { values, err := join(n.dataSources, n.groupByFields, n.documentMapping) if err != nil { @@ -136,7 +157,11 @@ func (n *groupNode) Next() (bool, error) { n.values = values.values for _, group := range n.values { + n.execInfo.groups++ + for _, childSelect := range n.childSelects { + n.execInfo.childSelections++ + subSelect := group.Fields[childSelect.Index] if subSelect == nil { // If the sub-select is nil we need to set it to an empty array and continue @@ -151,11 +176,15 @@ func (n *groupNode) Next() (bool, error) { // We must hide all child documents before the offset for i := uint64(0); i < childSelect.Limit.Offset && i < l; i++ { childDocs[i].Hidden = true + + n.execInfo.hiddenBeforeOffset++ } // We must hide all child documents after the offset plus limit for i := childSelect.Limit.Limit + childSelect.Limit.Offset; i < l; i++ { childDocs[i].Hidden = true + + n.execInfo.hiddenAfterLimit++ } } } @@ -171,10 +200,8 @@ func (n *groupNode) Next() (bool, error) { return false, nil } -// 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 *groupNode) Explain() (map[string]any, error) { - explainerMap := map[string]any{} +func (n *groupNode) simpleExplain() (map[string]any, error) { + simpleExplainMap := map[string]any{} // Get the parent level groupBy attribute(s). groupByFields := []string{} @@ -184,11 +211,11 @@ func (n *groupNode) Explain() (map[string]any, error) { field.Name, ) } - explainerMap["groupByFields"] = groupByFields + simpleExplainMap["groupByFields"] = groupByFields // Get the inner group (child) selection attribute(s). if len(n.childSelects) == 0 { - explainerMap["childSelects"] = nil + simpleExplainMap["childSelects"] = nil } else { childSelects := make([]map[string]any, 0, len(n.childSelects)) for _, child := range n.childSelects { @@ -269,8 +296,34 @@ func (n *groupNode) Explain() (map[string]any, error) { childSelects = append(childSelects, childExplainGraph) } - explainerMap["childSelects"] = childSelects + simpleExplainMap["childSelects"] = childSelects + } + + return simpleExplainMap, nil +} + +func (n *groupNode) excuteExplain() map[string]any { + return map[string]any{ + "iterations": n.execInfo.iterations, + "groups": n.execInfo.groups, + "childSelections": n.execInfo.childSelections, + "hiddenBeforeOffset": n.execInfo.hiddenBeforeOffset, + "hiddenAfterLimit": n.execInfo.hiddenAfterLimit, + "hiddenChildSelections": n.execInfo.hiddenBeforeOffset + n.execInfo.hiddenAfterLimit, } +} + +// 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 *groupNode) Explain(explainType request.ExplainType) (map[string]any, error) { + switch explainType { + case request.SimpleExplain: + return n.simpleExplain() + + case request.ExecuteExplain: + return n.excuteExplain(), nil - return explainerMap, nil + default: + return nil, ErrUnknownExplainRequestType + } } diff --git a/planner/limit.go b/planner/limit.go index 746c403df3..d3c2954d9b 100644 --- a/planner/limit.go +++ b/planner/limit.go @@ -11,6 +11,7 @@ package planner import ( + "github.com/sourcenetwork/defradb/client/request" "github.com/sourcenetwork/defradb/core" "github.com/sourcenetwork/defradb/planner/mapper" ) @@ -26,6 +27,13 @@ type limitNode struct { limit uint64 offset uint64 rowIndex uint64 + + execInfo limitExecInfo +} + +type limitExecInfo struct { + // Total number of times limitNode was executed. + iterations uint64 } // Limit creates a new limitNode initalized from the parser.Limit object. @@ -57,6 +65,8 @@ func (n *limitNode) Close() error { return n.plan.Close() } func (n *limitNode) Value() core.Doc { return n.plan.Value() } func (n *limitNode) Next() (bool, error) { + n.execInfo.iterations++ + // check if we're passed the limit if n.limit != 0 && n.rowIndex >= n.limit+n.offset { return false, nil @@ -80,15 +90,30 @@ func (n *limitNode) Next() (bool, error) { func (n *limitNode) Source() planNode { return n.plan } -func (n *limitNode) Explain() (map[string]any, error) { - exp := map[string]any{ +func (n *limitNode) simpleExplain() (map[string]any, error) { + simpleExplainMap := map[string]any{ limitLabel: n.limit, offsetLabel: n.offset, } if n.limit == 0 { - exp[limitLabel] = nil + simpleExplainMap[limitLabel] = nil } - return exp, nil + return simpleExplainMap, nil +} + +func (n *limitNode) Explain(explainType request.ExplainType) (map[string]any, error) { + switch explainType { + case request.SimpleExplain: + return n.simpleExplain() + + case request.ExecuteExplain: + return map[string]any{ + "iterations": n.execInfo.iterations, + }, nil + + default: + return nil, ErrUnknownExplainRequestType + } } diff --git a/planner/order.go b/planner/order.go index e0f4dbad60..7bbe0c91b0 100644 --- a/planner/order.go +++ b/planner/order.go @@ -12,6 +12,7 @@ package planner import ( "github.com/sourcenetwork/defradb/client" + "github.com/sourcenetwork/defradb/client/request" "github.com/sourcenetwork/defradb/core" "github.com/sourcenetwork/defradb/planner/mapper" ) @@ -59,6 +60,13 @@ type orderNode struct { // indicates if our underlying orderStrategy is still // consuming and sorting data. needSort bool + + execInfo orderExecInfo +} + +type orderExecInfo struct { + // Total number of times orderNode was executed. + iterations uint64 } // OrderBy creates a new orderNode which returns the underlying @@ -96,9 +104,7 @@ func (n *orderNode) Value() core.Doc { return n.valueIter.Value() } -// 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 *orderNode) Explain() (map[string]any, error) { +func (n *orderNode) simpleExplain() (map[string]any, error) { orderings := []map[string]any{} for _, element := range n.ordering { @@ -128,7 +134,26 @@ func (n *orderNode) Explain() (map[string]any, error) { }, nil } +// 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 *orderNode) Explain(explainType request.ExplainType) (map[string]any, error) { + switch explainType { + case request.SimpleExplain: + return n.simpleExplain() + + case request.ExecuteExplain: + return map[string]any{ + "iterations": n.execInfo.iterations, + }, nil + + default: + return nil, ErrUnknownExplainRequestType + } +} + func (n *orderNode) Next() (bool, error) { + n.execInfo.iterations++ + for n.needSort { // make sure our orderStrategy is initialized if n.orderStrategy == nil { diff --git a/planner/planner.go b/planner/planner.go index 4a9b53ace7..e464af2d24 100644 --- a/planner/planner.go +++ b/planner/planner.go @@ -438,33 +438,6 @@ func walkAndFindPlanType[T planNode](planNode planNode) (T, bool) { return targetType, true } -// explainRequest walks through the plan graph, and outputs the concrete planNodes that should -// be executed, maintaing their order in the plan graph (does not actually execute them). -func (p *Planner) explainRequest( - ctx context.Context, - planNode planNode, - explainType request.ExplainType, -) ([]map[string]any, error) { - switch explainType { - case request.SimpleExplain: - explainGraph, err := buildSimpleExplainGraph(planNode) - if err != nil { - return nil, err - } - - explainResult := []map[string]any{ - { - request.ExplainLabel: explainGraph, - }, - } - - return explainResult, nil - - default: - return nil, ErrUnknownExplainRequestType - } -} - // executeRequest executes the plan graph that represents the request that was made. func (p *Planner) executeRequest( ctx context.Context, diff --git a/planner/scan.go b/planner/scan.go index 3736452a6e..3c36fe86a8 100644 --- a/planner/scan.go +++ b/planner/scan.go @@ -12,12 +12,24 @@ package planner import ( "github.com/sourcenetwork/defradb/client" + "github.com/sourcenetwork/defradb/client/request" "github.com/sourcenetwork/defradb/core" "github.com/sourcenetwork/defradb/db/base" "github.com/sourcenetwork/defradb/db/fetcher" "github.com/sourcenetwork/defradb/planner/mapper" ) +type scanExecInfo struct { + // Total number of times scan was issued. + iterations uint64 + + // Total number of times attempted to fetch documents. + docFetches uint64 + + // Total number of documents that matched / passed the filter. + filterMatches uint64 +} + // scans an index for records type scanNode struct { documentIterator @@ -37,6 +49,8 @@ type scanNode struct { scanInitialized bool fetcher fetcher.Fetcher + + execInfo scanExecInfo } func (n *scanNode) Kind() string { @@ -81,6 +95,8 @@ func (n *scanNode) initScan() error { // Returns true, if there is a result, // and false otherwise. func (n *scanNode) Next() (bool, error) { + n.execInfo.iterations++ + if n.spans.HasValue && len(n.spans.Value) == 0 { return false, nil } @@ -92,6 +108,7 @@ func (n *scanNode) Next() (bool, error) { if err != nil { return false, err } + n.execInfo.docFetches++ if len(n.currentValue.Fields) == 0 { return false, nil @@ -102,6 +119,7 @@ func (n *scanNode) Next() (bool, error) { return false, err } if passed { + n.execInfo.filterMatches++ return true, nil } } @@ -132,26 +150,47 @@ func (n *scanNode) explainSpans() []map[string]any { return spansExplainer } -// 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 *scanNode) Explain() (map[string]any, error) { - explainerMap := map[string]any{} +func (n *scanNode) simpleExplain() (map[string]any, error) { + simpleExplainMap := map[string]any{} // Add the filter attribute if it exists. if n.filter == nil || n.filter.ExternalConditions == nil { - explainerMap[filterLabel] = nil + simpleExplainMap[filterLabel] = nil } else { - explainerMap[filterLabel] = n.filter.ExternalConditions + simpleExplainMap[filterLabel] = n.filter.ExternalConditions } // Add the collection attributes. - explainerMap[collectionNameLabel] = n.desc.Name - explainerMap[collectionIDLabel] = n.desc.IDString() + simpleExplainMap[collectionNameLabel] = n.desc.Name + simpleExplainMap[collectionIDLabel] = n.desc.IDString() // Add the spans attribute. - explainerMap[spansLabel] = n.explainSpans() + simpleExplainMap[spansLabel] = n.explainSpans() + + return simpleExplainMap, nil +} - return explainerMap, nil +func (n *scanNode) excuteExplain() map[string]any { + return map[string]any{ + "iterations": n.execInfo.iterations, + "docFetches": n.execInfo.docFetches, + "filterMatches": n.execInfo.filterMatches, + } +} + +// 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 *scanNode) Explain(explainType request.ExplainType) (map[string]any, error) { + switch explainType { + case request.SimpleExplain: + return n.simpleExplain() + + case request.ExecuteExplain: + return n.excuteExplain(), nil + + default: + return nil, ErrUnknownExplainRequestType + } } // Merge implements mergeNode diff --git a/planner/select.go b/planner/select.go index 59730b70a6..c2e44f3339 100644 --- a/planner/select.go +++ b/planner/select.go @@ -76,7 +76,7 @@ func (n *selectTopNode) Source() planNode { return n.planNode } // Explain method for selectTopNode returns no attributes but is used to // subscribe / opt-into being an explainablePlanNode. -func (n *selectTopNode) Explain() (map[string]any, error) { +func (n *selectTopNode) Explain(explainType request.ExplainType) (map[string]any, error) { // No attributes are returned for selectTopNode. return nil, nil } @@ -119,6 +119,16 @@ type selectNode struct { selectReq *mapper.Select groupSelects []*mapper.Select + + execInfo selectExecInfo +} + +type selectExecInfo struct { + // Total number of times selectNode was executed. + iterations uint64 + + // Total number of times top level select filter passed / matched. + filterMatches uint64 } func (n *selectNode) Kind() string { @@ -138,6 +148,8 @@ func (n *selectNode) Start() error { // remaining top level filtering, and // renders the doc. func (n *selectNode) Next() (bool, error) { + n.execInfo.iterations++ + for { if hasNext, err := n.source.Next(); !hasNext { return false, err @@ -153,6 +165,8 @@ func (n *selectNode) Next() (bool, error) { continue } + n.execInfo.filterMatches++ + if n.docKeys.HasValue() { docKey := n.currentValue.GetKey() for _, key := range n.docKeys.Value() { @@ -160,6 +174,7 @@ func (n *selectNode) Next() (bool, error) { return true, nil } } + continue } @@ -175,19 +190,35 @@ func (n *selectNode) Close() error { return n.source.Close() } -// 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 *selectNode) Explain() (map[string]any, error) { - explainerMap := map[string]any{} +func (n *selectNode) simpleExplain() (map[string]any, error) { + simpleExplainMap := map[string]any{} // Add the filter attribute if it exists. if n.filter == nil || n.filter.ExternalConditions == nil { - explainerMap[filterLabel] = nil + simpleExplainMap[filterLabel] = nil } else { - explainerMap[filterLabel] = n.filter.ExternalConditions + simpleExplainMap[filterLabel] = n.filter.ExternalConditions } - return explainerMap, nil + return simpleExplainMap, nil +} + +// 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 *selectNode) Explain(explainType request.ExplainType) (map[string]any, error) { + switch explainType { + case request.SimpleExplain: + return n.simpleExplain() + + case request.ExecuteExplain: + return map[string]any{ + "iterations": n.execInfo.iterations, + "filterMatches": n.execInfo.filterMatches, + }, nil + + default: + return nil, ErrUnknownExplainRequestType + } } // initSource is the main workhorse for recursively constructing diff --git a/planner/sum.go b/planner/sum.go index d5b2b29178..7bb14f2501 100644 --- a/planner/sum.go +++ b/planner/sum.go @@ -30,6 +30,13 @@ type sumNode struct { isFloat bool virtualFieldIndex int aggregateMapping []mapper.AggregateTarget + + execInfo sumExecInfo +} + +type sumExecInfo struct { + // Total number of times sumNode was executed. + iterations uint64 } func (p *Planner) Sum( @@ -149,32 +156,30 @@ func (n *sumNode) Close() error { return n.plan.Close() } 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]any, error) { +func (n *sumNode) simpleExplain() (map[string]any, error) { sourceExplanations := make([]map[string]any, len(n.aggregateMapping)) for i, source := range n.aggregateMapping { - explainerMap := map[string]any{} + simpleExplainMap := map[string]any{} // Add the filter attribute if it exists. if source.Filter == nil || source.Filter.ExternalConditions == nil { - explainerMap[filterLabel] = nil + simpleExplainMap[filterLabel] = nil } else { - explainerMap[filterLabel] = source.Filter.ExternalConditions + simpleExplainMap[filterLabel] = source.Filter.ExternalConditions } // Add the main field name. - explainerMap[fieldNameLabel] = source.Field.Name + simpleExplainMap[fieldNameLabel] = source.Field.Name // Add the child field name if it exists. if source.ChildTarget.HasValue { - explainerMap[childFieldNameLabel] = source.ChildTarget.Name + simpleExplainMap[childFieldNameLabel] = source.ChildTarget.Name } else { - explainerMap[childFieldNameLabel] = nil + simpleExplainMap[childFieldNameLabel] = nil } - sourceExplanations[i] = explainerMap + sourceExplanations[i] = simpleExplainMap } return map[string]any{ @@ -182,7 +187,26 @@ func (n *sumNode) Explain() (map[string]any, error) { }, nil } +// 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(explainType request.ExplainType) (map[string]any, error) { + switch explainType { + case request.SimpleExplain: + return n.simpleExplain() + + case request.ExecuteExplain: + return map[string]any{ + "iterations": n.execInfo.iterations, + }, nil + + default: + return nil, ErrUnknownExplainRequestType + } +} + func (n *sumNode) Next() (bool, error) { + n.execInfo.iterations++ + hasNext, err := n.plan.Next() if err != nil || !hasNext { return hasNext, err diff --git a/planner/top.go b/planner/top.go index d1ce550d8c..1f7764e091 100644 --- a/planner/top.go +++ b/planner/top.go @@ -120,8 +120,17 @@ func (p *topLevelNode) Children() []planNode { return p.children } -func (n *topLevelNode) Explain() (map[string]any, error) { - return map[string]any{}, nil +func (n *topLevelNode) Explain(explainType request.ExplainType) (map[string]any, error) { + switch explainType { + case request.SimpleExplain: + return map[string]any{}, nil + + case request.ExecuteExplain: + return map[string]any{}, nil + + default: + return nil, ErrUnknownExplainRequestType + } } func (n *topLevelNode) Next() (bool, error) { diff --git a/planner/type_join.go b/planner/type_join.go index cbca6de0fe..2f3ff0920d 100644 --- a/planner/type_join.go +++ b/planner/type_join.go @@ -12,6 +12,7 @@ package planner import ( "github.com/sourcenetwork/defradb/client" + "github.com/sourcenetwork/defradb/client/request" "github.com/sourcenetwork/defradb/connor" "github.com/sourcenetwork/defradb/core" "github.com/sourcenetwork/defradb/db/base" @@ -51,17 +52,16 @@ type typeIndexJoin struct { p *Planner - // root planNode - // subType planNode - // subTypeName string - // actual join plan, could be one of several strategies // based on the relationship of the sub types joinPlan planNode - // doc map[string]any + execInfo typeIndexJoinExecInfo +} - // spans core.Spans +type typeIndexJoinExecInfo struct { + // Total number of times typeIndexJoin node was executed. + iterations uint64 } func (p *Planner) makeTypeIndexJoin( @@ -117,6 +117,8 @@ func (n *typeIndexJoin) Spans(spans core.Spans) { } func (n *typeIndexJoin) Next() (bool, error) { + n.execInfo.iterations++ + return n.joinPlan.Next() } @@ -130,9 +132,7 @@ func (n *typeIndexJoin) Close() error { func (n *typeIndexJoin) Source() planNode { return n.joinPlan } -// 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 *typeIndexJoin) Explain() (map[string]any, error) { +func (n *typeIndexJoin) simpleExplain() (map[string]any, error) { const ( joinTypeLabel = "joinType" joinDirectionLabel = "direction" @@ -143,23 +143,23 @@ func (n *typeIndexJoin) Explain() (map[string]any, error) { joinRootLabel = "rootName" ) - explainerMap := map[string]any{} + simpleExplainMap := map[string]any{} // Add the type attribute. - explainerMap[joinTypeLabel] = n.joinPlan.Kind() + simpleExplainMap[joinTypeLabel] = n.joinPlan.Kind() switch joinType := n.joinPlan.(type) { case *typeJoinOne: // Add the direction attribute. if joinType.primary { - explainerMap[joinDirectionLabel] = joinDirectionPrimaryLabel + simpleExplainMap[joinDirectionLabel] = joinDirectionPrimaryLabel } else { - explainerMap[joinDirectionLabel] = joinDirectionSecondaryLabel + simpleExplainMap[joinDirectionLabel] = joinDirectionSecondaryLabel } // Add the attribute(s). - explainerMap[joinRootLabel] = joinType.subTypeFieldName - explainerMap[joinSubTypeNameLabel] = joinType.subTypeName + simpleExplainMap[joinRootLabel] = joinType.subTypeFieldName + simpleExplainMap[joinSubTypeNameLabel] = joinType.subTypeName subTypeExplainGraph, err := buildSimpleExplainGraph(joinType.subType) if err != nil { @@ -167,12 +167,12 @@ func (n *typeIndexJoin) Explain() (map[string]any, error) { } // Add the joined (subType) type's entire explain graph. - explainerMap[joinSubTypeLabel] = subTypeExplainGraph + simpleExplainMap[joinSubTypeLabel] = subTypeExplainGraph case *typeJoinMany: // Add the attribute(s). - explainerMap[joinRootLabel] = joinType.rootName - explainerMap[joinSubTypeNameLabel] = joinType.subTypeName + simpleExplainMap[joinRootLabel] = joinType.rootName + simpleExplainMap[joinSubTypeNameLabel] = joinType.subTypeName subTypeExplainGraph, err := buildSimpleExplainGraph(joinType.subType) if err != nil { @@ -180,13 +180,30 @@ func (n *typeIndexJoin) Explain() (map[string]any, error) { } // Add the joined (subType) type's entire explain graph. - explainerMap[joinSubTypeLabel] = subTypeExplainGraph + simpleExplainMap[joinSubTypeLabel] = subTypeExplainGraph default: - return explainerMap, client.NewErrUnhandledType("join plan", n.joinPlan) + return simpleExplainMap, client.NewErrUnhandledType("join plan", n.joinPlan) } - return explainerMap, nil + return simpleExplainMap, nil +} + +// 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 *typeIndexJoin) Explain(explainType request.ExplainType) (map[string]any, error) { + switch explainType { + case request.SimpleExplain: + return n.simpleExplain() + + case request.ExecuteExplain: + return map[string]any{ + "iterations": n.execInfo.iterations, + }, nil + + default: + return nil, ErrUnknownExplainRequestType + } } // Merge implements mergeNode diff --git a/planner/update.go b/planner/update.go index ac2eeb8e99..c13663ad77 100644 --- a/planner/update.go +++ b/planner/update.go @@ -14,6 +14,7 @@ import ( "encoding/json" "github.com/sourcenetwork/defradb/client" + "github.com/sourcenetwork/defradb/client/request" "github.com/sourcenetwork/defradb/core" "github.com/sourcenetwork/defradb/planner/mapper" ) @@ -34,10 +35,22 @@ type updateNode struct { isUpdating bool results planNode + + execInfo updateExecInfo +} + +type updateExecInfo struct { + // Total number of times updateNode was executed. + iterations uint64 + + // Total number of successful updates. + updates uint64 } // Next only returns once. func (n *updateNode) Next() (bool, error) { + n.execInfo.iterations++ + if n.isUpdating { for { next, err := n.results.Next() @@ -57,6 +70,8 @@ func (n *updateNode) Next() (bool, error) { if err != nil { return false, err } + + n.execInfo.updates++ } n.isUpdating = false @@ -96,19 +111,17 @@ func (n *updateNode) Close() error { func (n *updateNode) Source() planNode { return n.results } -// 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 *updateNode) Explain() (map[string]any, error) { - explainerMap := map[string]any{} +func (n *updateNode) simpleExplain() (map[string]any, error) { + simpleExplainMap := map[string]any{} // Add the document id(s) that request wants to update. - explainerMap[idsLabel] = n.ids + simpleExplainMap[idsLabel] = n.ids // Add the filter attribute if it exists, otherwise have it nil. if n.filter == nil || n.filter.ExternalConditions == nil { - explainerMap[filterLabel] = nil + simpleExplainMap[filterLabel] = nil } else { - explainerMap[filterLabel] = n.filter.ExternalConditions + simpleExplainMap[filterLabel] = n.filter.ExternalConditions } // Add the attribute that represents the patch to update with. @@ -117,9 +130,27 @@ func (n *updateNode) Explain() (map[string]any, error) { if err != nil { return nil, err } - explainerMap[dataLabel] = data + simpleExplainMap[dataLabel] = data + + return simpleExplainMap, nil +} - return explainerMap, nil +// 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 *updateNode) Explain(explainType request.ExplainType) (map[string]any, error) { + switch explainType { + case request.SimpleExplain: + return n.simpleExplain() + + case request.ExecuteExplain: + return map[string]any{ + "iterations": n.execInfo.iterations, + "updates": n.execInfo.updates, + }, nil + + default: + return nil, ErrUnknownExplainRequestType + } } func (p *Planner) UpdateDocs(parsed *mapper.Mutation) (planNode, error) { diff --git a/request/graphql/parser/request.go b/request/graphql/parser/request.go index d903f6e18d..b2680971bc 100644 --- a/request/graphql/parser/request.go +++ b/request/graphql/parser/request.go @@ -141,6 +141,10 @@ func parseExplainDirective(astDirective *ast.Directive) (immutable.Option[reques switch arg.Value.GetValue() { case schemaTypes.ExplainArgSimple: return immutable.Some(request.SimpleExplain), nil + + case schemaTypes.ExplainArgExecute: + return immutable.Some(request.ExecuteExplain), nil + default: return immutable.None[request.ExplainType](), ErrUnknownExplainType } diff --git a/request/graphql/schema/types/types.go b/request/graphql/schema/types/types.go index 7bac33adb8..dbbb09a0b9 100644 --- a/request/graphql/schema/types/types.go +++ b/request/graphql/schema/types/types.go @@ -21,6 +21,7 @@ const ( ExplainArgNameType string = "type" ExplainArgSimple string = "simple" + ExplainArgExecute string = "execute" ) var ( @@ -45,6 +46,11 @@ var ( Value: ExplainArgSimple, Description: "Simple explaination - dump of the plan graph.", }, + + ExplainArgExecute: &gql.EnumValueConfig{ + Value: ExplainArgExecute, + Description: "Deeper explaination - insights gathered by executing the plan graph.", + }, }, }) diff --git a/tests/integration/explain/execute/create_test.go b/tests/integration/explain/execute/create_test.go new file mode 100644 index 0000000000..e40343f7df --- /dev/null +++ b/tests/integration/explain/execute/create_test.go @@ -0,0 +1,62 @@ +// 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_execute + +import ( + "testing" + + testUtils "github.com/sourcenetwork/defradb/tests/integration" +) + +func TestExecuteExplainMutationRequestWithCreate(t *testing.T) { + test := testUtils.TestCase{ + + Description: "Explain (execute) mutation request with create.", + + Actions: []any{ + gqlSchemaExecuteExplain(), + + testUtils.Request{ + Request: `mutation @explain(type: execute) { + create_Author(data: "{\"name\": \"Shahzad Lone\",\"age\": 27,\"verified\": true}") { + name + } + }`, + + Results: []dataMap{ + { + "explain": dataMap{ + "executionSuccess": true, + "sizeOfResult": 1, + "planExecutions": uint64(2), + "createNode": dataMap{ + "iterations": uint64(2), + "selectTopNode": dataMap{ + "selectNode": dataMap{ + "iterations": uint64(1), + "filterMatches": uint64(1), + "scanNode": dataMap{ + "iterations": uint64(1), + "docFetches": uint64(1), + "filterMatches": uint64(1), + }, + }, + }, + }, + }, + }, + }, + }, + }, + } + + executeTestCase(t, test) +} diff --git a/tests/integration/explain/execute/dagscan_test.go b/tests/integration/explain/execute/dagscan_test.go new file mode 100644 index 0000000000..7133286b64 --- /dev/null +++ b/tests/integration/explain/execute/dagscan_test.go @@ -0,0 +1,108 @@ +// 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_execute + +import ( + "testing" + + testUtils "github.com/sourcenetwork/defradb/tests/integration" +) + +func TestExecuteExplainCommitsDagScan(t *testing.T) { + test := testUtils.TestCase{ + + Description: "Explain (execute) commits request - dagScan.", + + Actions: []any{ + gqlSchemaExecuteExplain(), + + // Authors + create2AuthorDocuments(), + + testUtils.Request{ + Request: `query @explain(type: execute) { + commits (dockey: "bae-7f54d9e0-cbde-5320-aa6c-5c8895a89138") { + links { + cid + } + } + }`, + + Results: []dataMap{ + { + "explain": dataMap{ + "executionSuccess": true, + "sizeOfResult": 5, + "planExecutions": uint64(6), + "selectTopNode": dataMap{ + "selectNode": dataMap{ + "iterations": uint64(6), + "filterMatches": uint64(5), + "dagScanNode": dataMap{ + "iterations": uint64(6), + }, + }, + }, + }, + }, + }, + }, + }, + } + + executeTestCase(t, test) +} + +func TestExecuteExplainLatestCommitsDagScan(t *testing.T) { + test := testUtils.TestCase{ + + Description: "Explain (execute) latest commits request - dagScan.", + + Actions: []any{ + gqlSchemaExecuteExplain(), + + // Author + create2AuthorDocuments(), + + testUtils.Request{ + Request: `query @explain(type: execute) { + latestCommits(dockey: "bae-7f54d9e0-cbde-5320-aa6c-5c8895a89138") { + cid + links { + cid + } + } + }`, + + Results: []dataMap{ + { + "explain": dataMap{ + "executionSuccess": true, + "sizeOfResult": 1, + "planExecutions": uint64(2), + "selectTopNode": dataMap{ + "selectNode": dataMap{ + "iterations": uint64(2), + "filterMatches": uint64(1), + "dagScanNode": dataMap{ + "iterations": uint64(2), + }, + }, + }, + }, + }, + }, + }, + }, + } + + executeTestCase(t, test) +} diff --git a/tests/integration/explain/execute/delete_test.go b/tests/integration/explain/execute/delete_test.go new file mode 100644 index 0000000000..eaefde6076 --- /dev/null +++ b/tests/integration/explain/execute/delete_test.go @@ -0,0 +1,113 @@ +// 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_execute + +import ( + "testing" + + testUtils "github.com/sourcenetwork/defradb/tests/integration" +) + +func TestExecuteExplainMutationRequestWithDeleteUsingID(t *testing.T) { + test := testUtils.TestCase{ + + Description: "Explain (execute) mutation request with deletion using id.", + + Actions: []any{ + gqlSchemaExecuteExplain(), + + // Addresses + create2AddressDocuments(), + + testUtils.Request{ + Request: `mutation @explain(type: execute) { + delete_ContactAddress(ids: ["bae-f01bf83f-1507-5fb5-a6a3-09ecffa3c692"]) { + city + } + }`, + + Results: []dataMap{ + { + "explain": dataMap{ + "executionSuccess": true, + "sizeOfResult": 1, + "planExecutions": uint64(2), + "deleteNode": dataMap{ + "iterations": uint64(2), + "selectTopNode": dataMap{ + "selectNode": dataMap{ + "iterations": uint64(2), + "filterMatches": uint64(1), + "scanNode": dataMap{ + "iterations": uint64(2), + "docFetches": uint64(2), + "filterMatches": uint64(1), + }, + }, + }, + }, + }, + }, + }, + }, + }, + } + + executeTestCase(t, test) +} + +func TestExecuteExplainMutationRequestWithDeleteUsingFilter(t *testing.T) { + test := testUtils.TestCase{ + + Description: "Explain (execute) mutation request with deletion using filter.", + + Actions: []any{ + gqlSchemaExecuteExplain(), + + // Author + create2AuthorDocuments(), + + testUtils.Request{ + Request: `mutation @explain(type: execute) { + delete_Author(filter: {name: {_like: "%Funke%"}}) { + name + } + }`, + + Results: []dataMap{ + { + "explain": dataMap{ + "executionSuccess": true, + "sizeOfResult": 1, + "planExecutions": uint64(2), + "deleteNode": dataMap{ + "iterations": uint64(2), + "selectTopNode": dataMap{ + "selectNode": dataMap{ + "iterations": uint64(2), + "filterMatches": uint64(1), + "scanNode": dataMap{ + "iterations": uint64(2), + "docFetches": uint64(3), + "filterMatches": uint64(1), + }, + }, + }, + }, + }, + }, + }, + }, + }, + } + + executeTestCase(t, test) +} diff --git a/tests/integration/explain/execute/fixture.go b/tests/integration/explain/execute/fixture.go new file mode 100644 index 0000000000..4025055b76 --- /dev/null +++ b/tests/integration/explain/execute/fixture.go @@ -0,0 +1,202 @@ +// 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_execute + +import ( + "testing" + + testUtils "github.com/sourcenetwork/defradb/tests/integration" +) + +type dataMap = map[string]any + +func gqlSchemaExecuteExplain() testUtils.SchemaUpdate { + return testUtils.SchemaUpdate{ + Schema: (` + type Article { + name: String + author: Author + pages: Int + } + + type Book { + name: String + author: Author + pages: Int + chapterPages: [Int!] + } + + type Author { + name: String + age: Int + verified: Boolean + books: [Book] + articles: [Article] + contact: AuthorContact + } + + type AuthorContact { + cell: String + email: String + author: Author + address: ContactAddress + } + + type ContactAddress { + city: String + country: String + contact: AuthorContact + } + `), + } +} + +func executeTestCase(t *testing.T, test testUtils.TestCase) { + testUtils.ExecuteTestCase( + t, + []string{"Article", "Book", "Author", "AuthorContact", "ContactAddress"}, + test, + ) +} + +func create3ArticleDocuments() []testUtils.CreateDoc { + return []testUtils.CreateDoc{ + { + CollectionID: 0, + Doc: `{ + + "name": "After Guantánamo, Another Injustice", + "pages": 100, + "author_id": "bae-7f54d9e0-cbde-5320-aa6c-5c8895a89138" + }`, + }, + { + CollectionID: 0, + Doc: `{ + "name": "To my dear readers", + "pages": 200, + "author_id": "bae-68cb395d-df73-5bcb-b623-615a140dee12" + }`, + }, + { + CollectionID: 0, + Doc: `{ + "name": "Twinklestar's Favourite Xmas Cookie", + "pages": 300, + "author_id": "bae-68cb395d-df73-5bcb-b623-615a140dee12" + }`, + }, + } +} + +func create3BookDocuments() []testUtils.CreateDoc { + return []testUtils.CreateDoc{ + { + CollectionID: 1, + Doc: `{ + "name": "Painted House", + "pages": 78, + "chapterPages": [1, 22, 33, 44, 55, 66], + "author_id": "bae-7f54d9e0-cbde-5320-aa6c-5c8895a89138" + }`, + }, + { + CollectionID: 1, + Doc: `{ + "name": "A Time for Mercy", + "pages": 333, + "chapterPages": [0, 22, 101, 321], + "author_id": "bae-7f54d9e0-cbde-5320-aa6c-5c8895a89138" + }`, + }, + { + CollectionID: 1, + Doc: `{ + "name": "Theif Lord", + "pages": 20, + "author_id": "bae-68cb395d-df73-5bcb-b623-615a140dee12" + }`, + }, + } +} + +func create2AuthorDocuments() []testUtils.CreateDoc { + return []testUtils.CreateDoc{ + { + CollectionID: 2, + // _key: "bae-7f54d9e0-cbde-5320-aa6c-5c8895a89138" + Doc: `{ + "name": "John Grisham", + "age": 65, + "verified": true, + "contact_id": "bae-4db5359b-7dbe-5778-b96f-d71d1e6d0871" + }`, + }, + { + CollectionID: 2, + // _key: "bae-68cb395d-df73-5bcb-b623-615a140dee12" + Doc: `{ + "name": "Cornelia Funke", + "age": 62, + "verified": false, + "contact_id": "bae-1f19fc5d-de4d-59a5-bbde-492be1757d65" + }`, + }, + } +} + +func create2AuthorContactDocuments() []testUtils.CreateDoc { + return []testUtils.CreateDoc{ + { + CollectionID: 3, + // "author_id": "bae-7f54d9e0-cbde-5320-aa6c-5c8895a89138" + // _key: "bae-4db5359b-7dbe-5778-b96f-d71d1e6d0871" + Doc: `{ + "cell": "5197212301", + "email": "john_grisham@example.com", + "address_id": "bae-c8448e47-6cd1-571f-90bd-364acb80da7b" + }`, + }, + { + CollectionID: 3, + // "author_id": "bae-68cb395d-df73-5bcb-b623-615a140dee12", + // _key: "bae-1f19fc5d-de4d-59a5-bbde-492be1757d65" + Doc: `{ + "cell": "5197212302", + "email": "cornelia_funke@example.com", + "address_id": "bae-f01bf83f-1507-5fb5-a6a3-09ecffa3c692" + }`, + }, + } +} + +func create2AddressDocuments() []testUtils.CreateDoc { + return []testUtils.CreateDoc{ + { + CollectionID: 4, + // "contact_id": "bae-4db5359b-7dbe-5778-b96f-d71d1e6d0871" + // _key: bae-c8448e47-6cd1-571f-90bd-364acb80da7b + Doc: `{ + "city": "Waterloo", + "country": "Canada" + }`, + }, + { + CollectionID: 4, + // "contact_id": ""bae-1f19fc5d-de4d-59a5-bbde-492be1757d65"" + // _key: bae-f01bf83f-1507-5fb5-a6a3-09ecffa3c692 + Doc: `{ + "city": "Brampton", + "country": "Canada" + }`, + }, + } +} diff --git a/tests/integration/explain/execute/group_test.go b/tests/integration/explain/execute/group_test.go new file mode 100644 index 0000000000..183ce890db --- /dev/null +++ b/tests/integration/explain/execute/group_test.go @@ -0,0 +1,73 @@ +// 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_execute + +import ( + "testing" + + testUtils "github.com/sourcenetwork/defradb/tests/integration" +) + +func TestExecuteExplainRequestWithGroup(t *testing.T) { + test := testUtils.TestCase{ + + Description: "Explain (execute) request with groupBy.", + + Actions: []any{ + gqlSchemaExecuteExplain(), + + // Books + create2AddressDocuments(), + + testUtils.Request{ + Request: `query @explain(type: execute) { + ContactAddress(groupBy: [country]) { + country + _group { + city + } + } + }`, + + Results: []dataMap{ + { + "explain": dataMap{ + "executionSuccess": true, + "sizeOfResult": 1, + "planExecutions": uint64(2), + "selectTopNode": dataMap{ + "groupNode": dataMap{ + "iterations": uint64(2), + "groups": uint64(1), + "childSelections": uint64(1), + "hiddenBeforeOffset": uint64(0), + "hiddenAfterLimit": uint64(0), + "hiddenChildSelections": uint64(0), + "selectNode": dataMap{ + "iterations": uint64(3), + "filterMatches": uint64(2), + "scanNode": dataMap{ + "iterations": uint64(4), + "docFetches": uint64(4), + "filterMatches": uint64(2), + }, + }, + }, + }, + }, + }, + }, + }, + }, + } + + executeTestCase(t, test) +} diff --git a/tests/integration/explain/execute/scan_test.go b/tests/integration/explain/execute/scan_test.go new file mode 100644 index 0000000000..ab2b5348da --- /dev/null +++ b/tests/integration/explain/execute/scan_test.go @@ -0,0 +1,248 @@ +// 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_execute + +import ( + "testing" + + testUtils "github.com/sourcenetwork/defradb/tests/integration" +) + +func TestExecuteExplainRequestWithAllDocumentsMatching(t *testing.T) { + test := testUtils.TestCase{ + + Description: "Explain (execute) request with all documents matching.", + + Actions: []any{ + gqlSchemaExecuteExplain(), + + testUtils.CreateDoc{ + CollectionID: 2, + + // bae-111e8e29-0530-52ae-815f-14c7ba46d277 + Doc: `{ + "name": "Andy", + "age": 64 + }`, + }, + + testUtils.CreateDoc{ + CollectionID: 2, + + // bae-e147be24-bf9c-5d38-8c7b-ad18e4034c53 + Doc: `{ + "name": "Shahzad", + "age": 48 + }`, + }, + + testUtils.Request{ + Request: `query @explain(type: execute) { + Author { + name + age + } + }`, + + Results: []dataMap{ + { + "explain": dataMap{ + "executionSuccess": true, + "sizeOfResult": 2, + "planExecutions": uint64(3), + "selectTopNode": dataMap{ + "selectNode": dataMap{ + "iterations": uint64(3), + "filterMatches": uint64(2), + "scanNode": dataMap{ + "iterations": uint64(3), + "docFetches": uint64(3), + "filterMatches": uint64(2), + }, + }, + }, + }, + }, + }, + }, + }, + } + + executeTestCase(t, test) +} + +func TestExecuteExplainRequestWithNoDocuments(t *testing.T) { + test := testUtils.TestCase{ + + Description: "Explain (execute) request with no documents.", + + Actions: []any{ + gqlSchemaExecuteExplain(), + + testUtils.Request{ + Request: `query @explain(type: execute) { + Author { + name + } + }`, + + Results: []dataMap{ + { + "explain": dataMap{ + "executionSuccess": true, + "sizeOfResult": 0, + "planExecutions": uint64(1), + "selectTopNode": dataMap{ + "selectNode": dataMap{ + "iterations": uint64(1), + "filterMatches": uint64(0), + "scanNode": dataMap{ + "iterations": uint64(1), + "docFetches": uint64(1), + "filterMatches": uint64(0), + }, + }, + }, + }, + }, + }, + }, + }, + } + + executeTestCase(t, test) +} + +func TestExecuteExplainRequestWithSomeDocumentsMatching(t *testing.T) { + test := testUtils.TestCase{ + + Description: "Explain (execute) request with some documents matching.", + + Actions: []any{ + gqlSchemaExecuteExplain(), + + testUtils.CreateDoc{ + CollectionID: 2, + + // bae-111e8e29-0530-52ae-815f-14c7ba46d277 + Doc: `{ + "name": "Andy", + "age": 64 + }`, + }, + + testUtils.CreateDoc{ + CollectionID: 2, + + // bae-e147be24-bf9c-5d38-8c7b-ad18e4034c53 + Doc: `{ + "name": "Shahzad", + "age": 48 + }`, + }, + + testUtils.Request{ + Request: `query @explain(type: execute) { + Author(filter: {name: {_eq: "Shahzad"}}) { + name + age + } + }`, + + Results: []dataMap{ + { + "explain": dataMap{ + "executionSuccess": true, + "planExecutions": uint64(2), + "sizeOfResult": 1, + "selectTopNode": dataMap{ + "selectNode": dataMap{ + "iterations": uint64(2), + "filterMatches": uint64(1), + "scanNode": dataMap{ + "iterations": uint64(2), + "docFetches": uint64(3), + "filterMatches": uint64(1), + }, + }, + }, + }, + }, + }, + }, + }, + } + + executeTestCase(t, test) +} + +func TestExecuteExplainRequestWithDocumentsButNoMatches(t *testing.T) { + test := testUtils.TestCase{ + + Description: "Explain (execute) request with documents but no matches.", + + Actions: []any{ + gqlSchemaExecuteExplain(), + + testUtils.CreateDoc{ + CollectionID: 2, + + // bae-111e8e29-0530-52ae-815f-14c7ba46d277 + Doc: `{ + "name": "Andy", + "age": 64 + }`, + }, + + testUtils.CreateDoc{ + CollectionID: 2, + + // bae-e147be24-bf9c-5d38-8c7b-ad18e4034c53 + Doc: `{ + "name": "Shahzad", + "age": 48 + }`, + }, + + testUtils.Request{ + Request: `query @explain(type: execute) { + Author(filter: {name: {_eq: "John"}}) { + name + age + } + }`, + + Results: []dataMap{ + { + "explain": dataMap{ + "executionSuccess": true, + "planExecutions": uint64(1), + "sizeOfResult": 0, + "selectTopNode": dataMap{ + "selectNode": dataMap{ + "iterations": uint64(1), + "filterMatches": uint64(0), + "scanNode": dataMap{ + "iterations": uint64(1), + "docFetches": uint64(3), + "filterMatches": uint64(0), + }, + }, + }, + }, + }, + }, + }, + }, + } + + executeTestCase(t, test) +} diff --git a/tests/integration/explain/execute/top_level_test.go b/tests/integration/explain/execute/top_level_test.go new file mode 100644 index 0000000000..5053896b5d --- /dev/null +++ b/tests/integration/explain/execute/top_level_test.go @@ -0,0 +1,248 @@ +// 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_execute + +import ( + "testing" + + testUtils "github.com/sourcenetwork/defradb/tests/integration" +) + +func TestExecuteExplainTopLevelAverageRequest(t *testing.T) { + test := testUtils.TestCase{ + + Description: "Explain (execute) request with top level average.", + + Actions: []any{ + gqlSchemaExecuteExplain(), + + testUtils.CreateDoc{ + CollectionID: 2, + + // bae-111e8e29-0530-52ae-815f-14c7ba46d277 + Doc: `{ + "name": "Andy", + "age": 64 + }`, + }, + + testUtils.CreateDoc{ + CollectionID: 2, + + // bae-e147be24-bf9c-5d38-8c7b-ad18e4034c53 + Doc: `{ + "name": "Shahzad", + "age": 48 + }`, + }, + + testUtils.Request{ + Request: `query @explain(type: execute) { + _avg( + Author: { + field: age + } + ) + }`, + + Results: []dataMap{ + { + "explain": dataMap{ + "executionSuccess": true, + "sizeOfResult": 1, + "planExecutions": uint64(2), + "topLevelNode": []dataMap{ + { + "selectTopNode": dataMap{ + "selectNode": dataMap{ + "iterations": uint64(3), + "filterMatches": uint64(2), + "scanNode": dataMap{ + "iterations": uint64(3), + "docFetches": uint64(3), + "filterMatches": uint64(2), + }, + }, + }, + }, + + { + "sumNode": dataMap{ + "iterations": uint64(1), + }, + }, + + { + "countNode": dataMap{ + "iterations": uint64(1), + }, + }, + + { + "averageNode": dataMap{ + + "iterations": uint64(1), + }, + }, + }, + }, + }, + }, + }, + }, + } + + executeTestCase(t, test) +} + +func TestExecuteExplainTopLevelCountRequest(t *testing.T) { + test := testUtils.TestCase{ + + Description: "Explain (execute) request with top level count.", + + Actions: []any{ + gqlSchemaExecuteExplain(), + + testUtils.CreateDoc{ + CollectionID: 2, + + // bae-111e8e29-0530-52ae-815f-14c7ba46d277 + Doc: `{ + "name": "Andy", + "age": 64 + }`, + }, + + testUtils.CreateDoc{ + CollectionID: 2, + + // bae-e147be24-bf9c-5d38-8c7b-ad18e4034c53 + Doc: `{ + "name": "Shahzad", + "age": 48 + }`, + }, + + testUtils.Request{ + Request: `query @explain(type: execute) { + _count(Author: {}) + }`, + + Results: []dataMap{ + { + "explain": dataMap{ + "executionSuccess": true, + "sizeOfResult": 1, + "planExecutions": uint64(2), + "topLevelNode": []dataMap{ + { + "selectTopNode": dataMap{ + "selectNode": dataMap{ + "iterations": uint64(3), + "filterMatches": uint64(2), + "scanNode": dataMap{ + "iterations": uint64(3), + "docFetches": uint64(3), + "filterMatches": uint64(2), + }, + }, + }, + }, + + { + "countNode": dataMap{ + "iterations": uint64(1), + }, + }, + }, + }, + }, + }, + }, + }, + } + + executeTestCase(t, test) +} + +func TestExecuteExplainTopLevelSumRequest(t *testing.T) { + test := testUtils.TestCase{ + + Description: "Explain (execute) request with top level sum.", + + Actions: []any{ + gqlSchemaExecuteExplain(), + + testUtils.CreateDoc{ + CollectionID: 2, + + // bae-111e8e29-0530-52ae-815f-14c7ba46d277 + Doc: `{ + "name": "Andy", + "age": 64 + }`, + }, + + testUtils.CreateDoc{ + CollectionID: 2, + + // bae-e147be24-bf9c-5d38-8c7b-ad18e4034c53 + Doc: `{ + "name": "Shahzad", + "age": 48 + }`, + }, + + testUtils.Request{ + Request: `query @explain(type: execute) { + _sum( + Author: { + field: age + } + ) + }`, + + Results: []dataMap{ + { + "explain": dataMap{ + "executionSuccess": true, + "sizeOfResult": 1, + "planExecutions": uint64(2), + "topLevelNode": []dataMap{ + { + "selectTopNode": dataMap{ + "selectNode": dataMap{ + "iterations": uint64(3), + "filterMatches": uint64(2), + "scanNode": dataMap{ + "iterations": uint64(3), + "docFetches": uint64(3), + "filterMatches": uint64(2), + }, + }, + }, + }, + + { + "sumNode": dataMap{ + "iterations": uint64(1), + }, + }, + }, + }, + }, + }, + }, + }, + } + + executeTestCase(t, test) +} diff --git a/tests/integration/explain/execute/type_join_test.go b/tests/integration/explain/execute/type_join_test.go new file mode 100644 index 0000000000..2c3a2448c1 --- /dev/null +++ b/tests/integration/explain/execute/type_join_test.go @@ -0,0 +1,201 @@ +// 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_execute + +import ( + "testing" + + testUtils "github.com/sourcenetwork/defradb/tests/integration" +) + +func TestExecuteExplainRequestWithAOneToOneJoin(t *testing.T) { + test := testUtils.TestCase{ + + Description: "Explain a one-to-one join relation query, with alias.", + + Actions: []any{ + gqlSchemaExecuteExplain(), + + // Authors + create2AuthorDocuments(), + + // Contacts + create2AuthorContactDocuments(), + + testUtils.Request{ + Request: `query @explain(type: execute) { + Author { + OnlyEmail: contact { + email + } + } + }`, + + Results: []dataMap{ + { + "explain": dataMap{ + "executionSuccess": true, + "sizeOfResult": 2, + "planExecutions": uint64(3), + "selectTopNode": dataMap{ + "selectNode": dataMap{ + "iterations": uint64(3), + "filterMatches": uint64(2), + "typeIndexJoin": dataMap{ + "iterations": uint64(3), + "scanNode": dataMap{ + "iterations": uint64(3), + "docFetches": uint64(3), + "filterMatches": uint64(2), + }, + }, + }, + }, + }, + }, + }, + }, + }, + } + + executeTestCase(t, test) +} + +func TestExecuteExplainWithMultipleOneToOneJoins(t *testing.T) { + test := testUtils.TestCase{ + + Description: "Explain (execute) with two one-to-one join relation.", + + Actions: []any{ + gqlSchemaExecuteExplain(), + + // Authors + create2AuthorDocuments(), + + // Contacts + create2AuthorContactDocuments(), + + testUtils.Request{ + Request: `query @explain(type: execute) { + Author { + OnlyEmail: contact { + email + } + contact { + cell + email + } + } + }`, + + Results: []dataMap{ + { + "explain": dataMap{ + "executionSuccess": true, + "sizeOfResult": 2, + "planExecutions": uint64(3), + "selectTopNode": dataMap{ + "selectNode": dataMap{ + "iterations": uint64(3), + "filterMatches": uint64(2), + "parallelNode": []dataMap{ + { + "typeIndexJoin": dataMap{ + "iterations": uint64(3), + "scanNode": dataMap{ + "iterations": uint64(3), + "docFetches": uint64(3), + "filterMatches": uint64(2), + }, + }, + }, + { + "typeIndexJoin": dataMap{ + "iterations": uint64(3), + "scanNode": dataMap{ + "iterations": uint64(3), + "docFetches": uint64(3), + "filterMatches": uint64(2), + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + } + + executeTestCase(t, test) +} + +func TestExecuteExplainWithTwoLevelDeepNestedJoins(t *testing.T) { + test := testUtils.TestCase{ + + Description: "Explain (execute) with two nested level deep one to one join.", + + Actions: []any{ + gqlSchemaExecuteExplain(), + + // Authors + create2AuthorDocuments(), + + // Contacts + create2AuthorContactDocuments(), + + // Addresses + create2AddressDocuments(), + + testUtils.Request{ + Request: `query @explain(type: execute) { + Author { + name + contact { + email + address { + city + } + } + } + }`, + + Results: []dataMap{ + { + "explain": dataMap{ + "executionSuccess": true, + "sizeOfResult": 2, + "planExecutions": uint64(3), + "selectTopNode": dataMap{ + "selectNode": dataMap{ + "iterations": uint64(3), + "filterMatches": uint64(2), + "typeIndexJoin": dataMap{ + "iterations": uint64(3), + "scanNode": dataMap{ + "iterations": uint64(3), + "docFetches": uint64(3), + "filterMatches": uint64(2), + }, + }, + }, + }, + }, + }, + }, + }, + }, + } + + executeTestCase(t, test) +} diff --git a/tests/integration/explain/execute/update_test.go b/tests/integration/explain/execute/update_test.go new file mode 100644 index 0000000000..5ff90d26c4 --- /dev/null +++ b/tests/integration/explain/execute/update_test.go @@ -0,0 +1,130 @@ +// 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_execute + +import ( + "testing" + + testUtils "github.com/sourcenetwork/defradb/tests/integration" +) + +func TestExecuteExplainMutationRequestWithUpdateUsingIDs(t *testing.T) { + test := testUtils.TestCase{ + + Description: "Explain (execute) mutation request with update using ids.", + + Actions: []any{ + gqlSchemaExecuteExplain(), + + // Addresses + create2AddressDocuments(), + + testUtils.Request{ + Request: `mutation @explain(type: execute) { + update_ContactAddress( + ids: [ + "bae-c8448e47-6cd1-571f-90bd-364acb80da7b", + "bae-f01bf83f-1507-5fb5-a6a3-09ecffa3c692" + ], + data: "{\"country\": \"USA\"}" + ) { + country + city + } + }`, + + Results: []dataMap{ + { + "explain": dataMap{ + "executionSuccess": true, + "sizeOfResult": 2, + "planExecutions": uint64(3), + "updateNode": dataMap{ + "iterations": uint64(3), + "updates": uint64(2), + "selectTopNode": dataMap{ + "selectNode": dataMap{ + "iterations": uint64(6), + "filterMatches": uint64(4), + "scanNode": dataMap{ + "iterations": uint64(6), + "docFetches": uint64(6), + "filterMatches": uint64(4), + }, + }, + }, + }, + }, + }, + }, + }, + }, + } + + executeTestCase(t, test) +} + +func TestExecuteExplainMutationRequestWithUpdateUsingFilter(t *testing.T) { + test := testUtils.TestCase{ + + Description: "Explain (execute) mutation request with update using filter.", + + Actions: []any{ + gqlSchemaExecuteExplain(), + + // Addresses + create2AddressDocuments(), + + testUtils.Request{ + Request: `mutation @explain(type: execute) { + update_ContactAddress( + filter: { + city: { + _eq: "Waterloo" + } + }, + data: "{\"country\": \"USA\"}" + ) { + country + city + } + }`, + + Results: []dataMap{ + { + "explain": dataMap{ + "executionSuccess": true, + "sizeOfResult": 1, + "planExecutions": uint64(2), + "updateNode": dataMap{ + "iterations": uint64(2), + "updates": uint64(1), + "selectTopNode": dataMap{ + "selectNode": dataMap{ + "iterations": uint64(4), + "filterMatches": uint64(2), + "scanNode": dataMap{ + "iterations": uint64(4), + "docFetches": uint64(6), + "filterMatches": uint64(2), + }, + }, + }, + }, + }, + }, + }, + }, + }, + } + + executeTestCase(t, test) +} diff --git a/tests/integration/explain/execute/with_average_test.go b/tests/integration/explain/execute/with_average_test.go new file mode 100644 index 0000000000..6cd5ef79b2 --- /dev/null +++ b/tests/integration/explain/execute/with_average_test.go @@ -0,0 +1,133 @@ +// 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_execute + +import ( + "testing" + + testUtils "github.com/sourcenetwork/defradb/tests/integration" +) + +func TestExecuteExplainAverageRequestOnArrayField(t *testing.T) { + test := testUtils.TestCase{ + + Description: "Explain (execute) request using average on array field.", + + Actions: []any{ + gqlSchemaExecuteExplain(), + + // Books + create3BookDocuments(), + + testUtils.Request{ + Request: `query @explain(type: execute) { + Book { + name + _avg(chapterPages: {}) + } + }`, + + Results: []dataMap{ + { + "explain": dataMap{ + "executionSuccess": true, + "sizeOfResult": 3, + "planExecutions": uint64(4), + "selectTopNode": dataMap{ + "averageNode": dataMap{ + "iterations": uint64(4), + "countNode": dataMap{ + "iterations": uint64(4), + "sumNode": dataMap{ + "iterations": uint64(4), + "selectNode": dataMap{ + "iterations": uint64(4), + "filterMatches": uint64(3), + "scanNode": dataMap{ + "iterations": uint64(4), + "docFetches": uint64(4), + "filterMatches": uint64(3), + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + } + + executeTestCase(t, test) +} + +func TestExplainExplainAverageRequestOnJoinedField(t *testing.T) { + test := testUtils.TestCase{ + + Description: "Explain (execute) request using average on joined field.", + + Actions: []any{ + gqlSchemaExecuteExplain(), + + // Books + create3BookDocuments(), + + // Authors + create2AuthorDocuments(), + + testUtils.Request{ + Request: `query @explain(type: execute) { + Author { + name + _avg(books: {field: pages}) + } + }`, + + Results: []dataMap{ + { + "explain": dataMap{ + "executionSuccess": true, + "sizeOfResult": 2, + "planExecutions": uint64(3), + "selectTopNode": dataMap{ + "averageNode": dataMap{ + "iterations": uint64(3), + "countNode": dataMap{ + "iterations": uint64(3), + "sumNode": dataMap{ + "iterations": uint64(3), + "selectNode": dataMap{ + "iterations": uint64(3), + "filterMatches": uint64(2), + "typeIndexJoin": dataMap{ + "iterations": uint64(3), + "scanNode": dataMap{ + "iterations": uint64(3), + "docFetches": uint64(3), + "filterMatches": uint64(2), + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + } + + executeTestCase(t, test) +} diff --git a/tests/integration/explain/execute/with_count_test.go b/tests/integration/explain/execute/with_count_test.go new file mode 100644 index 0000000000..deba255b83 --- /dev/null +++ b/tests/integration/explain/execute/with_count_test.go @@ -0,0 +1,72 @@ +// 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_execute + +import ( + "testing" + + testUtils "github.com/sourcenetwork/defradb/tests/integration" +) + +func TestExecuteExplainRequestWithCountOnOneToManyRelation(t *testing.T) { + test := testUtils.TestCase{ + + Description: "Explain (execute) request with count on one to many relation.", + + Actions: []any{ + gqlSchemaExecuteExplain(), + + // Books + create3BookDocuments(), + + // Authors + create2AuthorDocuments(), + + testUtils.Request{ + Request: `query @explain(type: execute) { + Author { + name + numberOfBooks: _count(books: {}) + } + }`, + + Results: []dataMap{ + { + "explain": dataMap{ + "executionSuccess": true, + "sizeOfResult": 2, + "planExecutions": uint64(3), + "selectTopNode": dataMap{ + "countNode": dataMap{ + "iterations": uint64(3), + "selectNode": dataMap{ + "iterations": uint64(3), + "filterMatches": uint64(2), + "typeIndexJoin": dataMap{ + "iterations": uint64(3), + "scanNode": dataMap{ + "iterations": uint64(3), + "docFetches": uint64(3), + "filterMatches": uint64(2), + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + } + + executeTestCase(t, test) +} diff --git a/tests/integration/explain/execute/with_limit_test.go b/tests/integration/explain/execute/with_limit_test.go new file mode 100644 index 0000000000..f3b48d0948 --- /dev/null +++ b/tests/integration/explain/execute/with_limit_test.go @@ -0,0 +1,122 @@ +// 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_execute + +import ( + "testing" + + testUtils "github.com/sourcenetwork/defradb/tests/integration" +) + +func TestExecuteExplainRequestWithBothLimitAndOffsetOnParent(t *testing.T) { + test := testUtils.TestCase{ + + Description: "Explain (execute) with both limit and offset on parent.", + + Actions: []any{ + gqlSchemaExecuteExplain(), + + // Books + create3BookDocuments(), + + testUtils.Request{ + Request: `query @explain(type: execute) { + Book(limit: 1, offset: 1) { + name + } + }`, + + Results: []dataMap{ + { + "explain": dataMap{ + "executionSuccess": true, + "sizeOfResult": 1, + "planExecutions": uint64(2), + "selectTopNode": dataMap{ + "limitNode": dataMap{ + "iterations": uint64(2), + "selectNode": dataMap{ + "iterations": uint64(2), + "filterMatches": uint64(2), + "scanNode": dataMap{ + "iterations": uint64(2), + "docFetches": uint64(2), + "filterMatches": uint64(2), + }, + }, + }, + }, + }, + }, + }, + }, + }, + } + + executeTestCase(t, test) +} + +func TestExecuteExplainRequestWithBothLimitAndOffsetOnParentAndLimitOnChild(t *testing.T) { + test := testUtils.TestCase{ + + Description: "Explain (execute) with both limit and offset on parent and limit on child.", + + Actions: []any{ + gqlSchemaExecuteExplain(), + + // Articles + create3ArticleDocuments(), + + // Authors + create2AuthorDocuments(), + + testUtils.Request{ + Request: `query @explain(type: execute) { + Author(limit: 1, offset: 1) { + name + articles(limit: 1) { + name + } + } + }`, + + Results: []dataMap{ + { + "explain": dataMap{ + "executionSuccess": true, + "planExecutions": uint64(2), + "sizeOfResult": 1, + "selectTopNode": dataMap{ + "limitNode": dataMap{ + "iterations": uint64(2), + "selectNode": dataMap{ + "iterations": uint64(2), + "filterMatches": uint64(2), + "typeIndexJoin": dataMap{ + "iterations": uint64(2), + "scanNode": dataMap{ + "iterations": uint64(2), + "docFetches": uint64(2), + "filterMatches": uint64(2), + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + } + + executeTestCase(t, test) +} diff --git a/tests/integration/explain/execute/with_order_test.go b/tests/integration/explain/execute/with_order_test.go new file mode 100644 index 0000000000..ae62f9d218 --- /dev/null +++ b/tests/integration/explain/execute/with_order_test.go @@ -0,0 +1,322 @@ +// 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_execute + +import ( + "testing" + + testUtils "github.com/sourcenetwork/defradb/tests/integration" +) + +func TestExecuteExplainRequestWithOrderFieldOnParent(t *testing.T) { + test := testUtils.TestCase{ + + Description: "Explain (execute) with order field on parent.", + + Actions: []any{ + gqlSchemaExecuteExplain(), + + // Authors + create2AuthorDocuments(), + + testUtils.Request{ + Request: `query @explain(type: execute) { + Author(order: {age: ASC}) { + name + age + } + }`, + + Results: []dataMap{ + { + "explain": dataMap{ + "executionSuccess": true, + "sizeOfResult": 2, + "planExecutions": uint64(3), + "selectTopNode": dataMap{ + "orderNode": dataMap{ + "iterations": uint64(3), + "selectNode": dataMap{ + "filterMatches": uint64(2), + "iterations": uint64(3), + "scanNode": dataMap{ + "iterations": uint64(3), + "docFetches": uint64(3), + "filterMatches": uint64(2), + }, + }, + }, + }, + }, + }, + }, + }, + }, + } + + executeTestCase(t, test) +} + +func TestExecuteExplainRequestWithMultiOrderFieldsOnParent(t *testing.T) { + test := testUtils.TestCase{ + + Description: "Explain (execute) with multiple order fields on parent.", + + Actions: []any{ + gqlSchemaExecuteExplain(), + + // Authors + testUtils.CreateDoc{ + CollectionID: 2, + + Doc: `{ + "name": "Andy", + "age": 64 + }`, + }, + + testUtils.CreateDoc{ + CollectionID: 2, + + Doc: `{ + "name": "Another64YearOld", + "age": 64 + }`, + }, + + testUtils.CreateDoc{ + CollectionID: 2, + + Doc: `{ + "name": "Shahzad", + "age": 48 + }`, + }, + + testUtils.CreateDoc{ + CollectionID: 2, + + Doc: `{ + "name": "Another48YearOld", + "age": 48 + }`, + }, + + testUtils.Request{ + Request: `query @explain(type: execute) { + Author(order: {age: ASC, name: DESC}) { + name + age + } + }`, + + Results: []dataMap{ + { + "explain": dataMap{ + "executionSuccess": true, + "sizeOfResult": 4, + "planExecutions": uint64(5), + "selectTopNode": dataMap{ + "orderNode": dataMap{ + "iterations": uint64(5), + "selectNode": dataMap{ + "filterMatches": uint64(4), + "iterations": uint64(5), + "scanNode": dataMap{ + "iterations": uint64(5), + "docFetches": uint64(5), + "filterMatches": uint64(4), + }, + }, + }, + }, + }, + }, + }, + }, + }, + } + + executeTestCase(t, test) +} + +func TestExecuteExplainRequestWithOrderFieldOnChild(t *testing.T) { + test := testUtils.TestCase{ + + Description: "Explain (execute) with order field on child.", + + Actions: []any{ + gqlSchemaExecuteExplain(), + + // Articles + create3ArticleDocuments(), + + // Authors + create2AuthorDocuments(), + + testUtils.Request{ + Request: `query @explain(type: execute) { + Author { + name + articles(order: {pages: DESC}) { + pages + } + } + }`, + + Results: []dataMap{ + { + "explain": dataMap{ + "executionSuccess": true, + "sizeOfResult": 2, + "planExecutions": uint64(3), + "selectTopNode": dataMap{ + "selectNode": dataMap{ + "iterations": uint64(3), + "filterMatches": uint64(2), + "typeIndexJoin": dataMap{ + "iterations": uint64(3), + "scanNode": dataMap{ + "iterations": uint64(3), + "docFetches": uint64(3), + "filterMatches": uint64(2), + }, + }, + }, + }, + }, + }, + }, + }, + }, + } + + executeTestCase(t, test) +} + +func TestExecuteExplainRequestWithOrderFieldOnBothParentAndChild(t *testing.T) { + test := testUtils.TestCase{ + + Description: "Explain (execute) with order field on both parent and child.", + + Actions: []any{ + gqlSchemaExecuteExplain(), + + // Articles + create3ArticleDocuments(), + + // Authors + create2AuthorDocuments(), + + testUtils.Request{ + Request: `query @explain(type: execute) { + Author(order: {age: ASC}) { + name + age + articles(order: {pages: DESC}) { + pages + } + } + }`, + + Results: []dataMap{ + { + "explain": dataMap{ + "executionSuccess": true, + "sizeOfResult": 2, + "planExecutions": uint64(3), + "selectTopNode": dataMap{ + "orderNode": dataMap{ + "iterations": uint64(3), + "selectNode": dataMap{ + "iterations": uint64(3), + "filterMatches": uint64(2), + "typeIndexJoin": dataMap{ + "iterations": uint64(3), + "scanNode": dataMap{ + "iterations": uint64(3), + "docFetches": uint64(3), + "filterMatches": uint64(2), + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + } + + executeTestCase(t, test) +} + +func TestExecuteExplainRequestWhereParentFieldIsOrderedByChildField(t *testing.T) { + test := testUtils.TestCase{ + + Description: "Explain (execute) where parent field is ordered by child field.", + + Actions: []any{ + gqlSchemaExecuteExplain(), + + // Articles + create3ArticleDocuments(), + + // Authors + create2AuthorDocuments(), + + testUtils.Request{ + Request: `query @explain(type: execute) { + Author( + order: { + articles: {pages: ASC} + } + ) { + name + articles { + pages + } + } + }`, + + Results: []dataMap{ + { + "explain": dataMap{ + "executionSuccess": true, + "sizeOfResult": 2, + "planExecutions": uint64(3), + "selectTopNode": dataMap{ + "orderNode": dataMap{ + "iterations": uint64(3), + "selectNode": dataMap{ + "iterations": uint64(3), + "filterMatches": uint64(2), + "typeIndexJoin": dataMap{ + "iterations": uint64(3), + "scanNode": dataMap{ + "iterations": uint64(3), + "docFetches": uint64(3), + "filterMatches": uint64(2), + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + } + + executeTestCase(t, test) +} diff --git a/tests/integration/explain/execute/with_sum_test.go b/tests/integration/explain/execute/with_sum_test.go new file mode 100644 index 0000000000..b42546ef2b --- /dev/null +++ b/tests/integration/explain/execute/with_sum_test.go @@ -0,0 +1,125 @@ +// 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_execute + +import ( + "testing" + + testUtils "github.com/sourcenetwork/defradb/tests/integration" +) + +func TestExecuteExplainRequestWithSumOfInlineArrayField(t *testing.T) { + test := testUtils.TestCase{ + + Description: "Explain (execute) request with sum on an inline array.", + + Actions: []any{ + gqlSchemaExecuteExplain(), + + // Books + create3BookDocuments(), + + testUtils.Request{ + Request: `query @explain(type: execute) { + Book { + name + NotSureWhySomeoneWouldSumTheChapterPagesButHereItIs: _sum(chapterPages: {}) + } + }`, + + Results: []dataMap{ + { + "explain": dataMap{ + "executionSuccess": true, + "sizeOfResult": 3, + "planExecutions": uint64(4), + "selectTopNode": dataMap{ + "sumNode": dataMap{ + "iterations": uint64(4), + "selectNode": dataMap{ + "iterations": uint64(4), + "filterMatches": uint64(3), + "scanNode": dataMap{ + "iterations": uint64(4), + "docFetches": uint64(4), + "filterMatches": uint64(3), + }, + }, + }, + }, + }, + }, + }, + }, + }, + } + + executeTestCase(t, test) +} + +func TestExecuteExplainRequestSumOfRelatedOneToManyField(t *testing.T) { + test := testUtils.TestCase{ + + Description: "Explain (execute) request with sum of a related one to many field.", + + Actions: []any{ + gqlSchemaExecuteExplain(), + + // Articles + create3ArticleDocuments(), + + // Authors + create2AuthorDocuments(), + + testUtils.Request{ + Request: `query @explain(type: execute) { + Author { + name + TotalPages: _sum( + articles: { + field: pages, + } + ) + } + }`, + + Results: []dataMap{ + { + "explain": dataMap{ + "executionSuccess": true, + "sizeOfResult": 2, + "planExecutions": uint64(3), + "selectTopNode": dataMap{ + "sumNode": dataMap{ + "iterations": uint64(3), + "selectNode": dataMap{ + "iterations": uint64(3), + "filterMatches": uint64(2), + "typeIndexJoin": dataMap{ + "iterations": uint64(3), + "scanNode": dataMap{ + "iterations": uint64(3), + "docFetches": uint64(3), + "filterMatches": uint64(2), + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + } + + executeTestCase(t, test) +} diff --git a/tests/integration/utils2.go b/tests/integration/utils2.go index bf54d0be42..39264ce0c3 100644 --- a/tests/integration/utils2.go +++ b/tests/integration/utils2.go @@ -1069,7 +1069,9 @@ func assertRequestResults( // compare results assert.Equal(t, len(expectedResults), len(resultantData), description) if len(expectedResults) == 0 { - assert.Equal(t, expectedResults, resultantData) + // Need `require` here otherwise will panic in the for loop that ranges over + // resultantData and tries to access expectedResults[0]. + require.Equal(t, expectedResults, resultantData) } for docIndex, result := range resultantData {