New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

*: rewrite builtin function str_to_date #4357

Merged
merged 8 commits into from Aug 30, 2017
View
@@ -131,7 +131,6 @@ var (
_ builtinFunc = &builtinYearWeekWithoutModeSig{}
_ builtinFunc = &builtinFromUnixTimeSig{}
_ builtinFunc = &builtinGetFormatSig{}
_ builtinFunc = &builtinStrToDateSig{}
_ builtinFunc = &builtinSysDateWithFspSig{}
_ builtinFunc = &builtinSysDateWithoutFspSig{}
_ builtinFunc = &builtinCurrentDateSig{}
@@ -161,6 +160,9 @@ var (
_ builtinFunc = &builtinTimestamp1ArgSig{}
_ builtinFunc = &builtinTimestamp2ArgsSig{}
_ builtinFunc = &builtinLastDaySig{}
_ builtinFunc = &builtinStrToDateDateSig{}
_ builtinFunc = &builtinStrToDateDatetimeSig{}
_ builtinFunc = &builtinStrToDateDurationSig{}
)
// handleInvalidTimeError reports error or warning depend on the context.
@@ -1257,37 +1259,129 @@ type strToDateFunctionClass struct {
baseFunctionClass
}
func (c *strToDateFunctionClass) getFunction(args []Expression, ctx context.Context) (builtinFunc, error) {
func (c *strToDateFunctionClass) getRetTp(arg Expression, ctx context.Context) (tp byte, fsp int) {
tp = mysql.TypeDatetime
if _, ok := arg.(*Constant); !ok {
return tp, types.MaxFsp
}
strArg := WrapWithCastAsString(arg, ctx)
format, isNull, err := strArg.EvalString(nil, ctx.GetSessionVars().StmtCtx)
if err != nil || isNull {
return
}
isDuration, isDate := types.GetFormatType(format)
if isDuration && !isDate {
tp = mysql.TypeDuration
} else if !isDuration && isDate {
tp = mysql.TypeDate
}

This comment has been minimized.

@zz-jason

zz-jason Aug 29, 2017

Member

what if !isDuration && !isDate and isDuration && isDate ?

@zz-jason

zz-jason Aug 29, 2017

Member

what if !isDuration && !isDate and isDuration && isDate ?

This comment has been minimized.

@XuHuaiyu

XuHuaiyu Aug 29, 2017

Contributor

both return mysql.TypeDatetime

@XuHuaiyu

XuHuaiyu Aug 29, 2017

Contributor

both return mysql.TypeDatetime

if strings.Index(format, "%f") >= 0 {
fsp = types.MaxFsp
}
return
}
// See https://dev.mysql.com/doc/refman/5.5/en/date-and-time-functions.html#function_str-to-date
func (c *strToDateFunctionClass) getFunction(args []Expression, ctx context.Context) (sig builtinFunc, err error) {
if err := c.verifyArgs(args); err != nil {
return nil, errors.Trace(err)
}
sig := &builtinStrToDateSig{newBaseBuiltinFunc(args, ctx)}
retTp, fsp := c.getRetTp(args[1], ctx)
switch retTp {
case mysql.TypeDate:
bf := newBaseBuiltinFuncWithTp(args, ctx, tpDatetime, tpString, tpString)
bf.tp.Tp, bf.tp.Flen, bf.tp.Decimal = mysql.TypeDate, mysql.MaxDateWidth, types.MinFsp
sig = &builtinStrToDateDateSig{baseTimeBuiltinFunc{bf}}
case mysql.TypeDatetime:
bf := newBaseBuiltinFuncWithTp(args, ctx, tpDatetime, tpString, tpString)
if fsp == types.MinFsp {
bf.tp.Flen, bf.tp.Decimal = mysql.MaxDatetimeWidthNoFsp, types.MinFsp
} else {
bf.tp.Flen, bf.tp.Decimal = mysql.MaxDatetimeWidthWithFsp, types.MaxFsp
}
sig = &builtinStrToDateDatetimeSig{baseTimeBuiltinFunc{bf}}
case mysql.TypeDuration:
bf := newBaseBuiltinFuncWithTp(args, ctx, tpDuration, tpString, tpString)
if fsp == types.MinFsp {
bf.tp.Flen, bf.tp.Decimal = mysql.MaxDurationWidthNoFsp, types.MinFsp
} else {
bf.tp.Flen, bf.tp.Decimal = mysql.MaxDurationWidthWithFsp, types.MaxFsp
}
sig = &builtinStrToDateDurationSig{baseDurationBuiltinFunc{bf}}
}
return sig.setSelf(sig), nil
}
type builtinStrToDateSig struct {
baseBuiltinFunc
type builtinStrToDateDateSig struct {
baseTimeBuiltinFunc
}
// eval evals a builtinStrToDateSig.
// See https://dev.mysql.com/doc/refman/5.5/en/date-and-time-functions.html#function_str-to-date
func (b *builtinStrToDateSig) eval(row []types.Datum) (d types.Datum, err error) {
args, err := b.evalArgs(row)
if err != nil {
return d, errors.Trace(err)
func (b *builtinStrToDateDateSig) evalTime(row []types.Datum) (types.Time, bool, error) {

This comment has been minimized.

@zz-jason

zz-jason Aug 29, 2017

Member

add the comment back

@zz-jason

zz-jason Aug 29, 2017

Member

add the comment back

sc := b.ctx.GetSessionVars().StmtCtx
date, isNull, err := b.args[0].EvalString(row, sc)
if isNull || err != nil {
return types.Time{}, isNull, errors.Trace(err)
}
format, isNull, err := b.args[1].EvalString(row, sc)
if isNull || err != nil {
return types.Time{}, isNull, errors.Trace(err)
}
date := args[0].GetString()
format := args[1].GetString()
var t types.Time
succ := t.StrToDate(date, format)
if !succ {
return types.Time{}, true, handleInvalidTimeError(b.ctx, types.ErrInvalidTimeFormat)
}
t.Type, t.Fsp = mysql.TypeDate, types.MinFsp
return t, false, nil
}
type builtinStrToDateDatetimeSig struct {
baseTimeBuiltinFunc
}
func (b *builtinStrToDateDatetimeSig) evalTime(row []types.Datum) (types.Time, bool, error) {
sc := b.ctx.GetSessionVars().StmtCtx
date, isNull, err := b.args[0].EvalString(row, sc)
if isNull || err != nil {
return types.Time{}, isNull, errors.Trace(err)
}
format, isNull, err := b.args[1].EvalString(row, sc)
if isNull || err != nil {
return types.Time{}, isNull, errors.Trace(err)
}
var t types.Time
succ := t.StrToDate(date, format)
if !succ {
d.SetNull()
return
return types.Time{}, true, handleInvalidTimeError(b.ctx, types.ErrInvalidTimeFormat)
}
t.Type, t.Fsp = mysql.TypeDatetime, b.tp.Decimal
return t, false, nil
}
d.SetMysqlTime(t)
return
type builtinStrToDateDurationSig struct {
baseDurationBuiltinFunc
}
// TODO: If the NO_ZERO_DATE or NO_ZERO_IN_DATE SQL mode is enabled, zero dates or part of dates are disallowed.
// In that case, STR_TO_DATE() returns NULL and generates a warning.
func (b *builtinStrToDateDurationSig) evalDuration(row []types.Datum) (types.Duration, bool, error) {
sc := b.ctx.GetSessionVars().StmtCtx
date, isNull, err := b.args[0].EvalString(row, sc)
if isNull || err != nil {
return types.Duration{}, isNull, errors.Trace(err)
}
format, isNull, err := b.args[1].EvalString(row, sc)
if isNull || err != nil {
return types.Duration{}, isNull, errors.Trace(err)
}
var t types.Time
succ := t.StrToDate(date, format)
if !succ {
return types.Duration{}, true, handleInvalidTimeError(b.ctx, types.ErrInvalidTimeFormat)
}
t.Fsp = b.tp.Decimal
dur, err := t.ConvertToDuration()
return dur, false, errors.Trace(err)
}
type sysDateFunctionClass struct {
@@ -1109,6 +1109,7 @@ func (s *testEvaluatorSuite) TestStrToDate(c *C) {
format := types.NewStringDatum(test.Format)
f, err := fc.getFunction(datumsToConstants([]types.Datum{date, format}), s.ctx)
c.Assert(err, IsNil)
c.Assert(f.isDeterministic(), IsTrue)
result, err := f.eval(nil)
c.Assert(err, IsNil)
if !test.Success {
@@ -1296,6 +1296,16 @@ func (s *testIntegrationSuite) TestTimeBuiltin(c *C) {
result = tk.MustQuery(`select dayname("2017-12-01"), dayname("0000-00-00"), dayname("0000-01-00"), dayname("0000-01-00 00:00:00")`)
result.Check(testkit.Rows("Friday <nil> <nil> <nil>"))
tk.MustQuery("show warnings").Check(testutil.RowsWithSep("|", "Warning|1105|invalid time format", "Warning|1105|invalid time format", "Warning|1105|invalid time format"))
// for str_to_date
result = tk.MustQuery("select str_to_date('01-01-2017', '%d-%m-%Y'), str_to_date('59:20:12 01-01-2017', '%s:%i:%H %d-%m-%Y'), str_to_date('59:20:12', '%s:%i:%H')")
result.Check(testkit.Rows("2017-01-01 2017-01-01 12:20:59 12:20:59"))
result = tk.MustQuery("select str_to_date('aaa01-01-2017', 'aaa%d-%m-%Y'), str_to_date('59:20:12 aaa01-01-2017', '%s:%i:%H aaa%d-%m-%Y'), str_to_date('59:20:12aaa', '%s:%i:%Haaa')")
result.Check(testkit.Rows("2017-01-01 2017-01-01 12:20:59 12:20:59"))
result = tk.MustQuery("select str_to_date('01-01-2017', '%d'), str_to_date('59', '%d-%Y')")
// TODO: MySQL returns "<nil> <nil>".
result.Check(testkit.Rows("0000-00-01 <nil>"))
tk.MustQuery("show warnings").Check(testutil.RowsWithSep("|", "Warning|1105|invalid time format"))
}
func (s *testIntegrationSuite) TestOpBuiltin(c *C) {
View
@@ -1564,6 +1564,12 @@ func (s *testPlanSuite) createTestCase4TimeFuncs() []typeInferTestCase {
{"quarter(c_set )", mysql.TypeLonglong, charset.CharsetBin, mysql.BinaryFlag, 1, 0},
{"quarter(c_enum )", mysql.TypeLonglong, charset.CharsetBin, mysql.BinaryFlag, 1, 0},
{"str_to_date(c_varchar, '%Y:%m:%d')", mysql.TypeDate, charset.CharsetBin, mysql.BinaryFlag, mysql.MaxDateWidth, types.MinFsp},
{"str_to_date(c_varchar, '%Y:%m:%d %H:%i:%s')", mysql.TypeDatetime, charset.CharsetBin, mysql.BinaryFlag, mysql.MaxDatetimeWidthNoFsp, types.MinFsp},
{"str_to_date(c_varchar, '%Y:%m:%d %H:%i:%s.%f')", mysql.TypeDatetime, charset.CharsetBin, mysql.BinaryFlag, mysql.MaxDatetimeWidthWithFsp, types.MaxFsp},
{"str_to_date(c_varchar, '%H:%i:%s')", mysql.TypeDuration, charset.CharsetBin, mysql.BinaryFlag, mysql.MaxDurationWidthNoFsp, types.MinFsp},
{"str_to_date(c_varchar, '%H:%i:%s.%f')", mysql.TypeDuration, charset.CharsetBin, mysql.BinaryFlag, mysql.MaxDurationWidthWithFsp, types.MaxFsp},
{"period_add(c_int_d , c_int_d)", mysql.TypeLonglong, charset.CharsetBin, mysql.BinaryFlag, 6, 0},
{"period_add(c_bigint_d , c_int_d)", mysql.TypeLonglong, charset.CharsetBin, mysql.BinaryFlag, 6, 0},
{"period_add(c_float_d , c_int_d)", mysql.TypeLonglong, charset.CharsetBin, mysql.BinaryFlag, 6, 0},
View
@@ -2043,6 +2043,49 @@ var dateFormatParserTable = map[string]dateFormatParser{
// "%y": yearTwoDigits, // Year, numeric (two digits)
}
// GetFormatType checks the type(Duration, Date or Datetime) of a format string.
func GetFormatType(format string) (isDuration, isDate bool) {
durationTokens := map[string]struct{}{

This comment has been minimized.

@zz-jason

zz-jason Aug 29, 2017

Member

can we put these two maps outside this function ?

@zz-jason

zz-jason Aug 29, 2017

Member

can we put these two maps outside this function ?

This comment has been minimized.

@XuHuaiyu

XuHuaiyu Aug 29, 2017

Contributor

We only use these maps in this function,
I think we can move them outside if necessary later.

@XuHuaiyu

XuHuaiyu Aug 29, 2017

Contributor

We only use these maps in this function,
I think we can move them outside if necessary later.

"%h": {},
"%H": {},
"%i": {},
"%I": {},
"%s": {},
"%S": {},
"%k": {},
"%l": {},
}
dateTokens := map[string]struct{}{
"%y": {},
"%Y": {},
"%m": {},
"%M": {},
"%c": {},
"%b": {},
"%D": {},
"%d": {},
"%e": {},
}
format = skipWhiteSpace(format)
for token, formatRemain, succ := getFormatToken(format); len(token) != 0; format = formatRemain {
if !succ {
isDuration, isDate = false, false
break
}
if _, ok := durationTokens[token]; ok {
isDuration = true
} else if _, ok := dateTokens[token]; ok {
isDate = true
}
if isDuration && isDate {
break
}
token, formatRemain, succ = getFormatToken(format)
}
return
}
func matchDateWithToken(t *mysqlTime, date string, token string, ctx map[string]int) (remain string, succ bool) {
if parse, ok := dateFormatParserTable[token]; ok {
return parse(t, date, ctx)
ProTip! Use n and p to navigate between commits in a pull request.