Skip to content

Commit

Permalink
sql: implement datetime builtins
Browse files Browse the repository at this point in the history
Previously, `make_date`, `make_timestamp`, and `make_timestamptz`
built-ins were not implemented. In addition, a new signature for
`date_trunc` was not implemented. This was inadequate because it caused
an incompatibility issue for tools that needed these datetime functions.
To address this, this patch adds said datetime built-ins.

Epic: none
Fixes: #108448

Release note (sql change): Datetime built-ins (make_date,
make_timestamp, and make_timestamptz) are implemented - allowing for
the creation of timestamps, timestamps with time zones, and dates. In
addition, date_trunc now allows for a timestamp to be truncated in a
provided timezone (to a provided precision).
  • Loading branch information
annrpom committed Aug 21, 2023
1 parent f3c92a0 commit bb1c653
Show file tree
Hide file tree
Showing 4 changed files with 205 additions and 1 deletion.
13 changes: 13 additions & 0 deletions docs/generated/sql/functions.md
Expand Up @@ -521,6 +521,11 @@ significant than <code>element</code> to zero (or one, for day and month)</p>
<p>Compatible elements: millennium, century, decade, year, quarter, month,
week, day, hour, minute, second, millisecond, microsecond.</p>
</span></td><td>Stable</td></tr>
<tr><td><a name="date_trunc"></a><code>date_trunc(element: <a href="string.html">string</a>, input: <a href="timestamp.html">timestamptz</a>, timezone: <a href="string.html">string</a>) &rarr; <a href="timestamp.html">timestamptz</a></code></td><td><span class="funcdesc"><p>Truncates <code>input</code> to precision <code>element</code> in the specified <code>timezone</code>. Sets all fields that are less
significant than <code>element</code> to zero (or one, for day and month)</p>
<p>Compatible elements: millennium, century, decade, year, quarter, month,
week, day, hour, minute, second, millisecond, microsecond.</p>
</span></td><td>Stable</td></tr>
<tr><td><a name="experimental_follower_read_timestamp"></a><code>experimental_follower_read_timestamp() &rarr; <a href="timestamp.html">timestamptz</a></code></td><td><span class="funcdesc"><p>Same as follower_read_timestamp. This name is deprecated.</p>
</span></td><td>Volatile</td></tr>
<tr><td><a name="experimental_strftime"></a><code>experimental_strftime(input: <a href="date.html">date</a>, extract_format: <a href="string.html">string</a>) &rarr; <a href="string.html">string</a></code></td><td><span class="funcdesc"><p>From <code>input</code>, extracts and formats the time as identified in <code>extract_format</code> using standard <code>strftime</code> notation (though not all formatting is supported).</p>
Expand Down Expand Up @@ -605,6 +610,14 @@ has no relationship with the commit order of concurrent transactions.</p>
and which stays constant throughout the transaction. This timestamp
has no relationship with the commit order of concurrent transactions.</p>
</span></td><td>Stable</td></tr>
<tr><td><a name="make_date"></a><code>make_date(year: <a href="int.html">int</a>, month: <a href="int.html">int</a>, day: <a href="int.html">int</a>) &rarr; <a href="date.html">date</a></code></td><td><span class="funcdesc"><p>Create date from year, month, and day fields (negative years signify BC).</p>
</span></td><td>Stable</td></tr>
<tr><td><a name="make_timestamp"></a><code>make_timestamp(year: <a href="int.html">int</a>, month: <a href="int.html">int</a>, day: <a href="int.html">int</a>, hour: <a href="int.html">int</a>, min: <a href="int.html">int</a>, sec: <a href="float.html">float</a>) &rarr; <a href="timestamp.html">timestamp</a></code></td><td><span class="funcdesc"><p>Create timestampfrom year, month, day, hour, minute, and seconds fields (negative years signify BC).</p>
</span></td><td>Stable</td></tr>
<tr><td><a name="make_timestamptz"></a><code>make_timestamptz(year: <a href="int.html">int</a>, month: <a href="int.html">int</a>, day: <a href="int.html">int</a>, hour: <a href="int.html">int</a>, min: <a href="int.html">int</a>, sec: <a href="float.html">float</a>) &rarr; <a href="timestamp.html">timestamptz</a></code></td><td><span class="funcdesc"><p>Create timestampwith time zone from year, month, day, hour, minute and seconds fields (negative years signify BC). If timezone is not specified, the current time zone is used.</p>
</span></td><td>Stable</td></tr>
<tr><td><a name="make_timestamptz"></a><code>make_timestamptz(year: <a href="int.html">int</a>, month: <a href="int.html">int</a>, day: <a href="int.html">int</a>, hour: <a href="int.html">int</a>, min: <a href="int.html">int</a>, sec: <a href="float.html">float</a>, timezone: <a href="string.html">string</a>) &rarr; <a href="timestamp.html">timestamptz</a></code></td><td><span class="funcdesc"><p>Create timestampwith time zone from year, month, day, hour, minute and seconds fields (negative years signify BC). If timezone is not specified, the current time zone is used.</p>
</span></td><td>Stable</td></tr>
<tr><td><a name="now"></a><code>now() &rarr; <a href="date.html">date</a></code></td><td><span class="funcdesc"><p>Returns the time of the current transaction.</p>
<p>The value is based on a timestamp picked when the transaction starts
and which stays constant throughout the transaction. This timestamp
Expand Down
89 changes: 89 additions & 0 deletions pkg/sql/logictest/testdata/logic_test/datetime
Expand Up @@ -2014,3 +2014,92 @@ SELECT * FROM ex WHERE ROW('1970-01-02 00:00:01.000001-04'::TIMESTAMPTZ) < '1970
query TTTTT
SELECT * FROM ex WHERE ROW('1970-01-03 00:00:01.000001-04'::TIMESTAMPTZ) < ROW('1970-01-02 00:00:01.000001-04');
----

