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

Commit

Permalink
Merge pull request #427 from erizocosmico/feature/like
Browse files Browse the repository at this point in the history
sql: implement LIKE expression
  • Loading branch information
ajnavarro authored Oct 9, 2018
2 parents 60313fe + 2535459 commit e09a1e3
Show file tree
Hide file tree
Showing 5 changed files with 238 additions and 0 deletions.
13 changes: 13 additions & 0 deletions engine_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -329,6 +329,19 @@ var queries = []struct {
`SHOW DATABASES`,
[]sql.Row{{"mydb"}},
},
{
`SELECT s FROM mytable WHERE s LIKE '%d row'`,
[]sql.Row{
{"second row"},
{"third row"},
},
},
{
`SELECT s FROM mytable WHERE s NOT LIKE '%d row'`,
[]sql.Row{
{"first row"},
},
},
}

func TestQueries(t *testing.T) {
Expand Down
136 changes: 136 additions & 0 deletions sql/expression/like.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
package expression

import (
"bytes"
"fmt"
"regexp"
"strings"

"gopkg.in/src-d/go-mysql-server.v0/internal/regex"
"gopkg.in/src-d/go-mysql-server.v0/sql"
)

// Like performs pattern matching against two strings.
type Like struct {
BinaryExpression
canCacheRegex bool
regex regex.Matcher
}

// NewLike creates a new LIKE expression.
func NewLike(left, right sql.Expression) sql.Expression {
var canCacheRegex = true
Inspect(right, func(e sql.Expression) bool {
if _, ok := e.(*GetField); ok {
canCacheRegex = false
}
return true
})

return &Like{BinaryExpression{left, right}, canCacheRegex, nil}
}

// Type implements the sql.Expression interface.
func (l *Like) Type() sql.Type { return sql.Boolean }

// Eval implements the sql.Expression interface.
func (l *Like) Eval(ctx *sql.Context, row sql.Row) (interface{}, error) {
span, ctx := ctx.Span("expression.Like")
defer span.Finish()

var re regex.Matcher
if l.regex == nil {
v, err := l.Right.Eval(ctx, row)
if err != nil {
return nil, err
}

v, err = sql.Text.Convert(v)
if err != nil {
return nil, err
}

re, err = regex.New(regex.Default(), patternToRegex(v.(string)))
if err != nil {
return nil, err
}

if l.canCacheRegex {
l.regex = re
}
} else {
re = l.regex
}

value, err := l.Left.Eval(ctx, row)
if err != nil {
return nil, err
}

value, err = sql.Text.Convert(value)
if err != nil {
return nil, err
}

return re.Match(value.(string)), nil
}

func (l *Like) String() string {
return fmt.Sprintf("%s LIKE %s", l.Left, l.Right)
}

// TransformUp implements the sql.Expression interface.
func (l *Like) TransformUp(f sql.TransformExprFunc) (sql.Expression, error) {
left, err := l.Left.TransformUp(f)
if err != nil {
return nil, err
}

right, err := l.Right.TransformUp(f)
if err != nil {
return nil, err
}

return f(NewLike(left, right))
}

func patternToRegex(pattern string) string {
var buf bytes.Buffer
buf.WriteRune('^')
var escaped bool
for _, r := range strings.Replace(regexp.QuoteMeta(pattern), `\\`, `\`, -1) {
switch r {
case '_':
if escaped {
buf.WriteRune(r)
} else {
buf.WriteRune('.')
}
case '%':
if !escaped {
buf.WriteString(".*")
} else {
buf.WriteRune(r)
}
case '\\':
if escaped {
buf.WriteString(`\\`)
} else {
escaped = true
continue
}
default:
if escaped {
buf.WriteString(`\`)
}
buf.WriteRune(r)
}

if escaped {
escaped = false
}
}

buf.WriteRune('$')
return buf.String()
}
65 changes: 65 additions & 0 deletions sql/expression/like_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
package expression

import (
"fmt"
"testing"

"github.com/stretchr/testify/require"
"gopkg.in/src-d/go-mysql-server.v0/sql"
)

func TestPatternToRegex(t *testing.T) {
testCases := []struct {
in, out string
}{
{`__`, `^..$`},
{`_%_`, `^..*.$`},
{`%_`, `^.*.$`},
{`_%`, `^..*$`},
{`a_b`, `^a.b$`},
{`a%b`, `^a.*b$`},
{`a.%b`, `^a\..*b$`},
{`a\%b`, `^a%b$`},
{`a\_b`, `^a_b$`},
{`a\\b`, `^a\\b$`},
{`a\\\_b`, `^a\\_b$`},
{`(ab)`, `^\(ab\)$`},
}

for _, tt := range testCases {
t.Run(tt.in, func(t *testing.T) {
require.Equal(t, tt.out, patternToRegex(tt.in))
})
}
}

func TestLike(t *testing.T) {
f := NewLike(
NewGetField(0, sql.Text, "", false),
NewGetField(1, sql.Text, "", false),
)

testCases := []struct {
pattern, value string
ok bool
}{
{"a__", "abc", true},
{"a__", "abcd", false},
{"a%b", "acb", true},
{"a%b", "acdkeflskjfdklb", true},
{"a%b", "ab", true},
{"a%b", "a", false},
{"a_b", "ab", false},
}

for _, tt := range testCases {
t.Run(fmt.Sprintf("%q LIKE %q", tt.value, tt.pattern), func(t *testing.T) {
value, err := f.Eval(sql.NewEmptyContext(), sql.NewRow(
tt.value,
tt.pattern,
))
require.NoError(t, err)
require.Equal(t, tt.ok, value)
})
}
}
4 changes: 4 additions & 0 deletions sql/parse/parse.go
Original file line number Diff line number Diff line change
Expand Up @@ -786,6 +786,10 @@ func comparisonExprToExpression(c *sqlparser.ComparisonExpr) (sql.Expression, er
return expression.NewIn(left, right), nil
case sqlparser.NotInStr:
return expression.NewNotIn(left, right), nil
case sqlparser.LikeStr:
return expression.NewLike(left, right), nil
case sqlparser.NotLikeStr:
return expression.NewNot(expression.NewLike(left, right)), nil
}
}

Expand Down
20 changes: 20 additions & 0 deletions sql/parse/parse_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -726,6 +726,26 @@ var fixtures = map[string]sql.Node{
plan.NewUnresolvedTable("foo"),
),
`SHOW DATABASES`: plan.NewShowDatabases(),
`SELECT * FROM foo WHERE i LIKE 'foo'`: plan.NewProject(
[]sql.Expression{expression.NewStar()},
plan.NewFilter(
expression.NewLike(
expression.NewUnresolvedColumn("i"),
expression.NewLiteral("foo", sql.Text),
),
plan.NewUnresolvedTable("foo"),
),
),
`SELECT * FROM foo WHERE i NOT LIKE 'foo'`: plan.NewProject(
[]sql.Expression{expression.NewStar()},
plan.NewFilter(
expression.NewNot(expression.NewLike(
expression.NewUnresolvedColumn("i"),
expression.NewLiteral("foo", sql.Text),
)),
plan.NewUnresolvedTable("foo"),
),
),
}

func TestParse(t *testing.T) {
Expand Down

0 comments on commit e09a1e3

Please sign in to comment.