This repository has been archived by the owner on Jan 28, 2021. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 110
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
sql: implement EXPLODE and generators
This pull request implements the EXPLODE expressions and generators, which are used together to provide generation of rows based on a column or expression that can yield multiple values. For this, there have been some quite core changes: - SQL method of sql.Type now returns the sqltypes.Value and an error. This avoids panics in this method. Since generators can yield errors it's better to just return an error and not just kill the server. - Array type can now internally handle either arrays or generators, making it transparent for the user. If it's used in an EXPLODE expression, the generator will be used. Otherwise, it will be converted automatically to an array and used without the user even knowing what happened behind the scenes. That way, we don't need yet another type to represent generators. Aside from that, there are some new additions: - New EXPLODE function, which is just a placeholder. EXPLODE type is the underlying type of its argument for resolution purposes. During analysis Explode nodes will be replaced by Generate functions and a Generate node. - New Generate function, which is the non-placeholder version of EXPLODE and the one that goes into the final execution tree. This one returns the same type as its argument and let's the Generate plan node be the one that returns the underlying type once the values are generated. - Generate node, which wraps a Project in which there is one and only one explode expression. - resolve_generators analysis rule, which will turn projects with an Explode expression into a Generate node with the project as a children, replacing the Explode expressions with Generate expressions. - validate_explode_usage validation rule, which will ensure explode is not used outside a Project node. Signed-off-by: Miguel Molina <miguel@erizocosmi.co>
- Loading branch information
1 parent
4dcaf78
commit 728f747
Showing
15 changed files
with
1,010 additions
and
66 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,97 @@ | ||
package analyzer | ||
|
||
import ( | ||
"gopkg.in/src-d/go-errors.v1" | ||
"github.com/src-d/go-mysql-server/sql" | ||
"github.com/src-d/go-mysql-server/sql/expression" | ||
"github.com/src-d/go-mysql-server/sql/expression/function" | ||
"github.com/src-d/go-mysql-server/sql/plan" | ||
) | ||
|
||
var ( | ||
errMultipleGenerators = errors.NewKind("there can't be more than 1 instance of EXPLODE in a SELECT") | ||
errExplodeNotArray = errors.NewKind("argument of type %q given to EXPLODE, expecting array") | ||
) | ||
|
||
func resolveGenerators(ctx *sql.Context, a *Analyzer, n sql.Node) (sql.Node, error) { | ||
return n.TransformUp(func(n sql.Node) (sql.Node, error) { | ||
p, ok := n.(*plan.Project) | ||
if !ok { | ||
return n, nil | ||
} | ||
|
||
projection := p.Projections | ||
|
||
g, err := findGenerator(projection) | ||
if err != nil { | ||
return nil, err | ||
} | ||
|
||
// There might be no generator in the project, in that case we don't | ||
// have to do anything. | ||
if g == nil { | ||
return n, nil | ||
} | ||
|
||
projection[g.idx] = g.expr | ||
|
||
var name string | ||
if n, ok := g.expr.(sql.Nameable); ok { | ||
name = n.Name() | ||
} else { | ||
name = g.expr.String() | ||
} | ||
|
||
return plan.NewGenerate( | ||
plan.NewProject(projection, p.Child), | ||
expression.NewGetField(g.idx, g.expr.Type(), name, g.expr.IsNullable()), | ||
), nil | ||
}) | ||
} | ||
|
||
type generator struct { | ||
idx int | ||
expr sql.Expression | ||
} | ||
|
||
// findGenerator will find in the given projection a generator column. If there | ||
// is no generator, it will return nil. | ||
// If there are is than one generator or the argument to explode is not an | ||
// array it will fail. | ||
// All occurrences of Explode will be replaced with Generate. | ||
func findGenerator(exprs []sql.Expression) (*generator, error) { | ||
var g = &generator{idx: -1} | ||
for i, e := range exprs { | ||
var found bool | ||
switch e := e.(type) { | ||
case *function.Explode: | ||
found = true | ||
g.expr = function.NewGenerate(e.Child) | ||
case *expression.Alias: | ||
if exp, ok := e.Child.(*function.Explode); ok { | ||
found = true | ||
g.expr = expression.NewAlias( | ||
function.NewGenerate(exp.Child), | ||
e.Name(), | ||
) | ||
} | ||
} | ||
|
||
if found { | ||
if g.idx >= 0 { | ||
return nil, errMultipleGenerators.New() | ||
} | ||
g.idx = i | ||
|
||
if !sql.IsArray(g.expr.Type()) { | ||
return nil, errExplodeNotArray.New(g.expr.Type()) | ||
} | ||
} | ||
} | ||
|
||
if g.expr == nil { | ||
return nil, nil | ||
} | ||
|
||
return g, nil | ||
} |
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,117 @@ | ||
package analyzer | ||
|
||
import ( | ||
"testing" | ||
|
||
"github.com/stretchr/testify/require" | ||
"gopkg.in/src-d/go-errors.v1" | ||
"github.com/src-d/go-mysql-server/sql" | ||
"github.com/src-d/go-mysql-server/sql/expression" | ||
"github.com/src-d/go-mysql-server/sql/expression/function" | ||
"github.com/src-d/go-mysql-server/sql/plan" | ||
) | ||
|
||
func TestResolveGenerators(t *testing.T) { | ||
testCases := []struct { | ||
name string | ||
node sql.Node | ||
expected sql.Node | ||
err *errors.Kind | ||
}{ | ||
{ | ||
name: "regular explode", | ||
node: plan.NewProject( | ||
[]sql.Expression{ | ||
expression.NewGetField(0, sql.Int64, "a", false), | ||
function.NewExplode(expression.NewGetField(1, sql.Array(sql.Int64), "b", false)), | ||
expression.NewGetField(2, sql.Int64, "c", false), | ||
}, | ||
plan.NewUnresolvedTable("foo", ""), | ||
), | ||
expected: plan.NewGenerate( | ||
plan.NewProject( | ||
[]sql.Expression{ | ||
expression.NewGetField(0, sql.Int64, "a", false), | ||
function.NewGenerate(expression.NewGetField(1, sql.Array(sql.Int64), "b", false)), | ||
expression.NewGetField(2, sql.Int64, "c", false), | ||
}, | ||
plan.NewUnresolvedTable("foo", ""), | ||
), | ||
expression.NewGetField(1, sql.Array(sql.Int64), "EXPLODE(b)", false), | ||
), | ||
err: nil, | ||
}, | ||
{ | ||
name: "explode with alias", | ||
node: plan.NewProject( | ||
[]sql.Expression{ | ||
expression.NewGetField(0, sql.Int64, "a", false), | ||
expression.NewAlias( | ||
function.NewExplode( | ||
expression.NewGetField(1, sql.Array(sql.Int64), "b", false), | ||
), | ||
"x", | ||
), | ||
expression.NewGetField(2, sql.Int64, "c", false), | ||
}, | ||
plan.NewUnresolvedTable("foo", ""), | ||
), | ||
expected: plan.NewGenerate( | ||
plan.NewProject( | ||
[]sql.Expression{ | ||
expression.NewGetField(0, sql.Int64, "a", false), | ||
expression.NewAlias( | ||
function.NewGenerate( | ||
expression.NewGetField(1, sql.Array(sql.Int64), "b", false), | ||
), | ||
"x", | ||
), | ||
expression.NewGetField(2, sql.Int64, "c", false), | ||
}, | ||
plan.NewUnresolvedTable("foo", ""), | ||
), | ||
expression.NewGetField(1, sql.Array(sql.Int64), "x", false), | ||
), | ||
err: nil, | ||
}, | ||
{ | ||
name: "non array type on explode", | ||
node: plan.NewProject( | ||
[]sql.Expression{ | ||
expression.NewGetField(0, sql.Int64, "a", false), | ||
function.NewExplode(expression.NewGetField(1, sql.Int64, "b", false)), | ||
}, | ||
plan.NewUnresolvedTable("foo", ""), | ||
), | ||
expected: nil, | ||
err: errExplodeNotArray, | ||
}, | ||
{ | ||
name: "more than one generator", | ||
node: plan.NewProject( | ||
[]sql.Expression{ | ||
expression.NewGetField(0, sql.Int64, "a", false), | ||
function.NewExplode(expression.NewGetField(1, sql.Array(sql.Int64), "b", false)), | ||
function.NewExplode(expression.NewGetField(2, sql.Array(sql.Int64), "c", false)), | ||
}, | ||
plan.NewUnresolvedTable("foo", ""), | ||
), | ||
expected: nil, | ||
err: errMultipleGenerators, | ||
}, | ||
} | ||
|
||
for _, tt := range testCases { | ||
t.Run(tt.name, func(t *testing.T) { | ||
require := require.New(t) | ||
result, err := resolveGenerators(sql.NewEmptyContext(), nil, tt.node) | ||
if tt.err != nil { | ||
require.Error(err) | ||
require.True(tt.err.Is(err)) | ||
} else { | ||
require.NoError(err) | ||
require.Equal(tt.expected, result) | ||
} | ||
}) | ||
} | ||
} |
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
Oops, something went wrong.