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
63 changes: 58 additions & 5 deletions enginetest/queries/queries.go
Original file line number Diff line number Diff line change
Expand Up @@ -7375,24 +7375,77 @@ 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"}},
},
{
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"}},
},
{
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)
Expected: []sql.Row{{nil}},
},
{
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
75 changes: 61 additions & 14 deletions sql/parse/dateparse/date.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,11 @@ import (
"time"
)

var (
dateSpecifiers = []uint8{'a', 'b', 'c', 'D', 'd', 'e', 'j', 'M', 'm', 'U', 'u', 'V', 'v', 'W', 'w', 'X', 'x', 'Y', 'y'}
timeSpecifiers = []uint8{'f', 'H', 'h', 'I', 'i', 'k', 'l', 'p', 'r', 'S', 's', 'T'}
)

// ParseDateWithFormat parses the date string according to the given
// format string, as defined in the MySQL specification.
//
Expand All @@ -15,10 +20,31 @@ 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) {
parsers, err := parsersFromFormatString(format)
func ParseDateWithFormat(date, format string) (interface{}, error) {
parsers, specifiers, err := parsersFromFormatString(format)
if err != nil {
return time.Time{}, err
return nil, err
}

hasDate := false
for _, s := range dateSpecifiers {
if _, ok := specifiers[s]; ok {
hasDate = true
break
}
}

hasTime := false
_, hasAmPm := specifiers['p']
for _, s := range timeSpecifiers {
if _, ok := specifiers[s]; ok {
// validate that am/pm is not used with 24 hour time specifiers
if (s == 'H' || s == 'k' || s == 'T') && hasAmPm {
return nil, fmt.Errorf("cannot use 24 hour time (H) with AM/PM (p)")
}
hasTime = true
break
}
}

// trim all leading and trailing whitespace
Expand All @@ -27,39 +53,52 @@ func ParseDateWithFormat(date, format string) (time.Time, error) {
// convert to all lowercase
date = strings.ToLower(date)

var result datetime
var dt datetime
target := date
for _, parser := range parsers {
target = takeAllSpaces(target)
rest, err := parser(&result, target)
rest, err := parser(&dt, target)
if err != nil {
return time.Time{}, err
return nil, err
}
target = rest
}

return evaluate(result)
var result string
if hasDate && hasTime {
result = fmt.Sprintf("%s %s", evaluateDate(dt), evaluateTime(dt))
} else if hasTime {
result = fmt.Sprintf("%s", evaluateTime(dt))
} else if hasDate {
result = fmt.Sprintf("%s", evaluateDate(dt))
} else {
return nil, fmt.Errorf("no value to evaluate")
}

return result, nil
}

// Convert the user-defined format string into a slice of parser functions
// which will later process the date string.
//
// Example format string: "%H:%i:%s".
func parsersFromFormatString(format string) ([]parser, error) {
func parsersFromFormatString(format string) ([]parser, map[uint8]bool, error) {
parsers := make([]parser, 0, len(format))
var specifiersInFormat = make(map[uint8]bool)
for i := 0; i < len(format); i++ {
char := format[i]
if char == '%' {
if len(format) <= i+1 {
return nil, fmt.Errorf("\"%%\" found at end of format string")
return nil, nil, fmt.Errorf("\"%%\" found at end of format string")
}
specifier := format[i+1]
specifiersInFormat[specifier] = true
parser, ok := formatSpecifiers[specifier]
if !ok {
return nil, fmt.Errorf("unknown format specifier \"%c\"", specifier)
return nil, nil, fmt.Errorf("unknown format specifier \"%c\"", specifier)
}
if parser == nil {
return nil, fmt.Errorf("format specifier \"%c\" not yet supported", specifier)
return nil, nil, fmt.Errorf("format specifier \"%c\" not yet supported", specifier)
}
parsers = append(parsers, wrapSpecifierParser(parser, specifier))

Expand All @@ -69,7 +108,8 @@ func parsersFromFormatString(format string) ([]parser, error) {
parsers = append(parsers, wrapLiteralParser(char))
}
}
return parsers, nil

return parsers, specifiersInFormat, nil
}

// Wrap a literal char parser, returning the corresponding
Expand Down Expand Up @@ -126,11 +166,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 +212,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