diff --git a/client/request/explain.go b/client/request/explain.go new file mode 100644 index 0000000000..e0f8a481cb --- /dev/null +++ b/client/request/explain.go @@ -0,0 +1,19 @@ +// 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 request + +// ExplainType does not represent which type is currently the default explain request type. +type ExplainType string + +// Types of explain requests. +const ( + SimpleExplain ExplainType = "simple" +) diff --git a/client/request/request.go b/client/request/request.go index 7ce42f05be..f8cd5875f2 100644 --- a/client/request/request.go +++ b/client/request/request.go @@ -10,6 +10,10 @@ package request +import ( + "github.com/sourcenetwork/immutable" +) + type Request struct { Queries []*OperationDefinition Mutations []*OperationDefinition @@ -18,7 +22,16 @@ type Request struct { type Selection any +// Directives contains all the optional and non-optional +// directives (and their additional data) that a request can have. +// +// An optional directive has a value if it's found in the request. +type Directives struct { + // ExplainType is an optional directive (`@explain`) and it's type information. + ExplainType immutable.Option[ExplainType] +} + type OperationDefinition struct { Selections []Selection - IsExplain bool + Directives Directives } diff --git a/planner/errors.go b/planner/errors.go index 489198452a..39d9479f06 100644 --- a/planner/errors.go +++ b/planner/errors.go @@ -13,7 +13,8 @@ package planner import "github.com/sourcenetwork/defradb/errors" const ( - errUnknownDependency string = "The given field does not exist" + errUnknownDependency string = "given field does not exist" + errFailedToClosePlan string = "failed to close the plan" ) var ( @@ -22,13 +23,19 @@ var ( ErrMissingQueryOrMutation = errors.New("query is missing query or mutation statements") ErrOperationDefinitionMissingSelection = errors.New("operationDefinition is missing selections") ErrFailedToFindGroupSource = errors.New("failed to identify group source") + ErrCantExplainSubscriptionRequest = errors.New("can not explain a subscription request") ErrGroupOutsideOfGroupBy = errors.New("_group may only be referenced when within a groupBy query") ErrMissingChildSelect = errors.New("expected child select but none was found") 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") ErrUnknownDependency = errors.New(errUnknownDependency) ) func NewErrUnknownDependency(name string) error { return errors.New(errUnknownDependency, errors.NewKV("Name", name)) } + +func NewErrFailedToClosePlan(inner error, location string) error { + return errors.Wrap(errFailedToClosePlan, inner, errors.NewKV("Location", location)) +} diff --git a/planner/explain.go b/planner/explain.go index 037becde69..7e94ae2c8a 100644 --- a/planner/explain.go +++ b/planner/explain.go @@ -52,7 +52,7 @@ const ( spansLabel = "spans" ) -// buildExplainGraph builds the explainGraph from the given top level plan. +// buildSimpleExplainGraph builds the explainGraph from the given top level plan. // // Request: // query @explain { @@ -80,7 +80,7 @@ const ( // } // ] // } -func buildExplainGraph(source planNode) (map[string]any, error) { +func buildSimpleExplainGraph(source planNode) (map[string]any, error) { explainGraph := map[string]any{} if source == nil { @@ -94,7 +94,7 @@ func buildExplainGraph(source planNode) (map[string]any, error) { // List to store all explain graphs of explainable children of MultiNode. multiChildExplainGraph := []map[string]any{} for _, childSource := range node.Children() { - childExplainGraph, err := buildExplainGraph(childSource) + childExplainGraph, err := buildSimpleExplainGraph(childSource) if err != nil { return nil, err } @@ -116,7 +116,7 @@ func buildExplainGraph(source planNode) (map[string]any, error) { // If not the last child then keep walking and explaining the root graph, // as long as there are more explainable nodes left under root. if node.Source() != nil { - indexJoinRootExplainGraph, err := buildExplainGraph(node.Source()) + indexJoinRootExplainGraph, err := buildSimpleExplainGraph(node.Source()) if err != nil { return nil, err } @@ -142,7 +142,7 @@ func buildExplainGraph(source planNode) (map[string]any, error) { // If not the last child then keep walking the graph to find more explainable nodes. // Also make sure the next source / child isn't a recursive `topLevelNode`. if next := node.Source(); next != nil && next.Kind() != topLevelNodeKind { - nextExplainGraph, err := buildExplainGraph(next) + nextExplainGraph, err := buildSimpleExplainGraph(next) if err != nil { return nil, err } @@ -158,7 +158,7 @@ func buildExplainGraph(source planNode) (map[string]any, error) { default: // Node is neither a MultiNode nor an "explainable" node. Skip over it but walk it's child(ren). var err error - explainGraph, err = buildExplainGraph(source.Source()) + explainGraph, err = buildSimpleExplainGraph(source.Source()) if err != nil { return nil, err } diff --git a/planner/planner.go b/planner/planner.go index 1eae72b7cb..1e2b753be9 100644 --- a/planner/planner.go +++ b/planner/planner.go @@ -12,13 +12,11 @@ package planner import ( "context" - "fmt" "github.com/sourcenetwork/defradb/client" "github.com/sourcenetwork/defradb/client/request" "github.com/sourcenetwork/defradb/core" "github.com/sourcenetwork/defradb/datastore" - "github.com/sourcenetwork/defradb/errors" "github.com/sourcenetwork/defradb/logging" "github.com/sourcenetwork/defradb/planner/mapper" ) @@ -445,19 +443,26 @@ func walkAndFindPlanType[T planNode](plan planNode) (T, bool) { func (p *Planner) explainRequest( ctx context.Context, plan planNode, + explainType request.ExplainType, ) ([]map[string]any, error) { - explainGraph, err := buildExplainGraph(plan) - if err != nil { - return nil, multiErr(err, plan.Close()) - } + switch explainType { + case request.SimpleExplain: + explainGraph, err := buildSimpleExplainGraph(plan) + if err != nil { + return nil, err + } - topExplainGraph := []map[string]any{ - { - request.ExplainLabel: explainGraph, - }, - } + explainResult := []map[string]any{ + { + request.ExplainLabel: explainGraph, + }, + } - return topExplainGraph, plan.Close() + return explainResult, nil + + default: + return nil, ErrUnknownExplainRequestType + } } // executeRequest executes the plan graph that represents the request that was made. @@ -466,12 +471,12 @@ func (p *Planner) executeRequest( plan planNode, ) ([]map[string]any, error) { if err := plan.Start(); err != nil { - return nil, multiErr(err, plan.Close()) + return nil, err } next, err := plan.Next() if err != nil { - return nil, multiErr(err, plan.Close()) + return nil, err } docs := []map[string]any{} @@ -483,37 +488,43 @@ func (p *Planner) executeRequest( next, err = plan.Next() if err != nil { - return nil, multiErr(err, plan.Close()) + return nil, err } } - if err = plan.Close(); err != nil { - return nil, err - } - return docs, err } -// RunRequest plans how to run the request, then attempts to run the request and returns the results. +// RunRequest classifies the type of request to run, runs it, and then returns the result(s). func (p *Planner) RunRequest( ctx context.Context, - query *request.Request, -) ([]map[string]any, error) { - plan, err := p.makePlan(query) - + req *request.Request, +) (result []map[string]any, err error) { + plan, err := p.makePlan(req) if err != nil { return nil, err } - isAnExplainRequest := - (len(query.Queries) > 0 && query.Queries[0].IsExplain) || - (len(query.Mutations) > 0 && query.Mutations[0].IsExplain) + defer func() { + if e := plan.Close(); e != nil { + err = NewErrFailedToClosePlan(e, "running request") + } + }() + + // Ensure subscription request doesn't ever end up with an explain directive. + if len(req.Subscription) > 0 && req.Subscription[0].Directives.ExplainType.HasValue() { + return nil, ErrCantExplainSubscriptionRequest + } - if isAnExplainRequest { - return p.explainRequest(ctx, plan) + if len(req.Queries) > 0 && req.Queries[0].Directives.ExplainType.HasValue() { + return p.explainRequest(ctx, plan, req.Queries[0].Directives.ExplainType.Value()) } - // This won't execute if it's an explain request. + if len(req.Mutations) > 0 && req.Mutations[0].Directives.ExplainType.HasValue() { + return p.explainRequest(ctx, plan, req.Mutations[0].Directives.ExplainType.Value()) + } + + // This won't / should NOT execute if it's any kind of explain request. return p.executeRequest(ctx, plan) } @@ -521,32 +532,27 @@ func (p *Planner) RunRequest( func (p *Planner) RunSubscriptionRequest( ctx context.Context, query *request.Select, -) ([]map[string]any, error) { +) (result []map[string]any, err error) { plan, err := p.makePlan(query) if err != nil { return nil, err } + defer func() { + if e := plan.Close(); e != nil { + err = NewErrFailedToClosePlan(e, "running subscription request") + } + }() + return p.executeRequest(ctx, plan) } -// MakePlan makes a plan from the parsed query. @TODO {defradb/issues/368}: Test this exported function. +// MakePlan makes a plan from the parsed query. +// +// Note: Caller is responsible to call the `Close()` method to free the allocated +// resources of the returned plan. +// +// @TODO {defradb/issues/368}: Test this exported function. func (p *Planner) MakePlan(query *request.Request) (planNode, error) { return p.makePlan(query) } - -// multiErr wraps all the non-nil errors and returns the wrapped error result. -func multiErr(errorsToWrap ...error) error { - var errs error - for _, err := range errorsToWrap { - if err == nil { - continue - } - if errs == nil { - errs = errors.New(err.Error()) - continue - } - errs = errors.Wrap(fmt.Sprintf("%s", errs), err) - } - return errs -} diff --git a/planner/type_join.go b/planner/type_join.go index 8fea996575..4e0d784fed 100644 --- a/planner/type_join.go +++ b/planner/type_join.go @@ -81,7 +81,6 @@ func (p *Planner) makeTypeIndexJoin( desc := parent.sourceInfo.collectionDescription typeFieldDesc, ok := desc.GetField(subType.Name) if !ok { - // return nil, errors.Wrap("Unknown field on sub selection") return nil, client.NewErrFieldNotExist(subType.Name) } @@ -162,7 +161,7 @@ func (n *typeIndexJoin) Explain() (map[string]any, error) { explainerMap[joinRootLabel] = joinType.subTypeFieldName explainerMap[joinSubTypeNameLabel] = joinType.subTypeName - subTypeExplainGraph, err := buildExplainGraph(joinType.subType) + subTypeExplainGraph, err := buildSimpleExplainGraph(joinType.subType) if err != nil { return nil, err } @@ -175,7 +174,7 @@ func (n *typeIndexJoin) Explain() (map[string]any, error) { explainerMap[joinRootLabel] = joinType.rootName explainerMap[joinSubTypeNameLabel] = joinType.subTypeName - subTypeExplainGraph, err := buildExplainGraph(joinType.subType) + subTypeExplainGraph, err := buildSimpleExplainGraph(joinType.subType) if err != nil { return nil, err } diff --git a/query/graphql/parser/errors.go b/query/graphql/parser/errors.go index ad697de167..6d014afcce 100644 --- a/query/graphql/parser/errors.go +++ b/query/graphql/parser/errors.go @@ -19,5 +19,8 @@ var ( ErrFailedToParseConditionValue = errors.New("failed to parse condition value from query filter statement") ErrEmptyDataPayload = errors.New("given data payload is empty") ErrUnknownMutationName = errors.New("unknown mutation name") + ErrInvalidExplainTypeArg = errors.New("invalid explain request type argument") + ErrInvalidNumberOfExplainArgs = errors.New("invalid number of arguments to an explain request") + ErrUnknownExplainType = errors.New("invalid / unknown explain type") ErrUnknownGQLOperation = errors.New("unknown GraphQL operation type") ) diff --git a/query/graphql/parser/mutation.go b/query/graphql/parser/mutation.go index 5ce9205317..2cb9a79b46 100644 --- a/query/graphql/parser/mutation.go +++ b/query/graphql/parser/mutation.go @@ -39,8 +39,6 @@ func parseMutationOperationDefinition( Selections: make([]request.Selection, len(def.SelectionSet.Selections)), } - qdef.IsExplain = parseExplainDirective(def.Directives) - for i, selection := range def.SelectionSet.Selections { switch node := selection.(type) { case *ast.Field: diff --git a/query/graphql/parser/query.go b/query/graphql/parser/query.go index a532fc7d9b..3897848952 100644 --- a/query/graphql/parser/query.go +++ b/query/graphql/parser/query.go @@ -18,6 +18,9 @@ import ( "github.com/sourcenetwork/immutable" "github.com/sourcenetwork/defradb/client" + "github.com/sourcenetwork/defradb/errors" + schemaTypes "github.com/sourcenetwork/defradb/query/graphql/schema/types" + "github.com/sourcenetwork/defradb/client/request" ) @@ -28,6 +31,7 @@ func ParseQuery(schema gql.Schema, doc *ast.Document) (*request.Request, []error if doc == nil { return nil, []error{client.NewErrUninitializeProperty("parseQuery", "doc")} } + r := &request.Request{ Queries: make([]*request.OperationDefinition, 0), Mutations: make([]*request.OperationDefinition, 0), @@ -35,67 +39,127 @@ func ParseQuery(schema gql.Schema, doc *ast.Document) (*request.Request, []error } for _, def := range doc.Definitions { - switch node := def.(type) { - case *ast.OperationDefinition: - switch node.Operation { - case "query": - // parse query operation definition. - qdef, err := parseQueryOperationDefinition(schema, node) - if err != nil { - return nil, err - } - r.Queries = append(r.Queries, qdef) - case "mutation": - // parse mutation operation definition. - mdef, err := parseMutationOperationDefinition(schema, node) - if err != nil { - return nil, []error{err} - } - r.Mutations = append(r.Mutations, mdef) - case "subscription": - // parse subscription operation definition. - sdef, err := parseSubscriptionOperationDefinition(schema, node) - if err != nil { - return nil, []error{err} - } - r.Subscription = append(r.Subscription, sdef) - default: - return nil, []error{ErrUnknownGQLOperation} + astOpDef, isOpDef := def.(*ast.OperationDefinition) + if !isOpDef { + continue + } + + switch astOpDef.Operation { + case "query": + parsedQueryOpDef, errs := parseQueryOperationDefinition(schema, astOpDef) + if errs != nil { + return nil, errs + } + + parsedDirectives, err := parseDirectives(astOpDef.Directives) + if errs != nil { + return nil, []error{err} + } + parsedQueryOpDef.Directives = parsedDirectives + + r.Queries = append(r.Queries, parsedQueryOpDef) + + case "mutation": + parsedMutationOpDef, err := parseMutationOperationDefinition(schema, astOpDef) + if err != nil { + return nil, []error{err} + } + + parsedDirectives, err := parseDirectives(astOpDef.Directives) + if err != nil { + return nil, []error{err} + } + parsedMutationOpDef.Directives = parsedDirectives + + r.Mutations = append(r.Mutations, parsedMutationOpDef) + + case "subscription": + parsedSubscriptionOpDef, err := parseSubscriptionOperationDefinition(schema, astOpDef) + if err != nil { + return nil, []error{err} + } + + parsedDirectives, err := parseDirectives(astOpDef.Directives) + if err != nil { + return nil, []error{err} } + parsedSubscriptionOpDef.Directives = parsedDirectives + + r.Subscription = append(r.Subscription, parsedSubscriptionOpDef) + + default: + return nil, []error{ErrUnknownGQLOperation} } } return r, nil } -// parseExplainDirective returns true if we parsed / detected the explain directive label -// in this ast, and false otherwise. -func parseExplainDirective(directives []*ast.Directive) bool { - // Iterate through all directives and ensure that the directive is at there. +// parseDirectives returns all directives that were found if parsing and validation succeeds, +// otherwise returns the first error that is encountered. +func parseDirectives(astDirectives []*ast.Directive) (request.Directives, error) { + // Set the default states of the directives if they aren't found and no error(s) occur. + explainDirective := immutable.None[request.ExplainType]() + + // Iterate through all directives and ensure that the directive we find are validated. // - Note: the location we don't need to worry about as the schema takes care of it, as when // request is made there will be a syntax error for directive usage at the wrong location, - // unless we add another directive named `@explain` at another location (which we should not). - for _, directive := range directives { - // The arguments pased to the directive are at `directive.Arguments`. - if directive.Name.Value == request.ExplainLabel { - return true + // unless we add another directive with the same name, for example `@explain` is added + // at another location (which we must avoid). + for _, astDirective := range astDirectives { + if astDirective == nil { + return request.Directives{}, errors.New("found a nil directive in the AST") + } + + if astDirective.Name.Value == request.ExplainLabel { + // Explain directive found, lets parse and validate the directive. + parsedExplainDirctive, err := parseExplainDirective(astDirective) + if err != nil { + return request.Directives{}, err + } + explainDirective = parsedExplainDirctive } } - return false + return request.Directives{ + ExplainType: explainDirective, + }, nil +} + +// parseExplainDirective parses the explain directive AST and returns an error if the parsing or +// validation goes wrong, otherwise returns the parsed explain type information. +func parseExplainDirective(astDirective *ast.Directive) (immutable.Option[request.ExplainType], error) { + if len(astDirective.Arguments) == 0 { + return immutable.Some(request.SimpleExplain), nil + } + + if len(astDirective.Arguments) != 1 { + return immutable.None[request.ExplainType](), ErrInvalidNumberOfExplainArgs + } + + arg := astDirective.Arguments[0] + if arg.Name.Value != schemaTypes.ExplainArgNameType { + return immutable.None[request.ExplainType](), ErrInvalidExplainTypeArg + } + + switch arg.Value.GetValue() { + case schemaTypes.ExplainArgSimple: + return immutable.Some(request.SimpleExplain), nil + default: + return immutable.None[request.ExplainType](), ErrUnknownExplainType + } } // parseQueryOperationDefinition parses the individual GraphQL // 'query' operations, which there may be multiple of. func parseQueryOperationDefinition( schema gql.Schema, - def *ast.OperationDefinition) (*request.OperationDefinition, []error) { + def *ast.OperationDefinition, +) (*request.OperationDefinition, []error) { qdef := &request.OperationDefinition{ Selections: make([]request.Selection, len(def.SelectionSet.Selections)), } - qdef.IsExplain = parseExplainDirective(def.Directives) - for i, selection := range def.SelectionSet.Selections { var parsedSelection request.Selection switch node := selection.(type) { diff --git a/query/graphql/parser/subscription.go b/query/graphql/parser/subscription.go index d8b67f24e0..fb27b070f2 100644 --- a/query/graphql/parser/subscription.go +++ b/query/graphql/parser/subscription.go @@ -27,8 +27,6 @@ func parseSubscriptionOperationDefinition( Selections: make([]request.Selection, len(def.SelectionSet.Selections)), } - sdef.IsExplain = parseExplainDirective(def.Directives) - for i, selection := range def.SelectionSet.Selections { switch node := selection.(type) { case *ast.Field: diff --git a/query/graphql/schema/types/types.go b/query/graphql/schema/types/types.go index 5dad828b91..97fa9826b0 100644 --- a/query/graphql/schema/types/types.go +++ b/query/graphql/schema/types/types.go @@ -18,6 +18,9 @@ const ( ExplainLabel string = "explain" PrimaryLabel string = "primary" RelationLabel string = "relation" + + ExplainArgNameType string = "type" + ExplainArgSimple string = "simple" ) var ( @@ -34,20 +37,21 @@ var ( }, }) + ExplainEnum = gql.NewEnum(gql.EnumConfig{ + Name: "ExplainType", + Values: gql.EnumValueConfigMap{ + ExplainArgSimple: &gql.EnumValueConfig{ + Value: ExplainArgSimple, + Description: "Simple explaination - dump of the plan graph.", + }, + }, + }) + ExplainDirective *gql.Directive = gql.NewDirective(gql.DirectiveConfig{ Name: ExplainLabel, Args: gql.FieldConfigArgument{ - "simple": &gql.ArgumentConfig{ - Type: gql.Boolean, - DefaultValue: true, - }, - "predict": &gql.ArgumentConfig{ - Type: gql.Boolean, - DefaultValue: false, - }, - "execute": &gql.ArgumentConfig{ - Type: gql.Boolean, - DefaultValue: false, + ExplainArgNameType: &gql.ArgumentConfig{ + Type: ExplainEnum, }, }, diff --git a/tests/integration/explain/default/basic_test.go b/tests/integration/explain/default/basic_test.go new file mode 100644 index 0000000000..3edc722f82 --- /dev/null +++ b/tests/integration/explain/default/basic_test.go @@ -0,0 +1,190 @@ +// 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_default + +import ( + "testing" + + testUtils "github.com/sourcenetwork/defradb/tests/integration" +) + +func TestExplainQuerySimpleOnFieldDirective_BadUsage(t *testing.T) { + test := testUtils.QueryTestCase{ + + Description: "Explain a query by providing the directive on wrong location (field).", + + Query: `query { + author @explain { + _key + name + age + } + }`, + + Docs: map[int][]string{ + 2: { + `{ + "name": "John", + "age": 21 + }`, + }, + }, + + ExpectedError: "Directive \"explain\" may not be used on FIELD.", + } + executeTestCase(t, test) +} + +func TestExplainQuerySimple(t *testing.T) { + test := testUtils.QueryTestCase{ + Description: "Explain a query with no filter", + + Query: `query @explain { + author { + _key + name + age + } + }`, + + Docs: map[int][]string{ + 2: { + `{ + "name": "John", + "age": 21 + }`, + }, + }, + + Results: []dataMap{ + { + "explain": dataMap{ + "selectTopNode": dataMap{ + "selectNode": dataMap{ + "filter": nil, + "scanNode": dataMap{ + "filter": nil, + "collectionID": "3", + "collectionName": "author", + "spans": []dataMap{ + { + "start": "/3", + "end": "/4", + }, + }, + }, + }, + }, + }, + }, + }, + } + + executeTestCase(t, test) +} + +func TestExplainQuerySimpleWithAlias(t *testing.T) { + test := testUtils.QueryTestCase{ + Description: "Explain a query with alias, no filter", + + Query: `query @explain { + author { + username: name + age: age + } + }`, + + Docs: map[int][]string{ + 2: { + `{ + "name": "John", + "age": 21 + }`, + }, + }, + + Results: []dataMap{ + { + "explain": dataMap{ + "selectTopNode": dataMap{ + "selectNode": dataMap{ + "filter": nil, + "scanNode": dataMap{ + "filter": nil, + "collectionID": "3", + "collectionName": "author", + "spans": []dataMap{ + { + "start": "/3", + "end": "/4", + }, + }, + }, + }, + }, + }, + }, + }, + } + + executeTestCase(t, test) +} + +func TestExplainQuerySimpleWithMultipleRows(t *testing.T) { + test := testUtils.QueryTestCase{ + Description: "Explain a query with no filter, mutiple rows", + + Query: `query @explain { + author { + name + age + } + }`, + + Docs: map[int][]string{ + 2: { + `{ + "name": "John", + "age": 21 + }`, + `{ + "name": "Bob", + "age": 27 + }`, + }, + }, + + Results: []dataMap{ + { + "explain": dataMap{ + "selectTopNode": dataMap{ + "selectNode": dataMap{ + "filter": nil, + "scanNode": dataMap{ + "filter": nil, + "collectionID": "3", + "collectionName": "author", + "spans": []dataMap{ + { + "start": "/3", + "end": "/4", + }, + }, + }, + }, + }, + }, + }, + }, + } + + executeTestCase(t, test) +} diff --git a/tests/integration/explain/simple/create_test.go b/tests/integration/explain/default/create_test.go similarity index 98% rename from tests/integration/explain/simple/create_test.go rename to tests/integration/explain/default/create_test.go index 7149bf3e3f..64a36028c2 100644 --- a/tests/integration/explain/simple/create_test.go +++ b/tests/integration/explain/default/create_test.go @@ -8,7 +8,7 @@ // by the Apache License, Version 2.0, included in the file // licenses/APL.txt. -package test_explain_simple +package test_explain_default import ( "testing" diff --git a/tests/integration/explain/simple/dagscan_test.go b/tests/integration/explain/default/dagscan_test.go similarity index 99% rename from tests/integration/explain/simple/dagscan_test.go rename to tests/integration/explain/default/dagscan_test.go index 256429be5c..0cc1078374 100644 --- a/tests/integration/explain/simple/dagscan_test.go +++ b/tests/integration/explain/default/dagscan_test.go @@ -8,7 +8,7 @@ // by the Apache License, Version 2.0, included in the file // licenses/APL.txt. -package test_explain_simple +package test_explain_default import ( "testing" diff --git a/tests/integration/explain/simple/delete_test.go b/tests/integration/explain/default/delete_test.go similarity index 99% rename from tests/integration/explain/simple/delete_test.go rename to tests/integration/explain/default/delete_test.go index 82b20178d3..8bbca35aed 100644 --- a/tests/integration/explain/simple/delete_test.go +++ b/tests/integration/explain/default/delete_test.go @@ -8,7 +8,7 @@ // by the Apache License, Version 2.0, included in the file // licenses/APL.txt. -package test_explain_simple +package test_explain_default import ( "testing" diff --git a/tests/integration/explain/simple/example_new_basic_test.go b/tests/integration/explain/default/example_new_basic_test.go similarity index 99% rename from tests/integration/explain/simple/example_new_basic_test.go rename to tests/integration/explain/default/example_new_basic_test.go index 0c1214e3d6..6d05598126 100644 --- a/tests/integration/explain/simple/example_new_basic_test.go +++ b/tests/integration/explain/default/example_new_basic_test.go @@ -8,7 +8,7 @@ // by the Apache License, Version 2.0, included in the file // licenses/APL.txt. -package test_explain_simple +package test_explain_default import ( "testing" diff --git a/tests/integration/explain/simple/example_new_join_test.go b/tests/integration/explain/default/example_new_join_test.go similarity index 99% rename from tests/integration/explain/simple/example_new_join_test.go rename to tests/integration/explain/default/example_new_join_test.go index f126e72289..4326107cae 100644 --- a/tests/integration/explain/simple/example_new_join_test.go +++ b/tests/integration/explain/default/example_new_join_test.go @@ -8,7 +8,7 @@ // by the Apache License, Version 2.0, included in the file // licenses/APL.txt. -package test_explain_simple +package test_explain_default import ( "testing" diff --git a/tests/integration/explain/simple/group_test.go b/tests/integration/explain/default/group_test.go similarity index 99% rename from tests/integration/explain/simple/group_test.go rename to tests/integration/explain/default/group_test.go index 43f5c292d7..071e20f72a 100644 --- a/tests/integration/explain/simple/group_test.go +++ b/tests/integration/explain/default/group_test.go @@ -8,7 +8,7 @@ // by the Apache License, Version 2.0, included in the file // licenses/APL.txt. -package test_explain_simple +package test_explain_default import ( "testing" diff --git a/tests/integration/explain/simple/group_with_average_test.go b/tests/integration/explain/default/group_with_average_test.go similarity index 99% rename from tests/integration/explain/simple/group_with_average_test.go rename to tests/integration/explain/default/group_with_average_test.go index 4013e362c8..52a99827e1 100644 --- a/tests/integration/explain/simple/group_with_average_test.go +++ b/tests/integration/explain/default/group_with_average_test.go @@ -8,7 +8,7 @@ // by the Apache License, Version 2.0, included in the file // licenses/APL.txt. -package test_explain_simple +package test_explain_default import ( "testing" diff --git a/tests/integration/explain/simple/group_with_dockey_test.go b/tests/integration/explain/default/group_with_dockey_test.go similarity index 99% rename from tests/integration/explain/simple/group_with_dockey_test.go rename to tests/integration/explain/default/group_with_dockey_test.go index 9386499a90..9684c13737 100644 --- a/tests/integration/explain/simple/group_with_dockey_test.go +++ b/tests/integration/explain/default/group_with_dockey_test.go @@ -8,7 +8,7 @@ // by the Apache License, Version 2.0, included in the file // licenses/APL.txt. -package test_explain_simple +package test_explain_default import ( "testing" diff --git a/tests/integration/explain/simple/group_with_filter_test.go b/tests/integration/explain/default/group_with_filter_test.go similarity index 99% rename from tests/integration/explain/simple/group_with_filter_test.go rename to tests/integration/explain/default/group_with_filter_test.go index e96c2a3ff2..101bc5bd15 100644 --- a/tests/integration/explain/simple/group_with_filter_test.go +++ b/tests/integration/explain/default/group_with_filter_test.go @@ -8,7 +8,7 @@ // by the Apache License, Version 2.0, included in the file // licenses/APL.txt. -package test_explain_simple +package test_explain_default import ( "testing" diff --git a/tests/integration/explain/simple/group_with_limit_test.go b/tests/integration/explain/default/group_with_limit_test.go similarity index 99% rename from tests/integration/explain/simple/group_with_limit_test.go rename to tests/integration/explain/default/group_with_limit_test.go index fd558162f0..a80070435b 100644 --- a/tests/integration/explain/simple/group_with_limit_test.go +++ b/tests/integration/explain/default/group_with_limit_test.go @@ -8,7 +8,7 @@ // by the Apache License, Version 2.0, included in the file // licenses/APL.txt. -package test_explain_simple +package test_explain_default import ( "testing" diff --git a/tests/integration/explain/simple/group_with_order_test.go b/tests/integration/explain/default/group_with_order_test.go similarity index 99% rename from tests/integration/explain/simple/group_with_order_test.go rename to tests/integration/explain/default/group_with_order_test.go index 63b1e45d0c..b5102c50ea 100644 --- a/tests/integration/explain/simple/group_with_order_test.go +++ b/tests/integration/explain/default/group_with_order_test.go @@ -8,7 +8,7 @@ // by the Apache License, Version 2.0, included in the file // licenses/APL.txt. -package test_explain_simple +package test_explain_default import ( "testing" diff --git a/tests/integration/explain/default/invalid_type_arg_test.go b/tests/integration/explain/default/invalid_type_arg_test.go new file mode 100644 index 0000000000..52db6befcd --- /dev/null +++ b/tests/integration/explain/default/invalid_type_arg_test.go @@ -0,0 +1,44 @@ +// 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_default + +import ( + "testing" + + testUtils "github.com/sourcenetwork/defradb/tests/integration" +) + +func TestInvalidExplainRequestTypeReturnsError(t *testing.T) { + test := testUtils.QueryTestCase{ + Description: "Invalid type of explain request should error.", + + Query: `query @explain(type: invalid) { + author { + _key + name + age + } + }`, + + Docs: map[int][]string{ + 2: { + `{ + "name": "John", + "age": 21 + }`, + }, + }, + + ExpectedError: "Argument \"type\" has invalid value invalid.\nExpected type \"ExplainType\", found invalid.", + } + + executeTestCase(t, test) +} diff --git a/tests/integration/explain/simple/top_with_average_test.go b/tests/integration/explain/default/top_with_average_test.go similarity index 99% rename from tests/integration/explain/simple/top_with_average_test.go rename to tests/integration/explain/default/top_with_average_test.go index 0fd5ae9f37..5ee4ba99a6 100644 --- a/tests/integration/explain/simple/top_with_average_test.go +++ b/tests/integration/explain/default/top_with_average_test.go @@ -8,7 +8,7 @@ // by the Apache License, Version 2.0, included in the file // licenses/APL.txt. -package test_explain_simple +package test_explain_default import ( "testing" diff --git a/tests/integration/explain/simple/top_with_count_test.go b/tests/integration/explain/default/top_with_count_test.go similarity index 99% rename from tests/integration/explain/simple/top_with_count_test.go rename to tests/integration/explain/default/top_with_count_test.go index 8076f3a419..847822dbfc 100644 --- a/tests/integration/explain/simple/top_with_count_test.go +++ b/tests/integration/explain/default/top_with_count_test.go @@ -8,7 +8,7 @@ // by the Apache License, Version 2.0, included in the file // licenses/APL.txt. -package test_explain_simple +package test_explain_default import ( "testing" diff --git a/tests/integration/explain/simple/top_with_sum_test.go b/tests/integration/explain/default/top_with_sum_test.go similarity index 99% rename from tests/integration/explain/simple/top_with_sum_test.go rename to tests/integration/explain/default/top_with_sum_test.go index 549eab367f..d4b1e16a23 100644 --- a/tests/integration/explain/simple/top_with_sum_test.go +++ b/tests/integration/explain/default/top_with_sum_test.go @@ -8,7 +8,7 @@ // by the Apache License, Version 2.0, included in the file // licenses/APL.txt. -package test_explain_simple +package test_explain_default import ( "testing" diff --git a/tests/integration/explain/simple/type_join_test.go b/tests/integration/explain/default/type_join_test.go similarity index 99% rename from tests/integration/explain/simple/type_join_test.go rename to tests/integration/explain/default/type_join_test.go index 9429de88b3..eb965e37cf 100644 --- a/tests/integration/explain/simple/type_join_test.go +++ b/tests/integration/explain/default/type_join_test.go @@ -8,7 +8,7 @@ // by the Apache License, Version 2.0, included in the file // licenses/APL.txt. -package test_explain_simple +package test_explain_default import ( "testing" diff --git a/tests/integration/explain/simple/update_test.go b/tests/integration/explain/default/update_test.go similarity index 99% rename from tests/integration/explain/simple/update_test.go rename to tests/integration/explain/default/update_test.go index 3d051f3fb3..2d40076e20 100644 --- a/tests/integration/explain/simple/update_test.go +++ b/tests/integration/explain/default/update_test.go @@ -8,7 +8,7 @@ // by the Apache License, Version 2.0, included in the file // licenses/APL.txt. -package test_explain_simple +package test_explain_default import ( "testing" diff --git a/tests/integration/explain/default/utils.go b/tests/integration/explain/default/utils.go new file mode 100644 index 0000000000..b580a66440 --- /dev/null +++ b/tests/integration/explain/default/utils.go @@ -0,0 +1,77 @@ +// 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_default + +import ( + "testing" + + testUtils "github.com/sourcenetwork/defradb/tests/integration" + explainUtils "github.com/sourcenetwork/defradb/tests/integration/explain" +) + +type dataMap = map[string]any + +var bookAuthorGQLSchema = (` + 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 + } + +`) + +// TODO: This should be resolved in ISSUE#953 (github.com/sourcenetwork/defradb). +func executeTestCase(t *testing.T, test testUtils.QueryTestCase) { + testUtils.ExecuteQueryTestCase( + t, + bookAuthorGQLSchema, + []string{"article", "book", "author", "authorContact", "contactAddress"}, + test, + ) +} + +func executeExplainTestCase(t *testing.T, test explainUtils.ExplainRequestTestCase) { + explainUtils.ExecuteExplainRequestTestCase( + t, + bookAuthorGQLSchema, + []string{"article", "book", "author", "authorContact", "contactAddress"}, + test, + ) +} diff --git a/tests/integration/explain/simple/with_average_test.go b/tests/integration/explain/default/with_average_test.go similarity index 99% rename from tests/integration/explain/simple/with_average_test.go rename to tests/integration/explain/default/with_average_test.go index bc32e53952..e39ecf82ca 100644 --- a/tests/integration/explain/simple/with_average_test.go +++ b/tests/integration/explain/default/with_average_test.go @@ -8,7 +8,7 @@ // by the Apache License, Version 2.0, included in the file // licenses/APL.txt. -package test_explain_simple +package test_explain_default import ( "testing" diff --git a/tests/integration/explain/simple/with_count_test.go b/tests/integration/explain/default/with_count_test.go similarity index 99% rename from tests/integration/explain/simple/with_count_test.go rename to tests/integration/explain/default/with_count_test.go index db9f232cea..27bed62dbf 100644 --- a/tests/integration/explain/simple/with_count_test.go +++ b/tests/integration/explain/default/with_count_test.go @@ -8,7 +8,7 @@ // by the Apache License, Version 2.0, included in the file // licenses/APL.txt. -package test_explain_simple +package test_explain_default import ( "testing" diff --git a/tests/integration/explain/simple/with_filter_key_test.go b/tests/integration/explain/default/with_filter_key_test.go similarity index 99% rename from tests/integration/explain/simple/with_filter_key_test.go rename to tests/integration/explain/default/with_filter_key_test.go index 5fa60f372b..ff4dcc955b 100644 --- a/tests/integration/explain/simple/with_filter_key_test.go +++ b/tests/integration/explain/default/with_filter_key_test.go @@ -8,7 +8,7 @@ // by the Apache License, Version 2.0, included in the file // licenses/APL.txt. -package test_explain_simple +package test_explain_default import ( "testing" diff --git a/tests/integration/explain/simple/with_filter_test.go b/tests/integration/explain/default/with_filter_test.go similarity index 99% rename from tests/integration/explain/simple/with_filter_test.go rename to tests/integration/explain/default/with_filter_test.go index 37cac7b5c0..c2b4d3ceb3 100644 --- a/tests/integration/explain/simple/with_filter_test.go +++ b/tests/integration/explain/default/with_filter_test.go @@ -8,7 +8,7 @@ // by the Apache License, Version 2.0, included in the file // licenses/APL.txt. -package test_explain_simple +package test_explain_default import ( "testing" diff --git a/tests/integration/explain/simple/with_limit_test.go b/tests/integration/explain/default/with_limit_test.go similarity index 99% rename from tests/integration/explain/simple/with_limit_test.go rename to tests/integration/explain/default/with_limit_test.go index e76814668c..ceb4a2b0a6 100644 --- a/tests/integration/explain/simple/with_limit_test.go +++ b/tests/integration/explain/default/with_limit_test.go @@ -8,7 +8,7 @@ // by the Apache License, Version 2.0, included in the file // licenses/APL.txt. -package test_explain_simple +package test_explain_default import ( "testing" diff --git a/tests/integration/explain/simple/with_order_test.go b/tests/integration/explain/default/with_order_test.go similarity index 99% rename from tests/integration/explain/simple/with_order_test.go rename to tests/integration/explain/default/with_order_test.go index d727eccef0..4b82d8fb34 100644 --- a/tests/integration/explain/simple/with_order_test.go +++ b/tests/integration/explain/default/with_order_test.go @@ -8,7 +8,7 @@ // by the Apache License, Version 2.0, included in the file // licenses/APL.txt. -package test_explain_simple +package test_explain_default import ( "testing" diff --git a/tests/integration/explain/simple/with_sum_test.go b/tests/integration/explain/default/with_sum_test.go similarity index 99% rename from tests/integration/explain/simple/with_sum_test.go rename to tests/integration/explain/default/with_sum_test.go index c887071ed7..e1509e42eb 100644 --- a/tests/integration/explain/simple/with_sum_test.go +++ b/tests/integration/explain/default/with_sum_test.go @@ -8,7 +8,7 @@ // by the Apache License, Version 2.0, included in the file // licenses/APL.txt. -package test_explain_simple +package test_explain_default import ( "testing" diff --git a/tests/integration/explain/simple/basic_test.go b/tests/integration/explain/simple/basic_test.go index 80280780e3..e5b54818d5 100644 --- a/tests/integration/explain/simple/basic_test.go +++ b/tests/integration/explain/simple/basic_test.go @@ -16,40 +16,11 @@ import ( testUtils "github.com/sourcenetwork/defradb/tests/integration" ) -func TestExplainQuerySimpleOnFieldDirective_BadUsage(t *testing.T) { +func TestSimpleExplainRequest(t *testing.T) { test := testUtils.QueryTestCase{ + Description: "Explain (simple) a basic request.", - Description: "Explain a query by providing the directive on wrong location (field).", - - Query: `query { - author @explain { - _key - name - age - } - }`, - - Docs: map[int][]string{ - 2: { - `{ - "name": "John", - "age": 21 - }`, - }, - }, - - Results: []dataMap{}, - - ExpectedError: "Directive \"explain\" may not be used on FIELD.", - } - executeTestCase(t, test) -} - -func TestExplainQuerySimple(t *testing.T) { - test := testUtils.QueryTestCase{ - Description: "Explain a query with no filter", - - Query: `query @explain { + Query: `query @explain(type: simple) { author { _key name @@ -92,101 +63,3 @@ func TestExplainQuerySimple(t *testing.T) { executeTestCase(t, test) } - -func TestExplainQuerySimpleWithAlias(t *testing.T) { - test := testUtils.QueryTestCase{ - Description: "Explain a query with alias, no filter", - - Query: `query @explain { - author { - username: name - age: age - } - }`, - - Docs: map[int][]string{ - 2: { - `{ - "name": "John", - "age": 21 - }`, - }, - }, - - Results: []dataMap{ - { - "explain": dataMap{ - "selectTopNode": dataMap{ - "selectNode": dataMap{ - "filter": nil, - "scanNode": dataMap{ - "filter": nil, - "collectionID": "3", - "collectionName": "author", - "spans": []dataMap{ - { - "start": "/3", - "end": "/4", - }, - }, - }, - }, - }, - }, - }, - }, - } - - executeTestCase(t, test) -} - -func TestExplainQuerySimpleWithMultipleRows(t *testing.T) { - test := testUtils.QueryTestCase{ - Description: "Explain a query with no filter, mutiple rows", - - Query: `query @explain { - author { - name - age - } - }`, - - Docs: map[int][]string{ - 2: { - `{ - "name": "John", - "age": 21 - }`, - `{ - "name": "Bob", - "age": 27 - }`, - }, - }, - - Results: []dataMap{ - { - "explain": dataMap{ - "selectTopNode": dataMap{ - "selectNode": dataMap{ - "filter": nil, - "scanNode": dataMap{ - "filter": nil, - "collectionID": "3", - "collectionName": "author", - "spans": []dataMap{ - { - "start": "/3", - "end": "/4", - }, - }, - }, - }, - }, - }, - }, - }, - } - - executeTestCase(t, test) -} diff --git a/tests/integration/explain/simple/utils.go b/tests/integration/explain/simple/utils.go index a27090bb95..b37044c73d 100644 --- a/tests/integration/explain/simple/utils.go +++ b/tests/integration/explain/simple/utils.go @@ -14,7 +14,6 @@ import ( "testing" testUtils "github.com/sourcenetwork/defradb/tests/integration" - explainUtils "github.com/sourcenetwork/defradb/tests/integration/explain" ) type dataMap = map[string]any @@ -57,7 +56,7 @@ var bookAuthorGQLSchema = (` `) -// TODO: This should be resolved in ISSUE#953 (github.com/sourcenetwork/defradb). +// TODO: This should be resolved in https://github.com/sourcenetwork/defradb/issues/953. func executeTestCase(t *testing.T, test testUtils.QueryTestCase) { testUtils.ExecuteQueryTestCase( t, @@ -67,11 +66,12 @@ func executeTestCase(t *testing.T, test testUtils.QueryTestCase) { ) } -func executeExplainTestCase(t *testing.T, test explainUtils.ExplainRequestTestCase) { - explainUtils.ExecuteExplainRequestTestCase( - t, - bookAuthorGQLSchema, - []string{"article", "book", "author", "authorContact", "contactAddress"}, - test, - ) -} +// TODO: This comment is removed in PR that resolves https://github.com/sourcenetwork/defradb/issues/953 +//func executeExplainTestCase(t *testing.T, test explainUtils.ExplainRequestTestCase) { +// explainUtils.ExecuteExplainRequestTestCase( +// t, +// bookAuthorGQLSchema, +// []string{"article", "book", "author", "authorContact", "contactAddress"}, +// test, +// ) +//}