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