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
6 changes: 6 additions & 0 deletions enginetest/queries/queries.go
Original file line number Diff line number Diff line change
Expand Up @@ -765,6 +765,12 @@ var SpatialQueryTests = []QueryTest{
}

var QueryTests = []QueryTest{
{
// Assert that SYSDATE() returns different times on each call in a query (unlike NOW())
// Using the maximum precision for fractional seconds, lets us see a difference.
Query: "select now() = sysdate(), sleep(0.1), now(6) < sysdate(6);",
Expected: []sql.Row{{true, 0, true}},
},
{
Query: "select 1 as x from xy having AVG(x) > 0",
Expected: []sql.Row{{1}},
Expand Down
1 change: 1 addition & 0 deletions sql/expression/function/registry.go
Original file line number Diff line number Diff line change
Expand Up @@ -265,6 +265,7 @@ var BuiltIns = []sql.Function{
sql.FunctionN{Name: "substring", Fn: NewSubstring},
sql.Function3{Name: "substring_index", Fn: NewSubstringIndex},
sql.Function1{Name: "sum", Fn: func(e sql.Expression) sql.Expression { return aggregation.NewSum(e) }},
sql.FunctionN{Name: "sysdate", Fn: NewSysdate},
sql.Function1{Name: "tan", Fn: NewTan},
sql.Function1{Name: "time", Fn: NewTime},
sql.Function2{Name: "time_format", Fn: NewTimeFormat},
Expand Down
28 changes: 26 additions & 2 deletions sql/expression/function/time.go
Original file line number Diff line number Diff line change
Expand Up @@ -823,7 +823,13 @@ const maxCurrTimestampPrecision = 6

// Now is a function that returns the current time.
type Now struct {
// prec stores the requested precision for fractional seconds.
prec sql.Expression
// alwaysUseExactTime controls whether the NOW() function gets the current time, or
// uses a cached value that records the starting time of the query. By default, a
// cached time is used, but in some cases (such as the SYSDATE() function), the func
// needs to always return the exact current time of each function invocation().
alwaysUseExactTime bool
}

func (n *Now) IsNonDeterministic() bool {
Expand Down Expand Up @@ -932,10 +938,17 @@ func (n *Now) Eval(ctx *sql.Context, row sql.Row) (interface{}, error) {
return nil, err
}

// For NOW(), we use the cached QueryTime, so that all NOW() calls in a query return the same value.
// SYSDATE() requires that we use the *exact* current time, and not use the cached version.
currentTime := ctx.QueryTime()
if n.alwaysUseExactTime {
currentTime = sql.Now()
}

// If no arguments, just return with 0 precision
// The way the parser is implemented 0 should always be passed in; have this here just in case
if n.prec == nil {
t, ok := gmstime.ConvertTimeZone(ctx.QueryTime(), gmstime.SystemTimezoneOffset(), sessionTimeZone)
t, ok := gmstime.ConvertTimeZone(currentTime, gmstime.SystemTimezoneOffset(), sessionTimeZone)
if !ok {
return nil, fmt.Errorf("invalid time zone: %s", sessionTimeZone)
}
Expand Down Expand Up @@ -975,7 +988,7 @@ func (n *Now) Eval(ctx *sql.Context, row sql.Row) (interface{}, error) {
}

// Get the timestamp
t, ok := gmstime.ConvertTimeZone(ctx.QueryTime(), gmstime.SystemTimezoneOffset(), sessionTimeZone)
t, ok := gmstime.ConvertTimeZone(currentTime, gmstime.SystemTimezoneOffset(), sessionTimeZone)
if !ok {
return nil, fmt.Errorf("invalid time zone: %s", sessionTimeZone)
}
Expand All @@ -1000,6 +1013,17 @@ func (n *Now) WithChildren(children ...sql.Expression) (sql.Expression, error) {
return NewNow(children...)
}

// NewSysdate returns a new SYSDATE() function, using the supplied |args| for an
// optional value for fractional second precision. The SYSDATE() function is a synonym
// for NOW(), but does NOT use the query's cached start time, and instead always returns
// the current time, even when executed multiple times in a query or stored procedure.
// https://dev.mysql.com/doc/refman/8.0/en/date-and-time-functions.html#function_sysdate
func NewSysdate(args ...sql.Expression) (sql.Expression, error) {
n, err := NewNow(args...)
n.(*Now).alwaysUseExactTime = true
return n, err
}

// SessionTimeZone returns a MySQL timezone offset string for the value of @@session_time_zone. If the session
// timezone is set to SYSTEM, then the system timezone offset is calculated and returned.
func SessionTimeZone(ctx *sql.Context) (string, error) {
Expand Down
56 changes: 56 additions & 0 deletions sql/expression/function/time_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -567,6 +567,62 @@ func TestNow(t *testing.T) {
}
}

// TestSysdate tests the SYSDATE() function, which should generally behave identically to NOW(), but unlike NOW(),
// SYSDATE() should always return the exact current time, and not the cached query start time. That behavior is
// tested in the enginetests, instead of these unit tests.
func TestSysdate(t *testing.T) {
f, _ := NewSysdate(expression.NewGetField(0, types.LongText, "foo", false))
date := time.Date(
2021, // year
1, // month
1, // day
8, // hour
30, // min
15, // sec
123456789, // nsec
time.UTC, // location (UTC)
)

testCases := []struct {
name string
row sql.Row
expected interface{}
err bool
}{
{"null date", sql.NewRow(nil), nil, true},
{"different int type", sql.NewRow(int8(0)), time.Date(date.Year(), date.Month(), date.Day(), date.Hour(), date.Minute(), date.Second(), 0, time.UTC), false},
{"precision of -1", sql.NewRow(-1), nil, true},
{"precision of 0", sql.NewRow(0), time.Date(date.Year(), date.Month(), date.Day(), date.Hour(), date.Minute(), date.Second(), 0, time.UTC), false},
{"precision of 1", sql.NewRow(1), time.Date(date.Year(), date.Month(), date.Day(), date.Hour(), date.Minute(), date.Second(), 100000000, time.UTC), false},
{"precision of 2", sql.NewRow(2), time.Date(date.Year(), date.Month(), date.Day(), date.Hour(), date.Minute(), date.Second(), 120000000, time.UTC), false},
{"precision of 3", sql.NewRow(3), time.Date(date.Year(), date.Month(), date.Day(), date.Hour(), date.Minute(), date.Second(), 123000000, time.UTC), false},
{"precision of 4", sql.NewRow(4), time.Date(date.Year(), date.Month(), date.Day(), date.Hour(), date.Minute(), date.Second(), 123400000, time.UTC), false},
{"precision of 5", sql.NewRow(5), time.Date(date.Year(), date.Month(), date.Day(), date.Hour(), date.Minute(), date.Second(), 123450000, time.UTC), false},
{"precision of 6", sql.NewRow(6), time.Date(date.Year(), date.Month(), date.Day(), date.Hour(), date.Minute(), date.Second(), 123456000, time.UTC), false},
{"precision of 7 which is too high", sql.NewRow(7), nil, true},
{"incorrect type", sql.NewRow("notanint"), nil, true},
}

for _, tt := range testCases {
t.Run(tt.name, func(t *testing.T) {
sql.RunWithNowFunc(func() time.Time {
return date
}, func() error {
ctx := sql.NewEmptyContext()
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)
}
return nil
})
})
}
}

func TestTime(t *testing.T) {
ctx := sql.NewEmptyContext()
f := NewTime(expression.NewGetField(0, types.LongText, "foo", false))
Expand Down
17 changes: 11 additions & 6 deletions sql/session.go
Original file line number Diff line number Diff line change
Expand Up @@ -284,18 +284,23 @@ var ctxNowFunc = time.Now
var ctxNowFuncMutex = &sync.Mutex{}

func RunWithNowFunc(nowFunc func() time.Time, fn func() error) error {
ctxNowFuncMutex.Lock()
defer ctxNowFuncMutex.Unlock()

initialNow := ctxNowFunc
ctxNowFunc = nowFunc
oldNowFunc := swapNowFunc(nowFunc)
defer func() {
ctxNowFunc = initialNow
swapNowFunc(oldNowFunc)
}()

return fn()
}

func swapNowFunc(newNowFunc func() time.Time) func() time.Time {
ctxNowFuncMutex.Lock()
defer ctxNowFuncMutex.Unlock()

oldNowFunc := ctxNowFunc
ctxNowFunc = newNowFunc
return oldNowFunc
}

func Now() time.Time {
ctxNowFuncMutex.Lock()
defer ctxNowFuncMutex.Unlock()
Expand Down