subtest make_date

query T colnames,rowsort
SELECT make_date(2013, 7, 15)::string
----
make_date
2013-07-15

query T colnames,rowsort
SELECT make_date(-2013, 7, 15)
----
make_date
-2013-07-15 00:00:00 +0000 +0000

statement error pq: make_date\(\): date field value out of range: 0-11-11
SELECT make_date(0, 11, 11)

subtest end

subtest make_timestamp

query T colnames,rowsort
SELECT make_timestamp(2013, 7, 15, 8, 15, 23.5)::string
----
make_timestamp
2013-07-15 08:15:23.5

query T colnames,rowsort
SELECT make_timestamp(-2013, 7, 15, 8, 15, 23.5)
----
make_timestamp
-2013-07-15 08:15:23.5 +0000 +0000

query T colnames,rowsort
SELECT make_timestamp(2013, 7, 15, 8, 15, 23.5231231244234)::string
----
make_timestamp
2013-07-15 08:15:23.523123

statement error pq: make_timestamp\(\): date field value out of range: 0-07-15
SELECT make_timestamp(0, 7, 15, 8, 15, 23.5);

subtest end

subtest make_timestamptz

statement ok
SET TIME ZONE 'EST';

query T colnames,rowsort
SELECT make_timestamptz(2013, 7, 15, 8, 15, 23.5)
----
make_timestamptz
2013-07-15 08:15:23.5 -0500 EST

query T colnames,rowsort
SELECT make_timestamptz(-2013, 7, 15, 8, 15, 23.5)
----
make_timestamptz
-2013-07-15 08:15:23.5 -0500 EST

query T colnames,rowsort
SELECT make_timestamptz(2013, 7, 15, 8, 15, 23.5231231244234)
----
make_timestamptz
2013-07-15 08:15:23.523123 -0500 EST

query T colnames,rowsort
SELECT make_timestamptz(2013, 7, 15, 8, 15, 23.5, 'America/New_York')
----
make_timestamptz
2013-07-15 07:15:23.5 -0500 EST

