diff --git a/enginetest/enginetests.go b/enginetest/enginetests.go index 8c04d1b642..b460f1fb2b 100644 --- a/enginetest/enginetests.go +++ b/enginetest/enginetests.go @@ -5302,14 +5302,28 @@ func TestPrepared(t *testing.T, harness Harness) { }, { Query: "SELECT DATE_ADD(TIMESTAMP(?), INTERVAL 1 DAY);", - Expected: []sql.Row{{time.Date(2022, time.October, 27, 13, 14, 15, 0, time.UTC)}}, + Expected: []sql.Row{{"2022-10-27 13:14:15"}}, + Bindings: map[string]*query.BindVariable{ + "v1": mustBuildBindVariable(time.Date(2022, time.October, 26, 13, 14, 15, 0, time.UTC)), + }, + }, + { + Query: "SELECT DATE_ADD(TIMESTAMP(?), INTERVAL 1 DAY);", + Expected: []sql.Row{{"2022-10-27 13:14:15"}}, Bindings: map[string]*query.BindVariable{ "v1": mustBuildBindVariable("2022-10-26 13:14:15"), }, }, { Query: "SELECT DATE_ADD(?, INTERVAL 1 DAY);", - Expected: []sql.Row{{time.Date(2022, time.October, 27, 13, 14, 15, 0, time.UTC)}}, + Expected: []sql.Row{{"2022-10-27 13:14:15"}}, + Bindings: map[string]*query.BindVariable{ + "v1": mustBuildBindVariable(time.Date(2022, time.October, 26, 13, 14, 15, 0, time.UTC)), + }, + }, + { + Query: "SELECT DATE_ADD(?, INTERVAL 1 DAY);", + Expected: []sql.Row{{"2022-10-27 13:14:15"}}, Bindings: map[string]*query.BindVariable{ "v1": mustBuildBindVariable("2022-10-26 13:14:15"), }, diff --git a/enginetest/queries/queries.go b/enginetest/queries/queries.go index 0d636a39fc..a192012e8f 100644 --- a/enginetest/queries/queries.go +++ b/enginetest/queries/queries.go @@ -6309,7 +6309,7 @@ Select * from ( }, { Query: "SELECT DATE_ADD('2018-05-02', INTERVAL 1 day)", - Expected: []sql.Row{{time.Date(2018, time.May, 3, 0, 0, 0, 0, time.UTC)}}, + Expected: []sql.Row{{"2018-05-03"}}, }, { Query: "SELECT DATE_ADD(DATE('2018-05-02'), INTERVAL 1 day)", @@ -6321,7 +6321,7 @@ Select * from ( }, { Query: "SELECT DATE_SUB('2018-05-02', INTERVAL 1 DAY)", - Expected: []sql.Row{{time.Date(2018, time.May, 1, 0, 0, 0, 0, time.UTC)}}, + Expected: []sql.Row{{"2018-05-01"}}, }, { Query: "SELECT DATE_SUB(DATE('2018-05-02'), INTERVAL 1 DAY)", diff --git a/sql/expression/function/date.go b/sql/expression/function/date.go index ccb4e6e954..7e087380ec 100644 --- a/sql/expression/function/date.go +++ b/sql/expression/function/date.go @@ -132,20 +132,43 @@ func (d *DateAdd) Eval(ctx *sql.Context, row sql.Row) (interface{}, error) { return nil, nil } - date, _, err = types.DatetimeMaxPrecision.Convert(date) + var dateVal interface{} + dateVal, _, err = types.DatetimeMaxPrecision.Convert(date) if err != nil { ctx.Warn(1292, err.Error()) return nil, nil } // return appropriate type - res := types.ValidateTime(delta.Add(date.(time.Time))) + res := types.ValidateTime(delta.Add(dateVal.(time.Time))) + if res == nil { + return nil, nil + } + resType := d.Type() if types.IsText(resType) { - return res, nil + // If the input is a properly formatted date/datetime string, the output should also be a string + if dateStr, isStr := date.(string); isStr { + if res.(time.Time).Nanosecond() > 0 { + return res.(time.Time).Format(sql.DatetimeLayoutNoTrim), nil + } + if isHmsInterval(d.Interval) { + return res.(time.Time).Format(sql.TimestampDatetimeLayout), nil + } + for _, layout := range types.DateOnlyLayouts { + if _, pErr := time.Parse(layout, dateStr); pErr != nil { + continue + } + return res.(time.Time).Format(sql.DateLayout), nil + } + } } + ret, _, err := resType.Convert(res) - return ret, err + if err != nil { + return nil, err + } + return ret, nil } func (d *DateAdd) String() string { @@ -256,20 +279,43 @@ func (d *DateSub) Eval(ctx *sql.Context, row sql.Row) (interface{}, error) { return nil, nil } - date, _, err = types.DatetimeMaxPrecision.Convert(date) + var dateVal interface{} + dateVal, _, err = types.DatetimeMaxPrecision.Convert(date) if err != nil { ctx.Warn(1292, err.Error()) return nil, nil } // return appropriate type - res := types.ValidateTime(delta.Sub(date.(time.Time))) + res := types.ValidateTime(delta.Sub(dateVal.(time.Time))) + if res == nil { + return nil, nil + } + resType := d.Type() if types.IsText(resType) { - return res, nil + // If the input is a properly formatted date/datetime string, the output should also be a string + if dateStr, isStr := date.(string); isStr { + if res.(time.Time).Nanosecond() > 0 { + return res.(time.Time).Format(sql.DatetimeLayoutNoTrim), nil + } + if isHmsInterval(d.Interval) { + return res.(time.Time).Format(sql.TimestampDatetimeLayout), nil + } + for _, layout := range types.DateOnlyLayouts { + if _, pErr := time.Parse(layout, dateStr); pErr != nil { + continue + } + return res.(time.Time).Format(sql.DateLayout), nil + } + } } + ret, _, err := resType.Convert(res) - return ret, err + if err != nil { + return nil, err + } + return ret, nil } func (d *DateSub) String() string { @@ -734,6 +780,20 @@ func (c CurrDate) WithChildren(children ...sql.Expression) (sql.Expression, erro return NoArgFuncWithChildren(c, children) } +func isYmdInterval(interval *expression.Interval) bool { + return strings.Contains(interval.Unit, "YEAR") || + strings.Contains(interval.Unit, "QUARTER") || + strings.Contains(interval.Unit, "MONTH") || + strings.Contains(interval.Unit, "WEEK") || + strings.Contains(interval.Unit, "DAY") +} + +func isHmsInterval(interval *expression.Interval) bool { + return strings.Contains(interval.Unit, "HOUR") || + strings.Contains(interval.Unit, "MINUTE") || + strings.Contains(interval.Unit, "SECOND") +} + // Determines the return type of a DateAdd/DateSub expression // Logic is based on https://dev.mysql.com/doc/refman/8.0/en/date-and-time-functions.html#function_date-add func dateOffsetType(input sql.Expression, interval *expression.Interval) sql.Type { @@ -747,31 +807,22 @@ func dateOffsetType(input sql.Expression, interval *expression.Interval) sql.Typ return types.Null } + if types.IsDatetimeType(inputType) || types.IsTimestampType(inputType) { + return types.DatetimeMaxPrecision + } + // set type flags isInputDate := inputType == types.Date isInputTime := inputType == types.Time - isInputDatetime := types.IsDatetimeType(inputType) || types.IsTimestampType(inputType) - - // result is Datetime if expression is Datetime or Timestamp - if isInputDatetime { - return types.DatetimeMaxPrecision - } // determine what kind of interval we're dealing with - isYmdInterval := strings.Contains(interval.Unit, "YEAR") || - strings.Contains(interval.Unit, "QUARTER") || - strings.Contains(interval.Unit, "MONTH") || - strings.Contains(interval.Unit, "WEEK") || - strings.Contains(interval.Unit, "DAY") - - isHmsInterval := strings.Contains(interval.Unit, "HOUR") || - strings.Contains(interval.Unit, "MINUTE") || - strings.Contains(interval.Unit, "SECOND") - isMixedInterval := isYmdInterval && isHmsInterval + isYmd := isYmdInterval(interval) + isHms := isHmsInterval(interval) + isMixed := isYmd && isHms // handle input of Date type if isInputDate { - if isHmsInterval || isMixedInterval { + if isHms || isMixed { // if interval contains time components, result is Datetime return types.DatetimeMaxPrecision } else { @@ -782,7 +833,7 @@ func dateOffsetType(input sql.Expression, interval *expression.Interval) sql.Typ // handle input of Time type if isInputTime { - if isYmdInterval || isMixedInterval { + if isYmd || isMixed { // if interval contains date components, result is Datetime return types.DatetimeMaxPrecision } else { @@ -793,7 +844,7 @@ func dateOffsetType(input sql.Expression, interval *expression.Interval) sql.Typ // handle dynamic input type if types.IsDeferredType(inputType) { - if isYmdInterval && !isHmsInterval { + if isYmd && !isHms { // if interval contains only date components, result is Date return types.Date } else { diff --git a/sql/expression/function/date_test.go b/sql/expression/function/date_test.go index cea57bf7ab..5231ad9f12 100644 --- a/sql/expression/function/date_test.go +++ b/sql/expression/function/date_test.go @@ -41,13 +41,43 @@ func TestAddDate(t *testing.T) { _, err = NewAddDate(expression.NewLiteral("2018-05-02", types.LongText)) require.Error(err) - // If the second argument is NOT an interval, then it's assumed to be a day interval - f, err := NewAddDate( + var expected, result interface{} + var f sql.Expression + + f, err = NewAddDate( + expression.NewLiteral(time.Date(2018, 5, 2, 12, 34, 56, 123456000, time.UTC), types.Date), + expression.NewLiteral(int64(1), types.Int64)) + require.NoError(err) + expected = time.Date(2018, 5, 3, 0, 0, 0, 0, time.UTC) + result, err = f.Eval(ctx, sql.Row{}) + require.NoError(err) + require.Equal(expected, result) + + f, err = NewAddDate( + expression.NewLiteral(time.Date(2018, 5, 2, 12, 34, 56, 0, time.UTC), types.Datetime), + expression.NewLiteral(int64(1), types.Int64)) + require.NoError(err) + expected = time.Date(2018, 5, 3, 12, 34, 56, 0, time.UTC) + result, err = f.Eval(ctx, sql.Row{}) + require.NoError(err) + require.Equal(expected, result) + + f, err = NewAddDate( + expression.NewLiteral(time.Date(2018, 5, 2, 12, 34, 56, 123456000, time.UTC), types.DatetimeMaxPrecision), + expression.NewLiteral(int64(1), types.Int64)) + require.NoError(err) + expected = time.Date(2018, 5, 3, 12, 34, 56, 123456000, time.UTC) + result, err = f.Eval(ctx, sql.Row{}) + require.NoError(err) + require.Equal(expected, result) + + // If the second argument is NOT an interval, then ADDDATE works exactly like DATE_ADD + f, err = NewAddDate( expression.NewLiteral("2018-05-02", types.LongText), expression.NewLiteral(int64(1), types.Int64)) require.NoError(err) - expected := time.Date(2018, time.May, 3, 0, 0, 0, 0, time.UTC) - result, err := f.Eval(ctx, sql.Row{}) + expected = "2018-05-03" + result, err = f.Eval(ctx, sql.Row{}) require.NoError(err) require.Equal(expected, result) @@ -60,6 +90,69 @@ func TestAddDate(t *testing.T) { require.NoError(err) require.Equal(expected, result) + f, err = NewAddDate( + expression.NewLiteral("2018-05-02", types.LongText), + expression.NewInterval(expression.NewLiteral(int64(1), types.Int64), "DAY")) + require.NoError(err) + expected = "2018-05-03" + result, err = f.Eval(ctx, sql.Row{}) + require.NoError(err) + require.Equal(expected, result) + + f, err = NewAddDate( + expression.NewLiteral("2018-05-02 12:34:56", types.LongText), + expression.NewInterval(expression.NewLiteral(int64(1), types.Int64), "DAY")) + require.NoError(err) + expected = "2018-05-03 12:34:56" + result, err = f.Eval(ctx, sql.Row{}) + require.NoError(err) + require.Equal(expected, result) + + f, err = NewAddDate( + expression.NewLiteral("2018-05-02 12:34:56.123", types.LongText), + expression.NewInterval(expression.NewLiteral(int64(1), types.Int64), "DAY")) + require.NoError(err) + expected = "2018-05-03 12:34:56.123000" + result, err = f.Eval(ctx, sql.Row{}) + require.NoError(err) + require.Equal(expected, result) + + f, err = NewAddDate( + expression.NewLiteral("2018-05-02 12:34:56.123456", types.LongText), + expression.NewInterval(expression.NewLiteral(int64(1), types.Int64), "DAY")) + require.NoError(err) + expected = "2018-05-03 12:34:56.123456" + result, err = f.Eval(ctx, sql.Row{}) + require.NoError(err) + require.Equal(expected, result) + + f, err = NewAddDate( + expression.NewLiteral("2018-05-02", types.LongText), + expression.NewInterval(expression.NewLiteral(int64(1), types.Int64), "SECOND")) + require.NoError(err) + expected = "2018-05-02 00:00:01" + result, err = f.Eval(ctx, sql.Row{}) + require.NoError(err) + require.Equal(expected, result) + + f, err = NewAddDate( + expression.NewLiteral("2018-05-02", types.LongText), + expression.NewInterval(expression.NewLiteral(int64(10), types.Int64), "MICROSECOND")) + require.NoError(err) + expected = "2018-05-02 00:00:00.000010" + result, err = f.Eval(ctx, sql.Row{}) + require.NoError(err) + require.Equal(expected, result) + + f, err = NewAddDate( + expression.NewLiteral("2018-05-02", types.LongText), + expression.NewInterval(expression.NewLiteral(int64(1), types.Int64), "MICROSECOND")) + require.NoError(err) + expected = "2018-05-02 00:00:00.000001" + result, err = f.Eval(ctx, sql.Row{}) + require.NoError(err) + require.Equal(expected, result) + // If the interval param is NULL, then NULL is returned f2, err := NewAddDate( expression.NewLiteral("2018-05-02", types.LongText), @@ -68,7 +161,12 @@ func TestAddDate(t *testing.T) { require.NoError(err) require.Nil(result) + f, err = NewAddDate( + expression.NewGetField(0, types.Int64, "foo", true), + expression.NewLiteral(int64(1), types.Int64)) + // If the date param is NULL, then NULL is returned + require.NoError(err) result, err = f.Eval(ctx, sql.Row{nil}) require.NoError(err) require.Nil(result) @@ -89,7 +187,7 @@ func TestAddDate(t *testing.T) { expression.NewLiteral("2018-05-02", types.Text), expression.NewLiteral(int64(1_000_000), types.Int64)) require.NoError(err) - expected = time.Date(4756, time.March, 29, 0, 0, 0, 0, time.UTC) + expected = "4756-03-29" result, err = f.Eval(ctx, sql.Row{}) require.NoError(err) require.Equal(expected, result) @@ -119,8 +217,7 @@ func TestDateAdd(t *testing.T) { ) require.NoError(err) - expected := time.Date(2018, time.May, 3, 0, 0, 0, 0, time.UTC) - + expected := "2018-05-03" result, err := f.Eval(ctx, sql.Row{"2018-05-02"}) require.NoError(err) require.Equal(expected, result) @@ -153,13 +250,43 @@ func TestSubDate(t *testing.T) { _, err = NewSubDate(expression.NewLiteral("2018-05-02", types.LongText)) require.Error(err) + var expected, result interface{} + var f sql.Expression + + f, err = NewSubDate( + expression.NewLiteral(time.Date(2018, 5, 2, 12, 34, 56, 123456000, time.UTC), types.Date), + expression.NewLiteral(int64(1), types.Int64)) + require.NoError(err) + expected = time.Date(2018, 5, 1, 0, 0, 0, 0, time.UTC) + result, err = f.Eval(ctx, sql.Row{}) + require.NoError(err) + require.Equal(expected, result) + + f, err = NewSubDate( + expression.NewLiteral(time.Date(2018, 5, 2, 12, 34, 56, 0, time.UTC), types.Datetime), + expression.NewLiteral(int64(1), types.Int64)) + require.NoError(err) + expected = time.Date(2018, 5, 1, 12, 34, 56, 0, time.UTC) + result, err = f.Eval(ctx, sql.Row{}) + require.NoError(err) + require.Equal(expected, result) + + f, err = NewSubDate( + expression.NewLiteral(time.Date(2018, 5, 2, 12, 34, 56, 123456000, time.UTC), types.DatetimeMaxPrecision), + expression.NewLiteral(int64(1), types.Int64)) + require.NoError(err) + expected = time.Date(2018, 5, 1, 12, 34, 56, 123456000, time.UTC) + result, err = f.Eval(ctx, sql.Row{}) + require.NoError(err) + require.Equal(expected, result) + // If the second argument is NOT an interval, then it's assumed to be a day interval - f, err := NewSubDate( + f, err = NewSubDate( expression.NewLiteral("2018-05-02", types.LongText), expression.NewLiteral(int64(1), types.Int64)) require.NoError(err) - expected := time.Date(2018, time.May, 1, 0, 0, 0, 0, time.UTC) - result, err := f.Eval(ctx, sql.Row{}) + expected = "2018-05-01" + result, err = f.Eval(ctx, sql.Row{}) require.NoError(err) require.Equal(expected, result) @@ -172,6 +299,69 @@ func TestSubDate(t *testing.T) { require.NoError(err) require.Equal(expected, result) + f, err = NewSubDate( + expression.NewLiteral("2018-05-02", types.LongText), + expression.NewInterval(expression.NewLiteral(int64(1), types.Int64), "DAY")) + require.NoError(err) + expected = "2018-05-01" + result, err = f.Eval(ctx, sql.Row{}) + require.NoError(err) + require.Equal(expected, result) + + f, err = NewSubDate( + expression.NewLiteral("2018-05-02 12:34:56", types.LongText), + expression.NewInterval(expression.NewLiteral(int64(1), types.Int64), "DAY")) + require.NoError(err) + expected = "2018-05-01 12:34:56" + result, err = f.Eval(ctx, sql.Row{}) + require.NoError(err) + require.Equal(expected, result) + + f, err = NewSubDate( + expression.NewLiteral("2018-05-02 12:34:56.123", types.LongText), + expression.NewInterval(expression.NewLiteral(int64(1), types.Int64), "DAY")) + require.NoError(err) + expected = "2018-05-01 12:34:56.123000" + result, err = f.Eval(ctx, sql.Row{}) + require.NoError(err) + require.Equal(expected, result) + + f, err = NewSubDate( + expression.NewLiteral("2018-05-02 12:34:56.123456", types.LongText), + expression.NewInterval(expression.NewLiteral(int64(1), types.Int64), "DAY")) + require.NoError(err) + expected = "2018-05-01 12:34:56.123456" + result, err = f.Eval(ctx, sql.Row{}) + require.NoError(err) + require.Equal(expected, result) + + f, err = NewSubDate( + expression.NewLiteral("2018-05-02", types.LongText), + expression.NewInterval(expression.NewLiteral(int64(1), types.Int64), "SECOND")) + require.NoError(err) + expected = "2018-05-01 23:59:59" + result, err = f.Eval(ctx, sql.Row{}) + require.NoError(err) + require.Equal(expected, result) + + f, err = NewSubDate( + expression.NewLiteral("2018-05-02", types.LongText), + expression.NewInterval(expression.NewLiteral(int64(10), types.Int64), "MICROSECOND")) + require.NoError(err) + expected = "2018-05-01 23:59:59.999990" + result, err = f.Eval(ctx, sql.Row{}) + require.NoError(err) + require.Equal(expected, result) + + f, err = NewSubDate( + expression.NewLiteral("2018-05-02", types.LongText), + expression.NewInterval(expression.NewLiteral(int64(1), types.Int64), "MICROSECOND")) + require.NoError(err) + expected = "2018-05-01 23:59:59.999999" + result, err = f.Eval(ctx, sql.Row{}) + require.NoError(err) + require.Equal(expected, result) + // If the interval param is NULL, then NULL is returned f2, err := NewSubDate( expression.NewLiteral("2018-05-02", types.LongText), @@ -180,6 +370,10 @@ func TestSubDate(t *testing.T) { require.NoError(err) require.Nil(result) + f, err = NewSubDate( + expression.NewGetField(0, types.Int64, "foo", true), + expression.NewLiteral(int64(1), types.Int64)) + // If the date param is NULL, then NULL is returned result, err = f.Eval(ctx, sql.Row{nil}) require.NoError(err) @@ -219,8 +413,7 @@ func TestDateSub(t *testing.T) { ) require.NoError(err) - expected := time.Date(2018, time.May, 1, 0, 0, 0, 0, time.UTC) - + expected := "2018-05-01" result, err := f.Eval(ctx, sql.Row{"2018-05-02"}) require.NoError(err) require.Equal(expected, result) diff --git a/sql/type.go b/sql/type.go index 1757ef4c99..c5ab25e50e 100644 --- a/sql/type.go +++ b/sql/type.go @@ -48,6 +48,10 @@ const ( // TimestampDatetimeLayout is the formatting string with the layout of the timestamp // using the format of Go "time" package. TimestampDatetimeLayout = "2006-01-02 15:04:05.999999" + + // DatetimeLayoutNoTrim is the formatting string with the layout of the datetime that + // doesn't trim trailing zeros + DatetimeLayoutNoTrim = "2006-01-02 15:04:05.000000" ) const ( diff --git a/sql/types/datetime.go b/sql/types/datetime.go index 692b3e8bda..f846ca3247 100644 --- a/sql/types/datetime.go +++ b/sql/types/datetime.go @@ -50,29 +50,32 @@ var ( // datetimeTypeMinTimestamp is the minimum representable Timestamp value, which is one second past the epoch. datetimeTypeMinTimestamp = time.Unix(1, 0) + DateOnlyLayouts = []string{ + "20060102", + "2006-1-2", + "2006-01-02", + "2006/01/02", + } + // TimestampDatetimeLayouts hold extra timestamps allowed for parsing. It does // not have all the layouts supported by mysql. Missing are two digit year // versions of common cases and dates that use non common separators. // // https://github.com/MariaDB/server/blob/mysql-5.5.36/sql-common/my_time.c#L124 - TimestampDatetimeLayouts = []string{ + TimestampDatetimeLayouts = append(DateOnlyLayouts, []string{ "2006-01-02 15:4", "2006-01-02 15:04", "2006-01-02 15:04:", "2006-01-02 15:04:.", "2006-01-02 15:04:05.", "2006-01-02 15:04:05.999999", - "2006-01-02", - "2006-1-2", "2006-1-2 15:4:5.999999", time.RFC3339, time.RFC3339Nano, "2006-01-02T15:04:05", "20060102150405", - "20060102", - "2006/01/02", "2006-01-02 15:04:05.999999999 -0700 MST", // represents standard Time.time.UTC() - } + }...) // zeroTime is 0000-01-01 00:00:00 UTC which is the closest Go can get to 0000-00-00 00:00:00 zeroTime = time.Unix(-62167219200, 0).UTC()