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

sql/expression/function: implement char_length and length functions #724

Merged
merged 1 commit into from
May 24, 2019
Merged
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
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ We support and actively test against certain third-party clients to ensure compa
|`AVG(expr)`|Returns the average value of expr in all rows.|
|`CEIL(number)`|Return the smallest integer value that is greater than or equal to `number`.|
|`CEILING(number)`|Return the smallest integer value that is greater than or equal to `number`.|
|`CHAR_LENGTH(str)`|Return the length of the string in characters.|
|`COALESCE(...)`|The function returns the first non-null value in a list.|
|`CONCAT(...)`|Concatenate any group of fields into a single string.|
|`CONCAT_WS(sep, ...)`|Concatenate any group of fields into a single string. The first argument is the separator for the rest of the arguments. The separator is added between the strings to be concatenated. The separator can be a string, as can the rest of the arguments. If the separator is NULL, the result is NULL.|
Expand All @@ -83,6 +84,7 @@ We support and actively test against certain third-party clients to ensure compa
|`IS_BINARY(blob)`|Returns whether a BLOB is a binary file or not.|
|`JSON_EXTRACT(json_doc, path, ...)`|Extracts data from a json document using json paths.|
|`LEAST(...)`|Returns the smaller numeric or string value.|
|`LENGTH(str)`|Return the length of the string in bytes.|
|`LN(X)`|Return the natural logarithm of X.|
|`LOG(X), LOG(B, X)`|If called with one parameter, this function returns the natural logarithm of X. If called with two parameters, this function returns the logarithm of X to the base B. If X is less than or equal to 0, or if B is less than or equal to 1, then NULL is returned.|
|`LOG10(X)`|Returns the base-10 logarithm of X.|
Expand Down
4 changes: 4 additions & 0 deletions engine_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1194,6 +1194,10 @@ var queries = []struct {
{nil, int64(3), "first"},
},
},
{
`SELECT CHAR_LENGTH('áé'), LENGTH('àè')`,
[]sql.Row{{int32(2), int32(4)}},
},
}

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

import (
"fmt"
"unicode/utf8"

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

// Length returns the length of a string or binary content, either in bytes
// or characters.
type Length struct {
expression.UnaryExpression
CountType CountType
}

// CountType is the kind of length count.
type CountType bool

const (
// NumBytes counts the number of bytes in a string or binary content.
NumBytes = CountType(false)
// NumChars counts the number of characters in a string or binary content.
NumChars = CountType(true)
)

// NewLength returns a new LENGTH function.
func NewLength(e sql.Expression) sql.Expression {
return &Length{expression.UnaryExpression{Child: e}, NumBytes}
}

// NewCharLength returns a new CHAR_LENGTH function.
func NewCharLength(e sql.Expression) sql.Expression {
return &Length{expression.UnaryExpression{Child: e}, NumChars}
}

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

nl := *l
nl.Child = child
return &nl, nil
}

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

func (l *Length) String() string {
if l.CountType == NumBytes {
return fmt.Sprintf("LENGTH(%s)", l.Child)
}
return fmt.Sprintf("CHAR_LENGTH(%s)", l.Child)
}

// Eval implements the sql.Expression interface.
func (l *Length) Eval(ctx *sql.Context, row sql.Row) (interface{}, error) {
val, err := l.Child.Eval(ctx, row)
if err != nil {
return nil, err
}

if val == nil {
return nil, nil
}

var content string
switch l.Child.Type() {
case sql.Blob:
val, err = sql.Blob.Convert(val)
if err != nil {
return nil, err
}

content = string(val.([]byte))
default:
val, err = sql.Text.Convert(val)
if err != nil {
return nil, err
}

content = string(val.(string))
}

if l.CountType == NumBytes {
return int32(len(content)), nil
}

return int32(utf8.RuneCountInString(content)), nil
}
104 changes: 104 additions & 0 deletions sql/expression/function/length_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
package function

import (
"testing"

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

func TestLength(t *testing.T) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe also add a test with an empty string.

testCases := []struct {
name string
input interface{}
inputType sql.Type
fn func(sql.Expression) sql.Expression
expected interface{}
}{
{
"length string",
"fóo",
sql.Text,
NewLength,
int32(4),
},
{
"length binary",
[]byte("fóo"),
sql.Blob,
NewLength,
int32(4),
},
{
"length empty",
"",
sql.Blob,
NewLength,
int32(0),
},
{
"length empty binary",
[]byte{},
sql.Blob,
NewLength,
int32(0),
},
{
"length nil",
nil,
sql.Blob,
NewLength,
nil,
},
{
"char_length string",
"fóo",
sql.Text,
NewCharLength,
int32(3),
},
{
"char_length binary",
[]byte("fóo"),
sql.Blob,
NewCharLength,
int32(3),
},
{
"char_length empty",
"",
sql.Blob,
NewCharLength,
int32(0),
},
{
"char_length empty binary",
[]byte{},
sql.Blob,
NewCharLength,
int32(0),
},
{
"char_length nil",
nil,
sql.Blob,
NewCharLength,
nil,
},
}

for _, tt := range testCases {
t.Run(tt.name, func(t *testing.T) {
require := require.New(t)

result, err := tt.fn(expression.NewGetField(0, tt.inputType, "foo", false)).Eval(
sql.NewEmptyContext(),
sql.Row{tt.input},
)

require.NoError(err)
require.Equal(tt.expected, result)
})
}
}
3 changes: 3 additions & 0 deletions sql/expression/function/registry.go
Original file line number Diff line number Diff line change
Expand Up @@ -85,4 +85,7 @@ var Defaults = []sql.Function{
sql.FunctionN{Name: "date_sub", Fn: NewDateSub},
sql.FunctionN{Name: "greatest", Fn: NewGreatest},
sql.FunctionN{Name: "least", Fn: NewLeast},
sql.Function1{Name: "length", Fn: NewLength},
sql.Function1{Name: "char_length", Fn: NewCharLength},
sql.Function1{Name: "character_length", Fn: NewCharLength},
}