statement error pq: make_timestamptz\(\): date field value out of range: 0-07-15
SELECT make_timestamptz(0, 7, 15, 8, 15, 23.5);

statement error pq: make_timestamptz\(\): date field value out of range: 0-07-15
SELECT make_timestamptz(0, 7, 15, 8, 15, 23.5, 'America/New_York');

query T
SELECT date_trunc('day', '2001-02-16 20:38:40+00'::timestamptz, 'Australia/Sydney')
----
2001-02-16 08:00:00 -0500 EST

statement error pq: date_trunc\(\): parsing as type timestamp: field month value 0 is out of range
SELECT date_trunc('day', '0-02-16 20:38:40+00'::timestamptz, 'Australia/Sydney');

subtest end
99 changes: 98 additions & 1 deletion pkg/sql/sem/builtins/builtins.go
Expand Up @@ -2496,11 +2496,42 @@ var regularBuiltins = map[string]builtinDefinition{
},
),

"make_date": makeBuiltin(
tree.FunctionProperties{Category: builtinconstants.CategoryDateAndTime},
tree.Overload{
Types: tree.ParamTypes{{Name: "year", Typ: types.Int}, {Name: "month", Typ: types.Int}, {Name: "day", Typ: types.Int}},
ReturnType: tree.FixedReturnType(types.Date),
Fn: func(_ context.Context, evalCtx *eval.Context, args tree.Datums) (tree.Datum, error) {
year := int(tree.MustBeDInt(args[0]))
month := time.Month(int(tree.MustBeDInt(args[1])))
day := int(tree.MustBeDInt(args[2]))
if year == 0 {
return nil, errors.Newf("date field value out of range: %d-%02d-%02d", year, month, day)
}
location := evalCtx.GetLocation()
return tree.NewDDateFromTime(time.Date(year, month, day, 0, 0, 0, 0, location))
},
Info: "Create date from year, month, and day fields (negative years signify BC).",
Volatility: volatility.Stable,
},
),

"make_timestamp": makeBuiltin(
tree.FunctionProperties{Category: builtinconstants.CategoryDateAndTime},
makeTimestampStatementBuiltinOverload(false, false),
),

"make_timestamptz": makeBuiltin(
tree.FunctionProperties{Category: builtinconstants.CategoryDateAndTime},
makeTimestampStatementBuiltinOverload(true, true),
makeTimestampStatementBuiltinOverload(true, false),
),

