diff --git a/sql/expression/function/registry.go b/sql/expression/function/registry.go index 7c4f221b56..197acedd1a 100644 --- a/sql/expression/function/registry.go +++ b/sql/expression/function/registry.go @@ -176,6 +176,7 @@ var BuiltIns = []sql.Function{ sql.Function0{Name: "pi", Fn: NewPi}, sql.Function2{Name: "pow", Fn: NewPower}, sql.Function2{Name: "power", Fn: NewPower}, + sql.Function1{Name: "quarter", Fn: NewQuarter}, sql.Function1{Name: "radians", Fn: NewRadians}, sql.FunctionN{Name: "rand", Fn: NewRand}, sql.FunctionN{Name: "regexp_like", Fn: NewRegexpLike}, diff --git a/sql/expression/function/time.go b/sql/expression/function/time.go index 8403a15a31..f2d0e4bb3a 100644 --- a/sql/expression/function/time.go +++ b/sql/expression/function/time.go @@ -50,7 +50,9 @@ func getDate(ctx *sql.Context, date, err := types.DatetimeMaxPrecision.ConvertWithoutRangeCheck(val) if err != nil { - date = types.DatetimeMaxPrecision.Zero().(time.Time) + ctx.Warn(1292, "Incorrect datetime value: '%s'", val) + return nil, nil + //date = types.DatetimeMaxPrecision.Zero().(time.Time) } return date, nil @@ -115,6 +117,60 @@ func (y *Year) WithChildren(children ...sql.Expression) (sql.Expression, error) return NewYear(children[0]), nil } +type Quarter struct { + expression.UnaryExpression +} + +var _ sql.FunctionExpression = (*Quarter)(nil) +var _ sql.CollationCoercible = (*Quarter)(nil) + +// NewQuarter creates a new Month UDF. +func NewQuarter(date sql.Expression) sql.Expression { + return &Quarter{expression.UnaryExpression{Child: date}} +} + +// FunctionName implements sql.FunctionExpression +func (q *Quarter) FunctionName() string { + return "quarter" +} + +// Description implements sql.FunctionExpression +func (q *Quarter) Description() string { + return "returns the quarter of the given date." +} + +func (q *Quarter) String() string { return fmt.Sprintf("%s(%s)", q.FunctionName(), q.Child) } + +// Type implements the Expression interface. +func (q *Quarter) Type() sql.Type { return types.Int32 } + +// CollationCoercibility implements the interface sql.CollationCoercible. +func (q *Quarter) CollationCoercibility(ctx *sql.Context) (collation sql.CollationID, coercibility byte) { + return sql.Collation_binary, 5 +} + +// Eval implements the Expression interface. +func (q *Quarter) Eval(ctx *sql.Context, row sql.Row) (interface{}, error) { + mon, err := getDatePart(ctx, q.UnaryExpression, row, month) + if err != nil { + return nil, err + } + + if mon == nil { + return nil, nil + } + + return (mon.(int32)-1)/3 + 1, nil +} + +// WithChildren implements the Expression interface. +func (q *Quarter) WithChildren(children ...sql.Expression) (sql.Expression, error) { + if len(children) != 1 { + return nil, sql.ErrInvalidChildrenNumber.New(q, len(children), 1) + } + return NewQuarter(children[0]), nil +} + // Month is a function that returns the month of a date. type Month struct { expression.UnaryExpression @@ -550,6 +606,9 @@ func (d *YearWeek) Eval(ctx *sql.Context, row sql.Row) (interface{}, error) { if err != nil { return nil, err } + if date == nil { + return nil, nil + } yyyy, ok := year(date).(int32) if !ok { return nil, sql.ErrInvalidArgumentDetails.New("YEARWEEK", "invalid year") diff --git a/sql/expression/function/time_test.go b/sql/expression/function/time_test.go index 185fb79a67..e96241c204 100644 --- a/sql/expression/function/time_test.go +++ b/sql/expression/function/time_test.go @@ -43,7 +43,7 @@ func TestTime_Year(t *testing.T) { expected interface{} err bool }{ - {"invalid type", sql.NewRow([]byte{0, 1, 2}), int32(0), false}, + {"invalid type", sql.NewRow([]byte{0, 1, 2}), nil, false}, {"date as string", sql.NewRow(stringDate), int32(2007), false}, {"date as time", sql.NewRow(time.Now()), int32(time.Now().UTC().Year()), false}, } @@ -73,7 +73,7 @@ func TestTime_Month(t *testing.T) { err bool }{ {"null date", sql.NewRow(nil), nil, false}, - {"invalid type", sql.NewRow([]byte{0, 1, 2}), int32(1), false}, + {"invalid type", sql.NewRow([]byte{0, 1, 2}), nil, false}, {"date as string", sql.NewRow(stringDate), int32(1), false}, {"date as time", sql.NewRow(time.Now()), int32(time.Now().UTC().Month()), false}, } @@ -92,6 +92,127 @@ func TestTime_Month(t *testing.T) { } } +func TestTime_Quarter(t *testing.T) { + ctx := sql.NewEmptyContext() + f := NewQuarter(expression.NewGetField(0, types.LongText, "foo", false)) + + testCases := []struct { + name string + row sql.Row + expected interface{} + err bool + }{ + { + name: "null date", + row: sql.NewRow(nil), + expected: nil, + }, + { + name: "1", + row: sql.NewRow(1), + expected: nil, + }, + { + name: "1.1", + row: sql.NewRow(1.1), + expected: nil, + }, + { + name: "invalid type", + row: sql.NewRow([]byte{0, 1, 2}), + expected: nil, + }, + { + name: "date as string", + row: sql.NewRow(stringDate), + expected: int32(1), + }, + { + name: "another date as string", + row: sql.NewRow("2008-08-01"), + expected: int32(3), + }, + { + name: "january", + row: sql.NewRow("2008-01-01"), + expected: int32(1), + }, + { + name: "february", + row: sql.NewRow("2008-02-01"), + expected: int32(1), + }, + { + name: "march", + row: sql.NewRow("2008-03-01"), + expected: int32(1), + }, + { + name: "april", + row: sql.NewRow("2008-04-01"), + expected: int32(2), + }, + { + name: "may", + row: sql.NewRow("2008-05-01"), + expected: int32(2), + }, + { + name: "june", + row: sql.NewRow("2008-06-01"), + expected: int32(2), + }, + { + name: "july", + row: sql.NewRow("2008-07-01"), + expected: int32(3), + }, + { + name: "august", + row: sql.NewRow("2008-08-01"), + expected: int32(3), + }, + { + name: "septemeber", + row: sql.NewRow("2008-09-01"), + expected: int32(3), + }, + { + name: "october", + row: sql.NewRow("2008-10-01"), + expected: int32(4), + }, + { + name: "november", + row: sql.NewRow("2008-11-01"), + expected: int32(4), + }, + { + name: "december", + row: sql.NewRow("2008-12-01"), + expected: int32(4), + }, + { + name: "date as time", + row: sql.NewRow(time.Now()), + expected: int32((time.Now().UTC().Month()-1)/3 + 1), + }, + } + + for _, tt := range testCases { + t.Run(tt.name, func(t *testing.T) { + require := require.New(t) + val, err := f.Eval(ctx, tt.row) + if tt.err { + require.Error(err) + } else { + require.NoError(err) + require.Equal(tt.expected, val) + } + }) + } +} + func TestTime_Day(t *testing.T) { ctx := sql.NewEmptyContext() f := NewDay(expression.NewGetField(0, types.LongText, "foo", false)) @@ -103,7 +224,7 @@ func TestTime_Day(t *testing.T) { err bool }{ {"null date", sql.NewRow(nil), nil, false}, - {"invalid type", sql.NewRow([]byte{0, 1, 2}), int32(1), false}, + {"invalid type", sql.NewRow([]byte{0, 1, 2}), nil, false}, {"date as string", sql.NewRow(stringDate), int32(2), false}, {"date as time", sql.NewRow(time.Now()), int32(time.Now().UTC().Day()), false}, } @@ -133,7 +254,7 @@ func TestTime_Weekday(t *testing.T) { err bool }{ {"null date", sql.NewRow(nil), nil, false}, - {"invalid type", sql.NewRow([]byte{0, 1, 2}), int32(5), false}, + {"invalid type", sql.NewRow([]byte{0, 1, 2}), nil, false}, {"date as string", sql.NewRow(stringDate), int32(1), false}, {"date as time", sql.NewRow(time.Now()), int32(time.Now().UTC().Weekday()+6) % 7, false}, } @@ -163,7 +284,7 @@ func TestTime_Hour(t *testing.T) { err bool }{ {"null date", sql.NewRow(nil), nil, false}, - {"invalid type", sql.NewRow([]byte{0, 1, 2}), int32(0), false}, + {"invalid type", sql.NewRow([]byte{0, 1, 2}), nil, false}, {"date as string", sql.NewRow(stringDate), int32(14), false}, {"date as time", sql.NewRow(time.Now()), int32(time.Now().UTC().Hour()), false}, } @@ -193,7 +314,7 @@ func TestTime_Minute(t *testing.T) { err bool }{ {"null date", sql.NewRow(nil), nil, false}, - {"invalid type", sql.NewRow([]byte{0, 1, 2}), int32(0), false}, + {"invalid type", sql.NewRow([]byte{0, 1, 2}), nil, false}, {"date as string", sql.NewRow(stringDate), int32(15), false}, {"date as time", sql.NewRow(time.Now()), int32(time.Now().UTC().Minute()), false}, } @@ -223,7 +344,7 @@ func TestTime_Second(t *testing.T) { err bool }{ {"null date", sql.NewRow(nil), nil, false}, - {"invalid type", sql.NewRow([]byte{0, 1, 2}), int32(0), false}, + {"invalid type", sql.NewRow([]byte{0, 1, 2}), nil, false}, {"date as string", sql.NewRow(stringDate), int32(16), false}, {"date as time", sql.NewRow(time.Now()), int32(time.Now().UTC().Second()), false}, } @@ -284,7 +405,7 @@ func TestTime_DayOfWeek(t *testing.T) { err bool }{ {"null date", sql.NewRow(nil), nil, false}, - {"invalid type", sql.NewRow([]byte{0, 1, 2}), int32(7), false}, + {"invalid type", sql.NewRow([]byte{0, 1, 2}), nil, false}, {"date as string", sql.NewRow(stringDate), int32(3), false}, {"date as time", sql.NewRow(time.Now()), int32(time.Now().UTC().Weekday() + 1), false}, } @@ -314,7 +435,7 @@ func TestTime_DayOfYear(t *testing.T) { err bool }{ {"null date", sql.NewRow(nil), nil, false}, - {"invalid type", sql.NewRow([]byte{0, 1, 2}), int32(1), false}, + {"invalid type", sql.NewRow([]byte{0, 1, 2}), nil, false}, {"date as string", sql.NewRow(stringDate), int32(2), false}, {"date as time", sql.NewRow(time.Now()), int32(time.Now().UTC().YearDay()), false}, } @@ -376,8 +497,8 @@ func TestYearWeek(t *testing.T) { expected interface{} err bool }{ - {"null date", sql.NewRow(nil), nil, true}, - {"invalid type", sql.NewRow([]byte{0, 1, 2}), int32(1), false}, + {"null date", sql.NewRow(nil), nil, false}, + {"invalid type", sql.NewRow([]byte{0, 1, 2}), nil, false}, {"date as string", sql.NewRow(stringDate), int32(200653), false}, } @@ -495,7 +616,7 @@ func TestDate(t *testing.T) { err bool }{ {"null date", sql.NewRow(nil), nil, false}, - {"invalid type", sql.NewRow([]byte{0, 1, 2}), types.Date.Zero().(time.Time).Format("2006-01-02"), false}, + {"invalid type", sql.NewRow([]byte{0, 1, 2}), nil, false}, {"date as string", sql.NewRow(stringDate), "2007-01-02", false}, {"date as time", sql.NewRow(time.Now().UTC()), time.Now().UTC().Format("2006-01-02"), false}, }