From d1101e12050cd8eb5b34990a3796f85920f25ba8 Mon Sep 17 00:00:00 2001 From: Shahzad Lone Date: Fri, 13 May 2022 10:37:26 -0400 Subject: [PATCH] wip: JSON formatting with explain returning a map[string]interface working. --- query/graphql/planner/explain.go | 135 ++++++++++++++++++++++++------- query/graphql/planner/limit.go | 3 +- query/graphql/planner/planner.go | 70 ++++++++-------- 3 files changed, 143 insertions(+), 65 deletions(-) diff --git a/query/graphql/planner/explain.go b/query/graphql/planner/explain.go index 60018a4c7e..52b0132f54 100644 --- a/query/graphql/planner/explain.go +++ b/query/graphql/planner/explain.go @@ -10,46 +10,127 @@ package planner -import ( - "strings" -) - -// Response: +// Request: // query @explain { // user { // _key // } // } -// Input to ExplainAllText: -// [ "selectTopNode" "selectNode" "scanNode" ] +// Response: +//{ +// "data": [ +// { +// "explain": { +// "Node => selectTopNode": { +// "Attribite": "Select Top Node", +// "Node => selectNode": { +// "Attribite": "Select Node", +// "Node => scanNode": { +// "Attribite": "Scan Node" +// } +// } +// } +// } +// } +// ] +//} -// Outputs: -// ---------------- -// Explain Request: -// ---------------- -// selectTopNode -// selectNode -// scanNode +func buildExplainGraph(source planNode) map[string]interface{} { -// ExplainAllText formats with proper indenting all the node names. -func ExplainAllText(nodesToExplain []string) []map[string]interface{} { + var explainGraph map[string]interface{} = map[string]interface{}{} - var explainResponse []map[string]interface{} + if source == nil { + return explainGraph + } + + // Walk the multiple children if it is a MultiNode (MultiNode itself is non-explainable). + multiNode, isMultiNode := source.(MultiNode) + if isMultiNode { + childrenSources := multiNode.Children() + for _, childSource := range childrenSources { + explainGraph = buildExplainGraph(childSource.Source()) + } + } - var explainBuilder string = "----------------\nExplain Request:\n----------------\n" + // Only explain the node if it is explainable. + explainableSource, isExplainable := source.(explainablePlanNode) + if isExplainable { + explainGraphBuilder := explainableSource.Explain() - for i, node := range nodesToExplain { - explainBuilder = explainBuilder + strings.Repeat("\t", i) + node + "\n" + // If not the last child then keep walking the graph to find more explainable nodes. + notLeafSource := explainableSource.Source() != nil + if notLeafSource { + childExplainGraph := buildExplainGraph(explainableSource.Source()) + for key, value := range childExplainGraph { + explainGraphBuilder[key] = value + } + } + + explainNodeLabelTitle := "Node => " + explainableSource.Kind() + explainGraph[explainNodeLabelTitle] = explainGraphBuilder } - // fmt.Println(explainBuilder) + return explainGraph + +} + +type explainablePlanNode interface { + planNode + Explain() map[string]interface{} +} + +// Compile time check for all planNodes that should be explainable (satisfy explainablePlanNode). +var ( + // _ explainablePlanNode = (*averageNode)(nil) + // _ explainablePlanNode = (*commitSelectNode)(nil) + // _ explainablePlanNode = (*countNode)(nil) + // _ explainablePlanNode = (*createNode)(nil) + // _ explainablePlanNode = (*dagScanNode)(nil) + // _ explainablePlanNode = (*deleteNode)(nil) + // _ explainablePlanNode = (*renderNode)(nil) + _ explainablePlanNode = (*scanNode)(nil) + _ explainablePlanNode = (*selectNode)(nil) + _ explainablePlanNode = (*selectTopNode)(nil) + // _ explainablePlanNode = (*sortNode)(nil) + // _ explainablePlanNode = (*sumNode)(nil) + // _ explainablePlanNode = (*typeIndexJoin)(nil) + // _ explainablePlanNode = (*updateNode)(nil) - return append( - explainResponse, - map[string]interface{}{ - "explain": explainBuilder, - }, - ) + // Internal Nodes that we don't want to expose / explain. + + // _ explainablePlanNode = (*commitSelectTopNode)(nil) + // _ explainablePlanNode = (*renderLimitNode)(nil) + // _ explainablePlanNode = (*groupNode)(nil) + // _ explainablePlanNode = (*hardLimitNode)(nil) + // _ explainablePlanNode = (*headsetScanNode)(nil) + // _ explainablePlanNode = (*parallelNode)(nil) + // _ explainablePlanNode = (*pipeNode)(nil) + // _ explainablePlanNode = (*typeJoinMany)(nil) + // _ explainablePlanNode = (*typeJoinOne)(nil) +) + +// Following are all the planNodes that are subscribing to the explainablePlanNode. + +func (n *selectTopNode) Explain() map[string]interface{} { + explainerMap := map[string]interface{}{ + "Attribite": "Select Top Node", + } + + return explainerMap +} + +func (n *selectNode) Explain() map[string]interface{} { + explainerMap := map[string]interface{}{ + "Attribite": "Select Node", + } + return explainerMap +} + +func (n *scanNode) Explain() map[string]interface{} { + explainerMap := map[string]interface{}{ + "Attribite": "Scan Node", + } + return explainerMap } diff --git a/query/graphql/planner/limit.go b/query/graphql/planner/limit.go index 4412dfad0a..6414bd9635 100644 --- a/query/graphql/planner/limit.go +++ b/query/graphql/planner/limit.go @@ -26,8 +26,7 @@ type hardLimitNode struct { rowIndex int64 } -// HardLimit creates a new hardLimitNode initalized from -// the parser.Limit object. +// HardLimit creates a new hardLimitNode initalized from the parser.Limit object. func (p *Planner) HardLimit(n *parser.Limit) (*hardLimitNode, error) { if n == nil { return nil, nil // nothing to do diff --git a/query/graphql/planner/planner.go b/query/graphql/planner/planner.go index 6c8ffeb1d0..c5808cb01e 100644 --- a/query/graphql/planner/planner.go +++ b/query/graphql/planner/planner.go @@ -29,39 +29,32 @@ var ( // planNode is an interface all nodes in the plan tree need to implement. The file `operations.go` // does a compile-time check of all the nodes that satisfy (implement) `planNode`. type planNode interface { - // Explain describes / explains the planNode (does not execute the plan). - Kind() string - - // Initializes or Re-Initializes an existing planNode - // Often called internally by Start() + // Initializes or Re-Initializes an existing planNode, often called internally by Start(). Init() error - // Starts any internal logic or processes - // required by the plan node. + // Starts any internal logic or processes required by the planNode. Should be called *after* Init(). Start() error - // Spans sets the planNodes target - // spans. This is primarily only used - // for a scanNode, but based on the tree - // structure, may need to be propagated - // Eg. From a selectNode -> scanNode. + // Spans sets the planNodes target spans. This is primarily only used for a scanNode, + // but based on the tree structure, may need to be propagated Eg. From a selectNode -> scanNode. Spans(core.Spans) - // Next processes the next result doc from - // the query. Can only be called *after* - // Start(). Can't be called again if any - // previous call returns false. + // Next processes the next result doc from the query. Can only be called *after* Start(). + // Can't be called again if any previous call returns false. Next() (bool, error) - // Values returns the value of the current doc + // Values returns the value of the current doc, should only be called *after* Next(). Value() map[string]interface{} - // Source returns the child planNode that - // generates the source values for this plan. - // If a plan has no source, return nil + // Source returns the child planNode that generates the source values for this plan. + // If a plan has no source, nil is returned. Source() planNode - // Close terminates the planNode execution releases its resources. + // Kind tells the name of concrete planNode type. + Kind() string + + // Close terminates the planNode execution and releases its resources. After this + // method is called you can only safely call Kind() and Source() methods. Close() error } @@ -150,14 +143,8 @@ func (p *Planner) newObjectMutationPlan(stmt *parser.Mutation) (planNode, error) } // makePlan creates a new plan from the parsed data, optimizes the plan and returns -// an initiated plan. -// @ask-question: is the caller responsible to `Close()` the plan as this function returns -// a plan with Init() call on it??? if yes then document it or return a -// non-initiated plan so the user is responsible and aware that they -// have to call Close() since they Initialized the plan. Also might be -// easier to handle by caller in that case to just handle it with a `defer` -// function rather than polluting that block scope at every return or exit -// call to remember to free the resources. example: `executeRequest()` function. +// an initiated plan. The caller of makePlan is also responsible of calling Close() +// on the plan to free it's resources. func (p *Planner) makePlan(stmt parser.Statement) (planNode, error) { plan, err := p.newPlan(stmt) if err != nil { @@ -440,22 +427,33 @@ func (p *Planner) explainRequest( ctx context.Context, plan planNode, ) ([]map[string]interface{}, error) { - src := plan - if src == nil { + if plan == nil { if errToLog := (plan.Close()); errToLog != nil { log.ErrorE(ctx, "Error: Failure closing plan while explaining `planNode`.", errToLog) } return nil, fmt.Errorf("Error: Can't explain an empty plan.") } - var explains []string - for src != nil { - explains = append(explains, src.Kind()) - src = src.Source() + // TEST CODE + // var explains []string + // src := plan + // for src != nil { + // explains = append(explains, src.Kind()) + // src = src.Source() + // } + // println("==================================") + // fmt.Println(strings.Join(explains, ", ")) + // println("==================================") + // +++++++++ + + var topExplainGraph []map[string]interface{} = []map[string]interface{}{ + { + parser.DirectiveLabel.ExplainLabel: buildExplainGraph(plan), + }, } err := plan.Close() - return ExplainAllText(explains), err + return topExplainGraph, err } // executeRequest executes the plan graph that represents the request that was made.