Skip to content
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

evalengine: Implement FROM_DAYS #15058

Merged
merged 8 commits into from
Jan 29, 2024
4 changes: 2 additions & 2 deletions go/mysql/datetime/datetime.go
Original file line number Diff line number Diff line change
Expand Up @@ -615,13 +615,13 @@ func (dt *DateTime) addInterval(itv *Interval) bool {
return false
}

dt.Date.year, dt.Date.month, dt.Date.day = mysqlDateFromDayNumber(daynum)
dt.Date.year, dt.Date.month, dt.Date.day = MysqlDateFromDayNumber(daynum)
return true

case itv.unit.HasDayParts():
daynum := mysqlDayNumber(dt.Date.Year(), dt.Date.Month(), dt.Date.Day())
daynum += itv.day
dt.Date.year, dt.Date.month, dt.Date.day = mysqlDateFromDayNumber(daynum)
dt.Date.year, dt.Date.month, dt.Date.day = MysqlDateFromDayNumber(daynum)
return true

case itv.unit.HasMonthParts():
Expand Down
8 changes: 4 additions & 4 deletions go/mysql/datetime/mydate.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,8 @@ package datetime
// day count that traditional datetime systems use (e.g. the daycount
// algorithm in the Go standard library). It is often off by one, possibly
// because of incorrect leap year handling, but the inverse of the algorithm
// in mysqlDateFromDayNumber takes this into account. Hence, the results
// of this function can ONLY be passed to mysqlDateFromDayNumber; using
// in MysqlDateFromDayNumber takes this into account. Hence, the results
// of this function can ONLY be passed to MysqlDateFromDayNumber; using
// a day number with one of Go's datetime APIs will return incorrect results.
// This API should only be used when performing datetime calculations (addition
// and subtraction), so that the results match MySQL's. All other date handling
Expand All @@ -46,15 +46,15 @@ func mysqlDayNumber(year, month, day int) int {
return days + year/4 - leapAdjust
}

