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
13 changes: 13 additions & 0 deletions enginetest/queries/queries.go
Original file line number Diff line number Diff line change
Expand Up @@ -9591,6 +9591,19 @@ from typestable`,
{3},
},
},

{
Query: "select to_days('2024-04-15');",
Expected: []sql.Row{
{739356},
},
},
{
Query: "select from_days(739356);",
Expected: []sql.Row{
{time.Date(2024, 4, 15, 0, 0, 0, 0, time.UTC)},
},
},
}

var KeylessQueries = []QueryTest{
Expand Down
12 changes: 12 additions & 0 deletions sql/expression/function/date_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,18 @@ func TestAddDate(t *testing.T) {
result, err = f.Eval(ctx, sql.Row{"asdasdasd"})
require.NoError(err)
require.Nil(result)

// If the second argument is NOT an interval, then it's assumed to be a day interval
t.Skip("Interval does not handle overflows correctly")
f, err = NewAddDate(
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)
result, err = f.Eval(ctx, sql.Row{})
require.NoError(err)
require.Equal(expected, result)

}

func TestDateAdd(t *testing.T) {
Expand Down
234 changes: 234 additions & 0 deletions sql/expression/function/days.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,234 @@
// Copyright 2024 Dolthub, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package function

import (
"fmt"
"time"

"github.com/dolthub/go-mysql-server/sql"
"github.com/dolthub/go-mysql-server/sql/expression"
"github.com/dolthub/go-mysql-server/sql/types"
)

// ToDays is a function that converts a date to a number of days since year 0.
type ToDays struct {
expression.UnaryExpression
}

var _ sql.FunctionExpression = (*ToDays)(nil)
var _ sql.CollationCoercible = (*ToDays)(nil)

// NewToDays creates a new ToDays function.
func NewToDays(date sql.Expression) sql.Expression {
return &ToDays{expression.UnaryExpression{Child: date}}
}

// CollationCoercibility implements sql.CollationCoercible
func (t *ToDays) CollationCoercibility(ctx *sql.Context) (collation sql.CollationID, coercibility byte) {
return sql.Collation_binary, 5
}

// String implements sql.Stringer
func (t *ToDays) String() string {
return fmt.Sprintf("%s(%s)", t.FunctionName(), t.Child.String())
}

// FunctionName implements sql.FunctionExpression
func (t *ToDays) FunctionName() string {
return "to_days"
}

// Description implements sql.FunctionExpression
func (t *ToDays) Description() string {
return "return the date argument converted to days"
}

// Type implements sql.Expression
func (t *ToDays) Type() sql.Type {
return types.Int64
}

// WithChildren implements sql.Expression
func (t *ToDays) WithChildren(children ...sql.Expression) (sql.Expression, error) {
if len(children) != 1 {
return nil, sql.ErrInvalidChildrenNumber.New(t, len(children), 1)
}
return NewToDays(children[0]), nil
}

// countLeapYears returns the number of leap years between year 0 and the given year
func countLeapYears(year int) int {
if year < 0 {
return 0
}
return year/4 - year/100 + year/400
}

// Eval implements sql.Expression
func (t *ToDays) Eval(ctx *sql.Context, row sql.Row) (interface{}, error) {
date, err := t.Child.Eval(ctx, row)
if err != nil {
return nil, err
}
if date == nil {
return nil, nil
}

// Special case for zero date
if dateStr, isStr := date.(string); isStr && (dateStr == types.ZeroDateStr || dateStr == types.ZeroTimestampDatetimeStr) {
return nil, nil
}

date, _, err = types.Date.Convert(date)
if err != nil {
ctx.Warn(1292, err.Error())
return nil, nil
}
d := date.(time.Time)

// Using zeroTime.Sub(date) doesn't work because it overflows time.Duration
// so we need to calculate the number of days manually
// Additionally, MySQL states that this function isn't really accurate for dates before the year 1582
years := d.Year()

// YearDay includes leap day, so we subtract 1 from years to not count it twice
res := 365*years + countLeapYears(years-1) + d.YearDay()
return res, nil
}

// FromDays is a function that returns date for a given number of days since year 0.
type FromDays struct {
expression.UnaryExpression
}

var _ sql.FunctionExpression = (*FromDays)(nil)
var _ sql.CollationCoercible = (*FromDays)(nil)

// NewFromDays creates a new FromDays function.
func NewFromDays(days sql.Expression) sql.Expression {
return &FromDays{expression.UnaryExpression{Child: days}}
}

// CollationCoercibility implements sql.CollationCoercible
func (f *FromDays) CollationCoercibility(ctx *sql.Context) (collation sql.CollationID, coercibility byte) {
return sql.Collation_binary, 5
}

// String implements sql.Stringer
func (f *FromDays) String() string {
return fmt.Sprintf("%s(%s)", f.FunctionName(), f.Child.String())
}

// FunctionName implements sql.FunctionExpression
func (f *FromDays) FunctionName() string {
return "from_days"
}

// Description implements sql.FunctionExpression
func (f *FromDays) Description() string {
return "convert a day number to a date"
}

// Type implements sql.Expression
func (f *FromDays) Type() sql.Type {
return types.Date
}

// WithChildren implements sql.Expression
func (f *FromDays) WithChildren(children ...sql.Expression) (sql.Expression, error) {
if len(children) != 1 {
return nil, sql.ErrInvalidChildrenNumber.New(f, len(children), 1)
}
return NewFromDays(children[0]), nil
}

const (
DaysPerYear = 365
DaysPer400Years = 400*DaysPerYear + 97
DaysPer100Years = 100*DaysPerYear + 24
DaysPer4Years = 4*DaysPerYear + 1
)

// daysToYear converts a number of days to number of years since year 0 (including leap years), and the remaining days
func daysToYear(days int64) (int64, int64) {
// Special case for year 0, which is not a leap year
years := int64(1)
days -= DaysPerYear

years += 400 * (days / DaysPer400Years)
days %= DaysPer400Years

years += 100 * (days / DaysPer100Years)
days %= DaysPer100Years

years += 4 * (days / DaysPer4Years)
days %= DaysPer4Years

years += days / DaysPerYear
days %= DaysPerYear

return years, days
}

func isLeapYear(year int64) bool {
return year != 0 && ((year%4 == 0 && year%100 != 0) || year%400 == 0)
}

var daysPerMonth = [12]int64{31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31}

// daysToMonth converts a number of days to the month and the remaining days in that month
func daysToMonth(year, days int64) (int64, int64) {
for i, m := range daysPerMonth {
if i == 1 && isLeapYear(year) {
m++ // leap day
}
if days < m {
return int64(i + 1), days
}
days -= m
}
return -1, -1 // should be impossible
}

// Eval implements sql.Expression
func (f *FromDays) Eval(ctx *sql.Context, row sql.Row) (interface{}, error) {
d, err := f.Child.Eval(ctx, row)
if err != nil {
return nil, err
}
if d == nil {
return nil, nil
}

d, _, err = types.Int64.Convert(d)
if err != nil {
ctx.Warn(1292, err.Error())
return "0000-00-00", nil
}

days, ok := d.(int64)
if !ok {
return "0000-00-00", nil
}

// For some reason, MySQL returns 0000-00-00 for days <= 365
if days <= DaysPerYear {
return "0000-00-00", nil
}
years, days := daysToYear(days)
months, days := daysToMonth(years, days)
return time.Date(int(years), time.Month(months), int(days), 0, 0, 0, 0, time.UTC), nil
}
Loading