// https://www.postgresql.org/docs/14/functions-datetime.html#FUNCTIONS-DATETIME-TABLE
//
// PostgreSQL documents date_trunc for text and double precision.
// It will also handle smallint, integer, bigint, decimal,
// numeric, real, and numeri like text inputs by casting them,
// numeric, real, and numeric like text inputs by casting them,
// so we support those for compatibility. This gives us the following
// function signatures:
//
Expand Down Expand Up @@ -3004,6 +3035,24 @@ value if you rely on the HLC for accuracy.`,
"week, day, hour, minute, second, millisecond, microsecond.",
Volatility: volatility.Stable,
},
tree.Overload{
Types: tree.ParamTypes{{Name: "element", Typ: types.String}, {Name: "input", Typ: types.TimestampTZ}, {Name: "timezone", Typ: types.String}},
ReturnType: tree.FixedReturnType(types.TimestampTZ),
Fn: func(_ context.Context, _ *eval.Context, args tree.Datums) (tree.Datum, error) {
timeSpan := strings.ToLower(string(tree.MustBeDString(args[0])))
fromTSTZ := tree.MustBeDTimestampTZ(args[1])
location, err := timeutil.TimeZoneStringToLocation(string(tree.MustBeDString(args[2])), timeutil.TimeZoneStringToLocationPOSIXStandard)
if err != nil {
return nil, err
}
return truncateTimestamp(fromTSTZ.Time.In(location), timeSpan)
},
Info: "Truncates `input` to precision `element` in the specified `timezone`. Sets all fields that are less\n" +
"significant than `element` to zero (or one, for day and month)\n\n" +
"Compatible elements: millennium, century, decade, year, quarter, month,\n" +
"week, day, hour, minute, second, millisecond, microsecond.",
Volatility: volatility.Stable,
},
),

"row_to_json": makeBuiltin(jsonProps(),
Expand Down Expand Up @@ -11254,3 +11303,51 @@ func bitmaskOp(aStr, bStr string, op func(byte, byte) byte) (*tree.DBitArray, er

return tree.ParseDBitArray(string(buf))
}

func makeTimestampStatementBuiltinOverload(withTZ bool, withInputTZ bool) tree.Overload {
info := "Create timestamp"
typs := tree.ParamTypes{{Name: "year", Typ: types.Int}, {Name: "month", Typ: types.Int}, {Name: "day", Typ: types.Int},
{Name: "hour", Typ: types.Int}, {Name: "min", Typ: types.Int}, {Name: "sec", Typ: types.Float}}
returnTyp := types.Timestamp
if withTZ {
info += "with time zone from year, month, day, hour, minute and seconds fields (negative years signify BC). If " +
"timezone is not specified, the current time zone is used."
returnTyp = types.TimestampTZ
if withInputTZ {
typs = append(typs, tree.ParamType{Name: "timezone", Typ: types.String})
}
} else {
info += "from year, month, day, hour, minute, and seconds fields (negative years signify BC)."
}
return tree.Overload{
Types: typs,
ReturnType: tree.FixedReturnType(returnTyp),
Fn: func(_ context.Context, evalCtx *eval.Context, args tree.Datums) (tree.Datum, error) {
year := int(tree.MustBeDInt(args[0]))
month := time.Month(int(tree.MustBeDInt(args[1])))
day := int(tree.MustBeDInt(args[2]))
location := evalCtx.GetLocation()
var err error
if withInputTZ && withTZ {
location, err = timeutil.TimeZoneStringToLocation(string(tree.MustBeDString(args[6])), timeutil.TimeZoneStringToLocationPOSIXStandard)
if err != nil {
return nil, err
}
}
if year == 0 {
return nil, errors.Newf("date field value out of range: %d-%02d-%02d", year, month, day)
}
hour := int(tree.MustBeDInt(args[3]))
min := int(tree.MustBeDInt(args[4]))
sec := float64(tree.MustBeDFloat(args[5]))
truncatedSec := math.Floor(sec)
nsec := math.Mod(sec, truncatedSec) * float64(time.Second)
if withTZ {
return tree.MakeDTimestampTZ(time.Date(year, month, day, hour, min, int(truncatedSec), int(nsec), location), time.Microsecond)
}
return tree.MakeDTimestamp(time.Date(year, month, day, hour, min, int(truncatedSec), int(nsec), location), time.Microsecond)
},
Info: info,
Volatility: volatility.Stable,
}
}
5 changes: 5 additions & 0 deletions pkg/sql/sem/builtins/fixed_oids.go
Expand Up @@ -2452,6 +2452,11 @@ var builtinOidsArray = []string{
2481: `bitmask_xor(a: string, b: string) -> varbit`,
2482: `bitmask_xor(a: varbit, b: string) -> varbit`,
2483: `bitmask_xor(a: string, b: varbit) -> varbit`,
2484: `make_date(year: int, month: int, day: int) -> date`,
2485: `make_timestamp(year: int, month: int, day: int, hour: int, min: int, sec: float) -> timestamp`,
2486: `make_timestamptz(year: int, month: int, day: int, hour: int, min: int, sec: float) -> timestamptz`,
2487: `make_timestamptz(year: int, month: int, day: int, hour: int, min: int, sec: float, timezone: string) -> timestamptz`,
2488: `date_trunc(element: string, input: timestamptz, timezone: string) -> timestamptz`,
}

var builtinOidsBySignature map[string]oid.Oid
Expand Down

0 comments on commit bb1c653

Please sign in to comment.