// mysqlDateFromDayNumber converts an absolute day number into a date (a year, month, day triplet).
// MysqlDateFromDayNumber converts an absolute day number into a date (a year, month, day triplet).
// This is an algorithm that has been reverse engineered from MySQL;
// the tables used as a reference can be found in `testdata/daynr_to_date.json`.
// See the warning from mysqlDayNumber: the day number used as an argument to
// this function must come from mysqlDayNumber or the results won't be correct.
// This API should only be used when performing datetime calculations (addition
// and subtraction), so that the results match MySQL's. All other date handling
// operations must use our helpers based on Go's standard library.
func mysqlDateFromDayNumber(daynr int) (uint16, uint8, uint8) {
func MysqlDateFromDayNumber(daynr int) (uint16, uint8, uint8) {
if daynr <= 365 || daynr >= 3652500 {
return 0, 0, 0
}
Expand Down
2 changes: 1 addition & 1 deletion go/mysql/datetime/mydate_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ func TestDayNumberFields(t *testing.T) {
require.NoError(t, err)

for _, tc := range expected {
y, m, d := mysqlDateFromDayNumber(tc[0])
y, m, d := MysqlDateFromDayNumber(tc[0])
assert.Equal(t, tc[1], int(y))
assert.Equal(t, tc[2], int(m))
assert.Equal(t, tc[3], int(d))
Expand Down
12 changes: 12 additions & 0 deletions go/vt/vtgate/evalengine/cached_size.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

17 changes: 17 additions & 0 deletions go/vt/vtgate/evalengine/compiler_asm.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ import (

"vitess.io/vitess/go/mysql/collations/charset/types"
"vitess.io/vitess/go/mysql/collations/colldata"
mysqldt "vitess.io/vitess/go/mysql/datetime"

"vitess.io/vitess/go/hack"
"vitess.io/vitess/go/mysql/collations"
Expand Down Expand Up @@ -3795,6 +3796,22 @@ func (asm *assembler) Fn_LAST_DAY() {
}, "FN LAST_DAY DATETIME(SP-1)")
}

func (asm *assembler) Fn_FROM_DAYS() {
asm.emit(func(env *ExpressionEnv) int {
arg := env.vm.stack[env.vm.sp-1].(*evalInt64)
y, m, d := mysqldt.MysqlDateFromDayNumber(int(arg.i))
if y == 0 && m == 0 && d == 0 {
env.vm.stack[env.vm.sp-1] = env.vm.arena.newEvalDate(datetime.Date{})
} else if y > 9999 {
env.vm.stack[env.vm.sp-1] = nil
} else {
dt := datetime.NewDateFromStd(time.Date(int(y), time.Month(m), int(d), 0, 0, 0, 0, env.currentTimezone()))
env.vm.stack[env.vm.sp-1] = env.vm.arena.newEvalDate(dt)
}
return 1
}, "FN FROM_DAYS INT64(SP-1)")
}

func (asm *assembler) Fn_QUARTER() {
asm.emit(func(env *ExpressionEnv) int {
if env.vm.stack[env.vm.sp-1] == nil {
Expand Down
47 changes: 47 additions & 0 deletions go/vt/vtgate/evalengine/fn_time.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import (
"vitess.io/vitess/go/hack"
"vitess.io/vitess/go/mysql/collations"
"vitess.io/vitess/go/mysql/datetime"
mysqldt "vitess.io/vitess/go/mysql/datetime"
"vitess.io/vitess/go/mysql/decimal"
"vitess.io/vitess/go/sqltypes"
)
Expand Down Expand Up @@ -111,6 +112,10 @@ type (
CallExpr
}

builtinFromDays struct {
CallExpr
}

builtinQuarter struct {
CallExpr
}
Expand Down Expand Up @@ -173,6 +178,7 @@ var _ IR = (*builtinMinute)(nil)
var _ IR = (*builtinMonth)(nil)
var _ IR = (*builtinMonthName)(nil)
var _ IR = (*builtinLastDay)(nil)
var _ IR = (*builtinFromDays)(nil)
var _ IR = (*builtinQuarter)(nil)
var _ IR = (*builtinSecond)(nil)
var _ IR = (*builtinTime)(nil)
Expand Down Expand Up @@ -1249,6 +1255,47 @@ func (call *builtinLastDay) compile(c *compiler) (ctype, error) {
return ctype{Type: sqltypes.Date, Flag: arg.Flag | flagNullable}, nil
}

func (b *builtinFromDays) eval(env *ExpressionEnv) (eval, error) {
arg, err := b.arg1(env)
if arg == nil || err != nil {
return nil, nil
}

days := evalToInt64(arg).i
y, m, d := mysqldt.MysqlDateFromDayNumber(int(days))

// mysql returns 0000-00-00 for days below 366 and above 3652499
if y == 0 && m == 0 && d == 0 {
return nil, nil
}

// mysql returns NULL if y is greater than 9999
if y > 9999 {
return nil, nil
}

dt := datetime.NewDateFromStd(time.Date(int(y), time.Month(m), int(d), 0, 0, 0, 0, env.currentTimezone()))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This conversion seems unneeded? I think you can just straight set the properties from what MysqlDateFromDayNumber returns?

var dt datetime.Date
dt.Year, dt.Month, dt.Day = mysqldt.MysqlDateFromDayNumber(int(arg.i))

Then you also only have to check dt.Year > 9999, the zero case also automatically falls out?

Same for the compiler, no need for the conversion there either through Go's Time library.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The struct fields of datetime.Date are not exported. So, I think we can't follow this.

Also, don't know why I imported mysql/datetime twice. Fixing it.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of exporting the existing function, we can also add a new one that returns the date already so you don’t have that issue.

I think we really should avoid going through the go type if not strictly needed here.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

got it. done.

return newEvalDate(dt, true), nil
}

func (call *builtinFromDays) compile(c *compiler) (ctype, error) {
arg, err := call.Arguments[0].compile(c)
if err != nil {
return ctype{}, err
}

skip := c.compileNullCheck1(arg)
switch arg.Type {
case sqltypes.Int64:
default:
c.asm.Convert_xi(1)
}

c.asm.Fn_FROM_DAYS()
c.asm.jumpDestination(skip)
return ctype{Type: sqltypes.Date, Flag: arg.Flag | flagNullable}, nil
}

func (b *builtinQuarter) eval(env *ExpressionEnv) (eval, error) {
date, err := b.arg1(env)
if err != nil {
Expand Down
23 changes: 23 additions & 0 deletions go/vt/vtgate/evalengine/testcases/cases.go
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,7 @@ var Cases = []TestCase{
{Run: FnMonth},
{Run: FnMonthName},
{Run: FnLastDay},
{Run: FnFromDays},
{Run: FnQuarter},
{Run: FnSecond},
{Run: FnTime},
Expand Down Expand Up @@ -1766,6 +1767,28 @@ func FnLastDay(yield Query) {
}
}

func FnFromDays(yield Query) {
for _, d := range inputConversions {
yield(fmt.Sprintf("FROM_DAYS(%s)", d), nil)
}

days := []string{
"0",
"1",
"366",
"365242",
"3652424",
"3652425",
beingnoble03 marked this conversation as resolved.
Show resolved Hide resolved
"3652500",
"3652499",
"730669",
}

for _, d := range days {
yield(fmt.Sprintf("FROM_DAYS(%s)", d), nil)
}
}

func FnQuarter(yield Query) {
for _, d := range inputConversions {
yield(fmt.Sprintf("QUARTER(%s)", d), nil)
Expand Down
5 changes: 5 additions & 0 deletions go/vt/vtgate/evalengine/translate_builtin.go
Original file line number Diff line number Diff line change
Expand Up @@ -419,6 +419,11 @@ func (ast *astCompiler) translateFuncExpr(fn *sqlparser.FuncExpr) (IR, error) {
return nil, argError(method)
}
return &builtinLastDay{CallExpr: call}, nil
case "from_days":
if len(args) != 1 {
return nil, argError(method)
}
return &builtinFromDays{CallExpr: call}, nil
case "quarter":
if len(args) != 1 {
return nil, argError(method)
Expand Down
Loading