Skip to content

Commit

Permalink
wip: JSON formatting with explain returning a map[string]interface wo…
Browse files Browse the repository at this point in the history
…rking.
  • Loading branch information
shahzadlone committed May 17, 2022
1 parent a5a85fd commit d1101e1
Show file tree
Hide file tree
Showing 3 changed files with 143 additions and 65 deletions.
135 changes: 108 additions & 27 deletions query/graphql/planner/explain.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
3 changes: 1 addition & 2 deletions query/graphql/planner/limit.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
70 changes: 34 additions & 36 deletions query/graphql/planner/planner.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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.
Expand Down

0 comments on commit d1101e1

Please sign in to comment.