featurebase/sql3/planner/planoptimizer.go
Seebs 7cf2c5b07e handle integers as comparisons for decimal fields
It's reasonable to allow "where x > 13" on decimal
fields. Handle at least int64 and float64.

Once this is up, we find that aggregates can return
non-values, such as nil, in some cases; for instance,
`percentile(x) where x > 13` can yield a nil if x is
never greater than 13, rather than making up a value
from zero data points. So we accept nil as a valid
result type in PQL aggregates.

As a result of this, change two tests which were
unintentionally testing for an arcane edge case bug
in which (1) we can't render a condition to PQL,
such as because you specified an integer for a decimal
field, and (2) the filter is using an aliased name,
in which we would end up failing to generate a PQL
filter, but *also* losing the SQL-layer filter, and
produce wrong results as though there were no filter.

We also alter the tests to use `o.price > 9`, because
this lets us generate three user names, but only two
distinct user names, so the test using DISTINCT returns
a different value than the test not using DISTINCT,
which helps us verify that it's actually working and
not just lucky.

As part of fixing that, there was an intermediate
state where we rejected as an error any case where
generating the PQL filter failed. This broke 21 more
test cases, but in all of those cases, the SQL filter
was actually working.

... But in two of them, we SHOULD have been able to
generate PQL, because they were testing bools for
null, which works fine. We just had a list of
field types we allowed null tests against and
omitted bool because I forgot that bool isn't always
just treated as a kind of mutex.
2023-04-07 15:52:26 -05:00

1424 lines
43 KiB
Go

