Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor: Rework document rendering to avoid data duplication and mutation #68

Merged
merged 2 commits into from
Dec 2, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions query/graphql/parser/query.go
Original file line number Diff line number Diff line change
Expand Up @@ -80,8 +80,9 @@ type Selection interface {
// fields, and query arguments like filters,
// limits, etc.
type Select struct {
Name string
Alias string
Name string
Alias string
CollectionName string

// Root is the top level query parsed type
Root SelectionType
Expand Down
38 changes: 0 additions & 38 deletions query/graphql/planner/commit.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,6 @@ func (n *commitSelectNode) Next() (bool, error) {
}

n.doc = n.source.Values()
n.renderDoc()
return true, nil
}

Expand Down Expand Up @@ -85,10 +84,6 @@ func (p *Planner) CommitSelect(parsed *parser.CommitSelect) (planNode, error) {
if err != nil {
return nil, err
}
err = commit.initFields(parsed)
if err != nil {
return nil, err
}
slct := parsed.ToSelect()
plan, err := p.SelectFromSource(slct, commit, false, nil)
if err != nil {
Expand Down Expand Up @@ -165,39 +160,6 @@ func (p *Planner) commitSelectAll(parsed *parser.CommitSelect) (*commitSelectNod
return commit, nil
}

// renderDoc applies the render meta-data to the
// links/previous sub selections for a commit type
// query.
func (n *commitSelectNode) renderDoc() {
for subfield, info := range n.subRenderInfo {
renderData := map[string]interface{}{
"numResults": info.numResults,
"fields": info.fields,
"aliases": info.aliases,
}
for _, subcommit := range n.doc[subfield].([]map[string]interface{}) {
subcommit["__render"] = renderData
}

}
}

func (n *commitSelectNode) initFields(parsed parser.Selection) error {
for _, selection := range parsed.GetSelections() {
switch node := selection.(type) {
case *parser.Select:
info := renderInfo{}
for _, f := range node.Fields {
info.fields = append(info.fields, f.GetName())
info.aliases = append(info.aliases, f.GetAlias())
info.numResults++
}
n.subRenderInfo[node.Name] = info
}
}
return nil
}

// commitSelectTopNode is a wrapper for the selectTopNode
// in the case where the select is actually a CommitSelect
type commitSelectTopNode struct {
Expand Down
146 changes: 89 additions & 57 deletions query/graphql/planner/render.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,25 +11,71 @@ package planner

import (
"github.com/sourcenetwork/defradb/core"
"github.com/sourcenetwork/defradb/query/graphql/parser"
)

// @todo: Rebuild render system.
// @body: Current render system embeds render meta-data
// into EVERY SINGLE returned object map. This can be drastically
// reduced. Related: Replace Values() result with a typed object
// instead of a raw map[string]interface{}

// the final field select and render
type renderNode struct { // selectNode??
type renderNode struct {
p *Planner
plan planNode

// fields []*base.FieldDescription
// aliases []string
renderInfo topLevelRenderInfo
}

type topLevelRenderInfo struct {
children []renderInfo
}

type renderInfo struct {
sourceFieldName string
destinationFieldName string
children []renderInfo
}

func (p *Planner) render() *renderNode {
return &renderNode{p: p}
func (p *Planner) render(parsed *parser.Select) *renderNode {
return &renderNode{
p: p,
renderInfo: buildTopLevelRenderInfo(parsed),
}
}

func buildTopLevelRenderInfo(parsed parser.Selection) topLevelRenderInfo {
childSelections := parsed.GetSelections()

info := topLevelRenderInfo{
children: make([]renderInfo, len(childSelections)),
}

for i, selection := range childSelections {
info.children[i] = buildRenderInfo(selection)
}

return info
}

func buildRenderInfo(parsed parser.Selection) renderInfo {
childSelections := parsed.GetSelections()
sourceFieldName := parsed.GetName()
alias := parsed.GetAlias()

var destinationFieldName string
if alias == "" {
destinationFieldName = sourceFieldName
} else {
destinationFieldName = alias
}

info := renderInfo{
sourceFieldName: sourceFieldName,
destinationFieldName: destinationFieldName,
children: make([]renderInfo, len(childSelections)),
}

for i, selection := range childSelections {
info.children[i] = buildRenderInfo(selection)
}

return info
}

func (n *renderNode) Init() error { return n.plan.Init() }
Expand All @@ -46,58 +92,44 @@ func (r *renderNode) Values() map[string]interface{} {
if doc == nil {
return doc
}
return r.render(doc)

result := map[string]interface{}{}
for _, renderInfo := range r.renderInfo.children {
renderInfo.render(doc, result)
}
return result
}

// render uses the __render map within the return doc via Values().
// it extracts the associated render meta-data, and returns a newly
// rendered map.
// The render rules are as follows:
// The doc returned by the plan has the following values:
// {
// ... document fields returned by scanPlan
// __render: {
// numRender: ... => the number of fields in the actual selectionset
// fields: ... => array of fields extracted from the raw query (Includes selection set + filter dependencies)
// aliases: ... => array of aliases, index matched to fields array.
// }
// }
func (r *renderNode) render(src map[string]interface{}) map[string]interface{} {
result := make(map[string]interface{})
if renderMap, ok := src["__render"].(map[string]interface{}); ok {
numRenderFields := renderMap["numResults"].(int)
fields := renderMap["fields"].([]string)
aliases := renderMap["aliases"].([]string)

for i := 0; i < numRenderFields; i++ {
field := fields[i]
var dst string
name := field
dst = name
alias := aliases[i]
if alias != "" {
dst = alias
// Renders the source document into the destination document using the given renderInfo.
// Function recursively handles any nested children defined in the render info.
func (r *renderInfo) render(src map[string]interface{}, destination map[string]interface{}) {
var resultValue interface{}
if val, ok := src[r.sourceFieldName]; ok {
switch v := val.(type) {
// If the current property is itself a map, we should render any properties of the child
case map[string]interface{}:
inner := map[string]interface{}{}
for _, child := range r.children {
child.render(v, inner)
}

if val, ok := src[name]; ok {
switch v := val.(type) {
case map[string]interface{}:
result[dst] = r.render(v)
case []map[string]interface{}:
subdocs := make([]map[string]interface{}, 0)
for _, subv := range v {
subdocs = append(subdocs, r.render(subv))
}
result[dst] = subdocs
default:
result[dst] = v
resultValue = inner
// If the current property is an array of maps, we should render each child map
case []map[string]interface{}:
subdocs := make([]map[string]interface{}, 0)
for _, subv := range v {
inner := map[string]interface{}{}
for _, child := range r.children {
child.render(subv, inner)
}
} else {
result[dst] = nil
subdocs = append(subdocs, inner)
}
resultValue = subdocs
default:
resultValue = v
}
} else {
return src
resultValue = nil
}
return result

destination[r.destinationFieldName] = resultValue
}
68 changes: 6 additions & 62 deletions query/graphql/planner/select.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,12 +46,6 @@ func (n *selectTopNode) Close() error {
return n.plan.Close()
}

type renderInfo struct {
numResults int
fields []string
aliases []string
}

type selectNode struct {
p *Planner

Expand Down Expand Up @@ -118,31 +112,12 @@ func (n *selectNode) Next() (bool, error) {
}

if passes {
n.renderDoc()
return true, err
// err :=
// return err == nil, err
}
// didn't pass, keep looping
}
}

// applies all the necessary rendering to doc
// as defined by the query statement. This includes
// aliases, and any transformations.
// Takes a doc map, and applies the necessary rendering.
// It also holds all the necessary render meta-data
// and ast parser data.
func (n *selectNode) renderDoc() error {
renderData := map[string]interface{}{
"numResults": n.renderInfo.numResults,
"fields": n.renderInfo.fields,
"aliases": n.renderInfo.aliases,
}
n.doc["__render"] = renderData
return nil
}

func (n *selectNode) Spans(spans core.Spans) {
n.source.Spans(spans)
}
Expand All @@ -161,8 +136,10 @@ func (n *selectNode) Close() error {
// the necessary filters. Its designed to work with the
// planner.Select construction call.
func (n *selectNode) initSource(parsed *parser.Select) error {
collectionName := parsed.Name
sourcePlan, err := n.p.getSource(collectionName)
if parsed.CollectionName == "" {
parsed.CollectionName = parsed.Name
}
sourcePlan, err := n.p.getSource(parsed.CollectionName)
if err != nil {
return err
}
Expand Down Expand Up @@ -196,39 +173,6 @@ func (n *selectNode) initSource(parsed *parser.Select) error {
}

func (n *selectNode) initFields(parsed *parser.Select) error {
n.renderInfo.numResults = 0
// subTypes := make([]*parser.Select, 0)

// iterate to build the render info
for _, field := range parsed.Fields {
switch node := field.(type) {
case *parser.Select:
// continue //ignore for now
// future:
// plan := n.p.Select(node)
// n.source := p.SubTypeIndexJoin(origScan, plan)
// f, found := n.sourceInfo.collectionDescription.GetField(node.GetName())
// if found {
// n.renderInfo.fields = append(n.renderInfo.fields, f.Name)
// }
n.renderInfo.fields = append(n.renderInfo.fields, node.GetName())
// subTypes = append(subTypes, node)
case *parser.Field, parser.Field:
// f, found := n.sourceInfo.collectionDescription.GetField(node.GetName())
// if found {
// n.renderInfo.fields = append(n.renderInfo.fields, f.Name)
// }
n.renderInfo.fields = append(n.renderInfo.fields, node.GetName())
}
n.renderInfo.aliases = append(n.renderInfo.aliases, field.GetAlias())
n.renderInfo.numResults++
}

// iterate to build sub plans
// for _, field := range parsed.Fields {

// }

// re-organize the fields slice into reverse-alphabetical
// this makes sure the reserved database fields that start with
// a "_" end up at the end. So if/when we build our MultiNode
Expand Down Expand Up @@ -351,7 +295,7 @@ func (p *Planner) SelectFromSource(parsed *parser.Select, source planNode, fromC

top := &selectTopNode{
source: s,
render: p.render(),
render: p.render(parsed),
limit: limitPlan,
sort: sortPlan,
group: groupPlan,
Expand Down Expand Up @@ -389,7 +333,7 @@ func (p *Planner) Select(parsed *parser.Select) (planNode, error) {

top := &selectTopNode{
source: s,
render: p.render(),
render: p.render(parsed),
limit: limitPlan,
sort: sortPlan,
group: groupPlan,
Expand Down
4 changes: 2 additions & 2 deletions query/graphql/planner/type_join.go
Original file line number Diff line number Diff line change
Expand Up @@ -189,7 +189,7 @@ func (p *Planner) makeTypeJoinOne(parent *selectNode, source planNode, subType *
return nil, errors.New("Relation is missing referenced field")
}

subType.Name = subTypeFieldDesc.Schema
subType.CollectionName = subTypeFieldDesc.Schema

selectPlan, err := p.SubSelect(subType)
if err != nil {
Expand Down Expand Up @@ -351,7 +351,7 @@ func (p *Planner) makeTypeJoinMany(parent *selectNode, source planNode, subType
if !ok {
return nil, errors.New("couldn't find subtype field description for typeJoin node")
}
subType.Name = subTypeFieldDesc.Schema
subType.CollectionName = subTypeFieldDesc.Schema

selectPlan, err := p.SubSelect(subType)
if err != nil {
Expand Down