Add support for Timestamp data type in SQL Select (#7185)
This change adds support for casting strings to Timestamp via CAST: `CAST('2010T' AS TIMESTAMP)` It also implements the following date-time functions: - UTCNOW() - DATE_ADD() - DATE_DIFF() - EXTRACT() For values passed to these functions, date-types are automatically inferred.master
parent
ea6d61ab1f
commit
f04f8bbc78
@ -0,0 +1,199 @@ |
|||||||
|
/* |
||||||
|
* Minio Cloud Storage, (C) 2019 Minio, 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 sql |
||||||
|
|
||||||
|
import ( |
||||||
|
"time" |
||||||
|
) |
||||||
|
|
||||||
|
const ( |
||||||
|
layoutYear = "2006T" |
||||||
|
layoutMonth = "2006-01T" |
||||||
|
layoutDay = "2006-01-02T" |
||||||
|
layoutMinute = "2006-01-02T15:04Z07:00" |
||||||
|
layoutSecond = "2006-01-02T15:04:05Z07:00" |
||||||
|
layoutNanosecond = "2006-01-02T15:04:05.999999999Z07:00" |
||||||
|
) |
||||||
|
|
||||||
|
var ( |
||||||
|
tformats = []string{ |
||||||
|
layoutYear, |
||||||
|
layoutMonth, |
||||||
|
layoutDay, |
||||||
|
layoutMinute, |
||||||
|
layoutSecond, |
||||||
|
layoutNanosecond, |
||||||
|
} |
||||||
|
oneNanoSecond = 1 |
||||||
|
) |
||||||
|
|
||||||
|
func parseSQLTimestamp(s string) (t time.Time, err error) { |
||||||
|
for _, f := range tformats { |
||||||
|
t, err = time.Parse(f, s) |
||||||
|
if err == nil { |
||||||
|
break |
||||||
|
} |
||||||
|
} |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
// FormatSQLTimestamp - returns the a string representation of the
|
||||||
|
// timestamp as used in S3 Select
|
||||||
|
func FormatSQLTimestamp(t time.Time) string { |
||||||
|
_, zoneOffset := t.Zone() |
||||||
|
hasZone := zoneOffset != 0 |
||||||
|
hasFracSecond := t.Nanosecond() != 0 |
||||||
|
hasSecond := t.Second() != 0 |
||||||
|
hasTime := t.Hour() != 0 || t.Minute() != 0 |
||||||
|
hasDay := t.Day() != 1 |
||||||
|
hasMonth := t.Month() != 1 |
||||||
|
|
||||||
|
switch { |
||||||
|
case hasFracSecond: |
||||||
|
return t.Format(layoutNanosecond) |
||||||
|
case hasSecond: |
||||||
|
return t.Format(layoutSecond) |
||||||
|
case hasTime || hasZone: |
||||||
|
return t.Format(layoutMinute) |
||||||
|
case hasDay: |
||||||
|
return t.Format(layoutDay) |
||||||
|
case hasMonth: |
||||||
|
return t.Format(layoutMonth) |
||||||
|
default: |
||||||
|
return t.Format(layoutYear) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
const ( |
||||||
|
timePartYear = "YEAR" |
||||||
|
timePartMonth = "MONTH" |
||||||
|
timePartDay = "DAY" |
||||||
|
timePartHour = "HOUR" |
||||||
|
timePartMinute = "MINUTE" |
||||||
|
timePartSecond = "SECOND" |
||||||
|
timePartTimezoneHour = "TIMEZONE_HOUR" |
||||||
|
timePartTimezoneMinute = "TIMEZONE_MINUTE" |
||||||
|
) |
||||||
|
|
||||||
|
func extract(what string, t time.Time) (v *Value, err error) { |
||||||
|
switch what { |
||||||
|
case timePartYear: |
||||||
|
return FromInt(int64(t.Year())), nil |
||||||
|
case timePartMonth: |
||||||
|
return FromInt(int64(t.Month())), nil |
||||||
|
case timePartDay: |
||||||
|
return FromInt(int64(t.Day())), nil |
||||||
|
case timePartHour: |
||||||
|
return FromInt(int64(t.Hour())), nil |
||||||
|
case timePartMinute: |
||||||
|
return FromInt(int64(t.Minute())), nil |
||||||
|
case timePartSecond: |
||||||
|
return FromInt(int64(t.Second())), nil |
||||||
|
case timePartTimezoneHour: |
||||||
|
_, zoneOffset := t.Zone() |
||||||
|
return FromInt(int64(zoneOffset / 3600)), nil |
||||||
|
case timePartTimezoneMinute: |
||||||
|
_, zoneOffset := t.Zone() |
||||||
|
return FromInt(int64((zoneOffset % 3600) / 60)), nil |
||||||
|
default: |
||||||
|
// This does not happen
|
||||||
|
return nil, errNotImplemented |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
func dateAdd(timePart string, qty float64, t time.Time) (*Value, error) { |
||||||
|
var duration time.Duration |
||||||
|
switch timePart { |
||||||
|
case timePartYear: |
||||||
|
return FromTimestamp(t.AddDate(int(qty), 0, 0)), nil |
||||||
|
case timePartMonth: |
||||||
|
return FromTimestamp(t.AddDate(0, int(qty), 0)), nil |
||||||
|
case timePartDay: |
||||||
|
return FromTimestamp(t.AddDate(0, 0, int(qty))), nil |
||||||
|
case timePartHour: |
||||||
|
duration = time.Duration(qty) * time.Hour |
||||||
|
case timePartMinute: |
||||||
|
duration = time.Duration(qty) * time.Minute |
||||||
|
case timePartSecond: |
||||||
|
duration = time.Duration(qty) * time.Second |
||||||
|
default: |
||||||
|
return nil, errNotImplemented |
||||||
|
} |
||||||
|
return FromTimestamp(t.Add(duration)), nil |
||||||
|
} |
||||||
|
|
||||||
|
const ( |
||||||
|
dayInNanoseconds = time.Hour * 24 |
||||||
|
) |
||||||
|
|
||||||
|
// dateDiff computes the difference between two times in terms of the
|
||||||
|
// `timePart` which can be years, months, days, hours, minutes or
|
||||||
|
// seconds. For difference in years, months or days, the time part,
|
||||||
|
// including timezone is ignored.
|
||||||
|
func dateDiff(timePart string, ts1, ts2 time.Time) (*Value, error) { |
||||||
|
if ts2.Before(ts1) { |
||||||
|
v, err := dateDiff(timePart, ts2, ts1) |
||||||
|
v.negate() |
||||||
|
return v, err |
||||||
|
} |
||||||
|
|
||||||
|
duration := ts2.Sub(ts1) |
||||||
|
y1, m1, d1 := ts1.Date() |
||||||
|
y2, m2, d2 := ts2.Date() |
||||||
|
dy, dm := int64(y2-y1), int64(m2-m1) |
||||||
|
|
||||||
|
switch timePart { |
||||||
|
case timePartYear: |
||||||
|
if m2 > m1 || (m2 == m1 && d2 >= d1) { |
||||||
|
return FromInt(dy), nil |
||||||
|
} |
||||||
|
return FromInt(dy - 1), nil |
||||||
|
case timePartMonth: |
||||||
|
months := 12 * dy |
||||||
|
if m2 >= m1 { |
||||||
|
months += dm |
||||||
|
} else { |
||||||
|
months += 12 + dm |
||||||
|
} |
||||||
|
if d2 < d1 { |
||||||
|
months-- |
||||||
|
} |
||||||
|
return FromInt(months), nil |
||||||
|
case timePartDay: |
||||||
|
// To compute the number of days between two times
|
||||||
|
// using the time package, zero out the time portions
|
||||||
|
// of the timestamps, compute the difference duration
|
||||||
|
// and then divide by the length of a day.
|
||||||
|
d1 := time.Date(y1, m1, d1, 0, 0, 0, 0, time.UTC) |
||||||
|
d2 := time.Date(y2, m2, d2, 0, 0, 0, 0, time.UTC) |
||||||
|
diff := d2.Sub(d1) |
||||||
|
days := diff / dayInNanoseconds |
||||||
|
return FromInt(int64(days)), nil |
||||||
|
case timePartHour: |
||||||
|
hours := duration / time.Hour |
||||||
|
return FromInt(int64(hours)), nil |
||||||
|
case timePartMinute: |
||||||
|
minutes := duration / time.Minute |
||||||
|
return FromInt(int64(minutes)), nil |
||||||
|
case timePartSecond: |
||||||
|
seconds := duration / time.Second |
||||||
|
return FromInt(int64(seconds)), nil |
||||||
|
default: |
||||||
|
|
||||||
|
} |
||||||
|
return nil, errNotImplemented |
||||||
|
} |
@ -0,0 +1,60 @@ |
|||||||
|
/* |
||||||
|
* Minio Cloud Storage, (C) 2019 Minio, 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 sql |
||||||
|
|
||||||
|
import ( |
||||||
|
"testing" |
||||||
|
"time" |
||||||
|
) |
||||||
|
|
||||||
|
func TestParseAndDisplaySQLTimestamp(t *testing.T) { |
||||||
|
beijing := time.FixedZone("", int((8 * time.Hour).Seconds())) |
||||||
|
fakeLosAngeles := time.FixedZone("", -int((8 * time.Hour).Seconds())) |
||||||
|
cases := []struct { |
||||||
|
s string |
||||||
|
t time.Time |
||||||
|
}{ |
||||||
|
{"2010T", time.Date(2010, 1, 1, 0, 0, 0, 0, time.UTC)}, |
||||||
|
{"2010-02T", time.Date(2010, 2, 1, 0, 0, 0, 0, time.UTC)}, |
||||||
|
{"2010-02-03T", time.Date(2010, 2, 3, 0, 0, 0, 0, time.UTC)}, |
||||||
|
{"2010-02-03T04:11Z", time.Date(2010, 2, 3, 4, 11, 0, 0, time.UTC)}, |
||||||
|
{"2010-02-03T04:11:30Z", time.Date(2010, 2, 3, 4, 11, 30, 0, time.UTC)}, |
||||||
|
{"2010-02-03T04:11:30.23Z", time.Date(2010, 2, 3, 4, 11, 30, 230000000, time.UTC)}, |
||||||
|
{"2010-02-03T04:11+08:00", time.Date(2010, 2, 3, 4, 11, 0, 0, beijing)}, |
||||||
|
{"2010-02-03T04:11:30+08:00", time.Date(2010, 2, 3, 4, 11, 30, 0, beijing)}, |
||||||
|
{"2010-02-03T04:11:30.23+08:00", time.Date(2010, 2, 3, 4, 11, 30, 230000000, beijing)}, |
||||||
|
{"2010-02-03T04:11:30-08:00", time.Date(2010, 2, 3, 4, 11, 30, 0, fakeLosAngeles)}, |
||||||
|
{"2010-02-03T04:11:30.23-08:00", time.Date(2010, 2, 3, 4, 11, 30, 230000000, fakeLosAngeles)}, |
||||||
|
} |
||||||
|
for i, tc := range cases { |
||||||
|
tval, err := parseSQLTimestamp(tc.s) |
||||||
|
if err != nil { |
||||||
|
t.Errorf("Case %d: Unexpected error: %v", i+1, err) |
||||||
|
continue |
||||||
|
} |
||||||
|
if !tval.Equal(tc.t) { |
||||||
|
t.Errorf("Case %d: Expected %v got %v", i+1, tc.t, tval) |
||||||
|
continue |
||||||
|
} |
||||||
|
|
||||||
|
tstr := FormatSQLTimestamp(tc.t) |
||||||
|
if tstr != tc.s { |
||||||
|
t.Errorf("Case %d: Expected %s got %s", i+1, tc.s, tstr) |
||||||
|
continue |
||||||
|
} |
||||||
|
} |
||||||
|
} |
Loading…
Reference in new issue