Extract most validation/initialization/preparation logic from pipeline execution steps into definition steps.
188 lines
4.8 KiB
Go
188 lines
4.8 KiB
Go
package dal
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
|
|
"github.com/cortezaproject/corteza-server/pkg/filter"
|
|
)
|
|
|
|
type (
|
|
// Join produces a series of joined rows from the provided sources based
|
|
// on the JoinPredicate.
|
|
//
|
|
// The join step produces an SQL left join-like output where all right rows
|
|
// have a corresponding left row.
|
|
Join struct {
|
|
Ident string
|
|
RelLeft string
|
|
RelRight string
|
|
// @todo allow multiple join predicates; for now (for easier indexing)
|
|
// only allow one (this is the same as we had before)
|
|
On JoinPredicate
|
|
Filter filter.Filter
|
|
filter internalFilter
|
|
|
|
OutAttributes []AttributeMapping
|
|
LeftAttributes []AttributeMapping
|
|
RightAttributes []AttributeMapping
|
|
|
|
relLeft PipelineStep
|
|
relRight PipelineStep
|
|
plan joinPlan
|
|
analysis stepAnalysis
|
|
}
|
|
|
|
// JoinPredicate determines the attributes the two datasets should get joined on
|
|
JoinPredicate struct {
|
|
Left string
|
|
Right string
|
|
}
|
|
|
|
// joinPlan outlines how the optimizer determined the two datasets should be
|
|
// joined on.
|
|
joinPlan struct {
|
|
// @todo add strategy when we have different strategies implemented
|
|
// strategy string
|
|
|
|
// partialScan indicates we can partially pull data from the two sources
|
|
// as the data is provided in the correct order.
|
|
partialScan bool
|
|
}
|
|
)
|
|
|
|
func (def *Join) Identifier() string {
|
|
return def.Ident
|
|
}
|
|
|
|
func (def *Join) Sources() []string {
|
|
return []string{def.RelLeft, def.RelRight}
|
|
}
|
|
|
|
func (def *Join) Attributes() [][]AttributeMapping {
|
|
return [][]AttributeMapping{def.OutAttributes}
|
|
}
|
|
|
|
func (def *Join) Analyze(ctx context.Context) (err error) {
|
|
def.analysis = stepAnalysis{
|
|
scanCost: costUnknown,
|
|
searchCost: costUnknown,
|
|
filterCost: costUnknown,
|
|
sortCost: costUnknown,
|
|
outputSize: sizeUnknown,
|
|
}
|
|
return
|
|
}
|
|
|
|
func (def *Join) Analysis() stepAnalysis {
|
|
return def.analysis
|
|
}
|
|
|
|
func (def *Join) Optimize(req internalFilter) (res internalFilter, err error) {
|
|
err = fmt.Errorf("not implemented")
|
|
return
|
|
}
|
|
|
|
// iterator initializes an iterator based on the provided pipeline step definition
|
|
func (def *Join) iterator(ctx context.Context, left, right Iterator) (out Iterator, err error) {
|
|
exec, err := def.init(ctx, left, right)
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
// Get pred. type
|
|
// @todo should validate that both pred. types are the same/compatible
|
|
var pt Type
|
|
for _, a := range def.LeftAttributes {
|
|
if a.Identifier() == def.On.Left {
|
|
pt = a.Properties().Type
|
|
break
|
|
}
|
|
}
|
|
|
|
return exec, exec.init(ctx, pt)
|
|
}
|
|
|
|
// dryrun performs step execution without interacting with the data
|
|
// @todo consider rewording this
|
|
func (def *Join) dryrun(ctx context.Context) (err error) {
|
|
_, err = def.init(ctx, nil, nil)
|
|
return
|
|
}
|
|
|
|
func (def *Join) init(ctx context.Context, left, right Iterator) (exec *joinLeft, err error) {
|
|
exec = &joinLeft{
|
|
leftSource: left,
|
|
rightSource: right,
|
|
}
|
|
|
|
// Convert the provided filter into an internal filter
|
|
if def.Filter != nil {
|
|
def.filter, err = toInternalFilter(def.Filter)
|
|
if err != nil {
|
|
return
|
|
}
|
|
}
|
|
|
|
// Collect attributes from the underlaying step in case own are not provided
|
|
if len(def.LeftAttributes) == 0 {
|
|
def.LeftAttributes = collectAttributes(def.relLeft)
|
|
}
|
|
if len(def.RightAttributes) == 0 {
|
|
def.RightAttributes = collectAttributes(def.relRight)
|
|
}
|
|
|
|
// @todo this isn't quite ok -- the ident of left/right must become the src of the out.
|
|
// An edge-case but it should be covered.
|
|
if len(def.OutAttributes) == 0 {
|
|
def.OutAttributes = append(def.LeftAttributes, def.RightAttributes...)
|
|
}
|
|
|
|
// Index attrs for validations
|
|
leftSrcAttrs := indexAttrs(def.LeftAttributes...)
|
|
rightSrcAttrs := indexAttrs(def.RightAttributes...)
|
|
outAttrs := indexAttrs(def.OutAttributes...)
|
|
|
|
for _, a := range def.OutAttributes {
|
|
if !leftSrcAttrs[a.Source()] && !rightSrcAttrs[a.Source()] {
|
|
return nil, fmt.Errorf("unknown attribute %s", a.Source())
|
|
}
|
|
}
|
|
|
|
// Generic validation
|
|
if !leftSrcAttrs[def.On.Left] {
|
|
return nil, fmt.Errorf("unknown join predicate attribute %s", def.On.Left)
|
|
}
|
|
if !rightSrcAttrs[def.On.Right] {
|
|
return nil, fmt.Errorf("unknown join predicate attribute %s", def.On.Right)
|
|
}
|
|
|
|
if len(def.OutAttributes) == 0 {
|
|
return nil, fmt.Errorf("no attributes specified")
|
|
}
|
|
if len(def.LeftAttributes) == 0 {
|
|
return nil, fmt.Errorf("no left attributes specified")
|
|
}
|
|
if len(def.RightAttributes) == 0 {
|
|
return nil, fmt.Errorf("no right attributes specified")
|
|
}
|
|
|
|
if def.On.Left == "" {
|
|
return nil, fmt.Errorf("no left attribute in the join predicate specified")
|
|
}
|
|
if def.On.Right == "" {
|
|
return nil, fmt.Errorf("no right attribute in the join predicate specified")
|
|
}
|
|
|
|
// order
|
|
for _, s := range def.filter.OrderBy() {
|
|
if _, ok := outAttrs[s.Column]; !ok {
|
|
return nil, fmt.Errorf("order attribute %s does not exist", s.Column)
|
|
}
|
|
}
|
|
|
|
exec.filter = def.filter
|
|
exec.def = *def
|
|
return
|
|
}
|