Skip to content

Commit

Permalink
builtins: add extract functionality for intervals
Browse files Browse the repository at this point in the history
This adds the postgres-esque implementation of extract for intervals.
Tested a variety of edge cases and compared against what postgres
returns.

Currently leaving the `extract_duration` function alone - we could
thereoetically get rid of it after this PR. It does behave slightly
differently as well.

Release note (sql change): Added new ability to call extract on an
interval, e.g. `extract(day from interval '254 days)`. This follows the
postgres implementation.
  • Loading branch information
otan committed Dec 20, 2019
1 parent 2f1e342 commit 17a344f
Show file tree
Hide file tree
Showing 4 changed files with 184 additions and 23 deletions.
4 changes: 4 additions & 0 deletions docs/generated/sql/functions.md
Original file line number Diff line number Diff line change
Expand Up @@ -368,6 +368,10 @@ return without an error.</p>
quarter, month, week, dayofweek, isodow, dayofyear, julian,
hour, minute, second, millisecond, microsecond, epoch</p>
</span></td></tr>
<tr><td><a name="extract"></a><code>extract(element: <a href="string.html">string</a>, input: <a href="interval.html">interval</a>) &rarr; <a href="float.html">float</a></code></td><td><span class="funcdesc"><p>Extracts <code>element</code> from <code>input</code>.</p>
<p>Compatible elements: millennium, century, decade, year,
month, day, hour, minute, second, millisecond, microsecond, epoch</p>
</span></td></tr>
<tr><td><a name="extract"></a><code>extract(element: <a href="string.html">string</a>, input: <a href="time.html">time</a>) &rarr; <a href="float.html">float</a></code></td><td><span class="funcdesc"><p>Extracts <code>element</code> from <code>input</code>.</p>
<p>Compatible elements: hour, minute, second, millisecond, microsecond, epoch</p>
</span></td></tr>
Expand Down
12 changes: 12 additions & 0 deletions pkg/sql/logictest/testdata/logic_test/interval
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,18 @@ select * from interval_duration_type order by id asc
2 12:56:07.40736 12:56:07.407 12:56:07.40736 12:56:07.407 12:56:00 12:56:07.407
3 366 days 12:34:56.123456 366 days 12:34:56.123 366 days 12:34:56.123456 366 days 12:34:56.123 366 days 12:34:00 366 days 12:34:56.123

subtest interval_extract_tests

query R
SELECT extract('second', interval '10:55:01.456')
----
1.456

query R
SELECT extract(minute from interval '10:55:01.456')
----
55

