Skip to content

Commit 483f5cc

Browse files
authored
support to_days() and from_days() (#2456)
1 parent 81cd0b0 commit 483f5cc

File tree

6 files changed

+534
-7
lines changed

6 files changed

+534
-7
lines changed

enginetest/queries/queries.go

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9598,6 +9598,19 @@ from typestable`,
95989598
{3},
95999599
},
96009600
},
9601+
9602+
{
9603+
Query: "select to_days('2024-04-15');",
9604+
Expected: []sql.Row{
9605+
{739356},
9606+
},
9607+
},
9608+
{
9609+
Query: "select from_days(739356);",
9610+
Expected: []sql.Row{
9611+
{time.Date(2024, 4, 15, 0, 0, 0, 0, time.UTC)},
9612+
},
9613+
},
96019614
}
96029615

96039616
var KeylessQueries = []QueryTest{

sql/expression/function/date_test.go

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,18 @@ func TestAddDate(t *testing.T) {
8181
result, err = f.Eval(ctx, sql.Row{"asdasdasd"})
8282
require.NoError(err)
8383
require.Nil(result)
84+
85+
// If the second argument is NOT an interval, then it's assumed to be a day interval
86+
t.Skip("Interval does not handle overflows correctly")
87+
f, err = NewAddDate(
88+
expression.NewLiteral("2018-05-02", types.Text),
89+
expression.NewLiteral(int64(1_000_000), types.Int64))
90+
require.NoError(err)
91+
expected = time.Date(4756, time.March, 29, 0, 0, 0, 0, time.UTC)
92+
result, err = f.Eval(ctx, sql.Row{})
93+
require.NoError(err)
94+
require.Equal(expected, result)
95+
8496
}
8597

8698
func TestDateAdd(t *testing.T) {

sql/expression/function/days.go

Lines changed: 234 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,234 @@
1+
// Copyright 2024 Dolthub, Inc.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package function
16+
17+
import (
18+
"fmt"
19+
"time"
20+
21+
"github.com/dolthub/go-mysql-server/sql"
22+
"github.com/dolthub/go-mysql-server/sql/expression"
23+
"github.com/dolthub/go-mysql-server/sql/types"
24+
)
25+
26+
// ToDays is a function that converts a date to a number of days since year 0.
27+
type ToDays struct {
28+
expression.UnaryExpression
29+
}
30+
31+
var _ sql.FunctionExpression = (*ToDays)(nil)
32+
var _ sql.CollationCoercible = (*ToDays)(nil)
33+
34+
// NewToDays creates a new ToDays function.
35+
func NewToDays(date sql.Expression) sql.Expression {
36+
return &ToDays{expression.UnaryExpression{Child: date}}
37+
}
38+
39+
// CollationCoercibility implements sql.CollationCoercible
40+
func (t *ToDays) CollationCoercibility(ctx *sql.Context) (collation sql.CollationID, coercibility byte) {
41+
return sql.Collation_binary, 5
42+
}
43+
44+
// String implements sql.Stringer
45+
func (t *ToDays) String() string {
46+
return fmt.Sprintf("%s(%s)", t.FunctionName(), t.Child.String())
47+
}
48+
49+
// FunctionName implements sql.FunctionExpression
50+
func (t *ToDays) FunctionName() string {
51+
return "to_days"
52+
}
53+
54+
// Description implements sql.FunctionExpression
55+
func (t *ToDays) Description() string {
56+
return "return the date argument converted to days"
57+
}
58+
59+
// Type implements sql.Expression
60+
func (t *ToDays) Type() sql.Type {
61+
return types.Int64
62+
}
63+
64+
// WithChildren implements sql.Expression
65+
func (t *ToDays) WithChildren(children ...sql.Expression) (sql.Expression, error) {
66+
if len(children) != 1 {
67+
return nil, sql.ErrInvalidChildrenNumber.New(t, len(children), 1)
68+
}
69+
return NewToDays(children[0]), nil
70+
}
71+
72+
// countLeapYears returns the number of leap years between year 0 and the given year
73+
func countLeapYears(year int) int {
74+
if year < 0 {
75+
return 0
76+
}
77+
return year/4 - year/100 + year/400
78+
}
79+
80+
// Eval implements sql.Expression
81+
func (t *ToDays) Eval(ctx *sql.Context, row sql.Row) (interface{}, error) {
82+
date, err := t.Child.Eval(ctx, row)
83+
if err != nil {
84+
return nil, err
85+
}
86+
if date == nil {
87+
return nil, nil
88+
}
89+
90+
// Special case for zero date
91+
if dateStr, isStr := date.(string); isStr && (dateStr == types.ZeroDateStr || dateStr == types.ZeroTimestampDatetimeStr) {
92+
return nil, nil
93+
}
94+
95+
date, _, err = types.Date.Convert(date)
96+
if err != nil {
97+
ctx.Warn(1292, err.Error())
98+
return nil, nil
99+
}
100+
d := date.(time.Time)
101+
102+
// Using zeroTime.Sub(date) doesn't work because it overflows time.Duration
103+
// so we need to calculate the number of days manually
104+
// Additionally, MySQL states that this function isn't really accurate for dates before the year 1582
105+
years := d.Year()
106+
107+
// YearDay includes leap day, so we subtract 1 from years to not count it twice
108+
res := 365*years + countLeapYears(years-1) + d.YearDay()
109+
return res, nil
110+
}
111+
112+
// FromDays is a function that returns date for a given number of days since year 0.
113+
type FromDays struct {
114+
expression.UnaryExpression
115+
}
116+
117+
var _ sql.FunctionExpression = (*FromDays)(nil)
118+
var _ sql.CollationCoercible = (*FromDays)(nil)
119+
120+
// NewFromDays creates a new FromDays function.
121+
func NewFromDays(days sql.Expression) sql.Expression {
122+
return &FromDays{expression.UnaryExpression{Child: days}}
123+
}
124+
125+
// CollationCoercibility implements sql.CollationCoercible
126+
func (f *FromDays) CollationCoercibility(ctx *sql.Context) (collation sql.CollationID, coercibility byte) {
127+
return sql.Collation_binary, 5
128+
}
129+
130+
// String implements sql.Stringer
131+
func (f *FromDays) String() string {
132+
return fmt.Sprintf("%s(%s)", f.FunctionName(), f.Child.String())
133+
}
134+
135+
// FunctionName implements sql.FunctionExpression
136+
func (f *FromDays) FunctionName() string {
137+
return "from_days"
138+
}
139+
140+
// Description implements sql.FunctionExpression
141+
func (f *FromDays) Description() string {
142+
return "convert a day number to a date"
143+
}
144+
145+
// Type implements sql.Expression
146+
func (f *FromDays) Type() sql.Type {
147+
return types.Date
148+
}
149+
150+
// WithChildren implements sql.Expression
151+
func (f *FromDays) WithChildren(children ...sql.Expression) (sql.Expression, error) {
152+
if len(children) != 1 {
153+
return nil, sql.ErrInvalidChildrenNumber.New(f, len(children), 1)
154+
}
155+
return NewFromDays(children[0]), nil
156+
}
157+
158+
const (
159+
DaysPerYear = 365
160+
DaysPer400Years = 400*DaysPerYear + 97
161+
DaysPer100Years = 100*DaysPerYear + 24
162+
DaysPer4Years = 4*DaysPerYear + 1
163+
)
164+
165+
// daysToYear converts a number of days to number of years since year 0 (including leap years), and the remaining days
166+
func daysToYear(days int64) (int64, int64) {
167+
// Special case for year 0, which is not a leap year
168+
years := int64(1)
169+
days -= DaysPerYear
170+
171+
years += 400 * (days / DaysPer400Years)
172+
days %= DaysPer400Years
173+
174+
years += 100 * (days / DaysPer100Years)
175+
days %= DaysPer100Years
176+
177+
years += 4 * (days / DaysPer4Years)
178+
days %= DaysPer4Years
179+
180+
years += days / DaysPerYear
181+
days %= DaysPerYear
182+
183+
return years, days
184+
}
185+
186+
func isLeapYear(year int64) bool {
187+
return year != 0 && ((year%4 == 0 && year%100 != 0) || year%400 == 0)
188+
}
189+
190+
var daysPerMonth = [12]int64{31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31}
191+
192+
// daysToMonth converts a number of days to the month and the remaining days in that month
193+
func daysToMonth(year, days int64) (int64, int64) {
194+
for i, m := range daysPerMonth {
195+
if i == 1 && isLeapYear(year) {
196+
m++ // leap day
197+
}
198+
if days < m {
199+
return int64(i + 1), days
200+
}
201+
days -= m
202+
}
203+
return -1, -1 // should be impossible
204+
}
205+
206+
// Eval implements sql.Expression
207+
func (f *FromDays) Eval(ctx *sql.Context, row sql.Row) (interface{}, error) {
208+
d, err := f.Child.Eval(ctx, row)
209+
if err != nil {
210+
return nil, err
211+
}
212+
if d == nil {
213+
return nil, nil
214+
}
215+
216+
d, _, err = types.Int64.Convert(d)
217+
if err != nil {
218+
ctx.Warn(1292, err.Error())
219+
return "0000-00-00", nil
220+
}
221+
222+
days, ok := d.(int64)
223+
if !ok {
224+
return "0000-00-00", nil
225+
}
226+
227+
// For some reason, MySQL returns 0000-00-00 for days <= 365
228+
if days <= DaysPerYear {
229+
return "0000-00-00", nil
230+
}
231+
years, days := daysToYear(days)
232+
months, days := daysToMonth(years, days)
233+
return time.Date(int(years), time.Month(months), int(days), 0, 0, 0, 0, time.UTC), nil
234+
}

0 commit comments

Comments
 (0)