forked from src-d/go-mysql-server
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Closes src-d#56 This PR adds support for the HAVING SQL clause, that allows filtering rows and has support for aggregation functions. For this, the following changes have been made: - New `Having` node, which is essentially a Filter, but it is a different node for the purpose of differentiating between the two of them during the analysis phase. - Having is now parsed. - A new rule for resolving the Having node has been added. Because of the way aggregations are executed (only inside a GroupBy node) it is not possible to execute them in any other node that's not a GroupBy. For this reason, this rule pushes down any aggregation on a Having node to its child, which is the GroupBy node. If the same aggregation is already done on the GroupBy, nothing will be added and the aggregation on the Having node will be replaced with a reference to the result of the aggregation in the GroupBy. Because pushing new aggregations to the GroupBy changes the resulting schema, a Project node is added wrapping the Having node projecting only the columns the GroupBy was initially projecting. Signed-off-by: Miguel Molina <miguel@erizocosmi.co>
- Loading branch information
1 parent
c4a4af4
commit c0e7c10
Showing
9 changed files
with
539 additions
and
15 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,151 @@ | ||
package analyzer | ||
|
||
import ( | ||
"reflect" | ||
|
||
"gopkg.in/src-d/go-errors.v1" | ||
"gopkg.in/src-d/go-mysql-server.v0/sql" | ||
"gopkg.in/src-d/go-mysql-server.v0/sql/expression" | ||
"gopkg.in/src-d/go-mysql-server.v0/sql/expression/function/aggregation" | ||
"gopkg.in/src-d/go-mysql-server.v0/sql/plan" | ||
) | ||
|
||
func resolveHaving(ctx *sql.Context, a *Analyzer, node sql.Node) (sql.Node, error) { | ||
return node.TransformUp(func(node sql.Node) (sql.Node, error) { | ||
having, ok := node.(*plan.Having) | ||
if !ok { | ||
return node, nil | ||
} | ||
|
||
if !having.Resolved() { | ||
return node, nil | ||
} | ||
|
||
// If there are no aggregations there is no need to check anything else | ||
// and we can just leave the node as it is. | ||
if !hasAggregations(having.Cond) { | ||
return node, nil | ||
} | ||
|
||
groupBy, ok := having.Child.(*plan.GroupBy) | ||
if !ok { | ||
return nil, errHavingNeedsGroupBy.New() | ||
} | ||
|
||
var aggregate = make([]sql.Expression, len(groupBy.Aggregate)) | ||
copy(aggregate, groupBy.Aggregate) | ||
|
||
// We need to find all the aggregations in the having that are already present in | ||
// the group by and replace them with a GetField. If the aggregation is not | ||
// present, we need to move it to the GroupBy and reference it with a GetField. | ||
cond, err := having.Cond.TransformUp(func(e sql.Expression) (sql.Expression, error) { | ||
agg, ok := e.(sql.Aggregation) | ||
if !ok { | ||
return e, nil | ||
} | ||
|
||
for i, expr := range aggregate { | ||
if aggregationEquals(agg, expr) { | ||
var name string | ||
if n, ok := expr.(sql.Nameable); ok { | ||
name = n.Name() | ||
} else { | ||
name = expr.String() | ||
} | ||
|
||
return expression.NewGetField( | ||
i, | ||
expr.Type(), | ||
name, | ||
expr.IsNullable(), | ||
), nil | ||
} | ||
} | ||
|
||
aggregate = append(aggregate, agg) | ||
return expression.NewGetField( | ||
len(aggregate)-1, | ||
agg.Type(), | ||
agg.String(), | ||
agg.IsNullable(), | ||
), nil | ||
}) | ||
if err != nil { | ||
return nil, err | ||
} | ||
|
||
var result sql.Node = plan.NewHaving( | ||
cond, | ||
plan.NewGroupBy(aggregate, groupBy.Grouping, groupBy.Child), | ||
) | ||
|
||
// If any aggregation was sent to the GroupBy aggregate, we will need | ||
// to wrap the new Having in a project that will get rid of all those | ||
// extra columns we added. | ||
if len(aggregate) != len(groupBy.Aggregate) { | ||
var projection = make([]sql.Expression, len(groupBy.Aggregate)) | ||
for i, e := range groupBy.Aggregate { | ||
var table, name string | ||
if t, ok := e.(sql.Tableable); ok { | ||
table = t.Table() | ||
} | ||
|
||
if n, ok := e.(sql.Nameable); ok { | ||
name = n.Name() | ||
} else { | ||
name = e.String() | ||
} | ||
|
||
projection[i] = expression.NewGetFieldWithTable( | ||
i, | ||
e.Type(), | ||
table, | ||
name, | ||
e.IsNullable(), | ||
) | ||
} | ||
result = plan.NewProject(projection, result) | ||
} | ||
|
||
return result, nil | ||
}) | ||
} | ||
|
||
func aggregationEquals(a, b sql.Expression) bool { | ||
// First unwrap aliases | ||
if alias, ok := b.(*expression.Alias); ok { | ||
b = alias.Child | ||
} else if alias, ok := a.(*expression.Alias); ok { | ||
a = alias.Child | ||
} | ||
|
||
switch a := a.(type) { | ||
case *aggregation.Count: | ||
// it doesn't matter what's inside a Count, the result will be | ||
// the same. | ||
_, ok := b.(*aggregation.Count) | ||
return ok | ||
case *aggregation.Sum, | ||
*aggregation.Avg, | ||
*aggregation.Min, | ||
*aggregation.Max: | ||
return reflect.DeepEqual(a, b) | ||
default: | ||
return false | ||
} | ||
} | ||
|
||
var errHavingNeedsGroupBy = errors.NewKind("found HAVING clause with no GROUP BY") | ||
|
||
func hasAggregations(expr sql.Expression) bool { | ||
var has bool | ||
expression.Inspect(expr, func(e sql.Expression) bool { | ||
_, ok := e.(sql.Aggregation) | ||
if ok { | ||
has = true | ||
return false | ||
} | ||
return true | ||
}) | ||
return has | ||
} |
Oops, something went wrong.