# tests various typmods of intervals
# matches subset of tests in src/test/regress/expected/interval.out
subtest interval_postgres_duration_type_tests
Expand Down
112 changes: 93 additions & 19 deletions pkg/sql/sem/builtins/builtins.go
Original file line number Diff line number Diff line change
Expand Up @@ -1821,13 +1821,25 @@ may increase either contention or retry errors, or both.`,
// extract timeSpan fromTime.
fromTS := args[1].(*tree.DTimestamp)
timeSpan := strings.ToLower(string(tree.MustBeDString(args[0])))
return extractStringFromTimestamp(ctx, fromTS.Time, timeSpan)
return extractTimeSpanFromTimestamp(ctx, fromTS.Time, timeSpan)
},
Info: "Extracts `element` from `input`.\n\n" +
"Compatible elements: millennium, century, decade, year, isoyear,\n" +
"quarter, month, week, dayofweek, isodow, dayofyear, julian,\n" +
"hour, minute, second, millisecond, microsecond, epoch",
},
tree.Overload{
Types: tree.ArgTypes{{"element", types.String}, {"input", types.Interval}},
ReturnType: tree.FixedReturnType(types.Float),
Fn: func(ctx *tree.EvalContext, args tree.Datums) (tree.Datum, error) {
fromInterval := args[1].(*tree.DInterval)
timeSpan := strings.ToLower(string(tree.MustBeDString(args[0])))
return extractTimeSpanFromInterval(fromInterval, timeSpan)
},
Info: "Extracts `element` from `input`.\n\n" +
"Compatible elements: millennium, century, decade, year,\n" +
"month, day, hour, minute, second, millisecond, microsecond, epoch",
},
tree.Overload{
Types: tree.ArgTypes{{"element", types.String}, {"input", types.Date}},
ReturnType: tree.FixedReturnType(types.Float),
Expand All @@ -1838,7 +1850,7 @@ may increase either contention or retry errors, or both.`,
if err != nil {
return nil, err
}
return extractStringFromTimestamp(ctx, fromTime, timeSpan)
return extractTimeSpanFromTimestamp(ctx, fromTime, timeSpan)
},
Info: "Extracts `element` from `input`.\n\n" +
"Compatible elements: millennium, century, decade, year, isoyear,\n" +
Expand All @@ -1851,7 +1863,7 @@ may increase either contention or retry errors, or both.`,
Fn: func(ctx *tree.EvalContext, args tree.Datums) (tree.Datum, error) {
fromTSTZ := args[1].(*tree.DTimestampTZ)
timeSpan := strings.ToLower(string(tree.MustBeDString(args[0])))
return extractStringFromTimestampTZ(ctx, fromTSTZ.Time.In(ctx.GetLocation()), timeSpan)
return extractTimeSpanFromTimestampTZ(ctx, fromTSTZ.Time.In(ctx.GetLocation()), timeSpan)
},
Info: "Extracts `element` from `input`.\n\n" +
"Compatible elements: millennium, century, decade, year, isoyear,\n" +
Expand All @@ -1865,7 +1877,7 @@ may increase either contention or retry errors, or both.`,
Fn: func(ctx *tree.EvalContext, args tree.Datums) (tree.Datum, error) {
fromTime := args[1].(*tree.DTime)
timeSpan := strings.ToLower(string(tree.MustBeDString(args[0])))
return extractStringFromTime(fromTime, timeSpan)
return extractTimeSpanFromTime(fromTime, timeSpan)
},
Info: "Extracts `element` from `input`.\n\n" +
"Compatible elements: hour, minute, second, millisecond, microsecond, epoch",
Expand All @@ -1876,7 +1888,7 @@ may increase either contention or retry errors, or both.`,
Fn: func(ctx *tree.EvalContext, args tree.Datums) (tree.Datum, error) {
fromTime := args[1].(*tree.DTimeTZ)
timeSpan := strings.ToLower(string(tree.MustBeDString(args[0])))
return extractStringFromTimeTZ(fromTime, timeSpan)
return extractTimeSpanFromTimeTZ(fromTime, timeSpan)
},
Info: "Extracts `element` from `input`.\n\n" +
"Compatible elements: hour, minute, second, millisecond, microsecond, epoch,\n" +
Expand Down Expand Up @@ -4612,15 +4624,24 @@ func arrayLower(arr *tree.DArray, dim int64) tree.Datum {
return arrayLower(a, dim-1)
}

const microsPerMilli = 1000
const millisPerSec = 1000
const secsPerHour = 3600
const secsPerMinute = 60
const secsPerDay = 86400
const (
microsPerMilli = 1000
millisPerSec = 1000
secsPerHour = 3600
secsPerMinute = 60
secsPerDay = 86400
// daysPerMonth is assumed to be 30.
// matches DAYS_PER_MONTH in postgres.
daysPerMonth = 30
// daysPerYear assumed to be a quarter of a leap year.
// matches DAYS_PER_YEAR in postgres.
daysPerYear = 365.25
monthsPerYear = 12
)

func extractStringFromTime(fromTime *tree.DTime, timeSpan string) (tree.Datum, error) {
func extractTimeSpanFromTime(fromTime *tree.DTime, timeSpan string) (tree.Datum, error) {
t := timeofday.TimeOfDay(*fromTime)
return extractStringFromTimeOfDay(t, timeSpan)
return extractTimeSpanFromTimeOfDay(t, timeSpan)
}

func extractTimezoneFromOffset(offsetSecs int32, timeSpan string) tree.Datum {
Expand All @@ -4637,7 +4658,7 @@ func extractTimezoneFromOffset(offsetSecs int32, timeSpan string) tree.Datum {
return nil
}

func extractStringFromTimeTZ(fromTime *tree.DTimeTZ, timeSpan string) (tree.Datum, error) {
func extractTimeSpanFromTimeTZ(fromTime *tree.DTimeTZ, timeSpan string) (tree.Datum, error) {
if ret := extractTimezoneFromOffset(-fromTime.OffsetSecs, timeSpan); ret != nil {
return ret, nil
}
Expand All @@ -4647,10 +4668,10 @@ func extractStringFromTimeTZ(fromTime *tree.DTimeTZ, timeSpan string) (tree.Datu
seconds := float64(time.Duration(fromTime.TimeOfDay))*float64(time.Microsecond)/float64(time.Second) + float64(fromTime.OffsetSecs)
return tree.NewDFloat(tree.DFloat(seconds)), nil
}
return extractStringFromTimeOfDay(fromTime.TimeOfDay, timeSpan)
return extractTimeSpanFromTimeOfDay(fromTime.TimeOfDay, timeSpan)
}

func extractStringFromTimeOfDay(t timeofday.TimeOfDay, timeSpan string) (tree.Datum, error) {
func extractTimeSpanFromTimeOfDay(t timeofday.TimeOfDay, timeSpan string) (tree.Datum, error) {
switch timeSpan {
case "hour", "hours":
return tree.NewDFloat(tree.DFloat(t.Hour())), nil
Expand Down Expand Up @@ -4689,7 +4710,7 @@ func dateToJulianDay(year int, month int, day int) int {
return jd
}

func extractStringFromTimestampTZ(
func extractTimeSpanFromTimestampTZ(
ctx *tree.EvalContext, fromTime time.Time, timeSpan string,
) (tree.Datum, error) {
_, offsetSecs := fromTime.Zone()
Expand All @@ -4700,12 +4721,65 @@ func extractStringFromTimestampTZ(
// time.Time's Year(), Month(), Day(), ISOWeek(), etc. all deal in terms
// of UTC, rather than as the timezone.
// Remedy this by assuming that the timezone is UTC (to prevent confusion)
// and offsetting time when using extractStringFromTimestamp.
// and offsetting time when using extractTimeSpanFromTimestamp.
pretendTime := fromTime.In(time.UTC).Add(time.Duration(offsetSecs) * time.Second)
return extractStringFromTimestamp(ctx, pretendTime, timeSpan)
return extractTimeSpanFromTimestamp(ctx, pretendTime, timeSpan)
}

func extractTimeSpanFromInterval(
fromInterval *tree.DInterval, timeSpan string,
) (tree.Datum, error) {
switch timeSpan {
case "millennia", "millennium", "millenniums":
return tree.NewDFloat(tree.DFloat(fromInterval.Months / (monthsPerYear * 1000))), nil

case "centuries", "century":
return tree.NewDFloat(tree.DFloat(fromInterval.Months / (monthsPerYear * 100))), nil

case "decade", "decades":
return tree.NewDFloat(tree.DFloat(fromInterval.Months / (monthsPerYear * 10))), nil

case "year", "years":
return tree.NewDFloat(tree.DFloat(fromInterval.Months / monthsPerYear)), nil

case "month", "months":
return tree.NewDFloat(tree.DFloat(fromInterval.Months % monthsPerYear)), nil

case "day", "days":
return tree.NewDFloat(tree.DFloat(fromInterval.Days)), nil

case "hour", "hours":
return tree.NewDFloat(tree.DFloat(fromInterval.Nanos() / int64(time.Hour))), nil

case "minute", "minutes":
// Remove the hour component.
return tree.NewDFloat(tree.DFloat((fromInterval.Nanos() % int64(time.Second*secsPerHour)) / int64(time.Minute))), nil

case "second", "seconds":
return tree.NewDFloat(tree.DFloat(float64(fromInterval.Nanos()%int64(time.Minute)) / float64(time.Second))), nil

case "millisecond", "milliseconds":
// This a PG extension not supported in MySQL.
return tree.NewDFloat(tree.DFloat(float64(fromInterval.Nanos()%int64(time.Minute)) / float64(time.Millisecond))), nil

case "microsecond", "microseconds":
return tree.NewDFloat(tree.DFloat(float64(fromInterval.Nanos()%int64(time.Minute)) / float64(time.Microsecond))), nil
case "epoch":
numYears := fromInterval.Months / 12
monthsModYear := fromInterval.Months % 12
return tree.NewDFloat(tree.DFloat(
(float64(fromInterval.Nanos()) / float64(time.Second)) +
float64(fromInterval.Days*secsPerDay) +
float64(numYears*secsPerDay)*daysPerYear +
float64(monthsModYear*daysPerMonth*secsPerDay),
)), nil
default:
return nil, pgerror.Newf(
pgcode.InvalidParameterValue, "unsupported timespan: %s", timeSpan)
}
}

func extractStringFromTimestamp(
func extractTimeSpanFromTimestamp(
_ *tree.EvalContext, fromTime time.Time, timeSpan string,
) (tree.Datum, error) {
switch timeSpan {
Expand Down
79 changes: 75 additions & 4 deletions pkg/sql/sem/builtins/builtins_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -246,7 +246,7 @@ func TestLPadRPad(t *testing.T) {
}
}

func TestExtractStringFromTimestamp(t *testing.T) {
func TestExtractTimeSpanFromTimestamp(t *testing.T) {
defer leaktest.AfterTest(t)()

utcPositiveOffset := time.FixedZone("otan happy time", 60*60*4+30*60)
Expand Down Expand Up @@ -309,7 +309,7 @@ func TestExtractStringFromTimestamp(t *testing.T) {

for _, tc := range testCases {
t.Run(fmt.Sprintf("%s_%s", tc.timeSpan, tc.input.Format(time.RFC3339)), func(t *testing.T) {
datum, err := extractStringFromTimestampTZ(nil, tc.input, tc.timeSpan)
datum, err := extractTimeSpanFromTimestampTZ(nil, tc.input, tc.timeSpan)
if tc.expectedError != "" {
assert.EqualError(t, err, tc.expectedError)
} else {
Expand All @@ -320,7 +320,7 @@ func TestExtractStringFromTimestamp(t *testing.T) {
}
}

func TestExtractStringFromTimeTZ(t *testing.T) {
func TestExtractTimeSpanFromTimeTZ(t *testing.T) {
defer leaktest.AfterTest(t)()

testCases := []struct {
Expand Down Expand Up @@ -351,7 +351,7 @@ func TestExtractStringFromTimeTZ(t *testing.T) {
timeTZ, err := tree.ParseDTimeTZ(nil, tc.timeTZString, time.Microsecond)
assert.NoError(t, err)

datum, err := extractStringFromTimeTZ(timeTZ, tc.timeSpan)
datum, err := extractTimeSpanFromTimeTZ(timeTZ, tc.timeSpan)
if tc.expectedError != "" {
assert.EqualError(t, err, tc.expectedError)
} else {
Expand All @@ -362,6 +362,77 @@ func TestExtractStringFromTimeTZ(t *testing.T) {
}
}

func TestExtractTimeSpanFromInterval(t *testing.T) {
defer leaktest.AfterTest(t)()

testCases := []struct {
timeSpan string
intervalStr string
expected *tree.DFloat
}{
{"millennia", "25000 months 1000 days", tree.NewDFloat(2)},
{"millennia", "-25000 months 1000 days", tree.NewDFloat(-2)},
{"millennium", "25000 months 1000 days", tree.NewDFloat(2)},
{"millenniums", "25000 months 1000 days", tree.NewDFloat(2)},

{"century", "25000 months 1000 days", tree.NewDFloat(20)},
{"century", "-25000 months 1000 days", tree.NewDFloat(-20)},
{"centuries", "25000 months 1000 days", tree.NewDFloat(20)},

{"decade", "25000 months 1000 days", tree.NewDFloat(208)},
{"decade", "-25000 months 1000 days", tree.NewDFloat(-208)},
{"decades", "25000 months 1000 days", tree.NewDFloat(208)},

{"year", "25000 months 1000 days", tree.NewDFloat(2083)},
{"year", "-25000 months 1000 days", tree.NewDFloat(-2083)},
{"years", "25000 months 1000 days", tree.NewDFloat(2083)},

{"month", "25000 months 1000 days", tree.NewDFloat(4)},
{"month", "-25000 months 1000 days", tree.NewDFloat(-4)},
{"months", "25000 months 1000 days", tree.NewDFloat(4)},

{"day", "25000 months 1000 days", tree.NewDFloat(1000)},
{"day", "-25000 months 1000 days", tree.NewDFloat(1000)},
{"day", "-25000 months -1000 days", tree.NewDFloat(-1000)},
{"days", "25000 months 1000 days", tree.NewDFloat(1000)},

{"hour", "25-1 100:56:01.123456", tree.NewDFloat(100)},
{"hour", "25-1 -100:56:01.123456", tree.NewDFloat(-100)},
{"hours", "25-1 100:56:01.123456", tree.NewDFloat(100)},

{"minute", "25-1 100:56:01.123456", tree.NewDFloat(56)},
{"minute", "25-1 -100:56:01.123456", tree.NewDFloat(-56)},
{"minutes", "25-1 100:56:01.123456", tree.NewDFloat(56)},

{"second", "25-1 100:56:01.123456", tree.NewDFloat(1.123456)},
{"second", "25-1 -100:56:01.123456", tree.NewDFloat(-1.123456)},
{"seconds", "25-1 100:56:01.123456", tree.NewDFloat(1.123456)},

{"millisecond", "25-1 100:56:01.123456", tree.NewDFloat(1123.456)},
{"millisecond", "25-1 -100:56:01.123456", tree.NewDFloat(-1123.456)},
{"milliseconds", "25-1 100:56:01.123456", tree.NewDFloat(1123.456)},

{"microsecond", "25-1 100:56:01.123456", tree.NewDFloat(1123456)},
{"microsecond", "25-1 -100:56:01.123456", tree.NewDFloat(-1123456)},
{"microseconds", "25-1 100:56:01.123456", tree.NewDFloat(1123456)},

{"epoch", "25-1 100:56:01.123456", tree.NewDFloat(791895361.123456)},
{"epoch", "25-1 -100:56:01.123456", tree.NewDFloat(791168638.876544)},
}

for _, tc := range testCases {
t.Run(fmt.Sprintf("%s as %s", tc.intervalStr, tc.timeSpan), func(t *testing.T) {
interval, err := tree.ParseDInterval(tc.intervalStr)
assert.NoError(t, err)

d, err := extractTimeSpanFromInterval(interval, tc.timeSpan)
assert.NoError(t, err)

assert.Equal(t, *tc.expected, *(d.(*tree.DFloat)))
})
}
}

func TestTruncateTimestamp(t *testing.T) {
defer leaktest.AfterTest(t)()

Expand Down

0 comments on commit 17a344f

Please sign in to comment.