Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 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
61 changes: 56 additions & 5 deletions enginetest/queries/queries.go
Original file line number Diff line number Diff line change
Expand Up @@ -7375,24 +7375,75 @@ var VersionedScripts = []ScriptTest{
var DateParseQueries = []QueryTest{
{
Query: "SELECT STR_TO_DATE('Jan 3, 2000', '%b %e, %Y')",
Expected: []sql.Row{{time.Date(2000, time.January, 3, 0, 0, 0, 0, time.Local)}},
Expected: []sql.Row{{"2000-01-03"}},
},
{
Query: "SELECT STR_TO_DATE('May 3, 10:23:00 PM 2000', '%b %e, %H:%i:%s %p %Y')",
Expected: []sql.Row{{time.Date(2000, time.May, 3, 22, 23, 0, 0, time.Local)}},
Query: "SELECT STR_TO_DATE('01,5,2013', '%d,%m,%Y')",
Expected: []sql.Row{{"2013-05-01"}},
},
{
Query: "SELECT STR_TO_DATE('May 1, 2013','%M %d,%Y')",
Expected: []sql.Row{{"2013-05-01"}},
},
{
Query: "SELECT STR_TO_DATE('a09:30:17','a%h:%i:%s')",
Expected: []sql.Row{{"09:30:17"}},
},
{
Query: "SELECT STR_TO_DATE('a09:30:17','%h:%i:%s')",
Expected: []sql.Row{{nil}},
},
{
Query: "SELECT STR_TO_DATE('09:30:17a','%h:%i:%s')",
Expected: []sql.Row{{"09:30:17"}},
},
{
Query: "SELECT STR_TO_DATE('09:30:17 pm','%h:%i:%s %p')",
Expected: []sql.Row{{"21:30:17"}},
},
{
Query: "SELECT STR_TO_DATE('9','%m')",
Expected: []sql.Row{{"0000-09-00"}},
},
{
Query: "SELECT STR_TO_DATE('9','%s')",
Expected: []sql.Row{{"00:00:09"}},
},
{
Query: "SELECT STR_TO_DATE('01/02/99 314', '%m/%e/%y %f')",
Expected: []sql.Row{{time.Date(1999, time.January, 2, 0, 0, 0, 314000, time.Local)}},
Expected: []sql.Row{{"1999-01-02 00:00:00.314000"}},
},
{
Query: "SELECT STR_TO_DATE('01/02/99 0', '%m/%e/%y %f')",
Expected: []sql.Row{{"1999-01-02 00:00:00.000000"}},
},
{
Query: "SELECT STR_TO_DATE('01/02/99 05:14:12 PM', '%m/%e/%y %r')",
Expected: []sql.Row{{time.Date(1999, time.January, 2, 17, 14, 12, 0, time.Local)}},
Expected: []sql.Row{{"1999-01-02 17:14:12"}},
},
{
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"}},
},
{
// returns null for mysql
Query: "SELECT STR_TO_DATE('May 3, 10:23:00 PM 2000', '%b %e, %H:%i:%s %p %Y')",
//Expected: []sql.Row{{nil}},
Expected: []sql.Row{{"2000-05-03 22:23:00"}},
},
{
Query: "SELECT STR_TO_DATE('abc','abc')",
Expected: []sql.Row{{nil}},
},
{
Query: "SELECT STR_TO_DATE('invalid', 'notvalid')",
Expected: []sql.Row{{nil}},
},
// Parsers for u, U, v, V, w, W, x and X are not supported yet.
//{
// Query: "STR_TO_DATE('2013 32 Tuesday', '%X %V %W')", // Tuesday of 32th week
// Expected: []sql.Row{{"2013-08-13"}},
//},
}

var InfoSchemaQueries = []QueryTest{
Expand Down
32 changes: 19 additions & 13 deletions sql/expression/function/str_to_date.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,48 +12,48 @@ func NewStrToDate(args ...sql.Expression) (sql.Expression, error) {
if len(args) != 2 {
return nil, sql.ErrInvalidArgumentNumber.New("STR_TO_DATE", 2, len(args))
}
return &StringToDatetime{
return &StrToDate{
Date: args[0],
Format: args[1],
}, nil
}

// StringToDatetime defines the built-in function STR_TO_DATE(str, format)
type StringToDatetime struct {
// StrToDate defines the built-in function STR_TO_DATE(str, format)
type StrToDate struct {
Date sql.Expression
Format sql.Expression
}

var _ sql.FunctionExpression = (*StringToDatetime)(nil)
var _ sql.FunctionExpression = (*StrToDate)(nil)

// Description implements sql.FunctionExpression
func (s StringToDatetime) 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 StringToDatetime) 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 StringToDatetime) String() string {
func (s StrToDate) String() string {
return fmt.Sprintf("STR_TO_DATE(%s, %s)", s.Date, s.Format)
}

// Type returns the expression type.
func (s StringToDatetime) Type() sql.Type {
func (s StrToDate) Type() sql.Type {
return sql.Datetime
}

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

// Eval evaluates the given row and returns a result.
func (s StringToDatetime) 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 All @@ -62,6 +62,7 @@ func (s StringToDatetime) Eval(ctx *sql.Context, row sql.Row) (interface{}, erro
if err != nil {
return nil, err
}

dateStr, ok := date.(string)
if !ok {
// TODO: improve this error
Expand All @@ -72,15 +73,20 @@ func (s StringToDatetime) Eval(ctx *sql.Context, row sql.Row) (interface{}, erro
// TODO: improve this error
return nil, sql.ErrInvalidType.New(fmt.Sprintf("%T", formatStr))
}

goTime, err := dateparse.ParseDateWithFormat(dateStr, formatStr)
if err != nil {
ctx.Warn(1411, fmt.Sprintf("Incorrect value: '%s' for function %s", dateStr, s.FunctionName()))
return nil, nil
}

// zero dates '0000-00-00' and '2010-00-13' are allowed,
// 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 StringToDatetime) Children() []sql.Expression {
func (s StrToDate) Children() []sql.Expression {
children := make([]sql.Expression, 0, 2)
if s.Date != nil {
children = append(children, s.Date)
Expand All @@ -91,10 +97,10 @@ func (s StringToDatetime) Children() []sql.Expression {
return children
}

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

func (s StringToDatetime) FunctionName() string {
func (s StrToDate) FunctionName() string {
return "str_to_date"
}
4 changes: 2 additions & 2 deletions sql/expression/function/str_to_date_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ func TestStrToDate(t *testing.T) {
fmtStr string
expected string
}{
{"standard", "Dec 26, 2000 2:13:15", "%b %e, %Y %T", "2000-12-26 02:13:15 -0600 CST"},
{"standard", "Dec 26, 2000 2:13:15", "%b %e, %Y %T", "2000-12-26 02:13:15"},
}

for _, tt := range testCases {
Expand All @@ -32,7 +32,7 @@ func TestStrToDate(t *testing.T) {
}
t.Run(tt.name, func(t *testing.T) {
dtime := eval(t, f, sql.NewRow(tt.dateStr, tt.fmtStr))
require.Equal(t, tt.expected, dtime.(time.Time).String())
require.Equal(t, tt.expected, dtime)
})
req := require.New(t)
req.True(f.IsNullable())
Expand Down
58 changes: 43 additions & 15 deletions sql/parse/dateparse/date.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,14 @@ import (
"time"
)

type OutType int

const (
DateTime OutType = iota
DateOnly
TimeOnly
)

// ParseDateWithFormat parses the date string according to the given
// format string, as defined in the MySQL specification.
//
Expand All @@ -15,10 +23,10 @@ import (
// More info: https://dev.mysql.com/doc/refman/8.0/en/date-and-time-functions.html#function_date-format
//
// Even more info: https://dev.mysql.com/doc/refman/8.0/en/date-and-time-functions.html#function_str-to-date
func ParseDateWithFormat(date, format string) (time.Time, error) {
func ParseDateWithFormat(date, format string) (interface{}, error) {
parsers, err := parsersFromFormatString(format)
if err != nil {
return time.Time{}, err
return nil, err
}

// trim all leading and trailing whitespace
Expand All @@ -28,17 +36,30 @@ func ParseDateWithFormat(date, format string) (time.Time, error) {
date = strings.ToLower(date)

var result datetime
var types = map[ParseType]bool{Time: false, Date: false, None: false}
target := date
for _, parser := range parsers {
target = takeAllSpaces(target)
rest, err := parser(&result, target)
rest, isTime, err := parser(&result, target)
if err != nil {
return time.Time{}, err
return nil, err
}
types[isTime] = true
target = rest
}

return evaluate(result)
var outType OutType
if types[Time] && types[Date] {
outType = DateTime
} else if types[Time] {
outType = TimeOnly
} else if types[Date] {
outType = DateOnly
} else {
return nil, fmt.Errorf("no value to evaluate")
}

return evaluate(result, outType)
}

// Convert the user-defined format string into a slice of parser functions
Expand Down Expand Up @@ -75,32 +96,32 @@ func parsersFromFormatString(format string) ([]parser, error) {
// Wrap a literal char parser, returning the corresponding
// typed error on failures.
func wrapLiteralParser(literal byte) parser {
return func(result *datetime, chars string) (rest string, err error) {
rest, err = literalParser(literal)(result, chars)
return func(result *datetime, chars string) (rest string, parseType ParseType, err error) {
rest, parseType, err = literalParser(literal)(result, chars)
if err != nil {
return "", ParseLiteralErr{
return "", parseType, ParseLiteralErr{
Literal: literal,
Tokens: chars,
err: err,
}
}
return rest, nil
return rest, parseType, nil
}
}

// Wrap a format specifier parser, returning the corresponding
// typed error on failures.
func wrapSpecifierParser(p parser, specifier byte) parser {
return func(result *datetime, chars string) (rest string, err error) {
rest, err = p(result, chars)
return func(result *datetime, chars string) (rest string, parseType ParseType, err error) {
rest, parseType, err = p(result, chars)
if err != nil {
return "", ParseSpecifierErr{
return "", parseType, ParseSpecifierErr{
Specifier: specifier,
Tokens: chars,
err: err,
}
}
return rest, nil
return rest, parseType, nil
}
}

Expand All @@ -126,11 +147,18 @@ type datetime struct {
hours *uint
minutes *uint
seconds *uint
miliseconds *uint
milliseconds *uint
microseconds *uint
nanoseconds *uint
}

func (dt *datetime) isEmpty() bool {
if dt.day == nil && dt.month == nil && dt.year == nil && dt.dayOfYear == nil && dt.weekOfYear == nil && dt.weekday == nil && dt.am == nil && dt.hours == nil && dt.minutes == nil && dt.seconds == nil && dt.milliseconds == nil && dt.microseconds == nil && dt.nanoseconds == nil {
return true
}
return false
}

// ParseSpecifierErr defines a error when attempting to parse
// the date string input according to a specified format directive.
type ParseSpecifierErr struct {
Expand Down Expand Up @@ -165,7 +193,7 @@ func (p ParseLiteralErr) Error() string {
// Reference: https://dev.mysql.com/doc/refman/8.0/en/date-and-time-functions.html#function_date-format
var formatSpecifiers = map[byte]parser{
// %a Abbreviated weekday name (Sun..Sat)
'a': parseWeedayAbbreviation,
'a': parseWeekdayAbbreviation,
// %b Abbreviated month name (Jan..Dec)
'b': parseMonthAbbreviation,
// %c Month, numeric (0..12)
Expand Down
Loading