Skip to content
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
34 changes: 21 additions & 13 deletions enginetest/queries/queries.go
Original file line number Diff line number Diff line change
Expand Up @@ -10506,59 +10506,67 @@ var VersionedScripts = []ScriptTest{
var DateParseQueries = []QueryTest{
{
Query: "SELECT STR_TO_DATE('Jan 3, 2000', '%b %e, %Y')",
Expected: []sql.Row{{"2000-01-03"}},
Expected: []sql.Row{{time.Date(2000, time.January, 3, 0, 0, 0, 0, time.UTC)}},
},
{
Query: "SELECT STR_TO_DATE('01,5,2013', '%d,%m,%Y')",
Expected: []sql.Row{{"2013-05-01"}},
Expected: []sql.Row{{time.Date(2013, time.May, 1, 0, 0, 0, 0, time.UTC)}},
},
{
Query: "SELECT STR_TO_DATE('May 1, 2013','%M %d,%Y')",
Expected: []sql.Row{{"2013-05-01"}},
Expected: []sql.Row{{time.Date(2013, time.May, 1, 0, 0, 0, 0, time.UTC)}},
},
{
Query: "SELECT STR_TO_DATE('a09:30:17','a%h:%i:%s')",
Expected: []sql.Row{{"09:30:17"}},
Expected: []sql.Row{{time.Date(-1, time.November, 30, 9, 30, 17, 0, time.UTC)}},
},
{
Query: "SELECT STR_TO_DATE('A09:30:17','A%h:%i:%s')",
Expected: []sql.Row{{time.Date(-1, time.November, 30, 9, 30, 17, 0, time.UTC)}},
},
{
Query: "SELECT STR_TO_DATE('a09:30:17','%h:%i:%s')",
Expected: []sql.Row{{nil}},
},
{
Query: "SELECT STR_TO_DATE('A09:30:17','a%h:%i:%s')",
Expected: []sql.Row{{nil}},
},
{
Query: "SELECT STR_TO_DATE('09:30:17a','%h:%i:%s')",
Expected: []sql.Row{{"09:30:17"}},
Expected: []sql.Row{{time.Date(-1, time.November, 30, 9, 30, 17, 0, time.UTC)}},
},
{
Query: "SELECT STR_TO_DATE('09:30:17 pm','%h:%i:%s %p')",
Expected: []sql.Row{{"21:30:17"}},
Expected: []sql.Row{{time.Date(-1, time.November, 30, 9, 30, 17, 0, time.UTC)}},
},
{
Query: "SELECT STR_TO_DATE('9','%m')",
Expected: []sql.Row{{"0000-09-00"}},
Expected: []sql.Row{{time.Date(0, time.August, 31, 0, 0, 0, 0, time.UTC)}},
},
{
Query: "SELECT STR_TO_DATE('9','%s')",
Expected: []sql.Row{{"00:00:09"}},
Expected: []sql.Row{{time.Date(-1, time.November, 30, 0, 0, 9, 0, time.UTC)}},
},
{
Query: "SELECT STR_TO_DATE('01/02/99 314', '%m/%e/%y %f')",
Expected: []sql.Row{{"1999-01-02 00:00:00.314000"}},
Expected: []sql.Row{{time.Date(1999, time.January, 2, 0, 0, 0, 314000, time.UTC)}},
},
{
Query: "SELECT STR_TO_DATE('01/02/99 0', '%m/%e/%y %f')",
Expected: []sql.Row{{"1999-01-02 00:00:00.000000"}},
Expected: []sql.Row{{time.Date(1999, time.January, 2, 0, 0, 0, 0, time.UTC)}},
},
{
Query: "SELECT STR_TO_DATE('01/02/99 05:14:12 PM', '%m/%e/%y %r')",
Expected: []sql.Row{{"1999-01-02 17:14:12"}},
Expected: []sql.Row{{time.Date(1999, time.January, 2, 5, 14, 12, 0, time.UTC)}},
},
{
Query: "SELECT STR_TO_DATE('May 3, 10:23:00 2000', '%b %e, %H:%i:%s %Y')",
Expected: []sql.Row{{"2000-05-03 10:23:00"}},
Expected: []sql.Row{{time.Date(2000, time.May, 3, 10, 23, 0, 0, time.UTC)}},
},
{
Query: "SELECT STR_TO_DATE('May 3, 10:23:00 PM 2000', '%b %e, %h:%i:%s %p %Y')",
Expected: []sql.Row{{"2000-05-03 22:23:00"}},
Expected: []sql.Row{{time.Date(2000, time.May, 3, 10, 23, 0, 0, time.UTC)}},
},
{
Query: "SELECT STR_TO_DATE('May 3, 10:23:00 PM 2000', '%b %e, %H:%i:%s %p %Y')", // cannot use 24 hour time (%H) with AM/PM (%p)
Expand Down
2 changes: 1 addition & 1 deletion sql/expression/function/registry.go
Original file line number Diff line number Diff line change
Expand Up @@ -218,7 +218,7 @@ var BuiltIns = []sql.Function{
sql.Function1{Name: "sleep", Fn: NewSleep},
sql.Function1{Name: "soundex", Fn: NewSoundex},
sql.Function1{Name: "sqrt", Fn: NewSqrt},
sql.FunctionN{Name: "str_to_date", Fn: NewStrToDate},
sql.Function2{Name: "str_to_date", Fn: NewStrToDate},
sql.FunctionN{Name: "subdate", Fn: NewSubDate},
sql.Function2{Name: "point", Fn: spatial.NewPoint},
sql.FunctionN{Name: "linestring", Fn: spatial.NewLineString},
Expand Down
102 changes: 61 additions & 41 deletions sql/expression/function/str_to_date.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,23 +3,12 @@ package function
import (
"fmt"

"github.com/dolthub/go-mysql-server/sql/planbuilder/dateparse"

"github.com/dolthub/go-mysql-server/sql"
"github.com/dolthub/go-mysql-server/sql/expression"
"github.com/dolthub/go-mysql-server/sql/planbuilder/dateparse"
"github.com/dolthub/go-mysql-server/sql/types"
)

// NewStrToDate constructs a new function expression from the given child expressions.
func NewStrToDate(args ...sql.Expression) (sql.Expression, error) {
if len(args) != 2 {
return nil, sql.ErrInvalidArgumentNumber.New("STR_TO_DATE", 2, len(args))
}
return &StrToDate{
Date: args[0],
Format: args[1],
}, nil
}

// StrToDate defines the built-in function STR_TO_DATE(str, format)
type StrToDate struct {
Date sql.Expression
Expand All @@ -29,40 +18,91 @@ type StrToDate struct {
var _ sql.FunctionExpression = (*StrToDate)(nil)
var _ sql.CollationCoercible = (*StrToDate)(nil)

// NewStrToDate constructs a new function expression from the given child expressions.
func NewStrToDate(arg1, arg2 sql.Expression) sql.Expression {
return &StrToDate{
Date: arg1,
Format: arg2,
}
}

func (s *StrToDate) FunctionName() string {
return "str_to_date"
}

// Description implements sql.FunctionExpression
func (s StrToDate) Description() string {
func (s *StrToDate) Description() string {
return "parses the date/datetime/timestamp expression according to the format specifier."
}

// Resolved returns whether the node is resolved.
func (s StrToDate) Resolved() bool {
func (s *StrToDate) Resolved() bool {
dateResolved := s.Date == nil || s.Date.Resolved()
formatResolved := s.Format == nil || s.Format.Resolved()
return dateResolved && formatResolved
}

func (s StrToDate) String() string {
func (s *StrToDate) String() string {
return fmt.Sprintf("%s(%s,%s)", s.FunctionName(), s.Date, s.Format)
}

// Type returns the expression type.
func (s StrToDate) Type() sql.Type {
// TODO: precision
func (s *StrToDate) Type() sql.Type {
// TODO: depending on the format, the return type can be a date, datetime or timestamp
// just make best guess for now
formatLit, isLit := s.Format.(*expression.Literal)
if !isLit {
return types.Datetime
}
format, err := formatLit.Eval(nil, nil)
if err != nil {
return types.Datetime
}
formatStr, isStr := format.(string)
if !isStr {
return types.Datetime
}

hasDate, hasTime, err := dateparse.HasDateOrTime(formatStr)
if err != nil {
return types.Datetime
}
if hasDate && hasTime {
return types.Datetime
}
if hasDate {
return types.Date
}
if hasTime {
return types.Time
}
return types.Datetime
}

// CollationCoercibility implements the interface sql.CollationCoercible.
func (StrToDate) CollationCoercibility(ctx *sql.Context) (collation sql.CollationID, coercibility byte) {
func (*StrToDate) CollationCoercibility(ctx *sql.Context) (collation sql.CollationID, coercibility byte) {
return sql.Collation_binary, 5
}

// IsNullable returns whether the expression can be null.
func (s StrToDate) IsNullable() bool {
func (s *StrToDate) IsNullable() bool {
return true
}

// Children returns the children expressions of this expression.
func (s *StrToDate) Children() []sql.Expression {
return []sql.Expression{s.Date, s.Format}
}

func (s *StrToDate) WithChildren(children ...sql.Expression) (sql.Expression, error) {
if len(children) != 2 {
return nil, sql.ErrInvalidArgumentNumber.New("STR_TO_DATE", 2, len(children))
}
return NewStrToDate(children[0], children[1]), nil
}

// Eval evaluates the given row and returns a result.
func (s StrToDate) Eval(ctx *sql.Context, row sql.Row) (interface{}, error) {
func (s *StrToDate) Eval(ctx *sql.Context, row sql.Row) (interface{}, error) {
date, err := s.Date.Eval(ctx, row)
if err != nil {
return nil, err
Expand Down Expand Up @@ -93,23 +133,3 @@ func (s StrToDate) Eval(ctx *sql.Context, row sql.Row) (interface{}, error) {
// but depends on strict sql_mode with NO_ZERO_DATE or NO_ZERO_IN_DATE modes enabled.
return goTime, nil
}

// Children returns the children expressions of this expression.
func (s StrToDate) Children() []sql.Expression {
children := make([]sql.Expression, 0, 2)
if s.Date != nil {
children = append(children, s.Date)
}
if s.Format != nil {
children = append(children, s.Format)
}
return children
}

func (s StrToDate) WithChildren(children ...sql.Expression) (sql.Expression, error) {
return NewStrToDate(children...)
}

func (s StrToDate) FunctionName() string {
return "str_to_date"
}
20 changes: 7 additions & 13 deletions sql/expression/function/str_to_date_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,25 +20,22 @@ func TestStrToDate(t *testing.T) {
fmtStr string
expected interface{}
}{
{"standard", "Dec 26, 2000 2:13:15", "%b %e, %Y %T", "2000-12-26 02:13:15"},
{"ymd", "20240101", "%Y%m%d", "2024-01-01"},
{"ymd", "2024121", "%Y%m%d", "2024-12-01"},
{"standard", "Dec 26, 2000 2:13:15", "%b %e, %Y %T", time.Date(2000, time.December, 26, 2, 13, 15, 0, time.UTC)},
{"ymd", "20240101", "%Y%m%d", time.Date(2024, time.January, 1, 0, 0, 0, 0, time.UTC)},
{"ymd", "2024121", "%Y%m%d", time.Date(2024, time.December, 1, 0, 0, 0, 0, time.UTC)},
{"ymd", "20241301", "%Y%m%d", nil},
{"ymd", "20240001", "%Y%m%d", nil},
{"ymd-with-time", "2024010203:04:05", "%Y%m%d%T", "2024-01-02 03:04:05"},
{"ymd-with-time", "202408122:03:04", "%Y%m%d%T", "2024-08-12 02:03:04"},
{"ymd-with-time", "2024010203:04:05", "%Y%m%d%T", time.Date(2024, time.January, 2, 3, 4, 5, 0, time.UTC)},
{"ymd-with-time", "202408122:03:04", "%Y%m%d%T", time.Date(2024, time.August, 12, 2, 3, 4, 0, time.UTC)},
// TODO: It shoud be nil, but returns "2024-02-31"
// {"ymd", "20240231", "%Y%m%d", nil},
}

for _, tt := range testCases {
f, err := NewStrToDate(
f := NewStrToDate(
expression.NewGetField(0, types.Text, "", true),
expression.NewGetField(1, types.Text, "", true),
)
if err != nil {
t.Fatal(err)
}
t.Run(tt.name, func(t *testing.T) {
dtime := eval(t, f, sql.NewRow(tt.dateStr, tt.fmtStr))
require.Equal(t, tt.expected, dtime)
Expand All @@ -60,13 +57,10 @@ func TestStrToDateFailure(t *testing.T) {
}

for _, tt := range testCases {
f, err := NewStrToDate(
f := NewStrToDate(
expression.NewGetField(0, types.Text, "", true),
expression.NewGetField(1, types.Text, "", true),
)
if err != nil {
t.Fatal(err)
}
t.Run(tt.name, func(t *testing.T) {
dtime := eval(t, f, sql.NewRow(tt.dateStr, tt.fmtStr))
require.Equal(t, nil, dtime)
Expand Down
Loading
Loading