Skip to content

Commit

Permalink
Merge pull request #74 from liquidata-inc/zachmu/datefns
Browse files Browse the repository at this point in the history
Support for DATETIME and TIMESTAMP functions, and support for evaluating function expressions in an AS OF clause.
  • Loading branch information
zachmu committed Mar 21, 2020
2 parents af60eac + a173b49 commit e575add
Show file tree
Hide file tree
Showing 5 changed files with 175 additions and 27 deletions.
16 changes: 16 additions & 0 deletions engine_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1388,6 +1388,14 @@ var queries = []queryTest{
"SELECT CONVERT('9999-12-31 23:59:59', DATETIME)",
[]sql.Row{{time.Date(9999, time.December, 31, 23, 59, 59, 0, time.UTC)}},
},
{
"SELECT DATETIME('9999-12-31 23:59:59')",
[]sql.Row{{time.Date(9999, time.December, 31, 23, 59, 59, 0, time.UTC)}},
},
{
"SELECT TIMESTAMP('2020-12-31 23:59:59')",
[]sql.Row{{time.Date(2020, time.December, 31, 23, 59, 59, 0, time.UTC)}},
},
{
"SELECT CONVERT('10000-12-31 23:59:59', DATETIME)",
[]sql.Row{{nil}},
Expand Down Expand Up @@ -1532,6 +1540,14 @@ var queries = []queryTest{
`SELECT NOW() - NOW()`,
[]sql.Row{{int64(0)}},
},
{
`SELECT DATETIME() - NOW()`,
[]sql.Row{{int64(0)}},
},
{
`SELECT TIMESTAMP() - NOW()`,
[]sql.Row{{int64(0)}},
},
{
`SELECT NOW() - (NOW() - INTERVAL 1 SECOND)`,
[]sql.Row{{int64(1)}},
Expand Down
56 changes: 30 additions & 26 deletions sql/analyzer/resolve_functions.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,31 +17,35 @@ func resolveFunctions(ctx *sql.Context, a *Analyzer, n sql.Node) (sql.Node, erro
return n, nil
}

return plan.TransformExpressionsUp(n, func(e sql.Expression) (sql.Expression, error) {
a.Log("transforming expression of type: %T", e)
if e.Resolved() {
return e, nil
}

uf, ok := e.(*expression.UnresolvedFunction)
if !ok {
return e, nil
}

n := uf.Name()
f, err := a.Catalog.Function(n)
if err != nil {
return nil, err
}

rf, err := f.Call(uf.Arguments...)
if err != nil {
return nil, err
}

a.Log("resolved function %q", n)

return rf, nil
})
return plan.TransformExpressionsUp(n, resolveFunctionsInExpr(a))
})
}

func resolveFunctionsInExpr(a *Analyzer) sql.TransformExprFunc {
return func(e sql.Expression) (sql.Expression, error) {
a.Log("transforming expression of type: %T", e)
if e.Resolved() {
return e, nil
}

uf, ok := e.(*expression.UnresolvedFunction)
if !ok {
return e, nil
}

n := uf.Name()
f, err := a.Catalog.Function(n)
if err != nil {
return nil, err
}

rf, err := f.Call(uf.Arguments...)
if err != nil {
return nil, err
}

a.Log("resolved function %q", n)

return rf, nil
}
}
11 changes: 10 additions & 1 deletion sql/analyzer/resolve_tables.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package analyzer
import (
"github.com/src-d/go-mysql-server/memory"
"github.com/src-d/go-mysql-server/sql"
"github.com/src-d/go-mysql-server/sql/expression"
"github.com/src-d/go-mysql-server/sql/plan"
)

Expand Down Expand Up @@ -39,7 +40,15 @@ func resolveTables(ctx *sql.Context, a *Analyzer, n sql.Node) (sql.Node, error)
}

