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 109
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #427 from erizocosmico/feature/like
sql: implement LIKE expression
- Loading branch information
Showing
5 changed files
with
238 additions
and
0 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
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() | ||
} |
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,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) | ||
}) | ||
} | ||
} |
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