3
0

Define base dal pipeline implementation

This commit is contained in:
Tomaž Jerman
2022-08-22 12:23:23 +02:00
parent f8595ca827
commit 1ad7323ce7
+350
View File
@@ -0,0 +1,350 @@
package dal
import (
"context"
"fmt"
)
type (
// Pipeline defines a series of steps performed over the data
//
// General outline of how a pipeline is used:
// 1. Populate the pipeline with steps to define the desired outcome
// 2. Analyze the pipeline to collect statistics and assist optimizers
// 3. Run structure optimization which may reorder/drop steps
// 4. Run step optimization which may re-configure individual steps such as
// push parts of a filter to lower level, request specific sort order, ...
// 5. Use the pipeline as an iterator to pull the data from.
Pipeline []PipelineStep
// PipelineStep defines an operation performed over the data in the pipeline
PipelineStep interface {
Identifier() string
Sources() []string
Initialize(context.Context, ...Iterator) (Iterator, error)
Analyze(ctx context.Context) error
Analysis() stepAnalysis
}
// Attribute mapping outlines specific attributes within a pipeline
// @todo reconsider this interface; potentially remove it or split it up
AttributeMapping interface {
Identifier() string
Expression() (expression string)
Source() (ident string)
Properties() MapProperties
}
// MapProperties describe the attribute such as it's type and constraints
MapProperties struct {
IsPrimary bool
Nullable bool
Type Type
}
)
// LinkSteps links related steps into a tree structure
//
// @todo make it return a new slice and not mutate the original
func (pp Pipeline) LinkSteps() (err error) {
// map steps by identifiers
steps := make(map[string]PipelineStep)
for _, s := range pp {
steps[s.Identifier()] = s
}
// Link up...
err = func() (err error) {
for _, s := range pp {
switch rs := s.(type) {
case *Aggregate:
rs.rel = steps[rs.RelSource]
if rs.rel == nil {
return fmt.Errorf("aggregate: missing source relation %s", rs.RelSource)
}
case *Join:
rs.relLeft = steps[rs.RelLeft]
rs.relRight = steps[rs.RelRight]
if rs.relLeft == nil {
return fmt.Errorf("join: missing left relation %s", rs.relLeft)
}
if rs.relRight == nil {
return fmt.Errorf("join: missing right relation %s", rs.relRight)
}
case *Link:
rs.relLeft = steps[rs.RelLeft]
rs.relRight = steps[rs.RelRight]
if rs.relLeft == nil {
return fmt.Errorf("link: missing left relation %s", rs.relLeft)
}
if rs.relRight == nil {
return fmt.Errorf("link: missing right relation %s", rs.relRight)
}
}
}
return
}()
if err != nil {
return fmt.Errorf("unable to link steps: %w", err)
}
return nil
}
// Analyze runs analysis over each step in the pipeline
//
// Step analysis hints to the optimizers as to how expensive specific operations
// are and the general dataset size involved.
func (pp Pipeline) Analyze(ctx context.Context) (err error) {
for _, p := range pp {
err = p.Analyze(ctx)
if err != nil {
return
}
}
return
}
// Optimize runs all optimization and returns an optimized pipeline
func (base Pipeline) Optimize(ctx context.Context) (optimized Pipeline, err error) {
base, err = base.OptimizeStructure(ctx)
if err != nil {
return
}
return base.OptimizeSteps(ctx)
}
// OptimizeStructure performs general pipeline structure optimizations such as
// restructuring and clobbering steps onto the datasource layer
func (base Pipeline) OptimizeStructure(ctx context.Context) (optimized Pipeline, err error) {
optimized = base.Clone()
return optimized, optimized.walkSubtrees(optimized.root(), func(step PipelineStep, isRoot bool) (out PipelineStep, err error) {
out = step
for _, opt := range pipelineOptimizers {
out, err = opt(out, isRoot)
if err != nil {
return
}
}
return
})
}
// OptimizeSteps performs step specific optimizations such as pushing filters
// on the lower levels, determining step-specific plans, ...
func (base Pipeline) OptimizeSteps(ctx context.Context) (optimized Pipeline, err error) {
optimized = base.Clone()
return optimized, optimized.optimizeSteps(base.root(), internalFilter{})
}
// Iterator constructs and returns an iterator based on the pipeline
func (pp Pipeline) Iterator(ctx context.Context) (out Iterator, err error) {
return pp.iterator(ctx, pp.root())
}
// // // // // // // // // // // // // // // // // // // // // // // // //
// Utilities
// iterator is the recursive counterpart to the .Iterator method
func (p Pipeline) iterator(ctx context.Context, s PipelineStep) (it Iterator, err error) {
switch s := s.(type) {
case *Datasource:
return s.Initialize(ctx)
case *Aggregate:
it, err = p.iterator(ctx, s.rel)
if err != nil {
return
}
return s.Initialize(ctx, it)
case *Join:
var left Iterator
var right Iterator
left, err = p.iterator(ctx, s.relLeft)
if err != nil {
return
}
right, err = p.iterator(ctx, s.relRight)
if err != nil {
return
}
return s.Initialize(ctx, left, right)
case *Link:
var left Iterator
var right Iterator
left, err = p.iterator(ctx, s.relLeft)
if err != nil {
return
}
right, err = p.iterator(ctx, s.relRight)
if err != nil {
return
}
return s.Initialize(ctx, left, right)
}
return nil, fmt.Errorf("unsupported step")
}
func (pp Pipeline) root() PipelineStep {
// @todo this is not ok; we need to analyze the tree, but I was lazy :seenoevil:
return pp[len(pp)-1]
}
// optimizeSteps is the recursive counterpart to the .OptimizeSteps method
func (p Pipeline) optimizeSteps(node PipelineStep, inF internalFilter) (err error) {
switch n := node.(type) {
case *Datasource:
inF, err = n.Optimize(inF)
if err != nil {
return
}
if !inF.empty() {
return fmt.Errorf("a datasource can not offload optimizations")
}
return
case *Aggregate:
inF, err = n.Optimize(inF)
if err != nil {
return
}
return p.optimizeSteps(n.rel, inF)
case *Join:
inF, err = n.Optimize(inF)
if err != nil {
return
}
err = p.optimizeSteps(n.relLeft, inF)
if err != nil {
return
}
err = p.optimizeSteps(n.relRight, inF)
if err != nil {
return
}
case *Link:
inF, err = n.Optimize(inF)
if err != nil {
return
}
err = p.optimizeSteps(n.relLeft, inF)
if err != nil {
return
}
err = p.optimizeSteps(n.relRight, inF)
if err != nil {
return
}
}
return
}
// walkSubtrees performs a DFS and invokes fn for every sub-tree node in the returning order
func (p Pipeline) walkSubtrees(root PipelineStep, fn func(step PipelineStep, isRoot bool) (PipelineStep, error)) (err error) {
err = p.walkSubtreesRec(root, true, fn)
if err != nil {
return
}
// dfsRec reports only subtrees; in case no sub tree was there, do it
switch root.(type) {
case *Join, *Link:
n, err := fn(root, true)
if err != nil {
return err
}
p.replace(root, n)
}
return
}
// walkSubtreesRec is the recursive counterpart to the .walkSubtrees method
func (p Pipeline) walkSubtreesRec(root PipelineStep, isRoot bool, fn func(step PipelineStep, isRoot bool) (PipelineStep, error)) (err error) {
var n PipelineStep
switch s := root.(type) {
case *Datasource:
// this one doesn't have anything under it
return
case *Aggregate:
return p.walkSubtreesRec(s.rel, false, fn)
case *Join:
err = p.walkSubtreesRec(s.relLeft, false, fn)
if err != nil {
return
}
err = p.walkSubtreesRec(s.relRight, false, fn)
if err != nil {
return
}
n, err = fn(root, isRoot)
if err != nil {
return
}
p.replace(root, n)
case *Link:
err = p.walkSubtreesRec(s.relLeft, false, fn)
if err != nil {
return
}
err = p.walkSubtreesRec(s.relRight, false, fn)
if err != nil {
return
}
n, err = fn(root, isRoot)
if err != nil {
return
}
p.replace(root, n)
}
return
}
func (pp Pipeline) replace(o, n PipelineStep) {
for i, p := range pp {
if p == o {
pp[i] = n
return
}
}
}
func (p Pipeline) Clone() (out Pipeline) {
out = make(Pipeline, 0, len(p))
for _, s := range p {
switch s := s.(type) {
case *Aggregate:
aux := *s
out = append(out, &aux)
case *Join:
aux := *s
out = append(out, &aux)
case *Link:
aux := *s
out = append(out, &aux)
default:
panic("unsupported step")
}
}
return
}