Skip to content
This repository was archived by the owner on Jan 28, 2021. It is now read-only.

sql: implement struct types #735

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 58 additions & 0 deletions engine_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2786,6 +2786,64 @@ func TestGenerators(t *testing.T) {
}
}

var structQueries = []struct {
query string
expected []sql.Row
}{
{
`SELECT s.i, t.s.t FROM t ORDER BY s.i`,
[]sql.Row{
{int64(1), "first"},
{int64(2), "second"},
{int64(3), "third"},
},
},
{
`SELECT s.i, s.t FROM t ORDER BY s.i`,
[]sql.Row{
{int64(1), "first"},
{int64(2), "second"},
{int64(3), "third"},
},
},
{
`SELECT s.i, COUNT(*) FROM t GROUP BY s.i`,
[]sql.Row{
{int64(1), int64(1)},
{int64(2), int64(1)},
{int64(3), int64(1)},
},
},
}

func TestStructs(t *testing.T) {
schema := sql.Schema{
{Name: "i", Type: sql.Int64},
{Name: "t", Type: sql.Text},
}
table := mem.NewPartitionedTable("t", sql.Schema{
{Name: "s", Type: sql.Struct(schema), Source: "t"},
}, testNumPartitions)

insertRows(
t, table,
sql.NewRow(map[string]interface{}{"i": int64(1), "t": "first"}),
sql.NewRow(map[string]interface{}{"i": int64(2), "t": "second"}),
sql.NewRow(map[string]interface{}{"i": int64(3), "t": "third"}),
)

db := mem.NewDatabase("db")
db.AddTable("t", table)

catalog := sql.NewCatalog()
catalog.AddDatabase(db)
e := sqle.New(catalog, analyzer.NewDefault(catalog), new(sqle.Config))

for _, q := range structQueries {
testQuery(t, e, q.query, q.expected)
}
}