if t.AsOf != nil {
asOf, err := t.AsOf.Eval(ctx, nil)
// This is necessary to use functions in AS OF expressions. Because function resolution happens after table
// resolution, we resolve any functions in the AsOf here in order to evaluate them immediately. A better solution
// might be to defer evaluating the expression until later in the analysis, but that requires bigger changes.
asOfExpr, err := expression.TransformUp(t.AsOf, resolveFunctionsInExpr(a))
if err != nil {
return nil, err
}

asOf, err := asOfExpr.Eval(ctx, nil)
if err != nil {
return nil, err
}
Expand Down
117 changes: 117 additions & 0 deletions sql/expression/function/date.go
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,123 @@ func (d *DateSub) String() string {
return fmt.Sprintf("DATE_SUB(%s, %s)", d.Date, d.Interval)
}

// TimestampConversion is a shorthand function for CONVERT(expr, TIMESTAMP)
type TimestampConversion struct {
clock clock
Date sql.Expression
}

func (t *TimestampConversion) Resolved() bool {
return t.Date == nil || t.Date.Resolved()
}

func (t *TimestampConversion) String() string {
if t.Date != nil {
return fmt.Sprintf("TIMESTAMP(%s)", t.Date)
} else {
return "TIMESTAMP()"
}}

func (t *TimestampConversion) Type() sql.Type {
return sql.Timestamp
}

func (t *TimestampConversion) IsNullable() bool {
return false
}

func (t *TimestampConversion) Eval(ctx *sql.Context, r sql.Row) (interface{}, error) {
if t.Date == nil {
return sql.Timestamp.Convert(t.clock())
}

e, err := t.Date.Eval(ctx, r)
if err != nil {
return nil, err
}
return sql.Timestamp.Convert(e)
}

func (t *TimestampConversion) Children() []sql.Expression {
if t.Date == nil {
return nil
}
return []sql.Expression{t.Date}
}

func (t *TimestampConversion) WithChildren(children ...sql.Expression) (sql.Expression, error) {
return NewTimestamp(children...)
}

func NewTimestamp(args ...sql.Expression) (sql.Expression, error) {
if len(args) > 1 {
return nil, sql.ErrInvalidArgumentNumber.New("TIMESTAMP", 1, len(args))
}
if len(args) == 0 {
return &TimestampConversion{clock: defaultClock}, nil
}
return &TimestampConversion{defaultClock, args[0]}, nil
}

// DatetimeConversion is a shorthand function for CONVERT(expr, DATETIME)
type DatetimeConversion struct {
clock clock
Date sql.Expression
}

func (t *DatetimeConversion) Resolved() bool {
return t.Date == nil || t.Date.Resolved()
}

func (t *DatetimeConversion) String() string {
if t.Date != nil {
return fmt.Sprintf("DATETIME(%s)", t.Date)
} else {
return "DATETIME()"
}
}

func (t *DatetimeConversion) Type() sql.Type {
return sql.Datetime
}

func (t *DatetimeConversion) IsNullable() bool {
return false
}

func (t *DatetimeConversion) Eval(ctx *sql.Context, r sql.Row) (interface{}, error) {
if t.Date == nil {
return sql.Datetime.Convert(t.clock())
}

e, err := t.Date.Eval(ctx, r)
if err != nil {
return nil, err
}
return sql.Datetime.Convert(e)
}

func (t *DatetimeConversion) Children() []sql.Expression {
if t.Date == nil {
return nil
}
return []sql.Expression{t.Date}
}

func (t *DatetimeConversion) WithChildren(children ...sql.Expression) (sql.Expression, error) {
return NewDatetime(children...)
}

func NewDatetime(args ...sql.Expression) (sql.Expression, error) {
if len(args) > 1 {
return nil, sql.ErrInvalidArgumentNumber.New("DATETIME", 1, len(args))
}
if len(args) == 0 {
return &DatetimeConversion{clock: defaultClock}, nil
}
return &DatetimeConversion{defaultClock, args[0]}, nil
}

// UnixTimestamp converts the argument to the number of seconds since
// 1970-01-01 00:00:00 UTC. With no argument, returns number of seconds since
// unix epoch for the current time.
Expand Down
2 changes: 2 additions & 0 deletions sql/expression/function/registry.go
Original file line number Diff line number Diff line change
Expand Up @@ -83,5 +83,7 @@ var Defaults = []sql.Function{
sql.FunctionN{Name: "substr", Fn: NewSubstring},
sql.FunctionN{Name: "substring", Fn: NewSubstring},
sql.FunctionN{Name: "unix_timestamp", Fn: NewUnixTimestamp},
sql.FunctionN{Name: "timestamp", Fn: NewTimestamp},
sql.FunctionN{Name: "datetime", Fn: NewDatetime},
sql.FunctionN{Name: "yearweek", Fn: NewYearWeek},
}

0 comments on commit e575add

Please sign in to comment.