// Copyright 2023 Molecula Corp. All rights reserved.
package planner
import (
"context"
"fmt"
"reflect"
"strings"
"github.com/featurebasedb/featurebase/v3/dax"
"github.com/featurebasedb/featurebase/v3/sql3"
"github.com/featurebasedb/featurebase/v3/sql3/parser"
"github.com/featurebasedb/featurebase/v3/sql3/planner/types"
)
//TODO(pok) give every expression an id 'Expr1234' and use that to match on
//TODO(pok) have a rule to eliminate PlanOpRelAlias
//TODO(pok) push filter down into join condition if terms reference either side of join
//TODO(pok) push order by down as far as possible
//TODO(pok) you can't group by _id in PQL, so we need to not use a PQL group by operator here
//TODO(pok) move constant folding to in here
//TODO(pok) push down filters thru subqueries with aliases
// OptimizerFunc is a function prototype for all optimizer rules.
type OptimizerFunc func(context.Context, *ExecutionPlanner, types.PlanOperator, *OptimizerScope) (types.PlanOperator, bool, error)
// a list of optimzer rules; order can be important important
var optimizerFunctions = []OptimizerFunc{
// fix expression references for having
removeUnusedExtractColumnReferences,
// if we have a distinct operator over a single projection,
// where the projection is on a table scan, use a PQL Distinct scan operator
tryToReplaceDistinctWithPQLDistinct,
// fix expression references for having
fixHavingReferences,
// push down filter predicates as far as possible,
pushdownFilters,
// try to use a PlanOpPQLFilteredDelete instead of PlanOpPQLConstRowDelete
tryToReplaceConstRowDeleteWithFilteredDelete,
// if we have a group by that has one TableScanOperator,
// try to use a PQL(multi)groupby operator instead
tryToReplaceGroupByWithPQLGroupBy,
// if we have a group by with no group by exprs that has
// one TableScanOperator, try to use a PQL aggregate operators instead
tryToReplaceGroupByWithPQLAggregate,
// update the columnIdx for all the qualified references in various operators
fixFieldRefs,
// update the columnIdx for all the references in the projections
// based on the child operator for a projection
fixProjectionReferences,
// if the query has one TableScanOperator then push the top
// expression down into that operator
pushdownPQLTop,
}
// OptimizerScope will be used in future for symbol resolution when CTEs and
// subquery support matures and we need to introduce the concept of scope to
// symbol resolution.
type OptimizerScope struct {
}
func dumpPlan(prefix []string, root types.PlanOperator, suffix string) {
// DEBUG !!
// for _, s := range prefix {
// log.Println(s)
// }
// jplan := root.Plan()
// a, _ := json.MarshalIndent(jplan, "", " ")
// log.Println(string(a))
// log.Println()
// DEBUG !!
}
// optimizePlan takes a plan from the compiler and executes a series of transforms on it to optimize it
func (p *ExecutionPlanner) optimizePlan(ctx context.Context, plan types.PlanOperator) (types.PlanOperator, error) {
dumpPlan(
[]string{"================================================================================", "plan pre-optimzation"},
plan,
"--------------------------------------------------------------------------------",
)
var err error
var result = plan
for _, ofunc := range optimizerFunctions {
result, err = p.optimizeNode(ctx, result, ofunc)
if err != nil {
return nil, err
}
}
dumpPlan(
[]string{"================================================================================", "plan post-optimzation"},
result,
"--------------------------------------------------------------------------------",
)
// check that result is a PlanOpQuery
_, ok := result.(*PlanOpQuery)
if !ok {
return nil, sql3.NewErrInternalf("unexpected root operator type '%T'", result)
}
return result, nil
}
func (p *ExecutionPlanner) optimizeNode(ctx context.Context, node types.PlanOperator, ofunc OptimizerFunc) (types.PlanOperator, error) {
op, same, err := ofunc(ctx, p, node, nil)
if err != nil {
return nil, err
}
if !same {
return op, nil
}
return node, nil
}
// a set of filters for a operator graph
type filterSet struct {
filterConditions []types.PlanExpression
filtersByRelation map[string][]types.PlanExpression
handledFilters []types.PlanExpression
relationAliases RelationAliasesMap
}
func newFilterSet(filter types.PlanExpression, filtersByTable map[string][]types.PlanExpression, tableAliases RelationAliasesMap) *filterSet {
return &filterSet{
filterConditions: splitOnAnd(filter),
filtersByRelation: filtersByTable,
relationAliases: tableAliases,
}
}
func (fs *filterSet) availableFiltersForTable(table string) []types.PlanExpression {
filters, ok := fs.filtersByRelation[table]
if !ok {
return nil
}
return remainingExpressions(filters, fs.handledFilters)
}
func (fs *filterSet) handledCount() int {
return len(fs.handledFilters)
}
func (fs *filterSet) markFiltersHandled(exprs ...types.PlanExpression) {
fs.handledFilters = append(fs.handledFilters, exprs...)
}
func (fs *filterSet) unhandledPredicates(ctx context.Context) []types.PlanExpression {
var available []types.PlanExpression
for _, e := range fs.filterConditions {
available = append(available, remainingExpressions([]types.PlanExpression{e}, fs.handledFilters)...)
}
return available
}
func remainingExpressions(allExprs, lessExprs []types.PlanExpression) []types.PlanExpression {
var remainder []types.PlanExpression
for _, e := range allExprs {
var found bool
for _, s := range lessExprs {
if reflect.DeepEqual(e, s) {
found = true
break
}
}
if !found {
remainder = append(remainder, e)
}
}
return remainder
}
// RelationAliasesMap is a map of aliases to Relations
type RelationAliasesMap map[string]types.IdentifiableByName
func (ta RelationAliasesMap) addAlias(alias types.IdentifiableByName, target types.IdentifiableByName) error {
lowerName := strings.ToLower(alias.Name())
if _, ok := ta[lowerName]; ok {
return sql3.NewErrInternalf("unexpected duplicate alias name")
}
ta[lowerName] = target
return nil
}
// build a map of alias names to relations
func getRelationAliases(n types.PlanOperator, scope *OptimizerScope) (RelationAliasesMap, error) {
var inspectErr error
aliases := make(RelationAliasesMap)
InspectPlan(n, func(node types.PlanOperator) bool {
if node == nil {
return false
}
switch node := node.(type) {
case *PlanOpRelAlias:
switch t := node.ChildOp.(type) {
case *PlanOpPQLTableScan:
inspectErr = aliases.addAlias(node, t)
case *PlanOpPQLDistinctScan:
inspectErr = aliases.addAlias(node, t)
case *PlanOpSubquery:
inspectErr = aliases.addAlias(node, t)
case *PlanOpTableValuedFunction:
inspectErr = aliases.addAlias(node, t)
default:
inspectErr = sql3.NewErrInternalf("unexpected alias child type '%T", node.ChildOp)
}
return false
case *PlanOpPQLTableScan:
inspectErr = aliases.addAlias(node, node)
return false
case *PlanOpPQLDistinctScan:
inspectErr = aliases.addAlias(node, node)
return false
}
return true
})
if inspectErr != nil {
return nil, inspectErr
}
return aliases, inspectErr
}
// governs how far down filter push down can go
func filterPushdownChildSelector(c ParentContext) bool {
switch c.Parent.(type) {
case *PlanOpRelAlias:
//definitely don't go any further than alias as parent
return false
}
return true
}
// governs how far down filter push down above tables can go
func filterPushdownAboveTablesChildSelector(c ParentContext) bool {
if !filterPushdownChildSelector(c) {
return false
}
switch c.Parent.(type) {
case *PlanOpFilter:
switch c.Operator.(type) {
case *PlanOpRelAlias, *PlanOpPQLTableScan, *PlanOpPQLDistinctScan:
return false
}
}
return true
}
// when we compile and create a PlanOpPQLTableScan we just add all the columns to the underlying extract. This is a bad idea, since
// extracts are expensive, more so when we are askign for columns we don't actually need. This function removes those uneeded references.
func removeUnusedExtractColumnReferences(ctx context.Context, a *ExecutionPlanner, n types.PlanOperator, scope *OptimizerScope) (types.PlanOperator, bool, error) {
// get all the qualifiedRefs across the plan
// using a map to eliminate dupes and we don't
// care about the order when iterating
refs := make(map[string]*qualifiedRefPlanExpression)
InspectOperatorExpressions(n, func(pe types.PlanExpression) bool {
switch qref := pe.(type) {
case *qualifiedRefPlanExpression:
refs[qref.String()] = qref
return false
}
return true
})
return TransformPlanOpWithParent(n, func(c ParentContext) bool { return true }, func(c ParentContext) (types.PlanOperator, bool, error) {
switch thisNode := c.Operator.(type) {
case *PlanOpPQLTableScan:
newExtractList := make([]string, 0)
// loop thru the extract list and make a new extract list
// with just the columns we need
alias, ok := c.Parent.(*PlanOpRelAlias)
if ok {
// handle the case where the parent is an alias
for _, ex := range thisNode.columns {
for _, ref := range refs {
if (strings.EqualFold(ref.tableName, thisNode.tableName) || strings.EqualFold(ref.tableName, alias.alias)) && strings.EqualFold(ex, ref.columnName) {
newExtractList = append(newExtractList, ex)
break
}
}
}
} else {
for _, ex := range thisNode.columns {
for _, ref := range refs {
if strings.EqualFold(ref.tableName, thisNode.tableName) && strings.EqualFold(ex, ref.columnName) {
newExtractList = append(newExtractList, ex)
break
}
}
}
}
// newExtractList should now contain just the cols that are referenced
return NewPlanOpPQLTableScan(a, thisNode.tableName, newExtractList, thisNode.hints), false, nil
default:
return thisNode, true, nil
}
})
}
// returns an expression given a list of expressions, if the list is > 2 expressions, all the individual
// expressions are ANDed together
func joinExprsWithAnd(exprs ...types.PlanExpression) types.PlanExpression {
switch len(exprs) {
case 0:
return nil
case 1:
return exprs[0]
default:
result := newBinOpPlanExpression(exprs[0], parser.AND, exprs[1], parser.NewDataTypeBool())
for _, e := range exprs[2:] {
result = newBinOpPlanExpression(result, parser.AND, e, parser.NewDataTypeBool())
}
return result
}
}
func removePushedDownConditions(ctx context.Context, a *ExecutionPlanner, node *PlanOpFilter, filters *filterSet) (types.PlanOperator, bool, error) {
if filters.handledCount() == 0 {
return node, true, nil
}
unhandled := filters.unhandledPredicates(ctx)
if len(unhandled) == 0 {
return node.ChildOp, false, nil
}
joinedExpr := joinExprsWithAnd(unhandled...)
return NewPlanOpFilter(a, joinedExpr, node.ChildOp), false, nil
}
func pushdownFiltersToFilterableRelations(ctx context.Context, a *ExecutionPlanner, tableNode types.PlanOperator, scope *OptimizerScope, filters *filterSet, tableAliases RelationAliasesMap) (types.PlanOperator, bool, error) {
var table types.IdentifiableByName
// only do this if it is a pql table scan
switch rel := tableNode.(type) {
case *PlanOpRelAlias:
switch rel.ChildOp.(type) {
case *PlanOpPQLTableScan:
table = rel
case *PlanOpPQLDistinctScan:
table = rel
default:
return tableNode, true, nil
}
case *PlanOpPQLTableScan:
table = rel
case *PlanOpPQLDistinctScan:
table = rel
default:
return tableNode, true, nil
}
// is the thing filterable?
ft, ok := table.(types.FilteredRelation)
if !ok || !ft.IsFilterable() {
return tableNode, true, nil
}
// do we have any filters for this table? if not, bail...
availableFilters := filters.availableFiltersForTable(table.Name())
if len(availableFilters) == 0 {
return tableNode, true, nil
}
tableFilters := make([]types.PlanExpression, 0)
timeQantumFilters := make([]types.PlanExpression, 0)
// can the filters be pushed down?
for _, tf := range availableFilters {
// try and generate a pql call graph, if we can't we can't push the filter down
_, err := a.generatePQLCallFromExpr(ctx, tf)
if err == nil {
// is this a time quantum call?
call, ok := tf.(*callPlanExpression)
if ok {
switch strings.ToUpper(call.name) {
case "RANGEQ":
timeQantumFilters = append(timeQantumFilters, tf)
default:
// it's a filter
tableFilters = append(tableFilters, tf)
}
} else {
// it's a filter
tableFilters = append(tableFilters, tf)
}
}
}
// did we end up with any filters?
if len(tableFilters)+len(timeQantumFilters) == 0 {
return tableNode, true, nil
}
var err error
var newOp types.PlanOperator
//deal with the filters
if len(tableFilters) > 0 {
filters.markFiltersHandled(tableFilters...)
// fix the field refs
tableFilters, _, err = fixFieldRefIndexesOnExpressions(ctx, scope, a, tableNode.Schema(), tableFilters...)
if err != nil {
return nil, true, err
}
newOp, err = ft.UpdateFilters(joinExprsWithAnd(tableFilters...))
if err != nil {
return nil, true, err
}
}
// deal with any time quantum related filters
if len(timeQantumFilters) > 0 {
filters.markFiltersHandled(timeQantumFilters...)
timeQantumFilters, _, err = fixFieldRefIndexesOnExpressions(ctx, scope, a, tableNode.Schema(), timeQantumFilters...)
if err != nil {
return nil, true, err
}
newOp, err = ft.UpdateTimeQuantumFilters(timeQantumFilters...)
if err != nil {
return nil, true, err
}
}
return newOp, false, nil
}
func pushdownFiltersToAboveRelation(ctx context.Context, a *ExecutionPlanner, tableNode types.PlanOperator, scope *OptimizerScope, filters *filterSet) (types.PlanOperator, bool, error) {
var table types.IdentifiableByName
// only do this if it is a pql table scan
switch rel := tableNode.(type) {
case *PlanOpRelAlias:
switch rel.ChildOp.(type) {
case *PlanOpPQLTableScan:
table = rel
case *PlanOpPQLDistinctScan:
table = rel
default:
return tableNode, true, nil
}
case *PlanOpPQLTableScan:
table = rel
case *PlanOpPQLDistinctScan:
table = rel
default:
return tableNode, true, nil
}
// reposition any remaining filters for a table to directly above the table itself
var pushedDownFilterExpression types.PlanExpression
if tableFilters := filters.availableFiltersForTable(table.Name()); len(tableFilters) > 0 {
filters.markFiltersHandled(tableFilters...)
// fix the field refs
handled, _, err := fixFieldRefIndexesOnExpressions(ctx, scope, a, tableNode.Schema(), tableFilters...)
if err != nil {
return nil, true, err
}
pushedDownFilterExpression = joinExprsWithAnd(handled...)
}
switch tableNode.(type) {
case *PlanOpRelAlias, *PlanOpPQLTableScan, *PlanOpPQLDistinctScan:
node := tableNode
if pushedDownFilterExpression != nil {
return NewPlanOpFilter(a, pushedDownFilterExpression, node), false, nil
}
return node, true, nil
default:
return nil, true, sql3.NewErrInternalf("unexpected op type '%T'", tableNode)
}
}
func pushdownFilters(ctx context.Context, a *ExecutionPlanner, n types.PlanOperator, scope *OptimizerScope) (types.PlanOperator, bool, error) {
tableAliases, err := getRelationAliases(n, scope)
if err != nil {
return nil, true, err
}
// push filter terms down into anything that supports being filtered directly
pushdownFiltersForFilterableRelations := func(n *PlanOpFilter, filters *filterSet) (types.PlanOperator, bool, error) {
return TransformPlanOpWithParent(n, filterPushdownChildSelector, func(c ParentContext) (types.PlanOperator, bool, error) {
switch node := c.Operator.(type) {
// for the filter in question remove any terms that have been pushed down
case *PlanOpFilter:
n, samePred, err := removePushedDownConditions(ctx, a, node, filters)
if err != nil {
return nil, true, err
}
return n, samePred, nil
// PlanOpPQLTableScan supports being filtered, PlanOpRelAlias is included here as a "transparent" op
case *PlanOpRelAlias, *PlanOpPQLTableScan, *PlanOpPQLDistinctScan:
n, samePred, err := pushdownFiltersToFilterableRelations(ctx, a, node, scope, filters, tableAliases)
if err != nil {
return nil, true, err
}
return n, samePred, nil
default:
return node, true, nil
}
})
}
pushdownFiltersCloseToRelations := func(n types.PlanOperator, filters *filterSet) (types.PlanOperator, bool, error) {
return TransformPlanOpWithParent(n, filterPushdownAboveTablesChildSelector, func(c ParentContext) (types.PlanOperator, bool, error) {
switch node := c.Operator.(type) {
case *PlanOpFilter:
n, same, err := removePushedDownConditions(ctx, a, node, filters)
if err != nil {
return nil, true, err
}
if same {
return n, true, nil
}
return n, false, nil
case *PlanOpRelAlias, *PlanOpPQLTableScan, *PlanOpPQLDistinctScan:
_, same, err := pushdownFiltersToAboveRelation(ctx, a, node, scope, filters)
if err != nil {
return nil, true, err
}
if same {
return node, true, nil
}
return node, false, nil
default:
return node, true, nil
}
})
}
// look for filter ops and push the conditions within them down to things that can be filtered
return TransformPlanOp(n, func(node types.PlanOperator) (types.PlanOperator, bool, error) {
switch thisNode := node.(type) {
case *PlanOpFilter:
// get the filter conditions from this filter in a map by table
filtersByTable := getFiltersByRelation(n)
// make a struct to hold the expression for this filter, the broken up filter conditions
// and a map of alias name to relations
filters := newFilterSet(thisNode.Predicate, filtersByTable, tableAliases)
// first push down filters to any op that supports a filter
newNode, sameA, err := pushdownFiltersForFilterableRelations(thisNode, filters)
if err != nil {
return nil, true, err
}
// second push down filters as close as possible to the relations they apply to
var sameB bool
newNode, sameB, err = pushdownFiltersCloseToRelations(newNode, filters)
if err != nil {
return nil, true, err
}
return newNode, sameA && sameB, nil
default:
return node, true, nil
}
})
}
// getFiltersByRelation returns a map of relations name to filter expressions for the op provided
func getFiltersByRelation(n types.PlanOperator) map[string][]types.PlanExpression {
filters := make(map[string][]types.PlanExpression)
InspectPlan(n, func(node types.PlanOperator) bool {
switch thisNode := node.(type) {
case *PlanOpFilter:
fs := exprToRelationFilters(thisNode.Predicate)
for k, exprs := range fs {
filters[k] = append(filters[k], exprs...)
}
}
return true
})
return filters
}
// exprToRelationFilters returns a map of relation name to filter expressions for the expression
// passed after the expression is split on AND.
func exprToRelationFilters(expr types.PlanExpression) map[string][]types.PlanExpression {
filters := make(map[string][]types.PlanExpression)
for _, expr := range splitOnAnd(expr) {
var seenTables = make(map[string]bool)
var lastTable string
hasSubquery := false
InspectExpression(expr, func(e types.PlanExpression) bool {
switch thisExpr := e.(type) {
case *qualifiedRefPlanExpression:
if !seenTables[thisExpr.tableName] {
seenTables[thisExpr.tableName] = true
lastTable = thisExpr.tableName
}
case *subqueryPlanExpression:
hasSubquery = true
return false
}
return true
})
if len(seenTables) == 1 && !hasSubquery {
filters[lastTable] = append(filters[lastTable], expr)
}
}
return filters
}
// splitOnAnd breaks binops that are AND expressions into a list recursively
func splitOnAnd(expr types.PlanExpression) []types.PlanExpression {
binOp, ok := expr.(*binOpPlanExpression)
if !ok || binOp.op != parser.AND {
return []types.PlanExpression{
expr,
}
}
return append(
splitOnAnd(binOp.lhs),
splitOnAnd(binOp.rhs)...,
)
}
func tryToReplaceGroupByWithPQLAggregate(ctx context.Context, a *ExecutionPlanner, n types.PlanOperator, scope *OptimizerScope) (types.PlanOperator, bool, error) {
//bail if there are any joins
joins, err := hasJoins(ctx, a, n, scope)
if err != nil {
return nil, false, err
}
if joins {
return n, true, nil
}
//go find the table scan operators
tables := getTableScanOperators(ctx, a, n, scope)
//only do this if we have one TableScanOperator
if len(tables) == 1 {
return TransformPlanOp(n, func(node types.PlanOperator) (types.PlanOperator, bool, error) {
switch thisNode := node.(type) {
case *PlanOpGroupBy:
//only do this if there are no group by expressions
if len(thisNode.GroupByExprs) != 0 {
return thisNode, true, nil
}
//table scan
table := tables[0]
// if the child of the group by is not the table scan, we bail here too
if thisNode.ChildOp != table {
return thisNode, true, nil
}
pkType, err := table.PrimaryKeyType()
if err != nil {
return thisNode, true, err
}
// we can push down to pql if:
// 1. the expression we are aggregating on is a qualifiedRef
// 2. it is a bsi type
// we always push down to pql if it's a ref and it's the _id column
for i, agg := range thisNode.Aggregates {
switch aggregable := agg.(type) {
case *countStarPlanExpression:
// it's a count(*) on a pql table scan, so add the arg
newChildren := []types.PlanExpression{newQualifiedRefPlanExpression(table.tableName, string(dax.PrimaryKeyFieldName), 0, pkType)}
newAgg, err := aggregable.WithChildren(newChildren...)
if err != nil {
return n, true, err
}
thisNode.Aggregates[i] = newAgg
// these two can't be done in PQL
case *corrPlanExpression, *varPlanExpression:
return thisNode, true, nil
case types.Aggregable:
switch ref := aggregable.FirstChildExpr().(type) {
case *qualifiedRefPlanExpression:
if !strings.EqualFold(ref.columnName, string(dax.PrimaryKeyFieldName)) && !typeIsBSI(ref.Type()) {
return thisNode, true, nil
}
default:
return thisNode, true, nil
}
}
}
// if we got to here we are good to go
ops := make([]*PlanOpPQLAggregate, 0)
for _, agg := range thisNode.Aggregates {
aggregable, ok := agg.(types.Aggregable)
if !ok {
return n, false, sql3.NewErrInternalf("unexpected aggregate function arg type '%T'", agg)
}
ops = append(ops, NewPlanOpPQLAggregate(a, table.tableName, aggregable, table.filter))
}
newOp := NewPlanOpPQLMultiAggregate(a, ops)
lenOps := len(ops)
if lenOps > 1 {
newOp.AddWarning(fmt.Sprintf("Multiple (%d) aggregates referenced in select list will result in multiple aggregate queries being executed.", lenOps))
}
return newOp, false, nil
default:
return thisNode, true, nil
}
})
}
return n, true, nil
}
func tryToReplaceDistinctWithPQLDistinct(ctx context.Context, a *ExecutionPlanner, n types.PlanOperator, scope *OptimizerScope) (types.PlanOperator, bool, error) {
// bail if no distinct
hasDistinct := false
InspectPlan(n, func(node types.PlanOperator) bool {
switch node.(type) {
case *PlanOpDistinct:
hasDistinct = true
return false
}
return true
})
if !hasDistinct {
return n, true, nil
}
// bail if has a group by
hasGroupBy := false
InspectPlan(n, func(node types.PlanOperator) bool {
switch node.(type) {
case *PlanOpGroupBy:
hasGroupBy = true
return false
}
return true
})
if hasGroupBy {
return n, true, nil
}
//bail if there are any joins
joins, err := hasJoins(ctx, a, n, scope)
if err != nil {
return nil, false, err
}
if joins {
return n, true, nil
}
//go find the table scan operators
tables := getTableScanOperators(ctx, a, n, scope)
//only do this if we have one TableScanOperator
if len(tables) != 1 {
return n, true, nil
}
replacedWithDistinct := false
// replace the scan with the distinct scan
return TransformPlanOp(n, func(node types.PlanOperator) (types.PlanOperator, bool, error) {
switch thisNode := node.(type) {
case *PlanOpDistinct:
if replacedWithDistinct {
return thisNode.ChildOp, false, nil
}
return thisNode, true, nil
case *PlanOpPQLTableScan:
// bail if there is more than one output column
if len(thisNode.columns) != 1 {
return thisNode, true, nil
}
// make sure it's not the _id column
if strings.EqualFold(thisNode.columns[0], string(dax.PrimaryKeyFieldName)) {
return thisNode, true, nil
}
// if it is a set type, check to see if we have query hint that tells us to flatten on this column
s := thisNode.Schema()
switch s[0].Type.(type) {
case *parser.DataTypeIDSet, *parser.DataTypeStringSet:
found := false
for _, h := range thisNode.hints {
if strings.EqualFold("flatten", h.name) {
for _, hp := range h.params {
if strings.EqualFold(s[0].ColumnName, hp) {
found = true
break
}
}
if found {
break
}
}
}
if !found {
return thisNode, true, nil
}
}
newOp, err := NewPlanOpPQLDistinctScan(a, thisNode.tableName, thisNode.columns[0])
if err != nil {
return nil, false, err
}
replacedWithDistinct = true
return newOp, false, nil
default:
return thisNode, true, nil
}
})
}
func tryToReplaceConstRowDeleteWithFilteredDelete(ctx context.Context, a *ExecutionPlanner, n types.PlanOperator, scope *OptimizerScope) (types.PlanOperator, bool, error) {
return TransformPlanOp(n, func(node types.PlanOperator) (types.PlanOperator, bool, error) {
switch node := node.(type) {
case *PlanOpPQLConstRowDelete:
switch child := node.ChildOp.(type) {
case *PlanOpPQLTableScan:
if child.filter != nil {
_, err := a.generatePQLCallFromExpr(ctx, child.filter)
if err == nil {
return NewPlanOpPQLFilteredDelete(a, node.tableName, child.filter), false, nil
}
}
return node, true, nil
default:
return node, true, nil
}
default:
return node, true, nil
}
})
}
func tryToReplaceGroupByWithPQLGroupBy(ctx context.Context, a *ExecutionPlanner, n types.PlanOperator, scope *OptimizerScope) (types.PlanOperator, bool, error) {
//bail if there are any joins
joins, err := hasJoins(ctx, a, n, scope)
if err != nil {
return nil, false, err
}
if joins {
return n, true, nil
}
//go find the table scan operators
tables := getTableScanOperators(ctx, a, n, scope)
//only do this if we have one TableScanOperator
if len(tables) != 1 {
return n, true, nil
}
return TransformPlanOp(n, func(node types.PlanOperator) (types.PlanOperator, bool, error) {
switch thisNode := node.(type) {
case *PlanOpGroupBy:
//only do this if we have group by expressions
if len(thisNode.GroupByExprs) == 0 {
return thisNode, true, nil
}
// get the table
table := tables[0]
// if we are grouping on set columns, see if we have any flatten query hints
for _, gbc := range thisNode.GroupByExprs {
gbcRef, ok := gbc.(*qualifiedRefPlanExpression)
if !ok {
// don't need to stop the world here
break
}
switch gbcRef.Type().(type) {
case *parser.DataTypeIDSet, *parser.DataTypeStringSet:
// we are grouping on a set, so see if we have any flatten hints,
// if we do, we can continue the transform
found := false
for _, h := range table.hints {
if strings.EqualFold("flatten", h.name) {
for _, hp := range h.params {
if strings.EqualFold(gbcRef.columnName, hp) {
found = true
break
}
}
if found {
break
}
}
}
if !found {
return thisNode, true, nil
}
}
}
// get the type of the _id column for this table
pkType, err := table.PrimaryKeyType()
if err != nil {
return thisNode, true, err
}
// for each of the aggregates, go make a PlanOpPQLGroupBy operator
ops := make([]*PlanOpPQLGroupBy, 0)
for _, agg := range thisNode.Aggregates {
aggregable, ok := agg.(types.Aggregable)
if !ok {
return thisNode, false, sql3.NewErrInternalf("unexpected aggregate function arg type '%T'", agg)
}
// if it's a count(*) on a pql table scan, so add the arg
star, ok := agg.(*countStarPlanExpression)
if ok {
newChildren := []types.PlanExpression{newQualifiedRefPlanExpression(table.tableName, string(dax.PrimaryKeyFieldName), 0, pkType)}
newAgg, err := star.WithChildren(newChildren...)
if err != nil {
return thisNode, true, err
}
aggregable = newAgg.(types.Aggregable)
}
ops = append(ops, NewPlanOpPQLGroupBy(a, table.tableName, thisNode.GroupByExprs, table.filter, aggregable))
}
// use a multi group by if more than 1 aggregate
if len(thisNode.Aggregates) > 1 {
newOp := NewPlanOpPQLMultiGroupBy(a, ops, thisNode.GroupByExprs)
return newOp, false, nil
}
// else only one aggregate
return ops[0], false, nil
default:
return thisNode, true, nil
}
})
}
func pushdownPQLTop(ctx context.Context, a *ExecutionPlanner, n types.PlanOperator, scope *OptimizerScope) (types.PlanOperator, bool, error) {
// bail if there are any joins
joins, err := hasJoins(ctx, a, n, scope)
if err != nil {
return nil, false, err
}
if joins {
return n, true, nil
}
// get a list of tables that have projections as parents
var tables []*PlanOpPQLTableScan
_, _, err = TransformPlanOpWithParent(n, func(c ParentContext) bool { return true }, func(c ParentContext) (types.PlanOperator, bool, error) {
parent := c.Parent
node := c.Operator
switch thisNode := node.(type) {
case *PlanOpPQLTableScan:
switch parent.(type) {
case *PlanOpProjection:
tables = append(tables, thisNode)
}
}
return node, true, nil
})
if err != nil {
return nil, false, err
}
// only do this if we have one TableScanOperator
if len(tables) == 1 {
return TransformPlanOp(n, func(node types.PlanOperator) (types.PlanOperator, bool, error) {
switch n := node.(type) {
case *PlanOpTop:
table := tables[0]
//set the topExpr for the PlanOpTableScan
table.topExpr = n.expr
//return the child of the top node to eliminate it
return n.ChildOp, false, nil
default:
return n, true, nil
}
})
}
return n, true, nil
}
// fixes references for a projection op depending on child
func fixProjectionReferences(ctx context.Context, a *ExecutionPlanner, n types.PlanOperator, scope *OptimizerScope) (types.PlanOperator, bool, error) {
return TransformPlanOp(n, func(node types.PlanOperator) (types.PlanOperator, bool, error) {
switch thisNode := node.(type) {
case *PlanOpProjection:
switch childOp := thisNode.ChildOp.(type) {
case *PlanOpGroupBy, *PlanOpHaving, *PlanOpPQLGroupBy, *PlanOpPQLMultiAggregate, *PlanOpPQLMultiGroupBy:
// get the child op schema
childSchema := childOp.Schema()
// for each of the projections...
for idx, pj := range thisNode.Projections {
// apply a transform
expr, _, err := TransformExpr(pj, func(e types.PlanExpression) (types.PlanExpression, bool, error) {
switch thisAggregate := e.(type) {
case types.Aggregable:
// if we have a Aggregable we can use the ordinal position of the matching projection
// as the column index
for idx, sc := range childSchema {
if strings.EqualFold(thisAggregate.String(), sc.ColumnName) {
ae := newQualifiedRefPlanExpression("", "", idx, e.Type())
return ae, false, nil
}
}
// if we get to here not finding a match we likely have an error
return nil, true, sql3.NewErrColumnNotFound(0, 0, thisAggregate.String())
case *qualifiedRefPlanExpression:
for idx, sc := range childSchema {
if matchesSchema(thisAggregate, sc) {
if idx != thisAggregate.columnIndex {
// update the column index
return newQualifiedRefPlanExpression(thisAggregate.tableName, thisAggregate.columnName, idx, thisAggregate.dataType), false, nil
}
return thisAggregate, true, nil
}
}
// we didn't find a match in the schema so we just bail unchanged
return e, true, nil
default:
return e, true, nil
}
}, func(parentExpr, childExpr types.PlanExpression) bool {
return true
})
if err != nil {
return thisNode, true, err
}
thisNode.Projections[idx] = expr
}
return thisNode, false, nil
// everything else that can be a child of projection
case *PlanOpRelAlias, *PlanOpFilter, *PlanOpPQLTableScan, *PlanOpPQLDistinctScan, *PlanOpNestedLoops, *PlanOpOrderBy:
exprs, same, err := fixFieldRefIndexesOnExpressions(ctx, scope, a, childOp.Schema(), thisNode.Projections...)
if err != nil {
return thisNode, true, err
}
thisNode.Projections = exprs
return thisNode, same, err
default:
return thisNode, true, nil
}
default:
return thisNode, true, nil
}
})
}
func fixFieldRefs(ctx context.Context, a *ExecutionPlanner, n types.PlanOperator, scope *OptimizerScope) (types.PlanOperator, bool, error) {
return TransformPlanOp(n, func(node types.PlanOperator) (types.PlanOperator, bool, error) {
switch thisNode := node.(type) {
case *PlanOpOrderBy:
switch childOp := thisNode.ChildOp.(type) {
case *PlanOpProjection:
expressions := thisNode.Expressions()
for _, ex := range expressions {
ref, ok := ex.(*qualifiedRefPlanExpression)
if !ok {
return nil, true, sql3.NewErrInternalf("unexpected expression type '%T'", ex)
}
for i, proj := range childOp.Projections {
if strings.EqualFold(ref.String(), proj.String()) {
ref.columnIndex = i
break
}
}
}
newNode, err := thisNode.WithUpdatedExpressions(expressions...)
if err != nil {
return nil, true, err
}
return newNode, false, nil
default:
// fix references for the expressions referenced in the order by list
schema := childOp.Schema()
expressions := thisNode.Expressions()
fixed, same, err := fixFieldRefIndexesOnExpressions(ctx, scope, a, schema, expressions...)
if err != nil {
return nil, true, err
}
newNode, err := thisNode.WithUpdatedExpressions(fixed...)
if err != nil {
return nil, true, err
}
return newNode, same, nil
}
case *PlanOpFilter:
// fix references for the expressions referenced in the filter predicate expression
schema := thisNode.Schema()
expressions := thisNode.Expressions()
fixed, same, err := fixFieldRefIndexesOnExpressions(ctx, scope, a, schema, expressions...)
if err != nil {
return nil, true, err
}
newNode, err := thisNode.WithUpdatedExpressions(fixed...)
if err != nil {
return nil, true, err
}
return newNode, same, nil
case *PlanOpNestedLoops:
// fix references for the expressions referenced in the join condition expression
schema := thisNode.Schema()
expressions := thisNode.Expressions()
fixed, same, err := fixFieldRefIndexesOnExpressions(ctx, scope, a, schema, expressions...)
if err != nil {
return nil, true, err
}
newNode, err := thisNode.WithUpdatedExpressions(fixed...)
if err != nil {
return nil, true, err
}
return newNode, same, nil
case *PlanOpGroupBy:
// fix references for the expressions referenced in the aggregate functions or the group by clause
schema := thisNode.ChildOp.Schema()
aggregateExpressions := thisNode.Aggregates
fixedAggregateExpressions, aggregateSame, err := fixFieldRefIndexesOnExpressions(ctx, scope, a, schema, aggregateExpressions...)
if err != nil {
return nil, true, err
}
groupByExpressions := thisNode.GroupByExprs
fixedGroupByExpressions, groupBySame, err := fixFieldRefIndexesOnExpressions(ctx, scope, a, schema, groupByExpressions...)
if err != nil {
return nil, true, err
}
newNode := NewPlanOpGroupBy(fixedAggregateExpressions, fixedGroupByExpressions, thisNode.ChildOp)
newNode.warnings = append(newNode.warnings, thisNode.warnings...)
return newNode, aggregateSame && groupBySame, nil
case *PlanOpPQLMultiGroupBy:
schema := thisNode.operators[0].Schema()
for idx, op := range thisNode.operators {
if idx > 0 {
opSchema := op.Schema()
last := opSchema[len(opSchema)-1]
schema = append(schema, last)
}
}
expressions := thisNode.Expressions()
fixed, same, err := fixFieldRefIndexesOnExpressions(ctx, scope, a, schema, expressions...)
if err != nil {
return nil, true, err
}
newNode, err := thisNode.WithUpdatedExpressions(fixed...)
if err != nil {
return nil, true, err
}
return newNode, same, nil
default:
return node, true, nil
}
})
}
func fixHavingReferences(ctx context.Context, a *ExecutionPlanner, n types.PlanOperator, scope *OptimizerScope) (types.PlanOperator, bool, error) {
return TransformPlanOp(n, func(node types.PlanOperator) (types.PlanOperator, bool, error) {
switch thisNode := node.(type) {
case *PlanOpHaving:
// fix references for the expressions referenced in the having predicate expression
schema := thisNode.Schema()
expressions := thisNode.Expressions()
fixed, same, err := fixFieldRefIndexesOnExpressionsForHaving(ctx, scope, a, schema, expressions...)
if err != nil {
return nil, true, err
}
newNode, err := thisNode.WithUpdatedExpressions(fixed...)
if err != nil {
return nil, true, err
}
return newNode, same, nil
default:
return node, true, nil
}
})
}
// inspects a plan op tree and returns false (or error) if there are read join operators
func hasJoins(ctx context.Context, a *ExecutionPlanner, n types.PlanOperator, scope *OptimizerScope) (bool, error) {
// assume false
result := false
InspectPlan(n, func(node types.PlanOperator) bool {
switch node.(type) {
case *PlanOpNestedLoops:
result = true
return false
}
return true
})
return result, nil
}
// inspects a plan op tree and returns a list (or error) of all the PlanOpTableScan operators
func getTableScanOperators(ctx context.Context, a *ExecutionPlanner, n types.PlanOperator, scope *OptimizerScope) []*PlanOpPQLTableScan {
var tables []*PlanOpPQLTableScan
//go find the table scan operators
InspectPlan(n, func(node types.PlanOperator) bool {
switch nd := node.(type) {
case *PlanOpPQLTableScan:
tables = append(tables, nd)
return false
}
return true
})
return tables
}
// inspects a plan op tree and returns a list (or error) of all the PlanOpProjection operators
func getPlanOpProjectionOperators(ctx context.Context, a *ExecutionPlanner, n types.PlanOperator, scope *OptimizerScope) []*PlanOpProjection {
var projs []*PlanOpProjection
InspectPlan(n, func(node types.PlanOperator) bool {
switch nd := node.(type) {
case *PlanOpProjection:
projs = append(projs, nd)
return false
}
return true
})
return projs
}
// inspects a plan op tree and returns a list (or error) of all the PlanOpNestedLoops operators
func getNestedLoopOperators(ctx context.Context, a *ExecutionPlanner, n types.PlanOperator, scope *OptimizerScope) []*PlanOpNestedLoops {
var joins []*PlanOpNestedLoops
InspectPlan(n, func(node types.PlanOperator) bool {
switch nd := node.(type) {
case *PlanOpNestedLoops:
joins = append(joins, nd)
return false
}
return true
})
return joins
}
// for a list of expressions and an operator schema, fix the references for any qualifiedRef expressions
func fixFieldRefIndexesOnExpressions(ctx context.Context, scope *OptimizerScope, a *ExecutionPlanner, schema types.Schema, expressions ...types.PlanExpression) ([]types.PlanExpression, bool, error) {
var result []types.PlanExpression
var res types.PlanExpression
var same bool
var err error
for i := range expressions {
e := expressions[i]
res, same, err = fixFieldRefIndexes(ctx, scope, a, schema, e)
if err != nil {
return nil, true, err
}
if !same {
if result == nil {
result = make([]types.PlanExpression, len(expressions))
copy(result, expressions)
}
result[i] = res
}
}
if len(result) > 0 {
return result, false, nil
}
return expressions, true, nil
}
func matchesSchema(qualifiedRef *qualifiedRefPlanExpression, col *types.PlannerColumn) bool {
if strings.EqualFold(qualifiedRef.Name(), col.ColumnName) {
if len(qualifiedRef.tableName) == 0 { // do we have a qualifier?
return true
}
if qualifiedRef.tableName == col.RelationName || qualifiedRef.tableName == col.AliasName {
return true
}
}
return false
}
func fixFieldRefIndexes(ctx context.Context, scope *OptimizerScope, a *ExecutionPlanner, schema types.Schema, exp types.PlanExpression) (types.PlanExpression, bool, error) {
return TransformExpr(exp, func(e types.PlanExpression) (types.PlanExpression, bool, error) {
switch typedExpr := e.(type) {
case *qualifiedRefPlanExpression:
for i, col := range schema {
newIndex := i
if matchesSchema(typedExpr, col) {
if newIndex != typedExpr.columnIndex {
// update the column index
return newQualifiedRefPlanExpression(typedExpr.tableName, typedExpr.columnName, newIndex, typedExpr.dataType), false, nil
}
return e, true, nil
}
}
return nil, true, sql3.NewErrColumnNotFound(0, 0, typedExpr.Name())
}
return e, true, nil
}, func(parentExpr, childExpr types.PlanExpression) bool {
return true
})
}
// for a list of expressions and an operator schema, fix the references for any qualifiedRef expressions
func fixFieldRefIndexesOnExpressionsForHaving(ctx context.Context, scope *OptimizerScope, a *ExecutionPlanner, schema types.Schema, expressions ...types.PlanExpression) ([]types.PlanExpression, bool, error) {
var result []types.PlanExpression
var res types.PlanExpression
var same bool
var err error
for i := range expressions {
e := expressions[i]
res, same, err = fixFieldRefIndexesForHaving(ctx, scope, a, schema, e)
if err != nil {
return nil, true, err
}
if !same {
if result == nil {
result = make([]types.PlanExpression, len(expressions))
copy(result, expressions)
}
result[i] = res
}
}
if len(result) > 0 {
return result, false, nil
}
return expressions, true, nil
}
func fixFieldRefIndexesForHaving(ctx context.Context, scope *OptimizerScope, a *ExecutionPlanner, schema types.Schema, exp types.PlanExpression) (types.PlanExpression, bool, error) {
return TransformExpr(exp, func(e types.PlanExpression) (types.PlanExpression, bool, error) {
switch typedExpr := e.(type) {
case *sumPlanExpression, *countPlanExpression, *countDistinctPlanExpression,
*avgPlanExpression, *minPlanExpression, *maxPlanExpression, *countStarPlanExpression,
*percentilePlanExpression:
for i, col := range schema {
if strings.EqualFold(typedExpr.String(), col.ColumnName) {
e := newQualifiedRefPlanExpression("", "", i, typedExpr.Type())
return e, false, nil
}
}
return nil, true, sql3.NewErrColumnNotFound(0, 0, typedExpr.String())
case *qualifiedRefPlanExpression:
for i, col := range schema {
newIndex := i
if matchesSchema(typedExpr, col) {
if newIndex != typedExpr.columnIndex {
// update the column index
return newQualifiedRefPlanExpression(typedExpr.tableName, typedExpr.columnName, newIndex, typedExpr.dataType), false, nil
}
return e, true, nil
}
}
return nil, true, sql3.NewErrColumnNotFound(0, 0, typedExpr.Name())
}
return e, true, nil
}, func(parentExpr, childExpr types.PlanExpression) bool {
switch parentExpr.(type) {
case *sumPlanExpression, *countPlanExpression, *countDistinctPlanExpression,
*avgPlanExpression, *minPlanExpression, *maxPlanExpression,
*percentilePlanExpression:
return false
default:
return true
}
})
}