func insertRows(t *testing.T, table sql.Inserter, rows ...sql.Row) {
t.Helper()

Expand Down
94 changes: 85 additions & 9 deletions sql/analyzer/resolve_columns.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import (
"sort"
"strings"

"github.com/src-d/go-mysql-server/internal/similartext"
"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/plan"
Expand Down Expand Up @@ -153,17 +152,24 @@ func qualifyExpression(
return col, nil
}

name, table := strings.ToLower(col.Name()), strings.ToLower(col.Table())
name, tableName := strings.ToLower(col.Name()), strings.ToLower(col.Table())
availableTables := dedupStrings(columns[name])
if table != "" {
table, ok := tables[table]
if tableName != "" {
table, ok := tables[tableName]
if !ok {
if len(tables) == 0 {
return nil, sql.ErrTableNotFound.New(col.Table())
// If the table does not exist but the column does, it may be a
// struct field access.
if columnExists(columns, tableName) {
return expression.NewUnresolvedField(
expression.NewUnresolvedColumn(col.Table()),
col.Name(),
), nil
}

similar := similartext.FindFromMap(tables, col.Table())
return nil, sql.ErrTableNotFound.New(col.Table() + similar)
// If it cannot be resolved, then pass along and let it fail
// somewhere else. Maybe we're missing some steps before this
// can be resolved.
return col, nil
}

// If the table exists but it's not available for this node it
Expand Down Expand Up @@ -213,6 +219,15 @@ func qualifyExpression(
}
}

func columnExists(columns map[string][]string, col string) bool {
for c := range columns {
if strings.ToLower(c) == strings.ToLower(col) {
return true
}
}
return false
}

func getNodeAvailableColumns(n sql.Node) map[string][]string {
var columns = make(map[string][]string)
getColumnsInNodes(n.Children(), columns)
Expand Down Expand Up @@ -374,7 +389,23 @@ func resolveColumnExpression(
return &deferredColumn{uc}, nil
default:
if table != "" {
return nil, ErrColumnTableNotFound.New(e.Table(), e.Name())
if isStructField(uc, columns) {
return expression.NewUnresolvedField(
expression.NewUnresolvedColumn(uc.Table()),
uc.Name(),
), nil
}

// If we manage to find any column with the given table, it's because
// the column does not exist.
for col := range columns {
if col.table == table {
return nil, ErrColumnTableNotFound.New(e.Table(), e.Name())
}
}

// In any other case, it's the table the one that does not exist.
return nil, sql.ErrTableNotFound.New(e.Table())
}

return nil, ErrColumnNotFound.New(e.Name())
Expand All @@ -390,6 +421,51 @@ func resolveColumnExpression(
), nil
}

func isStructField(c column, columns map[tableCol]indexedCol) bool {
for _, col := range columns {
if strings.ToLower(c.Table()) == strings.ToLower(col.Name) &&
sql.Field(col.Type, c.Name()) != nil {
return true
}
}
return false
}

var errFieldNotFound = errors.NewKind("field %s not found on struct %s")

func resolveStructFields(ctx *sql.Context, a *Analyzer, n sql.Node) (sql.Node, error) {
span, _ := ctx.Span("resolve_struct_fields")
defer span.Finish()

return plan.TransformUp(n, func(n sql.Node) (sql.Node, error) {
if n.Resolved() {
return n, nil
}

if _, ok := n.(sql.Expressioner); !ok {
return n, nil
}

return plan.TransformExpressions(n, func(e sql.Expression) (sql.Expression, error) {
f, ok := e.(*expression.UnresolvedField)
if !ok {
return e, nil
}

if !f.Struct.Resolved() {
return e, nil
}

field := sql.Field(f.Struct.Type(), f.Name)
if field == nil {
return nil, errFieldNotFound.New(f.Name, f.Struct)
}

return expression.NewGetStructField(f.Struct, f.Name), nil
})
})
}

// resolveGroupingColumns reorders the aggregation in a groupby so aliases
// defined in it can be resolved in the grouping of the groupby. To do so,
// all aliases are pushed down to a projection node under the group by.
Expand Down
20 changes: 15 additions & 5 deletions sql/analyzer/resolve_columns_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -186,14 +186,24 @@ func TestQualifyColumns(t *testing.T) {

node = plan.NewProject(
[]sql.Expression{
expression.NewUnresolvedQualifiedColumn("foo", "i"),
expression.NewUnresolvedQualifiedColumn("i", "some_field"),
},
plan.NewTableAlias("a", plan.NewResolvedTable(table)),
plan.NewResolvedTable(table),
)

_, err = f.Apply(sql.NewEmptyContext(), nil, node)
require.Error(err)
require.True(sql.ErrTableNotFound.Is(err))
expected = plan.NewProject(
[]sql.Expression{
expression.NewUnresolvedField(
expression.NewUnresolvedColumn("i"),
"some_field",
),
},
plan.NewResolvedTable(table),
)

result, err = f.Apply(sql.NewEmptyContext(), nil, node)
require.NoError(err)
require.Equal(expected, result)

node = plan.NewProject(
[]sql.Expression{
Expand Down
1 change: 1 addition & 0 deletions sql/analyzer/rules.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ var DefaultRules = []Rule{
{"resolve_grouping_columns", resolveGroupingColumns},
{"qualify_columns", qualifyColumns},
{"resolve_columns", resolveColumns},
{"resolve_struct_fields", resolveStructFields},
{"resolve_database", resolveDatabase},
{"resolve_star", resolveStar},
{"resolve_functions", resolveFunctions},
Expand Down
70 changes: 70 additions & 0 deletions sql/expression/get_field.go
Original file line number Diff line number Diff line change
Expand Up @@ -133,3 +133,73 @@ func (f *GetSessionField) WithChildren(children ...sql.Expression) (sql.Expressi
}
return f, nil
}

// GetStructField is an expression to get a field from a struct column.
type GetStructField struct {
Struct sql.Expression
Name string
}

// NewGetStructField creates a new GetStructField expression.
func NewGetStructField(s sql.Expression, fieldName string) *GetStructField {
return &GetStructField{s, fieldName}
}

// Children implements the Expression interface.
func (p *GetStructField) Children() []sql.Expression {
return []sql.Expression{p.Struct}
}

// Resolved implements the Expression interface.
func (p *GetStructField) Resolved() bool {
return p.Struct.Resolved()
}

func (p *GetStructField) column() *sql.Column {
return sql.Field(p.Struct.Type(), p.Name)
}

// IsNullable returns whether the field is nullable or not.
func (p *GetStructField) IsNullable() bool {
return p.Struct.IsNullable() || p.column().Nullable
}

// Type returns the type of the field.
func (p *GetStructField) Type() sql.Type {
return p.column().Type
}

// Eval implements the Expression interface.
func (p *GetStructField) Eval(ctx *sql.Context, row sql.Row) (interface{}, error) {
s, err := p.Struct.Eval(ctx, row)
if err != nil {
return nil, err
}

if s == nil {
return nil, nil
}

s, err = p.Struct.Type().Convert(s)
if err != nil {
return nil, err
}

if val, ok := s.(map[string]interface{})[p.Name]; ok {
return p.Type().Convert(val)
}

return nil, nil
}

// WithChildren implements the Expression interface.
func (p *GetStructField) WithChildren(children ...sql.Expression) (sql.Expression, error) {
if len(children) != 1 {
return nil, sql.ErrInvalidChildrenNumber.New(p, len(children), 1)
}
return &GetStructField{children[0], p.Name}, nil
}

func (p *GetStructField) String() string {
return fmt.Sprintf("%s.%s", p.Struct, p.Name)
}
48 changes: 48 additions & 0 deletions sql/expression/unresolved.go
Original file line number Diff line number Diff line change
Expand Up @@ -135,3 +135,51 @@ func (uf *UnresolvedFunction) WithChildren(children ...sql.Expression) (sql.Expr
}
return NewUnresolvedFunction(uf.name, uf.IsAggregate, children...), nil
}

// UnresolvedField is an unresolved expression to get a field from a struct column.
type UnresolvedField struct {
Struct sql.Expression
Name string
}

// NewUnresolvedField creates a new UnresolvedField expression.
func NewUnresolvedField(s sql.Expression, fieldName string) *UnresolvedField {
return &UnresolvedField{s, fieldName}
}

// Children implements the Expression interface.
func (p *UnresolvedField) Children() []sql.Expression {
return []sql.Expression{p.Struct}
}

// Resolved implements the Expression interface.
func (p *UnresolvedField) Resolved() bool {
return false
}

// IsNullable returns whether the field is nullable or not.
func (p *UnresolvedField) IsNullable() bool {
panic("unresolved field is a placeholder node, but IsNullable was called")
}

// Type returns the type of the field.
func (p *UnresolvedField) Type() sql.Type {
panic("unresolved field is a placeholder node, but Type was called")
}

// Eval implements the Expression interface.
func (p *UnresolvedField) Eval(ctx *sql.Context, row sql.Row) (interface{}, error) {
panic("unresolved field is a placeholder node, but Eval was called")
}

// WithChildren implements the Expression interface.
func (p *UnresolvedField) WithChildren(children ...sql.Expression) (sql.Expression, error) {
if len(children) != 1 {
return nil, sql.ErrInvalidChildrenNumber.New(p, len(children), 1)
}
return &UnresolvedField{children[0], p.Name}, nil
}

func (p *UnresolvedField) String() string {
return fmt.Sprintf("%s.%s", p.Struct, p.Name)
}
14 changes: 14 additions & 0 deletions sql/parse/parse.go
Original file line number Diff line number Diff line change
Expand Up @@ -779,6 +779,20 @@ func exprToExpression(e sqlparser.Expr) (sql.Expression, error) {
return expression.NewLiteral(nil, sql.Null), nil
case *sqlparser.ColName:
if !v.Qualifier.IsEmpty() {
// If we find something of the form A.B.C we're going to treat it
// as a struct field access.
// TODO: this should be handled better when GetFields support being
// qualified with the database.
if !v.Qualifier.Qualifier.IsEmpty() {
return expression.NewUnresolvedField(
expression.NewUnresolvedQualifiedColumn(
v.Qualifier.Qualifier.String(),
v.Qualifier.Name.String(),
),
v.Name.String(),
), nil
}

return expression.NewUnresolvedQualifiedColumn(
v.Qualifier.Name.String(),
v.Name.String(),
Expand Down
9 changes: 9 additions & 0 deletions sql/parse/parse_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1173,6 +1173,15 @@ var fixtures = map[string]sql.Node{
[]sql.Expression{},
plan.NewUnresolvedTable("foo", ""),
),
`SELECT a.b.c FROM a`: plan.NewProject(
[]sql.Expression{
expression.NewUnresolvedField(
expression.NewUnresolvedQualifiedColumn("a", "b"),
"c",
),
},
plan.NewUnresolvedTable("a", ""),
),
}

func TestParse(t *testing.T) {
Expand Down
Loading
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy