diff --git a/docs/generated/sql/functions.md b/docs/generated/sql/functions.md
index da03e66b2922..2af2ea2e9486 100644
--- a/docs/generated/sql/functions.md
+++ b/docs/generated/sql/functions.md
@@ -661,8 +661,14 @@ has no relationship with the commit order of concurrent transactions.
transaction_timestamp() → date | Returns the time of the current transaction.
diff --git a/pkg/BUILD.bazel b/pkg/BUILD.bazel
index f08182ce2828..b09f34e912cd 100644
--- a/pkg/BUILD.bazel
+++ b/pkg/BUILD.bazel
@@ -597,6 +597,7 @@ ALL_TESTS = [
"//pkg/util/timetz:timetz_test",
"//pkg/util/timeutil/pgdate:pgdate_test",
"//pkg/util/timeutil:timeutil_test",
+ "//pkg/util/tochar:tochar_test",
"//pkg/util/tracing/collector:collector_test",
"//pkg/util/tracing/grpcinterceptor:grpcinterceptor_test",
"//pkg/util/tracing/service:service_test",
@@ -2043,6 +2044,8 @@ GO_TARGETS = [
"//pkg/util/timeutil/ptp:ptp",
"//pkg/util/timeutil:timeutil",
"//pkg/util/timeutil:timeutil_test",
+ "//pkg/util/tochar:tochar",
+ "//pkg/util/tochar:tochar_test",
"//pkg/util/tracing/collector:collector",
"//pkg/util/tracing/collector:collector_test",
"//pkg/util/tracing/grpcinterceptor:grpcinterceptor",
@@ -2988,6 +2991,7 @@ GET_X_DATA_TARGETS = [
"//pkg/util/timeutil/gen:get_x_data",
"//pkg/util/timeutil/pgdate:get_x_data",
"//pkg/util/timeutil/ptp:get_x_data",
+ "//pkg/util/tochar:get_x_data",
"//pkg/util/tracing:get_x_data",
"//pkg/util/tracing/collector:get_x_data",
"//pkg/util/tracing/grpcinterceptor:get_x_data",
diff --git a/pkg/sql/logictest/testdata/logic_test/interval b/pkg/sql/logictest/testdata/logic_test/interval
index 38026cb2f0ee..83f99a035d85 100644
--- a/pkg/sql/logictest/testdata/logic_test/interval
+++ b/pkg/sql/logictest/testdata/logic_test/interval
@@ -624,6 +624,55 @@ SELECT i, i::INTERVAL FROM interval_parsing ORDER BY pk
-10 years 22 months -1 day 01:02:03 -8-2 -1 +1:02:03
-10 years 22 months -1 day -01:02:03 -8-2 -1 -1:02:03
+# Could not find any regress tests for to_char in PG, so we're making our own up!
+statement ok
+CREATE TABLE intvl_tbl (id SERIAL, d1 INTERVAL);
+INSERT INTO intvl_tbl (d1) VALUES
+ ('355 months 40 days 123:45:12'),
+ ('-400 months -30 days -100:12:13')
+
+query T
+SELECT to_char(d1, 'Y,YYY YYYY YYY YY Y CC Q MM WW DDD DD J')
+ FROM intvl_tbl ORDER BY id
+----
+0,029 0029 029 29 9 00 3 07 1528 10690 40 1731873
+0,-33 -0033 -033 -33 -3 00 0 -04 -1717 -12030 -30 1708823
+
+query T
+SELECT to_char(d1, 'FMY,YYY FMYYYY FMYYY FMYY FMY FMCC FMQ FMMM FMWW FMDDD FMDD FMJ')
+ FROM intvl_tbl ORDER BY id
+----
+0,029 29 29 29 9 0 3 7 1528 10690 40 1731873
+0,-33 -33 -33 -33 -3 0 0 -4 -1717 -12030 -30 1708823
+
+query T
+SELECT to_char(d1, 'HH HH12 HH24 MI SS SSSS')
+ FROM intvl_tbl ORDER BY id
+----
+03 03 123 45 12 445512
+-04 -04 -100 -12 -13 -360733
+
+query T
+SELECT to_char(d1, E'"HH:MI:SS is" HH:MI:SS "\\"text between quote marks\\""')
+ FROM intvl_tbl ORDER BY id
+----
+HH:MI:SS is 03:45:12 "text between quote marks"
+HH:MI:SS is -04:-12:-13 "text between quote marks"
+
+query T
+SELECT to_char(d1, 'HH24--text--MI--text--SS')
+ FROM intvl_tbl ORDER BY id
+----
+123--text--45--text--12
+-100--text---12--text---13
+
+query T
+SELECT to_char(d1, 'YYYYTH YYYYth Jth')
+ FROM intvl_tbl ORDER BY id
+----
+0029TH 0029th 1731873rd
+-0033RD -0033rd 1708823rd
+
# Test intervalstyle being respected in pg_indexes.
statement ok
diff --git a/pkg/sql/logictest/testdata/logic_test/pg_catalog b/pkg/sql/logictest/testdata/logic_test/pg_catalog
index 771b8e1999de..cca5b17ba028 100644
--- a/pkg/sql/logictest/testdata/logic_test/pg_catalog
+++ b/pkg/sql/logictest/testdata/logic_test/pg_catalog
@@ -3251,7 +3251,7 @@ FROM pg_proc p
JOIN pg_type t ON t.typinput = p.oid
WHERE t.typname = '_int4'
----
-2018 array_in array_in
+2021 array_in array_in
## #16285
## int2vectors should be 0-indexed
@@ -3287,7 +3287,7 @@ SELECT cur_max_builtin_oid FROM [SELECT max(oid) as cur_max_builtin_oid FROM pg_
query TT
SELECT proname, oid FROM pg_catalog.pg_proc WHERE oid = $cur_max_builtin_oid
----
-to_regtype 2038
+to_regtype 2041
## Ensure that unnest works with oid wrapper arrays
diff --git a/pkg/sql/logictest/testdata/logic_test/timestamp b/pkg/sql/logictest/testdata/logic_test/timestamp
index 9ceaeca7cb78..7a4e6a5fb443 100644
--- a/pkg/sql/logictest/testdata/logic_test/timestamp
+++ b/pkg/sql/logictest/testdata/logic_test/timestamp
@@ -550,3 +550,218 @@ query T
SELECT (t2 - t1) FROM t
----
1 day
+
+# Rough translation of the to_char tests from PostgreSQL.
+statement ok
+CREATE TABLE TIMESTAMPTZ_TBL (id SERIAL, d1 timestamp(2) with time zone);
+INSERT INTO TIMESTAMPTZ_TBL (d1) VALUES
+ ('1997-06-10 17:32:01 -07:00'),
+ ('2001-09-22T18:19:20');
+
+query T
+SELECT to_char(d1, 'DAY Day day DY Dy dy MONTH Month month MON Mon mon')
+ FROM TIMESTAMPTZ_TBL ORDER BY id
+----
+wednesday Wednesday wednesday wed Wed wed JUNE June june JUN Jun jun
+saturday Saturday saturday sat Sat sat SEPTEMBER September september SEP Sep sep
+
+query T
+SELECT to_char(d1, 'FMDAY FMDay FMday FMMONTH FMMonth FMmonth')
+ FROM TIMESTAMPTZ_TBL ORDER BY id
+----
+wednesday Wednesday wednesday JUNE June june
+saturday Saturday saturday SEPTEMBER September september
+
+query T
+SELECT to_char(d1, 'Y,YYY YYYY YYY YY Y CC Q MM WW DDD DD D J')
+ FROM TIMESTAMPTZ_TBL ORDER BY id
+----
+1,997 1997 997 97 7 20 2 06 24 162 11 4 2450611
+2,001 2001 001 01 1 21 3 09 38 265 22 7 2452175
+
+query T
+SELECT to_char(d1, 'FMY,YYY FMYYYY FMYYY FMYY FMY FMCC FMQ FMMM FMWW FMDDD FMDD FMD FMJ')
+ FROM TIMESTAMPTZ_TBL ORDER BY id
+----
+1,997 1997 997 97 7 20 2 6 24 162 11 4 2450611
+2,001 2001 1 1 1 21 3 9 38 265 22 7 2452175
+
+query T
+SELECT to_char(d1::timestamp, 'FMY,YYY FMYYYY FMYYY FMYY FMY FMCC FMQ FMMM FMWW FMDDD FMDD FMD FMJ')
+ FROM TIMESTAMPTZ_TBL ORDER BY id
+----
+1,997 1997 997 97 7 20 2 6 24 162 11 4 2450611
+2,001 2001 1 1 1 21 3 9 38 265 22 7 2452175
+
+query T
+SELECT to_char(d1, 'HH HH12 HH24 MI SS SSSS')
+ FROM TIMESTAMPTZ_TBL ORDER BY id
+----
+12 12 00 32 01 1921
+06 06 18 19 20 65960
+
+query T
+SELECT to_char(d1, E'"HH:MI:SS is" HH:MI:SS "\\"text between quote marks\\""')
+ FROM TIMESTAMPTZ_TBL ORDER BY id
+----
+HH:MI:SS is 12:32:01 "text between quote marks"
+HH:MI:SS is 06:19:20 "text between quote marks"
+
+query T
+SELECT to_char(d1, 'HH24--text--MI--text--SS')
+ FROM TIMESTAMPTZ_TBL ORDER BY id
+----
+00--text--32--text--01
+18--text--19--text--20
+
+query T
+SELECT to_char(d1, 'YYYYTH YYYYth Jth')
+ FROM TIMESTAMPTZ_TBL ORDER BY id
+----
+1997TH 1997th 2450611th
+2001ST 2001st 2452175th
+
+query T
+SELECT to_char(d1, 'YYYY A.D. YYYY a.d. YYYY bc HH:MI:SS P.M. HH:MI:SS p.m. HH:MI:SS pm')
+ FROM TIMESTAMPTZ_TBL ORDER BY id
+----
+1997 A.D. 1997 a.d. 1997 ad 12:32:01 A.M. 12:32:01 a.m. 12:32:01 am
+2001 A.D. 2001 a.d. 2001 ad 06:19:20 P.M. 06:19:20 p.m. 06:19:20 pm
+
+query T
+SELECT to_char(d1, 'IYYY IYY IY I IW IDDD ID')
+ FROM TIMESTAMPTZ_TBL ORDER BY id
+----
+1997 997 97 7 24 164 3
+2001 001 01 1 38 265 6
+
+query T
+SELECT to_char(d1, 'FMIYYY FMIYY FMIY FMI FMIW FMIDDD FMID')
+ FROM TIMESTAMPTZ_TBL ORDER BY id
+----
+1997 997 97 7 24 164 3
+2001 1 1 1 38 265 6
+
+query T
+SELECT to_char(d, 'FF1 FF2 FF3 FF4 FF5 FF6 ff1 ff2 ff3 ff4 ff5 ff6 MS US')
+ FROM (VALUES
+ ('2018-11-02 12:34:56'::timestamptz),
+ ('2018-11-02 12:34:56.78'::timestamptz),
+ ('2018-11-02 12:34:56.78901'::timestamptz),
+ ('2018-11-02 12:34:56.78901234'::timestamptz)
+ ) d(d)
+----
+0 00 000 0000 00000 000000 0 00 000 0000 00000 000000 000 000000
+7 78 780 7800 78000 780000 7 78 780 7800 78000 780000 780 780000
+7 78 789 7890 78901 789010 7 78 789 7890 78901 789010 789 789010
+7 78 789 7890 78901 789012 7 78 789 7890 78901 789012 789 789012
+
+query TT
+SET timezone = '00:00';
+SELECT to_char(now(), 'OF') as of_t, to_char(now(), 'TZH:TZM') as "TZH:TZM";
+----
++00 +00:00
+
+query TT
+SET timezone = '+02:00';
+SELECT to_char(now(), 'OF') as of_t, to_char(now(), 'TZH:TZM') as "TZH:TZM";
+----
+-02 -02:00
+
+query TT
+SET timezone = '-13:00';
+SELECT to_char(now(), 'OF') as of_t, to_char(now(), 'TZH:TZM') as "TZH:TZM";
+----
++13 +13:00
+
+query TT
+SET timezone = '-00:30';
+SELECT to_char(now(), 'OF') as of_t, to_char(now(), 'TZH:TZM') as "TZH:TZM";
+----
++00:30 +00:30
+
+query TT
+SET timezone = '00:30';
+SELECT to_char(now(), 'OF') as of_t, to_char(now(), 'TZH:TZM') as "TZH:TZM";
+----
+-00:30 -00:30
+
+query TT
+SET timezone = '-04:30';
+SELECT to_char(now(), 'OF') as of_t, to_char(now(), 'TZH:TZM') as "TZH:TZM";
+----
++04:30 +04:30
+
+query TT
+SET timezone = '04:30';
+SELECT to_char(now(), 'OF') as of_t, to_char(now(), 'TZH:TZM') as "TZH:TZM";
+----
+-04:30 -04:30
+
+query TT
+SET timezone = '-04:15';
+SELECT to_char(now(), 'OF') as of_t, to_char(now(), 'TZH:TZM') as "TZH:TZM";
+----
++04:15 +04:15
+
+query TT
+SET timezone = '04:15';
+SELECT to_char(now(), 'OF') as of_t, to_char(now(), 'TZH:TZM') as "TZH:TZM";
+----
+-04:15 -04:15
+
+query TT
+RESET timezone;
+-- Check of, tzh, tzm with various zone offsets.
+SET timezone = '00:00';
+SELECT to_char(now(), 'of') as of_t, to_char(now(), 'tzh:tzm') as "tzh:tzm";
+----
++00 +00:00
+
+query TT
+SET timezone = '+02:00';
+SELECT to_char(now(), 'of') as of_t, to_char(now(), 'tzh:tzm') as "tzh:tzm";
+----
+-02 -02:00
+
+query TT
+SET timezone = '-13:00';
+SELECT to_char(now(), 'of') as of_t, to_char(now(), 'tzh:tzm') as "tzh:tzm";
+----
++13 +13:00
+
+query TT
+SET timezone = '-00:30';
+SELECT to_char(now(), 'of') as of_t, to_char(now(), 'tzh:tzm') as "tzh:tzm";
+----
++00:30 +00:30
+
+query TT
+SET timezone = '00:30';
+SELECT to_char(now(), 'of') as of_t, to_char(now(), 'tzh:tzm') as "tzh:tzm";
+----
+-00:30 -00:30
+
+query TT
+SET timezone = '-04:30';
+SELECT to_char(now(), 'of') as of_t, to_char(now(), 'tzh:tzm') as "tzh:tzm";
+----
++04:30 +04:30
+
+query TT
+SET timezone = '04:30';
+SELECT to_char(now(), 'of') as of_t, to_char(now(), 'tzh:tzm') as "tzh:tzm";
+----
+-04:30 -04:30
+
+query TT
+SET timezone = '-04:15';
+SELECT to_char(now(), 'of') as of_t, to_char(now(), 'tzh:tzm') as "tzh:tzm";
+----
++04:15 +04:15
+
+query TT
+SET timezone = '04:15';
+SELECT to_char(now(), 'of') as of_t, to_char(now(), 'tzh:tzm') as "tzh:tzm";
+----
+-04:15 -04:15
diff --git a/pkg/sql/sem/builtins/BUILD.bazel b/pkg/sql/sem/builtins/BUILD.bazel
index 813a7a48dadc..9a4c2ea93aed 100644
--- a/pkg/sql/sem/builtins/BUILD.bazel
+++ b/pkg/sql/sem/builtins/BUILD.bazel
@@ -109,6 +109,7 @@ go_library(
"//pkg/util/timetz",
"//pkg/util/timeutil",
"//pkg/util/timeutil/pgdate",
+ "//pkg/util/tochar",
"//pkg/util/tracing",
"//pkg/util/tracing/tracingpb",
"//pkg/util/trigram",
diff --git a/pkg/sql/sem/builtins/builtins.go b/pkg/sql/sem/builtins/builtins.go
index 2b1ecc8d5eb5..6eb32d8eda2f 100644
--- a/pkg/sql/sem/builtins/builtins.go
+++ b/pkg/sql/sem/builtins/builtins.go
@@ -88,6 +88,7 @@ import (
"github.com/cockroachdb/cockroach/pkg/util/timetz"
"github.com/cockroachdb/cockroach/pkg/util/timeutil"
"github.com/cockroachdb/cockroach/pkg/util/timeutil/pgdate"
+ "github.com/cockroachdb/cockroach/pkg/util/tochar"
"github.com/cockroachdb/cockroach/pkg/util/tracing"
"github.com/cockroachdb/cockroach/pkg/util/tracing/tracingpb"
"github.com/cockroachdb/cockroach/pkg/util/trigram"
@@ -2436,6 +2437,42 @@ var regularBuiltins = map[string]builtinDefinition{
Info: "Convert an timestamp to a string assuming the ISO, MDY DateStyle.",
Volatility: volatility.Immutable,
},
+ tree.Overload{
+ Types: tree.ArgTypes{{"interval", types.Interval}, {"format", types.String}},
+ ReturnType: tree.FixedReturnType(types.String),
+ Fn: func(_ *eval.Context, args tree.Datums) (tree.Datum, error) {
+ d := tree.MustBeDInterval(args[0])
+ f := tree.MustBeDString(args[1])
+ s, err := tochar.DurationToChar(d.Duration, string(f))
+ return tree.NewDString(s), err
+ },
+ Info: "Convert an interval to a string using the given format.",
+ Volatility: volatility.Stable,
+ },
+ tree.Overload{
+ Types: tree.ArgTypes{{"timestamp", types.Timestamp}, {"format", types.String}},
+ ReturnType: tree.FixedReturnType(types.String),
+ Fn: func(_ *eval.Context, args tree.Datums) (tree.Datum, error) {
+ ts := tree.MustBeDTimestamp(args[0])
+ f := tree.MustBeDString(args[1])
+ s, err := tochar.TimeToChar(ts.Time, string(f))
+ return tree.NewDString(s), err
+ },
+ Info: "Convert an timestamp to a string using the given format.",
+ Volatility: volatility.Stable,
+ },
+ tree.Overload{
+ Types: tree.ArgTypes{{"timestamptz", types.TimestampTZ}, {"format", types.String}},
+ ReturnType: tree.FixedReturnType(types.String),
+ Fn: func(_ *eval.Context, args tree.Datums) (tree.Datum, error) {
+ ts := tree.MustBeDTimestampTZ(args[0])
+ f := tree.MustBeDString(args[1])
+ s, err := tochar.TimeToChar(ts.Time, string(f))
+ return tree.NewDString(s), err
+ },
+ Info: "Convert a timestamp with time zone to a string using the given format.",
+ Volatility: volatility.Stable,
+ },
tree.Overload{
Types: tree.ArgTypes{{"date", types.Date}},
ReturnType: tree.FixedReturnType(types.String),
@@ -9130,24 +9167,6 @@ func extractTimeSpanFromTimeOfDay(t timeofday.TimeOfDay, timeSpan string) (tree.
}
}
-// dateToJulianDay is based on the date2j function in PostgreSQL 10.5.
-func dateToJulianDay(year int, month int, day int) int {
- if month > 2 {
- month++
- year += 4800
- } else {
- month += 13
- year += 4799
- }
-
- century := year / 100
- jd := year*365 - 32167
- jd += year/4 - century + century/4
- jd += 7834*month/256 + day
-
- return jd
-}
-
func extractTimeSpanFromTimestampTZ(
ctx *eval.Context, fromTime time.Time, timeSpan string,
) (tree.Datum, error) {
@@ -9274,7 +9293,7 @@ func extractTimeSpanFromTimestamp(
return tree.NewDFloat(tree.DFloat(fromTime.YearDay())), nil
case "julian":
- julianDay := float64(dateToJulianDay(fromTime.Year(), int(fromTime.Month()), fromTime.Day())) +
+ julianDay := float64(pgdate.DateToJulianDay(fromTime.Year(), int(fromTime.Month()), fromTime.Day())) +
(float64(fromTime.Hour()*duration.SecsPerHour+fromTime.Minute()*duration.SecsPerMinute+fromTime.Second())+
float64(fromTime.Nanosecond())/float64(time.Second))/duration.SecsPerDay
return tree.NewDFloat(tree.DFloat(julianDay)), nil
diff --git a/pkg/sql/sem/tree/BUILD.bazel b/pkg/sql/sem/tree/BUILD.bazel
index cdb40f077de8..e8dd40e5c1b5 100644
--- a/pkg/sql/sem/tree/BUILD.bazel
+++ b/pkg/sql/sem/tree/BUILD.bazel
@@ -59,7 +59,6 @@ go_library(
"import.go",
"indexed_vars.go",
"insert.go",
- "interval.go",
"name_part.go",
"name_resolution.go",
"object_name.go",
@@ -181,7 +180,6 @@ go_test(
"function_definition_test.go",
"function_name_test.go",
"indexed_vars_test.go",
- "interval_test.go",
"json_test.go",
"main_test.go",
"name_part_test.go",
diff --git a/pkg/sql/sem/tree/datum.go b/pkg/sql/sem/tree/datum.go
index a6a48548a485..1009fa932fdd 100644
--- a/pkg/sql/sem/tree/datum.go
+++ b/pkg/sql/sem/tree/datum.go
@@ -20,7 +20,6 @@ import (
"strconv"
"strings"
"time"
- "unicode"
"unicode/utf8"
"unsafe"
@@ -3107,7 +3106,7 @@ func ParseDIntervalWithTypeMetadata(
func ParseIntervalWithTypeMetadata(
style duration.IntervalStyle, s string, itm types.IntervalTypeMetadata,
) (duration.Duration, error) {
- d, err := parseInterval(style, s, itm)
+ d, err := duration.ParseInterval(style, s, itm)
if err != nil {
return d, err
}
@@ -3115,48 +3114,6 @@ func ParseIntervalWithTypeMetadata(
return d, nil
}
-func parseInterval(
- style duration.IntervalStyle, s string, itm types.IntervalTypeMetadata,
-) (duration.Duration, error) {
- // At this time the only supported interval formats are:
- // - SQL standard.
- // - Postgres compatible.
- // - iso8601 format (with designators only), see interval.go for
- // sources of documentation.
- // - Golang time.parseDuration compatible.
-
- // If it's a blank string, exit early.
- if len(s) == 0 {
- return duration.Duration{}, MakeParseError(s, types.Interval, nil)
- }
- if s[0] == 'P' {
- // If it has a leading P we're most likely working with an iso8601
- // interval.
- dur, err := iso8601ToDuration(s)
- if err != nil {
- return duration.Duration{}, MakeParseError(s, types.Interval, err)
- }
- return dur, nil
- }
- if strings.IndexFunc(s, unicode.IsLetter) == -1 {
- // If it has no letter, then we're most likely working with a SQL standard
- // interval, as both postgres and golang have letter(s) and iso8601 has been tested.
- dur, err := sqlStdToDuration(s, itm)
- if err != nil {
- return duration.Duration{}, MakeParseError(s, types.Interval, err)
- }
- return dur, nil
- }
-
- // We're either a postgres string or a Go duration.
- // Our postgres syntax parser also supports golang, so just use that for both.
- dur, err := parseDuration(style, s, itm)
- if err != nil {
- return duration.Duration{}, MakeParseError(s, types.Interval, err)
- }
- return dur, nil
-}
-
// ResolvedType implements the TypedExpr interface.
func (*DInterval) ResolvedType() *types.T {
return types.Interval
diff --git a/pkg/testutils/lint/lint_test.go b/pkg/testutils/lint/lint_test.go
index 641016ddd5fd..dcbeae391771 100644
--- a/pkg/testutils/lint/lint_test.go
+++ b/pkg/testutils/lint/lint_test.go
@@ -1693,6 +1693,10 @@ func TestLint(t *testing.T) {
// The Observability Service doesn't want this blunt rule.
stream.GrepNot("pkg/obsservice.*error strings should not be capitalized or end with punctuation or a newline"),
+ // The constants here are copied from PG.
+ stream.GrepNot("pkg/util/tochar/constants.go.*don't use ALL_CAPS in Go names"),
+ stream.GrepNot("pkg/util/tochar/constants.go.*don't use underscores in Go names"),
+
stream.GrepNot("pkg/sql/job_exec_context_test_util.go.*exported method ExtendedEvalContext returns unexported type"),
stream.GrepNot("pkg/sql/job_exec_context_test_util.go.*exported method SessionDataMutatorIterator returns unexported type"),
diff --git a/pkg/util/duration/BUILD.bazel b/pkg/util/duration/BUILD.bazel
index aeebc17e5416..866538eea1bc 100644
--- a/pkg/util/duration/BUILD.bazel
+++ b/pkg/util/duration/BUILD.bazel
@@ -5,13 +5,17 @@ load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test")
go_library(
name = "duration",
- srcs = ["duration.go"],
+ srcs = [
+ "duration.go",
+ "parse.go",
+ ],
embed = [":duration_go_proto"],
importpath = "github.com/cockroachdb/cockroach/pkg/util/duration",
visibility = ["//visibility:public"],
deps = [
"//pkg/sql/pgwire/pgcode",
"//pkg/sql/pgwire/pgerror",
+ "//pkg/sql/types",
"//pkg/util/arith",
"@com_github_cockroachdb_apd_v3//:apd",
"@com_github_cockroachdb_errors//:errors",
@@ -21,10 +25,15 @@ go_library(
go_test(
name = "duration_test",
size = "small",
- srcs = ["duration_test.go"],
+ srcs = [
+ "duration_test.go",
+ "parse_test.go",
+ ],
args = ["-test.timeout=55s"],
embed = [":duration"],
deps = [
+ "//pkg/sql/types",
+ "//pkg/util/leaktest",
"//pkg/util/log",
"//pkg/util/timeutil",
"@com_github_stretchr_testify//require",
diff --git a/pkg/util/duration/duration.go b/pkg/util/duration/duration.go
index 5a162b435917..ea0d34dee9a3 100644
--- a/pkg/util/duration/duration.go
+++ b/pkg/util/duration/duration.go
@@ -17,10 +17,12 @@ import (
"math/big"
"strings"
"time"
+ "unicode"
"github.com/cockroachdb/apd/v3"
"github.com/cockroachdb/cockroach/pkg/sql/pgwire/pgcode"
"github.com/cockroachdb/cockroach/pkg/sql/pgwire/pgerror"
+ "github.com/cockroachdb/cockroach/pkg/sql/types"
"github.com/cockroachdb/cockroach/pkg/util/arith"
"github.com/cockroachdb/errors"
)
@@ -1024,3 +1026,55 @@ func Truncate(d time.Duration, r time.Duration) time.Duration {
}
return d - (d % r)
}
+
+// ParseInterval parses the given interval in the given style.
+func ParseInterval(
+ style IntervalStyle, s string, itm types.IntervalTypeMetadata,
+) (Duration, error) {
+ // At this time the only supported interval formats are:
+ // - SQL standard.
+ // - Postgres compatible.
+ // - iso8601 format (with designators only), see interval.go for
+ // sources of documentation.
+ // - Golang time.parseDuration compatible.
+
+ // If it's a blank string, exit early.
+ if len(s) == 0 {
+ return Duration{}, makeParseError(s, types.Interval, nil)
+ }
+ if s[0] == 'P' {
+ // If it has a leading P we're most likely working with an iso8601
+ // interval.
+ dur, err := iso8601ToDuration(s)
+ if err != nil {
+ return Duration{}, makeParseError(s, types.Interval, err)
+ }
+ return dur, nil
+ }
+ if strings.IndexFunc(s, unicode.IsLetter) == -1 {
+ // If it has no letter, then we're most likely working with a SQL standard
+ // interval, as both postgres and golang have letter(s) and iso8601 has been tested.
+ dur, err := sqlStdToDuration(s, itm)
+ if err != nil {
+ return Duration{}, makeParseError(s, types.Interval, err)
+ }
+ return dur, nil
+ }
+
+ // We're either a postgres string or a Go duration.
+ // Our postgres syntax parser also supports golang, so just use that for both.
+ dur, err := parseDuration(style, s, itm)
+ if err != nil {
+ return Duration{}, makeParseError(s, types.Interval, err)
+ }
+ return dur, nil
+}
+
+func makeParseError(s string, typ *types.T, err error) error {
+ if err != nil {
+ return pgerror.Wrapf(err, pgcode.InvalidTextRepresentation,
+ "could not parse %q as type %s", s, typ)
+ }
+ return pgerror.Newf(pgcode.InvalidTextRepresentation,
+ "could not parse %q as type %s", s, typ)
+}
diff --git a/pkg/sql/sem/tree/interval.go b/pkg/util/duration/parse.go
similarity index 85%
rename from pkg/sql/sem/tree/interval.go
rename to pkg/util/duration/parse.go
index e5a2cabdf827..afff4d2e0d88 100644
--- a/pkg/sql/sem/tree/interval.go
+++ b/pkg/util/duration/parse.go
@@ -8,7 +8,7 @@
// by the Apache License, Version 2.0, included in the file
// licenses/APL.txt.
-package tree
+package duration
import (
"math"
@@ -19,7 +19,6 @@ import (
"github.com/cockroachdb/cockroach/pkg/sql/pgwire/pgcode"
"github.com/cockroachdb/cockroach/pkg/sql/pgwire/pgerror"
"github.com/cockroachdb/cockroach/pkg/sql/types"
- "github.com/cockroachdb/cockroach/pkg/util/duration"
"github.com/cockroachdb/errors"
)
@@ -153,17 +152,17 @@ func (l *intervalLexer) consumeSpaces() {
}
// ISO Units.
-var isoDateUnitMap = map[string]duration.Duration{
- "D": duration.MakeDuration(0, 1, 0),
- "W": duration.MakeDuration(0, 7, 0),
- "M": duration.MakeDuration(0, 0, 1),
- "Y": duration.MakeDuration(0, 0, 12),
+var isoDateUnitMap = map[string]Duration{
+ "D": MakeDuration(0, 1, 0),
+ "W": MakeDuration(0, 7, 0),
+ "M": MakeDuration(0, 0, 1),
+ "Y": MakeDuration(0, 0, 12),
}
-var isoTimeUnitMap = map[string]duration.Duration{
- "S": duration.MakeDuration(time.Second.Nanoseconds(), 0, 0),
- "M": duration.MakeDuration(time.Minute.Nanoseconds(), 0, 0),
- "H": duration.MakeDuration(time.Hour.Nanoseconds(), 0, 0),
+var isoTimeUnitMap = map[string]Duration{
+ "S": MakeDuration(time.Second.Nanoseconds(), 0, 0),
+ "M": MakeDuration(time.Minute.Nanoseconds(), 0, 0),
+ "H": MakeDuration(time.Hour.Nanoseconds(), 0, 0),
}
const errInvalidSQLDuration = "invalid input syntax for type interval %s"
@@ -185,8 +184,8 @@ func newInvalidSQLDurationError(s string) error {
// See the following links for examples:
// - http://www.postgresql.org/docs/9.1/static/datatype-datetime.html#DATATYPE-INTERVAL-INPUT-EXAMPLES
// - http://www.ibm.com/support/knowledgecenter/SSGU8G_12.1.0/com.ibm.esqlc.doc/ids_esqlc_0190.htm
-func sqlStdToDuration(s string, itm types.IntervalTypeMetadata) (duration.Duration, error) {
- var d duration.Duration
+func sqlStdToDuration(s string, itm types.IntervalTypeMetadata) (Duration, error) {
+ var d Duration
parts := strings.Fields(s)
if len(parts) > 3 || len(parts) == 0 {
return d, newInvalidSQLDurationError(s)
@@ -265,7 +264,7 @@ func sqlStdToDuration(s string, itm types.IntervalTypeMetadata) (duration.Durati
if err != nil {
return d, newInvalidSQLDurationError(s)
}
- d = d.Add(duration.MakeDuration(0, 1, 0).MulFloat(days))
+ d = d.Add(MakeDuration(0, 1, 0).MulFloat(days))
}
hms = hms[1:]
@@ -324,9 +323,9 @@ func sqlStdToDuration(s string, itm types.IntervalTypeMetadata) (duration.Durati
return d, newInvalidSQLDurationError(s)
}
- d = d.Add(duration.MakeDuration(time.Hour.Nanoseconds(), 0, 0).Mul(mult * hours))
- d = d.Add(duration.MakeDuration(time.Minute.Nanoseconds(), 0, 0).Mul(mult * mins))
- d = d.Add(duration.MakeDuration(time.Second.Nanoseconds(), 0, 0).MulFloat(float64(mult) * secs))
+ d = d.Add(MakeDuration(time.Hour.Nanoseconds(), 0, 0).Mul(mult * hours))
+ d = d.Add(MakeDuration(time.Minute.Nanoseconds(), 0, 0).Mul(mult * mins))
+ d = d.Add(MakeDuration(time.Second.Nanoseconds(), 0, 0).MulFloat(float64(mult) * secs))
} else if strings.ContainsRune(part, '-') {
// Try to parse as Year-Month.
if parsedIdx >= yearMonthParsed {
@@ -351,7 +350,7 @@ func sqlStdToDuration(s string, itm types.IntervalTypeMetadata) (duration.Durati
month, errMonth = strconv.Atoi(yms[1])
}
if errYear == nil && errMonth == nil {
- delta := duration.MakeDuration(0, 0, 1).Mul(int64(year)*12 + int64(month))
+ delta := MakeDuration(0, 0, 1).Mul(int64(year)*12 + int64(month))
if mult < 0 {
d = d.Sub(delta)
} else {
@@ -371,26 +370,26 @@ func sqlStdToDuration(s string, itm types.IntervalTypeMetadata) (duration.Durati
// It must be part because nothing has been parsed.
switch itm.DurationField.DurationType {
case types.IntervalDurationType_YEAR:
- d = d.Add(duration.MakeDuration(0, 0, 12).MulFloat(value * float64(mult)))
+ d = d.Add(MakeDuration(0, 0, 12).MulFloat(value * float64(mult)))
case types.IntervalDurationType_MONTH:
- d = d.Add(duration.MakeDuration(0, 0, 1).MulFloat(value * float64(mult)))
+ d = d.Add(MakeDuration(0, 0, 1).MulFloat(value * float64(mult)))
case types.IntervalDurationType_DAY:
- d = d.Add(duration.MakeDuration(0, 1, 0).MulFloat(value * float64(mult)))
+ d = d.Add(MakeDuration(0, 1, 0).MulFloat(value * float64(mult)))
case types.IntervalDurationType_HOUR:
- d = d.Add(duration.MakeDuration(time.Hour.Nanoseconds(), 0, 0).MulFloat(value * float64(mult)))
+ d = d.Add(MakeDuration(time.Hour.Nanoseconds(), 0, 0).MulFloat(value * float64(mult)))
case types.IntervalDurationType_MINUTE:
- d = d.Add(duration.MakeDuration(time.Minute.Nanoseconds(), 0, 0).MulFloat(value * float64(mult)))
+ d = d.Add(MakeDuration(time.Minute.Nanoseconds(), 0, 0).MulFloat(value * float64(mult)))
case types.IntervalDurationType_SECOND, types.IntervalDurationType_UNSET:
- d = d.Add(duration.MakeDuration(time.Second.Nanoseconds(), 0, 0).MulFloat(value * float64(mult)))
+ d = d.Add(MakeDuration(time.Second.Nanoseconds(), 0, 0).MulFloat(value * float64(mult)))
case types.IntervalDurationType_MILLISECOND:
- d = d.Add(duration.MakeDuration(time.Millisecond.Nanoseconds(), 0, 0).MulFloat(value * float64(mult)))
+ d = d.Add(MakeDuration(time.Millisecond.Nanoseconds(), 0, 0).MulFloat(value * float64(mult)))
default:
return d, errors.AssertionFailedf("unhandled DurationField constant %#v", itm.DurationField)
}
parsedIdx = hmsParsed
} else if parsedIdx == hmsParsed {
// Day part.
- delta := duration.MakeDuration(0, 1, 0).MulFloat(value)
+ delta := MakeDuration(0, 1, 0).MulFloat(value)
if mult < 0 {
d = d.Sub(delta)
} else {
@@ -412,8 +411,8 @@ func sqlStdToDuration(s string, itm types.IntervalTypeMetadata) (duration.Durati
// - http://www.postgresql.org/docs/9.1/static/datatype-datetime.html#DATATYPE-INTERVAL-INPUT-EXAMPLES
// - https://en.wikipedia.org/wiki/ISO_8601#Time_intervals
// - https://en.wikipedia.org/wiki/ISO_8601#Durations
-func iso8601ToDuration(s string) (duration.Duration, error) {
- var d duration.Duration
+func iso8601ToDuration(s string) (Duration, error) {
+ var d Duration
if len(s) == 0 || s[0] != 'P' {
return d, newInvalidSQLDurationError(s)
}
@@ -457,9 +456,9 @@ func iso8601ToDuration(s string) (duration.Duration, error) {
// unitMap defines for each unit name what is the time duration for
// that unit.
var unitMap = func(
- units map[string]duration.Duration,
+ units map[string]Duration,
aliases map[string][]string,
-) map[string]duration.Duration {
+) map[string]Duration {
for a, alist := range aliases {
// Pluralize.
units[a+"s"] = units[a]
@@ -469,18 +468,18 @@ var unitMap = func(
}
}
return units
-}(map[string]duration.Duration{
+}(map[string]Duration{
// Use DecodeDuration here because ns is the only unit for which we do not
// want to round nanoseconds since it is only used for multiplication.
- "microsecond": duration.MakeDuration(time.Microsecond.Nanoseconds(), 0, 0),
- "millisecond": duration.MakeDuration(time.Millisecond.Nanoseconds(), 0, 0),
- "second": duration.MakeDuration(time.Second.Nanoseconds(), 0, 0),
- "minute": duration.MakeDuration(time.Minute.Nanoseconds(), 0, 0),
- "hour": duration.MakeDuration(time.Hour.Nanoseconds(), 0, 0),
- "day": duration.MakeDuration(0, 1, 0),
- "week": duration.MakeDuration(0, 7, 0),
- "month": duration.MakeDuration(0, 0, 1),
- "year": duration.MakeDuration(0, 0, 12),
+ "microsecond": MakeDuration(time.Microsecond.Nanoseconds(), 0, 0),
+ "millisecond": MakeDuration(time.Millisecond.Nanoseconds(), 0, 0),
+ "second": MakeDuration(time.Second.Nanoseconds(), 0, 0),
+ "minute": MakeDuration(time.Minute.Nanoseconds(), 0, 0),
+ "hour": MakeDuration(time.Hour.Nanoseconds(), 0, 0),
+ "day": MakeDuration(0, 1, 0),
+ "week": MakeDuration(0, 7, 0),
+ "month": MakeDuration(0, 0, 1),
+ "year": MakeDuration(0, 0, 12),
}, map[string][]string{
// Include PostgreSQL's unit keywords for compatibility; see
// https://github.com/postgres/postgres/blob/a01d0fa1d889cc2003e1941e8b98707c4d701ba9/src/backend/utils/adt/datetime.c#L175-L240
@@ -502,9 +501,9 @@ var unitMap = func(
// format (e.g. '1 day 2 hours', '1 day 03:02:04', etc.) or golang
// format (e.g. '1d2h', '1d3h2m4s', etc.)
func parseDuration(
- style duration.IntervalStyle, s string, itm types.IntervalTypeMetadata,
-) (duration.Duration, error) {
- var d duration.Duration
+ style IntervalStyle, s string, itm types.IntervalTypeMetadata,
+) (Duration, error) {
+ var d Duration
l := intervalLexer{str: s, offset: 0, err: nil}
l.consumeSpaces()
@@ -516,7 +515,7 @@ func parseDuration(
// If we have strictly one negative at the beginning belonging to a
// in SQL Standard parsing, treat everything as negative.
isSQLStandardNegative :=
- style == duration.IntervalStyle_SQL_STANDARD &&
+ style == IntervalStyle_SQL_STANDARD &&
(l.offset+1) < len(l.str) && l.str[l.offset] == '-' &&
!strings.ContainsAny(l.str[l.offset+1:], "+-")
if isSQLStandardNegative {
@@ -568,7 +567,7 @@ func parseDuration(
pgcode.InvalidDatetimeFormat, "interval: missing unit at position %d: %q", l.offset, s)
}
if isSQLStandardNegative {
- return duration.MakeDuration(
+ return MakeDuration(
-d.Nanos(),
-d.Days,
-d.Months,
@@ -579,7 +578,7 @@ func parseDuration(
func (l *intervalLexer) parseShortDuration(
h int64, hasSign bool, itm types.IntervalTypeMetadata,
-) (duration.Duration, error) {
+) (Duration, error) {
sign := int64(1)
if hasSign {
sign = -1
@@ -588,7 +587,7 @@ func (l *intervalLexer) parseShortDuration(
// first number, so that we can check here there are no unwanted
// spaces.
if l.str[l.offset] != ':' {
- return duration.Duration{}, pgerror.Newf(
+ return Duration{}, pgerror.Newf(
pgcode.InvalidDatetimeFormat, "interval: invalid format %s", l.str[l.offset:])
}
l.offset++
@@ -596,7 +595,7 @@ func (l *intervalLexer) parseShortDuration(
m, hasDecimal, mp := l.consumeNum()
if m < 0 {
- return duration.Duration{}, pgerror.Newf(
+ return Duration{}, pgerror.Newf(
pgcode.InvalidDatetimeFormat, "interval: invalid format: %s", l.str)
}
// We have three possible formats:
@@ -608,7 +607,7 @@ func (l *intervalLexer) parseShortDuration(
// represent minutes. Get this out of the way first.
if hasDecimal {
l.consumeSpaces()
- return duration.MakeDuration(
+ return MakeDuration(
h*time.Minute.Nanoseconds()+
sign*(m*time.Second.Nanoseconds()+
floatToNanos(mp)),
@@ -627,7 +626,7 @@ func (l *intervalLexer) parseShortDuration(
l.offset++
s, _, sp = l.consumeNum()
if s < 0 {
- return duration.Duration{}, pgerror.Newf(
+ return Duration{}, pgerror.Newf(
pgcode.InvalidDatetimeFormat, "interval: invalid format: %s", l.str)
}
}
@@ -635,13 +634,13 @@ func (l *intervalLexer) parseShortDuration(
l.consumeSpaces()
if !hasSecondsComponent && itm.DurationField.IsMinuteToSecond() {
- return duration.MakeDuration(
+ return MakeDuration(
h*time.Minute.Nanoseconds()+sign*(m*time.Second.Nanoseconds()),
0,
0,
), nil
}
- return duration.MakeDuration(
+ return MakeDuration(
h*time.Hour.Nanoseconds()+
sign*(m*time.Minute.Nanoseconds()+
int64(mp*float64(time.Minute.Nanoseconds()))+
@@ -656,7 +655,7 @@ func (l *intervalLexer) parseShortDuration(
// given as second argument multiplied by the factor in the third
// argument. For computing fractions there are 30 days to a month and
// 24 hours to a day.
-func addFrac(d duration.Duration, unit duration.Duration, f float64) (duration.Duration, error) {
+func addFrac(d Duration, unit Duration, f float64) (Duration, error) {
if unit.Months > 0 {
f = f * float64(unit.Months)
d.Months += int64(f)
@@ -671,7 +670,7 @@ func addFrac(d duration.Duration, unit duration.Duration, f float64) (duration.D
// months. Do not continue to add precision to the interval.
// See issue #55226 for more details on this.
default:
- return duration.Duration{}, errors.AssertionFailedf("unhandled unit type %v", unit)
+ return Duration{}, errors.AssertionFailedf("unhandled unit type %v", unit)
}
} else if unit.Days > 0 {
f = f * float64(unit.Days)
diff --git a/pkg/sql/sem/tree/interval_test.go b/pkg/util/duration/parse_test.go
similarity index 95%
rename from pkg/sql/sem/tree/interval_test.go
rename to pkg/util/duration/parse_test.go
index 5523fecb07a2..7348bca938d6 100644
--- a/pkg/sql/sem/tree/interval_test.go
+++ b/pkg/util/duration/parse_test.go
@@ -8,13 +8,12 @@
// by the Apache License, Version 2.0, included in the file
// licenses/APL.txt.
-package tree
+package duration
import (
"testing"
"github.com/cockroachdb/cockroach/pkg/sql/types"
- "github.com/cockroachdb/cockroach/pkg/util/duration"
"github.com/cockroachdb/cockroach/pkg/util/leaktest"
"github.com/cockroachdb/cockroach/pkg/util/log"
)
@@ -181,9 +180,9 @@ func TestValidSQLIntervalSyntax(t *testing.T) {
t.Fatalf(`%q: got "%s", expected "%s"`, test.input, s, test.output)
}
- for style := range duration.IntervalStyle_value {
+ for style := range IntervalStyle_value {
t.Run(style, func(t *testing.T) {
- styleVal := duration.IntervalStyle(duration.IntervalStyle_value[style])
+ styleVal := IntervalStyle(IntervalStyle_value[style])
dur2, err := parseDuration(styleVal, s, test.itm)
if err != nil {
t.Fatalf(`%q: repr "%s" is not parsable: %v`, test.input, s, err)
@@ -195,7 +194,7 @@ func TestValidSQLIntervalSyntax(t *testing.T) {
}
// Test that a Datum recognizes the format.
- di, err := parseInterval(styleVal, test.input, test.itm)
+ di, err := ParseInterval(styleVal, test.input, test.itm)
if err != nil {
t.Fatalf(`%q: unrecognized as datum: %v`, test.input, err)
}
@@ -421,11 +420,11 @@ func TestPGIntervalSyntax(t *testing.T) {
}
for _, test := range testData {
t.Run(test.input, func(t *testing.T) {
- for style := range duration.IntervalStyle_value {
+ for style := range IntervalStyle_value {
t.Run(style, func(t *testing.T) {
- styleVal := duration.IntervalStyle(duration.IntervalStyle_value[style])
+ styleVal := IntervalStyle(IntervalStyle_value[style])
expectedError := test.error
- if test.errorSQLStandard != "" && styleVal == duration.IntervalStyle_SQL_STANDARD {
+ if test.errorSQLStandard != "" && styleVal == IntervalStyle_SQL_STANDARD {
expectedError = test.errorSQLStandard
}
@@ -445,7 +444,7 @@ func TestPGIntervalSyntax(t *testing.T) {
}
s := dur.String()
expected := test.output
- if test.outputSQLStandard != "" && styleVal == duration.IntervalStyle_SQL_STANDARD {
+ if test.outputSQLStandard != "" && styleVal == IntervalStyle_SQL_STANDARD {
expected = test.outputSQLStandard
}
if s != expected {
@@ -462,7 +461,7 @@ func TestPGIntervalSyntax(t *testing.T) {
}
// Test that a Datum recognizes the format.
- di, err := parseInterval(styleVal, test.input, test.itm)
+ di, err := ParseInterval(styleVal, test.input, test.itm)
if err != nil {
t.Fatalf(`%q: unrecognized as datum: %v`, test.input, err)
}
@@ -573,9 +572,9 @@ func TestISO8601IntervalSyntax(t *testing.T) {
t.Fatalf(`%q: got "%s", expected "%s"`, test.input, s, test.output)
}
- for style := range duration.IntervalStyle_value {
+ for style := range IntervalStyle_value {
t.Run(style, func(t *testing.T) {
- dur2, err := parseDuration(duration.IntervalStyle(duration.IntervalStyle_value[style]), s, test.itm)
+ dur2, err := parseDuration(IntervalStyle(IntervalStyle_value[style]), s, test.itm)
if err != nil {
t.Fatalf(`%q: repr "%s" is not parsable: %v`, test.input, s, err)
}
@@ -585,7 +584,7 @@ func TestISO8601IntervalSyntax(t *testing.T) {
}
// Test that a Datum recognizes the format.
- di, err := parseInterval(duration.IntervalStyle(duration.IntervalStyle_value[style]), test.input, test.itm)
+ di, err := ParseInterval(IntervalStyle(IntervalStyle_value[style]), test.input, test.itm)
if err != nil {
t.Fatalf(`%q: unrecognized as datum: %v`, test.input, err)
}
@@ -597,7 +596,7 @@ func TestISO8601IntervalSyntax(t *testing.T) {
// Test that ISO 8601 output format also round-trips
s4 := dur.ISO8601String()
- di2, err := parseInterval(duration.IntervalStyle(duration.IntervalStyle_value[style]), s4, test.itm)
+ di2, err := ParseInterval(IntervalStyle(IntervalStyle_value[style]), s4, test.itm)
if err != nil {
t.Fatalf(`%q: ISO8601String "%s" unrecognized as datum: %v`, test.input, s4, err)
}
diff --git a/pkg/util/timeutil/pgdate/field_extract.go b/pkg/util/timeutil/pgdate/field_extract.go
index da5e94f1a92f..3fe0afac37bb 100644
--- a/pkg/util/timeutil/pgdate/field_extract.go
+++ b/pkg/util/timeutil/pgdate/field_extract.go
@@ -861,7 +861,7 @@ func (fe *fieldExtract) SetDayOfYear(chunk numberChunk) error {
if !ok {
return errors.AssertionFailedf("year must be set before day of year")
}
- y, m, d := julianDayToDate(dateToJulianDay(y, 1, 1) + chunk.v - 1)
+ y, m, d := julianDayToDate(DateToJulianDay(y, 1, 1) + chunk.v - 1)
if err := fe.Reset(fieldYear, y); err != nil {
return err
}
diff --git a/pkg/util/timeutil/pgdate/math.go b/pkg/util/timeutil/pgdate/math.go
index be43838b5a74..21e9ebeacbfa 100644
--- a/pkg/util/timeutil/pgdate/math.go
+++ b/pkg/util/timeutil/pgdate/math.go
@@ -20,8 +20,8 @@ var daysInMonth = [2][13]int{
{0, 31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31},
}
-// dateToJulianDay is based on the date2j function in PostgreSQL 10.5.
-func dateToJulianDay(year int, month int, day int) int {
+// DateToJulianDay is based on the date2j function in PostgreSQL 10.5.
+func DateToJulianDay(year int, month int, day int) int {
if month > 2 {
month++
year += 4800
diff --git a/pkg/util/tochar/BUILD.bazel b/pkg/util/tochar/BUILD.bazel
new file mode 100644
index 000000000000..f4b268f91c34
--- /dev/null
+++ b/pkg/util/tochar/BUILD.bazel
@@ -0,0 +1,39 @@
+load("//build/bazelutil/unused_checker:unused.bzl", "get_x_data")
+load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test")
+
+go_library(
+ name = "tochar",
+ srcs = [
+ "constants.go",
+ "tochar.go",
+ ],
+ importpath = "github.com/cockroachdb/cockroach/pkg/util/tochar",
+ visibility = ["//visibility:public"],
+ deps = [
+ "//pkg/sql/pgwire/pgcode",
+ "//pkg/sql/pgwire/pgerror",
+ "//pkg/util/duration",
+ "//pkg/util/timeutil/pgdate",
+ "@com_github_cockroachdb_errors//:errors",
+ ],
+)
+
+go_test(
+ name = "tochar_test",
+ srcs = ["tochar_test.go"],
+ args = ["-test.timeout=295s"],
+ data = glob(["testdata/**"]),
+ embed = [":tochar"],
+ deps = [
+ "//pkg/sql/pgwire/pgerror",
+ "//pkg/sql/types",
+ "//pkg/util/duration",
+ "//pkg/util/timeutil",
+ "//pkg/util/timeutil/pgdate",
+ "@com_github_cockroachdb_datadriven//:datadriven",
+ "@com_github_cockroachdb_errors//:errors",
+ "@com_github_stretchr_testify//require",
+ ],
+)
+
+get_x_data(name = "get_x_data")
diff --git a/pkg/util/tochar/constants.go b/pkg/util/tochar/constants.go
new file mode 100644
index 000000000000..1d7641168b24
--- /dev/null
+++ b/pkg/util/tochar/constants.go
@@ -0,0 +1,378 @@
+// Copyright 2022 The Cockroach Authors.
+//
+// Use of this software is governed by the Business Source License
+// included in the file licenses/BSL.txt.
+//
+// As of the Change Date specified in that file, in accordance with
+// the Business Source License, use of this software will be governed
+// by the Apache License, Version 2.0, included in the file
+// licenses/APL.txt.
+
+package tochar
+
+// fromCharDateMode is used to denote date conventions.
+// Taken from https://github.com/postgres/postgres/blob/b0b72c64a0ce7bf5dd78a80b33d85c89c943ad0d/src/backend/utils/adt/formatting.c#L170-L175.
+type fromCharDateMode int
+
+const (
+ fromCharDateNone = 0
+ fromCharDateGregorian
+ fromCharDateISOWeek
+)
+
+// Taken from https://github.com/postgres/postgres/blob/b0b72c64a0ce7bf5dd78a80b33d85c89c943ad0d/src/backend/utils/adt/formatting.c#L632.
+// Denotes a "keyword" used for to_char.
+type dchPos int
+
+// This block is word-for-word out of the enum for dch_pos in PG.
+const (
+ DCH_A_D dchPos = iota
+ DCH_A_M
+ DCH_AD
+ DCH_AM
+ DCH_B_C
+ DCH_BC
+ DCH_CC
+ DCH_DAY
+ DCH_DDD
+ DCH_DD
+ DCH_DY
+ DCH_Day
+ DCH_Dy
+ DCH_D
+ DCH_FF1
+ DCH_FF2
+ DCH_FF3
+ DCH_FF4
+ DCH_FF5
+ DCH_FF6
+ DCH_FX
+ DCH_HH24
+ DCH_HH12
+ DCH_HH
+ DCH_IDDD
+ DCH_ID
+ DCH_IW
+ DCH_IYYY
+ DCH_IYY
+ DCH_IY
+ DCH_I
+ DCH_J
+ DCH_MI
+ DCH_MM
+ DCH_MONTH
+ DCH_MON
+ DCH_MS
+ DCH_Month
+ DCH_Mon
+ DCH_OF
+ DCH_P_M
+ DCH_PM
+ DCH_Q
+ DCH_RM
+ DCH_SSSSS
+ DCH_SSSS
+ DCH_SS
+ DCH_TZH
+ DCH_TZM
+ DCH_TZ
+ DCH_US
+ DCH_WW
+ DCH_W
+ DCH_Y_YYY
+ DCH_YYYY
+ DCH_YYY
+ DCH_YY
+ DCH_Y
+ DCH_a_d
+ DCH_a_m
+ DCH_ad
+ DCH_am
+ DCH_b_c
+ DCH_bc
+ DCH_cc
+ DCH_day
+ DCH_ddd
+ DCH_dd
+ DCH_dy
+ DCH_d
+ DCH_ff1
+ DCH_ff2
+ DCH_ff3
+ DCH_ff4
+ DCH_ff5
+ DCH_ff6
+ DCH_fx
+ DCH_hh24
+ DCH_hh12
+ DCH_hh
+ DCH_iddd
+ DCH_id
+ DCH_iw
+ DCH_iyyy
+ DCH_iyy
+ DCH_iy
+ DCH_i
+ DCH_j
+ DCH_mi
+ DCH_mm
+ DCH_month
+ DCH_mon
+ DCH_ms
+ DCH_of
+ DCH_p_m
+ DCH_pm
+ DCH_q
+ DCH_rm
+ DCH_sssss
+ DCH_ssss
+ DCH_ss
+ DCH_tzh
+ DCH_tzm
+ DCH_tz
+ DCH_us
+ DCH_ww
+ DCH_w
+ DCH_y_yyy
+ DCH_yyyy
+ DCH_yyy
+ DCH_yy
+ DCH_y
+
+ DCH_last
+)
+
+// KeyWord is used to denote properties from the dchPos.
+// Taken from https://github.com/postgres/postgres/blob/b0b72c64a0ce7bf5dd78a80b33d85c89c943ad0d/src/backend/utils/adt/formatting.c#L177-L184
+type keyWord struct {
+ name string
+ id dchPos
+ isDigit bool
+ dateMode fromCharDateMode
+}
+
+// dchKeywords is an array of keyWords, ordered in ASCII order.
+// Taken from https://github.com/postgres/postgres/blob/b0b72c64a0ce7bf5dd78a80b33d85c89c943ad0d/src/backend/utils/adt/formatting.c#L798.
+var dchKeywords = []keyWord{
+ {"A.D.", DCH_A_D, false, fromCharDateNone}, /* A */
+ {"A.M.", DCH_A_M, false, fromCharDateNone},
+ {"AD", DCH_AD, false, fromCharDateNone},
+ {"AM", DCH_AM, false, fromCharDateNone},
+ {"B.C.", DCH_B_C, false, fromCharDateNone}, /* B */
+ {"BC", DCH_BC, false, fromCharDateNone},
+ {"CC", DCH_CC, true, fromCharDateNone}, /* C */
+ {"DAY", DCH_DAY, false, fromCharDateNone}, /* D */
+ {"DDD", DCH_DDD, true, fromCharDateGregorian},
+ {"DD", DCH_DD, true, fromCharDateGregorian},
+ {"DY", DCH_DY, false, fromCharDateNone},
+ {"Day", DCH_Day, false, fromCharDateNone},
+ {"Dy", DCH_Dy, false, fromCharDateNone},
+ {"D", DCH_D, true, fromCharDateGregorian},
+ {"FF1", DCH_FF1, false, fromCharDateNone}, /* F */
+ {"FF2", DCH_FF2, false, fromCharDateNone},
+ {"FF3", DCH_FF3, false, fromCharDateNone},
+ {"FF4", DCH_FF4, false, fromCharDateNone},
+ {"FF5", DCH_FF5, false, fromCharDateNone},
+ {"FF6", DCH_FF6, false, fromCharDateNone},
+ {"FX", DCH_FX, false, fromCharDateNone},
+ {"HH24", DCH_HH24, true, fromCharDateNone}, /* H */
+ {"HH12", DCH_HH12, true, fromCharDateNone},
+ {"HH", DCH_HH, true, fromCharDateNone},
+ {"IDDD", DCH_IDDD, true, fromCharDateISOWeek}, /* I */
+ {"ID", DCH_ID, true, fromCharDateISOWeek},
+ {"IW", DCH_IW, true, fromCharDateISOWeek},
+ {"IYYY", DCH_IYYY, true, fromCharDateISOWeek},
+ {"IYY", DCH_IYY, true, fromCharDateISOWeek},
+ {"IY", DCH_IY, true, fromCharDateISOWeek},
+ {"I", DCH_I, true, fromCharDateISOWeek},
+ {"J", DCH_J, true, fromCharDateNone}, /* J */
+ {"MI", DCH_MI, true, fromCharDateNone}, /* M */
+ {"MM", DCH_MM, true, fromCharDateGregorian},
+ {"MONTH", DCH_MONTH, false, fromCharDateGregorian},
+ {"MON", DCH_MON, false, fromCharDateGregorian},
+ {"MS", DCH_MS, true, fromCharDateNone},
+ {"Month", DCH_Month, false, fromCharDateGregorian},
+ {"Mon", DCH_Mon, false, fromCharDateGregorian},
+ {"OF", DCH_OF, false, fromCharDateNone}, /* O */
+ {"P.M.", DCH_P_M, false, fromCharDateNone}, /* P */
+ {"PM", DCH_PM, false, fromCharDateNone},
+ {"Q", DCH_Q, true, fromCharDateNone}, /* Q */
+ {"RM", DCH_RM, false, fromCharDateGregorian}, /* R */
+ {"SSSSS", DCH_SSSS, true, fromCharDateNone}, /* S */
+ {"SSSS", DCH_SSSS, true, fromCharDateNone},
+ {"SS", DCH_SS, true, fromCharDateNone},
+ {"TZH", DCH_TZH, false, fromCharDateNone}, /* T */
+ {"TZM", DCH_TZM, true, fromCharDateNone},
+ {"TZ", DCH_TZ, false, fromCharDateNone},
+ {"US", DCH_US, true, fromCharDateNone}, /* U */
+ {"WW", DCH_WW, true, fromCharDateGregorian}, /* W */
+ {"W", DCH_W, true, fromCharDateGregorian},
+ {"Y,YYY", DCH_Y_YYY, true, fromCharDateGregorian}, /* Y */
+ {"YYYY", DCH_YYYY, true, fromCharDateGregorian},
+ {"YYY", DCH_YYY, true, fromCharDateGregorian},
+ {"YY", DCH_YY, true, fromCharDateGregorian},
+ {"Y", DCH_Y, true, fromCharDateGregorian},
+ {"a.d.", DCH_a_d, false, fromCharDateNone}, /* a */
+ {"a.m.", DCH_a_m, false, fromCharDateNone},
+ {"ad", DCH_ad, false, fromCharDateNone},
+ {"am", DCH_am, false, fromCharDateNone},
+ {"b.c.", DCH_b_c, false, fromCharDateNone}, /* b */
+ {"bc", DCH_bc, false, fromCharDateNone},
+ {"cc", DCH_CC, true, fromCharDateNone}, /* c */
+ {"day", DCH_day, false, fromCharDateNone}, /* d */
+ {"ddd", DCH_DDD, true, fromCharDateGregorian},
+ {"dd", DCH_DD, true, fromCharDateGregorian},
+ {"dy", DCH_dy, false, fromCharDateNone},
+ {"d", DCH_D, true, fromCharDateGregorian},
+ {"ff1", DCH_FF1, false, fromCharDateNone}, /* f */
+ {"ff2", DCH_FF2, false, fromCharDateNone},
+ {"ff3", DCH_FF3, false, fromCharDateNone},
+ {"ff4", DCH_FF4, false, fromCharDateNone},
+ {"ff5", DCH_FF5, false, fromCharDateNone},
+ {"ff6", DCH_FF6, false, fromCharDateNone},
+ {"fx", DCH_FX, false, fromCharDateNone},
+ {"hh24", DCH_HH24, true, fromCharDateNone}, /* h */
+ {"hh12", DCH_HH12, true, fromCharDateNone},
+ {"hh", DCH_HH, true, fromCharDateNone},
+ {"iddd", DCH_IDDD, true, fromCharDateISOWeek}, /* i */
+ {"id", DCH_ID, true, fromCharDateISOWeek},
+ {"iw", DCH_IW, true, fromCharDateISOWeek},
+ {"iyyy", DCH_IYYY, true, fromCharDateISOWeek},
+ {"iyy", DCH_IYY, true, fromCharDateISOWeek},
+ {"iy", DCH_IY, true, fromCharDateISOWeek},
+ {"i", DCH_I, true, fromCharDateISOWeek},
+ {"j", DCH_J, true, fromCharDateNone}, /* j */
+ {"mi", DCH_MI, true, fromCharDateNone}, /* m */
+ {"mm", DCH_MM, true, fromCharDateGregorian},
+ {"month", DCH_month, false, fromCharDateGregorian},
+ {"mon", DCH_mon, false, fromCharDateGregorian},
+ {"ms", DCH_MS, true, fromCharDateNone},
+ {"of", DCH_OF, false, fromCharDateNone}, /* o */
+ {"p.m.", DCH_p_m, false, fromCharDateNone}, /* p */
+ {"pm", DCH_pm, false, fromCharDateNone},
+ {"q", DCH_Q, true, fromCharDateNone}, /* q */
+ {"rm", DCH_rm, false, fromCharDateGregorian}, /* r */
+ {"sssss", DCH_SSSS, true, fromCharDateNone}, /* s */
+ {"ssss", DCH_SSSS, true, fromCharDateNone},
+ {"ss", DCH_SS, true, fromCharDateNone},
+ {"tzh", DCH_TZH, false, fromCharDateNone}, /* t */
+ {"tzm", DCH_TZM, true, fromCharDateNone},
+ {"tz", DCH_tz, false, fromCharDateNone},
+ {"us", DCH_US, true, fromCharDateNone}, /* u */
+ {"ww", DCH_WW, true, fromCharDateGregorian}, /* w */
+ {"w", DCH_W, true, fromCharDateGregorian},
+ {"y,yyy", DCH_Y_YYY, true, fromCharDateGregorian}, /* y */
+ {"yyyy", DCH_YYYY, true, fromCharDateGregorian},
+ {"yyy", DCH_YYY, true, fromCharDateGregorian},
+ {"yy", DCH_YY, true, fromCharDateGregorian},
+ {"y", DCH_Y, true, fromCharDateGregorian},
+
+ /* last */
+ //{"", 0, false, 0},
+}
+
+// Months in roman-numeral
+// (Must be in reverse order for seq_search (in FROM_CHAR), because
+// 'VIII' must have higher precedence than 'V')
+var (
+ ucMonthRomanNumerals = []string{"XII", "XI", "X", "IX", "VIII", "VII", "VI", "V", "IV", "III", "II", "I"}
+ lcMonthRomanNumerals = []string{"xii", "xi", "x", "ix", "viii", "vii", "vi", "v", "iv", "iii", "ii", "i"}
+)
+
+// From https://github.com/postgres/postgres/blob/b0b72c64a0ce7bf5dd78a80b33d85c89c943ad0d/src/backend/utils/adt/formatting.c#L194-L198.
+type formatNodeType int
+
+const (
+ formatNodeEnd formatNodeType = iota
+ formatNodeAction
+ formatNodeChar
+ formatNodeSeparator
+ formatNodeSpace
+)
+
+type formatNode struct {
+ typ formatNodeType
+ character string
+ suffix dchSuffix
+ key *keyWord
+}
+
+// dchIndex maps ASCII characters to the start index in dchKeywords of possible
+// matches.
+var dchIndex = []dchPos{
+ /*---- first 0..31 chars are skipped ----*/
+
+ -1, -1, -1, -1, -1, -1, -1, -1,
+ -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
+ -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
+ -1, -1, -1, -1, -1, DCH_A_D, DCH_B_C, DCH_CC, DCH_DAY, -1,
+ DCH_FF1, -1, DCH_HH24, DCH_IDDD, DCH_J, -1, -1, DCH_MI, -1, DCH_OF,
+ DCH_P_M, DCH_Q, DCH_RM, DCH_SSSSS, DCH_TZH, DCH_US, -1, DCH_WW, -1, DCH_Y_YYY,
+ -1, -1, -1, -1, -1, -1, -1, DCH_a_d, DCH_b_c, DCH_cc,
+ DCH_day, -1, DCH_ff1, -1, DCH_hh24, DCH_iddd, DCH_j, -1, -1, DCH_mi,
+ -1, DCH_of, DCH_p_m, DCH_q, DCH_rm, DCH_sssss, DCH_tzh, DCH_us, -1, DCH_ww,
+ -1, DCH_y_yyy, -1, -1, -1, -1,
+
+ /*---- chars over 126 are skipped ----*/
+}
+
+// From https://github.com/postgres/postgres/blob/master/src/backend/utils/adt/formatting.c#L155.
+type keySuffixType int
+
+const (
+ keySuffixPrefix keySuffixType = iota
+ keySuffixPostfix
+)
+
+type dchSuffix int
+
+// This blocks contains all dch_suffix types used in PG.
+const (
+ DCH_S_FM dchSuffix = 0x01
+ DCH_S_TH dchSuffix = 0x02
+ DCH_S_th dchSuffix = 0x04
+ DCH_S_SP dchSuffix = 0x08
+ DCH_S_TM dchSuffix = 0x10
+)
+
+func (d dchSuffix) FM() bool {
+ return d&DCH_S_FM > 0
+}
+
+func (d dchSuffix) SP() bool {
+ return d&DCH_S_SP > 0
+}
+
+func (d dchSuffix) TM() bool {
+ return d&DCH_S_TM > 0
+}
+
+func (d dchSuffix) THth() bool {
+ return d.TH() || d.th()
+}
+
+func (d dchSuffix) TH() bool {
+ return d&DCH_S_TH > 0
+}
+
+func (d dchSuffix) th() bool {
+ return d&DCH_S_th > 0
+}
+
+type keySuffix struct {
+ name string
+ id dchSuffix
+ typ keySuffixType
+}
+
+var dchSuffixes = []keySuffix{
+ {"FM", DCH_S_FM, keySuffixPrefix},
+ {"fm", DCH_S_FM, keySuffixPrefix},
+ {"TM", DCH_S_TM, keySuffixPrefix},
+ {"tm", DCH_S_TM, keySuffixPrefix},
+ {"TH", DCH_S_TH, keySuffixPostfix},
+ {"th", DCH_S_th, keySuffixPostfix},
+ {"SP", DCH_S_SP, keySuffixPostfix},
+ /* last */
+ //{"", 0, -1},
+}
diff --git a/pkg/util/tochar/testdata/ampm.ddt b/pkg/util/tochar/testdata/ampm.ddt
new file mode 100644
index 000000000000..6d8d1b180abb
--- /dev/null
+++ b/pkg/util/tochar/testdata/ampm.ddt
@@ -0,0 +1,81 @@
+# Test AM,PM
+
+timestamp
+A.M.
+2020-02-22 03:16:17
+2020-02-22 12:16:17
+2020-02-22 15:16:17
+----
+2020-02-22 03:16:17: A.M.
+2020-02-22 12:16:17: P.M.
+2020-02-22 15:16:17: P.M.
+
+timestamp
+P.M.
+2020-02-22 03:16:17
+2020-02-22 12:16:17
+2020-02-22 15:16:17
+----
+2020-02-22 03:16:17: A.M.
+2020-02-22 12:16:17: P.M.
+2020-02-22 15:16:17: P.M.
+
+timestamp
+AM
+2020-02-22 03:16:17
+2020-02-22 12:16:17
+2020-02-22 15:16:17
+----
+2020-02-22 03:16:17: AM
+2020-02-22 12:16:17: PM
+2020-02-22 15:16:17: PM
+
+timestamp
+PM
+2020-02-22 03:16:17
+2020-02-22 12:16:17
+2020-02-22 15:16:17
+----
+2020-02-22 03:16:17: AM
+2020-02-22 12:16:17: PM
+2020-02-22 15:16:17: PM
+
+timestamp
+a.m.
+2020-02-22 03:16:17
+2020-02-22 12:16:17
+2020-02-22 15:16:17
+----
+2020-02-22 03:16:17: a.m.
+2020-02-22 12:16:17: p.m.
+2020-02-22 15:16:17: p.m.
+
+timestamp
+p.m.
+2020-02-22 03:16:17
+2020-02-22 12:16:17
+2020-02-22 15:16:17
+----
+2020-02-22 03:16:17: a.m.
+2020-02-22 12:16:17: p.m.
+2020-02-22 15:16:17: p.m.
+
+timestamp
+am
+2020-02-22 03:16:17
+2020-02-22 12:16:17
+2020-02-22 15:16:17
+----
+2020-02-22 03:16:17: am
+2020-02-22 12:16:17: pm
+2020-02-22 15:16:17: pm
+
+timestamp
+pm
+2020-02-22 03:16:17
+2020-02-22 12:16:17
+2020-02-22 15:16:17
+----
+2020-02-22 03:16:17: am
+2020-02-22 12:16:17: pm
+2020-02-22 15:16:17: pm
diff --git a/pkg/util/tochar/testdata/bcad.ddt b/pkg/util/tochar/testdata/bcad.ddt
new file mode 100644
index 000000000000..807f379f27a6
--- /dev/null
+++ b/pkg/util/tochar/testdata/bcad.ddt
@@ -0,0 +1,99 @@
+timestamp
+BC
+0001-02-22 BC 00:15:18
+0001-02-22 00:15:18
+----
+0001-02-22 BC 00:15:18: BC
+0001-02-22 00:15:18: AD
+
+timestamp
+AD
+0001-02-22 BC 00:15:18
+0001-02-22 00:15:18
+----
+0001-02-22 BC 00:15:18: BC
+0001-02-22 00:15:18: AD
+
+timestamp
+B.C.
+0001-02-22 BC 00:15:18
+0001-02-22 00:15:18
+----
+0001-02-22 BC 00:15:18: B.C.
+0001-02-22 00:15:18: A.D.
+
+timestamp
+A.D.
+0001-02-22 BC 00:15:18
+0001-02-22 00:15:18
+----
+0001-02-22 BC 00:15:18: B.C.
+0001-02-22 00:15:18: A.D.
+
+timestamp
+bc
+0001-02-22 BC 00:15:18
+0001-02-22 00:15:18
+----
+0001-02-22 BC 00:15:18: bc
+0001-02-22 00:15:18: ad
+
+timestamp
+ad
+0001-02-22 BC 00:15:18
+0001-02-22 00:15:18
+----
+0001-02-22 BC 00:15:18: bc
+0001-02-22 00:15:18: ad
+
+timestamp
+b.c.
+0001-02-22 BC 00:15:18
+0001-02-22 00:15:18
+----
+0001-02-22 BC 00:15:18: b.c.
+0001-02-22 00:15:18: a.d.
+
+timestamp
+a.d.
+0001-02-22 BC 00:15:18
+0001-02-22 00:15:18
+----
+0001-02-22 BC 00:15:18: b.c.
+0001-02-22 00:15:18: a.d.
+
+interval_fmt
+1 day
+a.d.
+b.c.
+A.D.
+B.C.
+ad
+bc
+AD
+BC
+----
+a.d.: [ERROR] invalid format specification for an interval value
+ DETAIL: format a.d.
+ HINT: intervals are not tied to specific calendar dates
+b.c.: [ERROR] invalid format specification for an interval value
+ DETAIL: format b.c.
+ HINT: intervals are not tied to specific calendar dates
+A.D.: [ERROR] invalid format specification for an interval value
+ DETAIL: format A.D.
+ HINT: intervals are not tied to specific calendar dates
+B.C.: [ERROR] invalid format specification for an interval value
+ DETAIL: format B.C.
+ HINT: intervals are not tied to specific calendar dates
+ad: [ERROR] invalid format specification for an interval value
+ DETAIL: format ad
+ HINT: intervals are not tied to specific calendar dates
+bc: [ERROR] invalid format specification for an interval value
+ DETAIL: format bc
+ HINT: intervals are not tied to specific calendar dates
+AD: [ERROR] invalid format specification for an interval value
+ DETAIL: format AD
+ HINT: intervals are not tied to specific calendar dates
+BC: [ERROR] invalid format specification for an interval value
+ DETAIL: format BC
+ HINT: intervals are not tied to specific calendar dates
diff --git a/pkg/util/tochar/testdata/parse.ddt b/pkg/util/tochar/testdata/parse.ddt
new file mode 100644
index 000000000000..049e80004652
--- /dev/null
+++ b/pkg/util/tochar/testdata/parse.ddt
@@ -0,0 +1,5 @@
+timestamp
+YYYY-MM-DD HH24:MI:SS.US "\"ignore YY\"" \i\"gnore_me+
+2020-01-02 01:03:05
+----
+2020-01-02 01:03:05: 2020-01-02 01:03:05.000000 "ignore YY" \0"gnore_me+
diff --git a/pkg/util/tochar/testdata/time.ddt b/pkg/util/tochar/testdata/time.ddt
new file mode 100644
index 000000000000..92d21745914d
--- /dev/null
+++ b/pkg/util/tochar/testdata/time.ddt
@@ -0,0 +1,134 @@
+# Test HH12, HH.
+
+timestamp
+HH12:MI:SS
+2020-02-22 00:16:17
+2020-02-22 03:16:17
+2020-02-22 12:16:17
+2020-02-22 15:16:17
+----
+2020-02-22 00:16:17: 12:16:17
+2020-02-22 03:16:17: 03:16:17
+2020-02-22 12:16:17: 12:16:17
+2020-02-22 15:16:17: 03:16:17
+
+interval
+HH
+-50:01:02
+-23:01:02
+00:01:02
+15:16:18
+222:12:12
+----
+-50:01:02: -02
+-23:01:02: -11
+00:01:02: 12
+15:16:18: 03
+222:12:12: 06
+
+timestamp
+HHth:MITH:SSth
+2020-02-22 00:15:18
+2020-02-22 03:11:01
+2020-02-22 12:12:02
+2020-02-22 15:03:13
+----
+2020-02-22 00:15:18: 12th:15TH:18th
+2020-02-22 03:11:01: 03rd:11TH:01st
+2020-02-22 12:12:02: 12th:12TH:02nd
+2020-02-22 15:03:13: 03rd:03RD:13th
+
+# Test HH24
+
+timestamp
+FMHH24:MI:SS
+2020-02-22 00:16:17
+2020-02-22 03:16:17
+2020-02-22 12:16:17
+2020-02-22 15:16:17
+----
+2020-02-22 00:16:17: 0:16:17
+2020-02-22 03:16:17: 3:16:17
+2020-02-22 12:16:17: 12:16:17
+2020-02-22 15:16:17: 15:16:17
+
+interval
+HH24
+-50:01:02
+-23:01:02
+00:01:02
+15:16:18
+222:12:12
+----
+-50:01:02: -50
+-23:01:02: -23
+00:01:02: 00
+15:16:18: 15
+222:12:12: 222
+
+# Test fractional seconds.
+timestamp_fmt
+2020-02-22 03:16:17.123456
+HH:MI:SS.FF1
+HH:MI:SS.FF2
+HH:MI:SS.FF3
+HH:MI:SS.FF4
+HH:MI:SS.FF5
+HH:MI:SS.FF6
+HH:MI:SS.MS
+HH:MI:SS.US
+----
+HH:MI:SS.FF1: 03:16:17.1
+HH:MI:SS.FF2: 03:16:17.12
+HH:MI:SS.FF3: 03:16:17.123
+HH:MI:SS.FF4: 03:16:17.1234
+HH:MI:SS.FF5: 03:16:17.12345
+HH:MI:SS.FF6: 03:16:17.123456
+HH:MI:SS.MS: 03:16:17.123
+HH:MI:SS.US: 03:16:17.123456
+
+interval_fmt
+-15:04:05.123
+HH:MI:SS.FF1
+HH:MI:SS.FF2
+HH:MI:SS.FF3
+HH:MI:SS.FF4
+HH:MI:SS.FF5
+HH:MI:SS.FF6
+HH:MI:SS.MS
+HH:MI:SS.US
+HH24:MI:SS.US
+----
+HH:MI:SS.FF1: -03:-04:-05.-1
+HH:MI:SS.FF2: -03:-04:-05.-12
+HH:MI:SS.FF3: -03:-04:-05.-123
+HH:MI:SS.FF4: -03:-04:-05.-1230
+HH:MI:SS.FF5: -03:-04:-05.-12300
+HH:MI:SS.FF6: -03:-04:-05.-123000
+HH:MI:SS.MS: -03:-04:-05.-123
+HH:MI:SS.US: -03:-04:-05.-123000
+HH24:MI:SS.US: -15:-04:-05.-123000
+
+
+# Test SSSS
+timestamp
+SSSS
+2020-02-22 00:16:17
+2020-02-22 03:16:17
+2020-02-22 12:16:17
+2020-02-22 15:16:17
+----
+2020-02-22 00:16:17: 977
+2020-02-22 03:16:17: 11777
+2020-02-22 12:16:17: 44177
+2020-02-22 15:16:17: 54977
+
+interval
+SSSS
+1 day 01:02:03.456
+1 month 2 days 01:02:03.456
+13 months 3 days 01:02:03.456
+----
+1 day 01:02:03.456: 3723
+1 month 2 days 01:02:03.456: 3723
+13 months 3 days 01:02:03.456: 3723
diff --git a/pkg/util/tochar/testdata/tz.ddt b/pkg/util/tochar/testdata/tz.ddt
new file mode 100644
index 000000000000..bbbcb9561b37
--- /dev/null
+++ b/pkg/util/tochar/testdata/tz.ddt
@@ -0,0 +1,71 @@
+timestamp_fmt
+2020-02-22 00:16:17
+tz
+TZ
+TZH
+TZM
+OF
+FMOF
+----
+tz: utc
+TZ: UTC
+TZH: +00
+TZM: 00
+OF: +00
+FMOF: +0
+
+timestamp_fmt tz=Australia/Adelaide
+2020-02-22 00:16:17
+tz
+TZ
+TZH
+TZM
+OF
+FMOF
+----
+tz: acdt
+TZ: ACDT
+TZH: +10
+TZM: 30
+OF: +10:30
+FMOF: +10:30
+
+timestamp_fmt tz=America/St_Johns
+2020-02-22 00:16:17
+tz
+TZ
+TZH
+TZM
+OF
+FMOF
+----
+tz: nst
+TZ: NST
+TZH: -03
+TZM: 30
+OF: -03:30
+FMOF: -3:30
+
+interval_fmt
+1 day
+tz
+TZ
+TZH
+TZM
+OF
+----
+tz: [ERROR] invalid format specification for an interval value
+ DETAIL: format tz
+ HINT: intervals are not tied to specific calendar dates
+TZ: [ERROR] invalid format specification for an interval value
+ DETAIL: format TZ
+ HINT: intervals are not tied to specific calendar dates
+TZH: [ERROR] invalid format specification for an interval value
+ DETAIL: format TZH
+ HINT: intervals are not tied to specific calendar dates
+TZM: [ERROR] invalid format specification for an interval value
+ DETAIL: format TZM
+ HINT: intervals are not tied to specific calendar dates
+OF: [ERROR] invalid format specification for an interval value
+ DETAIL: format OF
+ HINT: intervals are not tied to specific calendar dates
diff --git a/pkg/util/tochar/testdata/ymd.ddt b/pkg/util/tochar/testdata/ymd.ddt
new file mode 100644
index 000000000000..8b404444444f
--- /dev/null
+++ b/pkg/util/tochar/testdata/ymd.ddt
@@ -0,0 +1,583 @@
+# Test various month operations.
+timestamp_fmt
+2020-07-22 00:15:18
+MONTHX
+MonthX
+monthX
+FMMONTHX
+FMMonthX
+FMmonthX
+MON
+Mon
+mon
+MM
+MMTH
+MMth
+FMMM
+----
+MONTHX: JULY X
+MonthX: July X
+monthX: july X
+FMMONTHX: JULYX
+FMMonthX: JulyX
+FMmonthX: julyX
+MON: JUL
+Mon: Jul
+mon: jul
+MM: 07
+MMTH: 07TH
+MMth: 07th
+FMMM: 7
+
+interval_fmt
+1 hour
+MONTHX
+MonthX
+monthX
+FMMONTHX
+FMMonthX
+FMmonthX
+MON
+Mon
+mon
+MM
+MMTH
+MMth
+FMMM
+----
+MONTHX: [ERROR] invalid format specification for an interval value
+ DETAIL: format MONTH
+ HINT: intervals are not tied to specific calendar dates
+MonthX: [ERROR] invalid format specification for an interval value
+ DETAIL: format Month
+ HINT: intervals are not tied to specific calendar dates
+monthX: [ERROR] invalid format specification for an interval value
+ DETAIL: format month
+ HINT: intervals are not tied to specific calendar dates
+FMMONTHX: [ERROR] invalid format specification for an interval value
+ DETAIL: format MONTH
+ HINT: intervals are not tied to specific calendar dates
+FMMonthX: [ERROR] invalid format specification for an interval value
+ DETAIL: format Month
+ HINT: intervals are not tied to specific calendar dates
+FMmonthX: [ERROR] invalid format specification for an interval value
+ DETAIL: format month
+ HINT: intervals are not tied to specific calendar dates
+MON: [ERROR] invalid format specification for an interval value
+ DETAIL: format MON
+ HINT: intervals are not tied to specific calendar dates
+Mon: [ERROR] invalid format specification for an interval value
+ DETAIL: format Mon
+ HINT: intervals are not tied to specific calendar dates
+mon: [ERROR] invalid format specification for an interval value
+ DETAIL: format mon
+ HINT: intervals are not tied to specific calendar dates
+MM: 00
+MMTH: 00TH
+MMth: 00th
+FMMM: 0
+
+interval_fmt
+25 months 3 days 1 hour
+MM: 00
+MMTH: 00TH
+MMth: 00th
+FMMM: 0
+----
+MM: 00: 01: 00
+MMTH: 00TH: 01ST: 00TH
+MMth: 00th: 01st: 00th
+FMMM: 0: 1: 0
+
+# Test day of week operations.
+timestamp_fmt
+2020-07-18 00:15:18
+DAYX
+DayX
+dayX
+FMDAYX
+FMDayX
+FMdayX
+DY
+Dy
+dy
+D
+ID
+Dth
+IDTH
+----
+DAYX: saturday X
+DayX: Saturday X
+dayX: saturday X
+FMDAYX: saturdayX
+FMDayX: SaturdayX
+FMdayX: saturdayX
+DY: sat
+Dy: Sat
+dy: sat
+D: 7
+ID: 6
+Dth: 7th
+IDTH: 6TH
+
+timestamp_fmt
+2020-07-19 00:15:18
+DAYX
+DayX
+dayX
+FMDAYX
+FMDayX
+FMdayX
+DY
+Dy
+dy
+D
+ID
+Dth
+IDTH
+----
+DAYX: sunday X
+DayX: Sunday X
+dayX: sunday X
+FMDAYX: sundayX
+FMDayX: SundayX
+FMdayX: sundayX
+DY: sun
+Dy: Sun
+dy: sun
+D: 1
+ID: 7
+Dth: 1st
+IDTH: 7TH
+
+interval_fmt
+1 hour
+DAYX
+DayX
+dayX
+FMDAYX
+FMDayX
+FMdayX
+DY
+Dy
+dy
+D
+ID
+Dth
+IDTH
+----
+DAYX: [ERROR] invalid format specification for an interval value
+ DETAIL: format DAY
+ HINT: intervals are not tied to specific calendar dates
+DayX: [ERROR] invalid format specification for an interval value
+ DETAIL: format Day
+ HINT: intervals are not tied to specific calendar dates
+dayX: [ERROR] invalid format specification for an interval value
+ DETAIL: format day
+ HINT: intervals are not tied to specific calendar dates
+FMDAYX: [ERROR] invalid format specification for an interval value
+ DETAIL: format DAY
+ HINT: intervals are not tied to specific calendar dates
+FMDayX: [ERROR] invalid format specification for an interval value
+ DETAIL: format Day
+ HINT: intervals are not tied to specific calendar dates
+FMdayX: [ERROR] invalid format specification for an interval value
+ DETAIL: format day
+ HINT: intervals are not tied to specific calendar dates
+DY: [ERROR] invalid format specification for an interval value
+ DETAIL: format DY
+ HINT: intervals are not tied to specific calendar dates
+Dy: [ERROR] invalid format specification for an interval value
+ DETAIL: format Dy
+ HINT: intervals are not tied to specific calendar dates
+dy: [ERROR] invalid format specification for an interval value
+ DETAIL: format dy
+ HINT: intervals are not tied to specific calendar dates
+D: [ERROR] invalid format specification for an interval value
+ DETAIL: format D
+ HINT: intervals are not tied to specific calendar dates
+ID: [ERROR] invalid format specification for an interval value
+ DETAIL: format ID
+ HINT: intervals are not tied to specific calendar dates
+Dth: [ERROR] invalid format specification for an interval value
+ DETAIL: format D
+ HINT: intervals are not tied to specific calendar dates
+IDTH: [ERROR] invalid format specification for an interval value
+ DETAIL: format ID
+ HINT: intervals are not tied to specific calendar dates
+
+# Test day of year operations.
+timestamp_fmt
+2020-07-20 00:15:18
+DDD
+DDDTH
+IDDD
+----
+DDD: 202
+DDDTH: 202ND
+IDDD: 204
+
+interval_fmt
+3 years 1 day
+DDD
+DDDTH
+IDDD
+----
+DDD: 1081
+DDDTH: 1081ST
+IDDD: [ERROR] invalid format specification for an interval value
+ DETAIL: format IDDD
+ HINT: intervals are not tied to specific calendar dates
+
+timestamp
+IDDD
+2019-12-29 00:15:18
+2019-12-30 00:15:18
+2020-01-03 01:02:03
+2020-01-04 01:02:03
+2020-01-05 01:02:03
+2020-01-06 01:02:03
+----
+2019-12-29 00:15:18: 364
+2019-12-30 00:15:18: 001
+2020-01-03 01:02:03: 005
+2020-01-04 01:02:03: 006
+2020-01-05 01:02:03: 007
+2020-01-06 01:02:03: 008
+
+# Test day operations.
+timestamp_fmt
+2020-07-03 01:13:15
+DD
+FMDD
+FMDDth
+----
+DD: 03
+FMDD: 3
+FMDDth: 3rd
+
+interval_fmt
+37 months 5 days
+DD
+FMDD
+FMDDth
+----
+DD: 05
+FMDD: 5
+FMDDth: 5th
+
+# Test years and isoyears.
+timestamp_fmt
+0005-01-02 01:02:03
+YYYY
+IYYY
+YYYYth
+FMIYYY
+YYY
+IYY
+YY
+IY
+Y
+I
+----
+YYYY: 0005
+IYYY: 0004
+YYYYth: 0005th
+FMIYYY: 4
+YYY: 005
+IYY: 004
+YY: 05
+IY: 04
+Y: 5
+I: 4
+
+interval_fmt
+37 months 5 days
+0005-01-02 01:02:03
+YYYY
+IYYY
+YYYYth
+FMIYYY
+YYY
+IYY
+YY
+IY
+Y
+I
+----
+0005-01-02 01:02:03: 0005-01-02 01:02:03
+YYYY: 0003
+IYYY: [ERROR] invalid format specification for an interval value
+ DETAIL: format IYYY
+ HINT: intervals are not tied to specific calendar dates
+YYYYth: 0003rd
+FMIYYY: [ERROR] invalid format specification for an interval value
+ DETAIL: format IYYY
+ HINT: intervals are not tied to specific calendar dates
+YYY: 003
+IYY: [ERROR] invalid format specification for an interval value
+ DETAIL: format IYY
+ HINT: intervals are not tied to specific calendar dates
+YY: 03
+IY: [ERROR] invalid format specification for an interval value
+ DETAIL: format IY
+ HINT: intervals are not tied to specific calendar dates
+Y: 3
+I: [ERROR] invalid format specification for an interval value
+ DETAIL: format I
+ HINT: intervals are not tied to specific calendar dates
+
+interval_fmt
+-37 months 5 days
+YYYY
+IYYY
+YYYYth
+FMIYYY
+YYY
+IYY
+YY
+IY
+Y
+I
+----
+YYYY: -0003
+IYYY: [ERROR] invalid format specification for an interval value
+ DETAIL: format IYYY
+ HINT: intervals are not tied to specific calendar dates
+YYYYth: -0003rd
+FMIYYY: [ERROR] invalid format specification for an interval value
+ DETAIL: format IYYY
+ HINT: intervals are not tied to specific calendar dates
+YYY: -003
+IYY: [ERROR] invalid format specification for an interval value
+ DETAIL: format IYY
+ HINT: intervals are not tied to specific calendar dates
+YY: -03
+IY: [ERROR] invalid format specification for an interval value
+ DETAIL: format IY
+ HINT: intervals are not tied to specific calendar dates
+Y: -3
+I: [ERROR] invalid format specification for an interval value
+ DETAIL: format I
+ HINT: intervals are not tied to specific calendar dates
+
+timestamp_fmt
+0003-01-02 BC 01:02:03
+YYYY
+IYYY
+YYYYth
+FMIYYY
+YYY
+IYY
+YY
+IY
+Y
+I
+----
+YYYY: 0003
+IYYY: 0003
+YYYYth: 0003rd
+FMIYYY: 3
+YYY: 003
+IYY: 003
+YY: 03
+IY: 03
+Y: 3
+I: 3
+
+# Test comma years
+timestamp
+Y,YYY
+0005-01-02 BC 01:02:03
+0005-01-02 01:02:03
+0105-01-02 01:02:03
+2005-01-02 01:02:03
+20005-01-02 01:02:03
+----
+0005-01-02 BC 01:02:03: 0,005
+0005-01-02 01:02:03: 0,005
+0105-01-02 01:02:03: 0,105
+2005-01-02 01:02:03: 2,005
+20005-01-02 01:02:03: 20,005
+
+interval
+Y,YYY
+3 months
+15 months
+12500 months
+-120 months
+-1500 months
+----
+3 months: 0,000
+15 months: 0,001
+12500 months: 1,041
+-120 months: 0,-10
+-1500 months: 0,-125
+
+# Week
+timestamp_fmt
+2022-01-07 01:02:03
+WW
+FMWWth
+IW
+FMIW
+FMIWth
+----
+WW: 01
+FMWWth: 1st
+IW: 01
+FMIW: 1
+FMIWth: 1st
+
+interval_fmt
+36 months
+WW
+FMWWth
+IW
+FMIW
+FMIWth
+----
+WW: 155
+FMWWth: 155th
+IW: [ERROR] invalid format specification for an interval value
+ DETAIL: format IW
+ HINT: intervals are not tied to specific calendar dates
+FMIW: [ERROR] invalid format specification for an interval value
+ DETAIL: format IW
+ HINT: intervals are not tied to specific calendar dates
+FMIWth: [ERROR] invalid format specification for an interval value
+ DETAIL: format IW
+ HINT: intervals are not tied to specific calendar dates
+
+# Quarters
+timestamp
+Q
+2022-01-01 01:02:03
+2022-02-01 01:02:03
+2022-03-01 01:02:03
+2022-04-01 01:02:03
+2022-05-01 01:02:03
+2022-06-01 01:02:03
+2022-07-01 01:02:03
+2022-08-01 01:02:03
+2022-09-01 01:02:03
+2022-10-01 01:02:03
+2022-11-01 01:02:03
+2022-12-01 01:02:03
+----
+2022-01-01 01:02:03: 1
+2022-02-01 01:02:03: 1
+2022-03-01 01:02:03: 1
+2022-04-01 01:02:03: 2
+2022-05-01 01:02:03: 2
+2022-06-01 01:02:03: 2
+2022-07-01 01:02:03: 3
+2022-08-01 01:02:03: 3
+2022-09-01 01:02:03: 3
+2022-10-01 01:02:03: 4
+2022-11-01 01:02:03: 4
+2022-12-01 01:02:03: 4
+
+interval
+Q
+1 month
+3 months
+12 months
+13 months
+----
+1 month: 1
+3 months: 1
+12 months:
+13 months: 1
+
+# Century
+timestamp
+CC
+0005-01-02 BC 01:02:03
+1005-01-02 BC 01:02:03
+1999-01-01 01:02:03
+2000-01-01 01:02:03
+2001-01-01 01:02:03
+----
+0005-01-02 BC 01:02:03: -01
+1005-01-02 BC 01:02:03: -11
+1999-01-01 01:02:03: 20
+2000-01-01 01:02:03: 20
+2001-01-01 01:02:03: 21
+
+timestamp
+FMCCth
+0005-01-02 BC 01:02:03
+----
+0005-01-02 BC 01:02:03: -1st
+
+interval
+CC
+12000 months
+5 months
+-12000 months
+----
+12000 months: 10
+5 months: 00
+-12000 months: -10
+
+# Week of month.
+timestamp
+Wth
+2020-01-01 01:02:03
+2020-01-06 01:02:03
+2020-01-07 01:02:03
+2020-01-08 01:02:03
+----
+2020-01-01 01:02:03: 1st
+2020-01-06 01:02:03: 1st
+2020-01-07 01:02:03: 1st
+2020-01-08 01:02:03: 2nd
+
+# Julian
+timestamp
+Jth
+2020-04-07 01:02:03
+----
+2020-04-07 01:02:03: 2458947th
+
+interval
+J
+3 years 6 months 01:02:03
+----
+3 years 6 months 01:02:03: 1722306
+
+# RM
+timestamp
+RM"x"
+2020-02-01 BC 01:02:03
+2020-12-01 01:02:03
+2020-01-01 01:02:03
+----
+2020-02-01 BC 01:02:03: II x
+2020-12-01 01:02:03: XII x
+2020-01-01 01:02:03: I x
+
+timestamp
+rm"< spaces here"
+2020-02-01 BC 01:02:03
+2020-12-01 01:02:03
+2020-01-01 01:02:03
+----
+2020-02-01 BC 01:02:03: ii < spaces here
+2020-12-01 01:02:03: xii < spaces here
+2020-01-01 01:02:03: i < spaces here
+
+interval
+FMRM" < no spaces here"
+1 day
+1 month 1 day
+-1 month 1 day
+-13 months
+13 months
+----
+1 day: < no spaces here
+1 month 1 day: I < no spaces here
+-1 month 1 day: XI < no spaces here
+-13 months: XI < no spaces here
+13 months: I < no spaces here
diff --git a/pkg/util/tochar/tochar.go b/pkg/util/tochar/tochar.go
new file mode 100644
index 000000000000..993be69abce9
--- /dev/null
+++ b/pkg/util/tochar/tochar.go
@@ -0,0 +1,720 @@
+// Copyright 2022 The Cockroach Authors.
+//
+// Use of this software is governed by the Business Source License
+// included in the file licenses/BSL.txt.
+//
+// As of the Change Date specified in that file, in accordance with
+// the Business Source License, use of this software will be governed
+// by the Apache License, Version 2.0, included in the file
+// licenses/APL.txt.
+
+package tochar
+
+import (
+ "fmt"
+ "strings"
+ "time"
+ "unicode"
+ "unicode/utf8"
+
+ "github.com/cockroachdb/cockroach/pkg/sql/pgwire/pgcode"
+ "github.com/cockroachdb/cockroach/pkg/sql/pgwire/pgerror"
+ "github.com/cockroachdb/cockroach/pkg/util/duration"
+ "github.com/cockroachdb/cockroach/pkg/util/timeutil/pgdate"
+ "github.com/cockroachdb/errors"
+)
+
+// TimeToChar converts a time and a `to_char` format string to a string.
+func TimeToChar(t time.Time, f string) (string, error) {
+ return timeToChar(timeWrapper{t}, parseFormat(f))
+}
+
+// DurationToChar converts a duration and a `to_char` format string to a string.
+func DurationToChar(d duration.Duration, f string) (string, error) {
+ // TODO(#sql-experience): consider caching parse formats.
+ return timeToChar(makeToCharDuration(d), parseFormat(f))
+}
+
+// timeInterface is intended as a pass through to timeToChar.
+type timeInterface interface {
+ Year() int
+ Month() time.Month
+ Day() int
+ Minute() int
+ Hour() int
+ Second() int
+ Nanosecond() int
+ YearDay() int
+ Weekday() time.Weekday
+ ISOWeek() (int, int)
+ Zone() (string, int)
+ isInterval() bool
+}
+
+// timeWrapper wraps time.Time to implement timeInterface.
+type timeWrapper struct {
+ time.Time
+}
+
+func (timeWrapper) isInterval() bool {
+ return false
+}
+
+// toCharDuration maps as a duration to usable for timeInterface.
+// We use the same logic as PG to convert intervals to a fake time here.
+type toCharDuration struct {
+ year, day int
+ month time.Month
+ hour, minute, second, nanos int
+ yday int
+}
+
+// makeToCharDuration converts a duration.Duration to a toCharDuration.
+func makeToCharDuration(d duration.Duration) toCharDuration {
+ n := time.Duration(d.Nanos())
+ return toCharDuration{
+ year: int(d.Months / 12),
+ month: time.Month(d.Months % 12),
+ day: int(d.Days),
+ nanos: int(n % time.Second),
+ second: int(n/time.Second) % 60,
+ minute: int(n/time.Minute) % 60,
+ hour: int(n / time.Hour),
+ // Approximation that PostgreSQL uses.
+ yday: int(d.Months*30 + d.Days),
+ }
+}
+
+func (d toCharDuration) Year() int {
+ return d.year
+}
+
+func (d toCharDuration) Month() time.Month {
+ return d.month
+}
+
+func (d toCharDuration) Day() int {
+ return d.day
+}
+
+func (d toCharDuration) Minute() int {
+ return d.minute
+}
+
+func (d toCharDuration) Hour() int {
+ return d.hour
+}
+
+func (d toCharDuration) Second() int {
+ return d.second
+}
+
+func (d toCharDuration) Nanosecond() int {
+ return d.nanos
+}
+
+func (d toCharDuration) YearDay() int {
+ return d.yday
+}
+
+func (d toCharDuration) Weekday() time.Weekday {
+ return time.Monday
+}
+
+func (d toCharDuration) ISOWeek() (int, int) {
+ return 0, 0
+}
+
+func (d toCharDuration) Zone() (string, int) {
+ return "", 0
+}
+
+func (d toCharDuration) isInterval() bool {
+ return true
+}
+
+func makeIntervalUnsupportedError(s string) error {
+ return errors.WithDetailf(
+ errors.WithHint(
+ pgerror.New(
+ pgcode.InvalidDatetimeFormat,
+ "invalid format specification for an interval value",
+ ),
+ "intervals are not tied to specific calendar dates",
+ ),
+ "format %s",
+ s,
+ )
+}
+
+// timeToChar maps to DCHToChar.
+func timeToChar(t timeInterface, formatNodes []formatNode) (string, error) {
+ var sb strings.Builder
+ for _, fn := range formatNodes {
+ if fn.typ != formatNodeAction {
+ sb.WriteString(fn.character)
+ continue
+ }
+
+ // NOTE(otan): postgres does localization logic if TM is specified.
+ // This is not supported on a number of these fields.
+ switch fn.key.id {
+ case DCH_A_M, DCH_P_M:
+ if t.Hour() >= 12 {
+ sb.WriteString("P.M.")
+ } else {
+ sb.WriteString("A.M.")
+ }
+ case DCH_AM, DCH_PM:
+ if t.Hour() >= 12 {
+ sb.WriteString("PM")
+ } else {
+ sb.WriteString("AM")
+ }
+ case DCH_a_m, DCH_p_m:
+ if t.Hour() >= 12 {
+ sb.WriteString("p.m.")
+ } else {
+ sb.WriteString("a.m.")
+ }
+ case DCH_am, DCH_pm:
+ if t.Hour() >= 12 {
+ sb.WriteString("pm")
+ } else {
+ sb.WriteString("am")
+ }
+ case DCH_HH, DCH_HH12:
+ hour := t.Hour()
+ if hour%12 == 0 {
+ hour = 12
+ } else {
+ hour = hour % 12
+ }
+ // Note t.Hour() and hour are deliberately different.
+ sb.WriteString(fmt.Sprintf("%0*d", fn.suffix.zeroPad2PlusNeg(t.Hour()), hour))
+ sb.WriteString(fn.suffix.thVal(hour))
+ case DCH_HH24:
+ sb.WriteString(fmt.Sprintf("%0*d", fn.suffix.zeroPad2PlusNeg(t.Hour()), t.Hour()))
+ sb.WriteString(fn.suffix.thVal(t.Hour()))
+ case DCH_MI:
+ sb.WriteString(fmt.Sprintf("%0*d", fn.suffix.zeroPad2PlusNeg(t.Minute()), t.Minute()))
+ sb.WriteString(fn.suffix.thVal(t.Minute()))
+ case DCH_SS:
+ sb.WriteString(fmt.Sprintf("%0*d", fn.suffix.zeroPad2PlusNeg(t.Second()), t.Second()))
+ sb.WriteString(fn.suffix.thVal(t.Second()))
+ case DCH_FF1:
+ val := t.Nanosecond() / 100000000
+ sb.WriteString(fmt.Sprintf("%01d", val))
+ sb.WriteString(fn.suffix.thVal(val))
+ case DCH_FF2:
+ val := t.Nanosecond() / 10000000
+ sb.WriteString(fmt.Sprintf("%02d", val))
+ sb.WriteString(fn.suffix.thVal(val))
+ case DCH_FF3, DCH_MS:
+ val := t.Nanosecond() / 1000000
+ sb.WriteString(fmt.Sprintf("%03d", val))
+ sb.WriteString(fn.suffix.thVal(val))
+ case DCH_FF4:
+ val := t.Nanosecond() / 100000
+ sb.WriteString(fmt.Sprintf("%04d", val))
+ sb.WriteString(fn.suffix.thVal(val))
+ case DCH_FF5:
+ val := t.Nanosecond() / 10000
+ sb.WriteString(fmt.Sprintf("%05d", val))
+ sb.WriteString(fn.suffix.thVal(val))
+ case DCH_FF6, DCH_US:
+ val := t.Nanosecond() / 1000
+ sb.WriteString(fmt.Sprintf("%06d", val))
+ sb.WriteString(fn.suffix.thVal(val))
+ case DCH_SSSS:
+ val := t.Hour()*60*60 + t.Minute()*60 + t.Second()
+ sb.WriteString(fmt.Sprintf("%d", val))
+ sb.WriteString(fn.suffix.thVal(val))
+ case DCH_tz:
+ if t.isInterval() {
+ return "", makeIntervalUnsupportedError(fn.key.name)
+ }
+ n, _ := t.Zone()
+ sb.WriteString(strings.ToLower(n))
+ case DCH_TZ:
+ if t.isInterval() {
+ return "", makeIntervalUnsupportedError(fn.key.name)
+ }
+ n, _ := t.Zone()
+ sb.WriteString(n)
+ case DCH_TZH:
+ if t.isInterval() {
+ return "", makeIntervalUnsupportedError(fn.key.name)
+ }
+ _, offset := t.Zone()
+ posStr := "+"
+ if offset < 0 {
+ posStr = "-"
+ offset = -offset
+ }
+ sb.WriteString(posStr)
+ sb.WriteString(fmt.Sprintf("%02d", offset/3600))
+ case DCH_TZM:
+ if t.isInterval() {
+ return "", makeIntervalUnsupportedError(fn.key.name)
+ }
+ _, offset := t.Zone()
+ if offset < 0 {
+ offset = -offset
+ }
+ sb.WriteString(fmt.Sprintf("%02d", (offset%3600)/60))
+ case DCH_OF:
+ if t.isInterval() {
+ return "", makeIntervalUnsupportedError(fn.key.name)
+ }
+ _, offset := t.Zone()
+ posStr := "+"
+ if offset < 0 {
+ posStr = "-"
+ offset = -offset
+ }
+ sb.WriteString(posStr)
+ var zeroPad int
+ if !fn.suffix.FM() {
+ zeroPad = 2
+ }
+ sb.WriteString(fmt.Sprintf("%0*d", zeroPad, offset/3600))
+ minOffset := (offset % 3600) / 60
+ if minOffset != 0 {
+ sb.WriteString(fmt.Sprintf(":%02d", minOffset))
+ }
+ case DCH_A_D, DCH_B_C:
+ if t.isInterval() {
+ return "", makeIntervalUnsupportedError(fn.key.name)
+ }
+ if t.Year() <= 0 {
+ sb.WriteString("B.C.")
+ } else {
+ sb.WriteString("A.D.")
+ }
+ case DCH_AD, DCH_BC:
+ if t.isInterval() {
+ return "", makeIntervalUnsupportedError(fn.key.name)
+ }
+ if t.Year() <= 0 {
+ sb.WriteString("BC")
+ } else {
+ sb.WriteString("AD")
+ }
+ case DCH_a_d, DCH_b_c:
+ if t.isInterval() {
+ return "", makeIntervalUnsupportedError(fn.key.name)
+ }
+ if t.Year() <= 0 {
+ sb.WriteString("b.c.")
+ } else {
+ sb.WriteString("a.d.")
+ }
+ case DCH_ad, DCH_bc:
+ if t.isInterval() {
+ return "", makeIntervalUnsupportedError(fn.key.name)
+ }
+ if t.Year() <= 0 {
+ sb.WriteString("bc")
+ } else {
+ sb.WriteString("ad")
+ }
+ case DCH_MONTH, DCH_Month, DCH_month:
+ if t.isInterval() {
+ return "", makeIntervalUnsupportedError(fn.key.name)
+ }
+ m := t.Month().String()
+ switch fn.key.id {
+ case DCH_MONTH:
+ m = strings.ToUpper(m)
+ case DCH_month:
+ m = strings.ToLower(m)
+ }
+ sb.WriteString(fmt.Sprintf("%*s", fn.suffix.zeroPad(-9), m))
+ case DCH_MON, DCH_Mon, DCH_mon:
+ if t.isInterval() {
+ return "", makeIntervalUnsupportedError(fn.key.name)
+ }
+ m := t.Month().String()[:3]
+ switch fn.key.id {
+ case DCH_MON:
+ m = strings.ToUpper(m)
+ case DCH_mon:
+ m = strings.ToLower(m)
+ }
+ sb.WriteString(m)
+ case DCH_MM:
+ val := int(t.Month())
+ sb.WriteString(fmt.Sprintf("%0*d", fn.suffix.zeroPad2PlusNeg(val), val))
+ sb.WriteString(fn.suffix.thVal(val))
+ case DCH_DAY, DCH_Day, DCH_day:
+ if t.isInterval() {
+ return "", makeIntervalUnsupportedError(fn.key.name)
+ }
+ d := t.Weekday().String()
+ switch fn.key.id {
+ case DCH_DAY:
+ d = strings.ToLower(d)
+ case DCH_day:
+ d = strings.ToLower(d)
+ }
+ sb.WriteString(fmt.Sprintf("%*s", fn.suffix.zeroPad(-9), d))
+ case DCH_DY, DCH_Dy, DCH_dy:
+ if t.isInterval() {
+ return "", makeIntervalUnsupportedError(fn.key.name)
+ }
+ d := t.Weekday().String()[:3]
+ switch fn.key.id {
+ case DCH_DY:
+ d = strings.ToLower(d)
+ case DCH_dy:
+ d = strings.ToLower(d)
+ }
+ sb.WriteString(d)
+ case DCH_DDD:
+ val := t.YearDay()
+ sb.WriteString(fmt.Sprintf("%0*d", fn.suffix.zeroPad(3), val))
+ sb.WriteString(fn.suffix.thVal(val))
+ case DCH_DD:
+ sb.WriteString(fmt.Sprintf("%0*d", fn.suffix.zeroPad(2), t.Day()))
+ sb.WriteString(fn.suffix.thVal(t.Day()))
+ case DCH_IDDD:
+ // Added by us, as ISOWeek() does not work.
+ if t.isInterval() {
+ return "", makeIntervalUnsupportedError(fn.key.name)
+ }
+ _, w := t.ISOWeek()
+ val := (w-1)*7 + int(t.Weekday())
+ if t.Weekday() == 0 {
+ val += 7
+ }
+ sb.WriteString(fmt.Sprintf("%0*d", fn.suffix.zeroPad(3), val))
+ sb.WriteString(fn.suffix.thVal(val))
+ case DCH_D:
+ if t.isInterval() {
+ return "", makeIntervalUnsupportedError(fn.key.name)
+ }
+ val := int(t.Weekday()) + 1
+ sb.WriteString(fmt.Sprintf("%d", val))
+ sb.WriteString(fn.suffix.thVal(val))
+ case DCH_ID:
+ if t.isInterval() {
+ return "", makeIntervalUnsupportedError(fn.key.name)
+ }
+ val := int(t.Weekday())
+ if val == 0 {
+ val = 7
+ }
+ sb.WriteString(fmt.Sprintf("%d", val))
+ sb.WriteString(fn.suffix.thVal(val))
+ case DCH_WW:
+ val := (t.YearDay()-1)/7 + 1
+ sb.WriteString(fmt.Sprintf("%0*d", fn.suffix.zeroPad(2), val))
+ sb.WriteString(fn.suffix.thVal(val))
+ case DCH_IW:
+ // Added by us, as ISOWeek() does not work.
+ if t.isInterval() {
+ return "", makeIntervalUnsupportedError(fn.key.name)
+ }
+ _, val := t.ISOWeek()
+ sb.WriteString(fmt.Sprintf("%0*d", fn.suffix.zeroPad(2), val))
+ sb.WriteString(fn.suffix.thVal(val))
+ case DCH_Q:
+ // For intervals.
+ if int(t.Month()) == 0 {
+ break
+ }
+ val := (int(t.Month())-1)/3 + 1
+ sb.WriteString(fmt.Sprintf("%d", val))
+ sb.WriteString(fn.suffix.thVal(val))
+ case DCH_CC:
+ var val int
+ if t.isInterval() {
+ val = t.Year() / 100
+ } else {
+ if t.Year() > 0 {
+ val = (t.Year()-1)/100 + 1
+ } else {
+ val = t.Year()/100 - 1
+ }
+ }
+ if val <= 99 || val >= 99 {
+ sb.WriteString(fmt.Sprintf("%0*d", fn.suffix.zeroPad2PlusNeg(val), val))
+ } else {
+ sb.WriteString(fmt.Sprintf("%d", val))
+ }
+ sb.WriteString(fn.suffix.thVal(val))
+ case DCH_Y_YYY:
+ year := t.Year()
+ if !t.isInterval() && year < 0 {
+ year = -year + 1
+ }
+ preComma := year / 1000
+ postComma := year % 1000
+ sb.WriteString(fmt.Sprintf("%d,%03d", preComma, postComma))
+ sb.WriteString(fn.suffix.thVal(postComma))
+ case DCH_YYYY, DCH_IYYY, DCH_YYY, DCH_IYY, DCH_YY, DCH_IY, DCH_Y, DCH_I:
+ val := t.Year()
+ switch fn.key.id {
+ case DCH_IYYY, DCH_IYY, DCH_IY, DCH_I:
+ // Added by us, as ISOWeek() does not work.
+ if t.isInterval() {
+ return "", makeIntervalUnsupportedError(fn.key.name)
+ }
+ val, _ = t.ISOWeek()
+ }
+ if !t.isInterval() && val < 0 {
+ val = -val + 1
+ }
+ zeroPad := 0
+ if !fn.suffix.FM() {
+ switch fn.key.id {
+ case DCH_IYYY, DCH_YYYY:
+ zeroPad = 4
+ case DCH_IYY, DCH_YYY:
+ zeroPad = 3
+ case DCH_IY, DCH_YY:
+ zeroPad = 2
+ case DCH_I, DCH_Y:
+ zeroPad = 1
+ }
+ }
+ switch fn.key.id {
+ case DCH_IYY, DCH_YYY:
+ val %= 1000
+ case DCH_IY, DCH_YY:
+ val %= 100
+ case DCH_I, DCH_Y:
+ val %= 10
+ }
+ // For intervals, negative values get an extra digit.
+ if t.isInterval() && val < 0 {
+ zeroPad++
+ }
+ sb.WriteString(fmt.Sprintf("%0*d", zeroPad, val))
+ sb.WriteString(fn.suffix.thVal(val))
+ case DCH_RM, DCH_rm:
+ if t.Month() == 0 && t.Year() == 0 {
+ continue
+ }
+ // Roman numerals are in reverse order, which is why the indexes
+ // are reversed.
+ var idx int
+ if t.Month() == 0 {
+ if t.Year() >= 0 {
+ idx = 0
+ } else {
+ idx = 11
+ }
+ } else if t.Month() < 0 {
+ idx = -1 * int(t.Month())
+ } else {
+ idx = 12 - int(t.Month())
+ }
+ var str string
+ if fn.key.id == DCH_RM {
+ str = ucMonthRomanNumerals[idx]
+ } else {
+ str = lcMonthRomanNumerals[idx]
+ }
+ sb.WriteString(fmt.Sprintf("%*s", fn.suffix.zeroPad(-4), str))
+ case DCH_W:
+ val := (t.Day()-1)/7 + 1
+ sb.WriteString(fmt.Sprintf("%d", val))
+ sb.WriteString(fn.suffix.thVal(val))
+ case DCH_J:
+ val := pgdate.DateToJulianDay(t.Year(), int(t.Month()), t.Day())
+ sb.WriteString(fmt.Sprintf("%d", val))
+ sb.WriteString(fn.suffix.thVal(val))
+ case DCH_FX, DCH_fx:
+ // Does nothing.
+ default:
+ return "", errors.AssertionFailedf("unexpected key for to_char: %s", errors.Safe(fn.key.name))
+ }
+ }
+ return sb.String(), nil
+}
+
+func (d dchSuffix) thVal(val int) string {
+ if d.THth() {
+ return getTH(val, d.TH())
+ }
+ return ""
+}
+
+// getTH returns the suffix for the given value as a position, e.g.
+// 1 => st for 1st, 4 => th for 4th.
+func getTH(v int, upper bool) string {
+ if v < 0 {
+ v = -v
+ }
+ v = v % 100
+ // 11, 12 and 13 are still -th, so don't go in here.
+ if v < 11 || v > 13 {
+ switch v % 10 {
+ case 1:
+ if upper {
+ return "ST"
+ }
+ return "st"
+ case 2:
+ if upper {
+ return "ND"
+ }
+ return "nd"
+ case 3:
+ if upper {
+ return "RD"
+ }
+ return "rd"
+ }
+ }
+ if upper {
+ return "TH"
+ }
+ return "th"
+}
+
+func (d dchSuffix) zeroPad(nonFMVal int) int {
+ var zeroPad int
+ if !d.FM() {
+ zeroPad = nonFMVal
+ }
+ return zeroPad
+}
+
+// zeroPad2PlusNeg pads 2 if FM is not set, accounting for the negative digit
+// where necessary.
+func (d dchSuffix) zeroPad2PlusNeg(val int) int {
+ var zeroPad int
+ if !d.FM() {
+ if val >= 0 {
+ zeroPad = 2
+ } else {
+ zeroPad = 3
+ }
+ }
+ return zeroPad
+}
+
+// parseFormat matches parse_format. We do not take in a flags like PG as we only do
+// datetime related items (the logic here will change if we support numeric types).
+// Taken from https://github.com/postgres/postgres/blob/b0b72c64a0ce7bf5dd78a80b33d85c89c943ad0d/src/backend/utils/adt/formatting.c#L1146.
+// TODO(#sql-experience): consider caching parse formats.
+func parseFormat(f string) []formatNode {
+ var ret []formatNode
+ for len(f) > 0 {
+ var suffix dchSuffix
+ if s := suffSearch(f, keySuffixPrefix); s != nil {
+ suffix |= s.id
+ f = f[len(s.name):]
+ }
+
+ if len(f) > 0 {
+ kw := indexSeqSearch(f)
+ if kw != nil {
+ ret = append(ret, formatNode{
+ typ: formatNodeAction,
+ suffix: suffix,
+ key: kw,
+ })
+ f = f[len(kw.name):]
+ if len(f) > 0 {
+ if s := suffSearch(f, keySuffixPostfix); s != nil {
+ ret[len(ret)-1].suffix |= s.id
+ f = f[len(s.name):]
+ }
+ }
+ } else {
+ if f[0] == '"' {
+ // Process any quoted text.
+ f = f[1:]
+ for len(f) > 0 {
+ if f[0] == '"' {
+ f = f[1:]
+ break
+ }
+ // Escape the next character.
+ if f[0] == '\\' {
+ f = f[1:]
+ }
+ r, size := utf8.DecodeRuneInString(f)
+ ret = append(ret, formatNode{
+ typ: formatNodeChar,
+ character: string(r),
+ })
+ f = f[size:]
+ }
+ } else {
+ // Outside double-quoted strings, backslash is only special if it
+ // immediately precedes a double quote.
+ if f[0] == '\\' && len(f) > 1 && f[1] == '"' {
+ f = f[1:]
+ }
+
+ r, size := utf8.DecodeRuneInString(f)
+ typ := formatNodeChar
+ if isSeparatorChar(r) {
+ typ = formatNodeSeparator
+ } else if unicode.IsSpace(r) {
+ typ = formatNodeSpace
+ }
+ ret = append(ret, formatNode{
+ typ: typ,
+ character: string(r),
+ })
+ f = f[size:]
+ }
+ }
+ }
+ }
+ ret = append(ret, formatNode{typ: formatNodeEnd})
+ return ret
+}
+
+func isSeparatorChar(c rune) bool {
+ /* ASCII printable character, but not letter or digit */
+ return c > 0x20 && c < 0x7F &&
+ !(c >= 'A' && c <= 'Z') &&
+ !(c >= 'a' && c <= 'z') &&
+ !(c >= '0' && c <= '9')
+}
+
+// indexSeqSearch matches index_seq_search.
+// Taken from https://github.com/postgres/postgres/blob/b0b72c64a0ce7bf5dd78a80b33d85c89c943ad0d/src/backend/utils/adt/formatting.c#L194-L198.
+func indexSeqSearch(s string) *keyWord {
+ if len(s) == 0 || s[0] <= ' ' || s[0] >= '~' {
+ return nil
+ }
+ idxOffset := int(s[0] - ' ')
+ if idxOffset > -1 && idxOffset < len(dchIndex) {
+ pos := int(dchIndex[idxOffset])
+ for pos >= 0 && pos < len(dchKeywords) {
+ k := dchKeywords[pos]
+ if strings.HasPrefix(s, k.name) {
+ return &dchKeywords[pos]
+ }
+ pos++
+ // Since we have dchKeywords sorted in alphabetical order, so if we don't
+ // match the first character, break.
+ if pos >= len(dchKeywords) || dchKeywords[pos].name[0] != s[0] {
+ break
+ }
+ }
+ }
+ return nil
+}
+
+// suffSearch matches suff_search.
+// Taken from https://github.com/postgres/postgres/blob/b0b72c64a0ce7bf5dd78a80b33d85c89c943ad0d/src/backend/utils/adt/formatting.c#L1146.
+func suffSearch(s string, typ keySuffixType) *keySuffix {
+ for _, suff := range dchSuffixes {
+ if suff.typ != typ {
+ continue
+ }
+ if strings.HasPrefix(s, suff.name) {
+ return &suff
+ }
+ }
+ return nil
+}
diff --git a/pkg/util/tochar/tochar_test.go b/pkg/util/tochar/tochar_test.go
new file mode 100644
index 000000000000..ba4772611e12
--- /dev/null
+++ b/pkg/util/tochar/tochar_test.go
@@ -0,0 +1,173 @@
+// Copyright 2022 The Cockroach Authors.
+//
+// Use of this software is governed by the Business Source License
+// included in the file licenses/BSL.txt.
+//
+// As of the Change Date specified in that file, in accordance with
+// the Business Source License, use of this software will be governed
+// by the Apache License, Version 2.0, included in the file
+// licenses/APL.txt.
+
+package tochar
+
+import (
+ "strings"
+ "testing"
+ "time"
+
+ "github.com/cockroachdb/cockroach/pkg/sql/pgwire/pgerror"
+ "github.com/cockroachdb/cockroach/pkg/sql/types"
+ "github.com/cockroachdb/cockroach/pkg/util/duration"
+ "github.com/cockroachdb/cockroach/pkg/util/timeutil"
+ "github.com/cockroachdb/cockroach/pkg/util/timeutil/pgdate"
+ "github.com/cockroachdb/datadriven"
+ "github.com/cockroachdb/errors"
+ "github.com/stretchr/testify/require"
+)
+
+// TestDataDriven tests the relevant to_char facilities.
+func TestDataDriven(t *testing.T) {
+ now := timeutil.Now().UTC()
+ ds := pgdate.DateStyle{
+ Style: pgdate.Style_ISO,
+ Order: pgdate.Order_YMD,
+ }
+ is := duration.IntervalStyle_POSTGRES
+ datadriven.Walk(t, "testdata", func(t *testing.T, path string) {
+ datadriven.RunTest(t, path, func(t *testing.T, d *datadriven.TestData) string {
+ var ret strings.Builder
+ switch d.Cmd {
+ case "timestamp":
+ tz := parseArgs(t, d.CmdArgs)
+ // Input is format, followed by timestamps separated by newlines.
+ // Output is the same timestamps in the given format.
+ lines := strings.Split(d.Input, "\n")
+ for i, in := range lines[1:] {
+ if i > 0 {
+ ret.WriteString("\n")
+ }
+ ts, _, err := pgdate.ParseTimestamp(now, ds, in)
+ require.NoError(t, err)
+ r, err := TimeToChar(ts.In(tz), lines[0])
+ if err != nil {
+ ret.WriteString(in + ": [ERROR] " + err.Error() + errorDetails(err))
+ } else {
+ ret.WriteString(in + ": " + r)
+ }
+ }
+ case "timestamp_fmt":
+ tz := parseArgs(t, d.CmdArgs)
+ // Input is timestamp, followed by formats separated by newlines.
+ // Output is the timestamp in the given formats.
+ lines := strings.Split(d.Input, "\n")
+ ts, _, err := pgdate.ParseTimestamp(now, ds, lines[0])
+ for i, in := range lines[1:] {
+ if i > 0 {
+ ret.WriteString("\n")
+ }
+ require.NoError(t, err)
+ r, err := TimeToChar(ts.In(tz), in)
+ if err != nil {
+ ret.WriteString(in + ": [ERROR] " + err.Error() + errorDetails(err))
+ } else {
+ ret.WriteString(in + ": " + r)
+ }
+ }
+ case "interval":
+ // Input is format, followed by intervals separated by newlines.
+ // Output is the same intervals in the given format.
+ lines := strings.Split(d.Input, "\n")
+ for i, in := range lines[1:] {
+ if i > 0 {
+ ret.WriteString("\n")
+ }
+ ivl, err := duration.ParseInterval(is, in, types.IntervalTypeMetadata{})
+ require.NoError(t, err)
+ r, err := DurationToChar(ivl, lines[0])
+ if err != nil {
+ ret.WriteString(in + ": [ERROR] " + err.Error() + errorDetails(err))
+ } else {
+ ret.WriteString(in + ": " + r)
+ }
+ }
+ case "interval_fmt":
+ // Input is interval, followed by formats separated by newlines.
+ // Output is the interval in the given formats.
+ lines := strings.Split(d.Input, "\n")
+ ivl, err := duration.ParseInterval(is, lines[0], types.IntervalTypeMetadata{})
+ require.NoError(t, err)
+ for i, in := range lines[1:] {
+ if i > 0 {
+ ret.WriteString("\n")
+ }
+ require.NoError(t, err)
+ r, err := DurationToChar(ivl, in)
+ if err != nil {
+ ret.WriteString(in + ": [ERROR] " + err.Error() + errorDetails(err))
+ } else {
+ ret.WriteString(in + ": " + r)
+ }
+ }
+ default:
+ t.Errorf("unknown command %s", d.Cmd)
+ }
+ return ret.String()
+ })
+ })
+}
+
+func errorDetails(err error) string {
+ var sb strings.Builder
+ for _, detail := range errors.GetAllDetails(err) {
+ sb.WriteString("\n DETAIL: ")
+ sb.WriteString(detail)
+ }
+ for _, hint := range errors.GetAllHints(err) {
+ sb.WriteString("\n HINT: ")
+ sb.WriteString(hint)
+ }
+ return sb.String()
+}
+
+// TestAllTimestamps does a basic sanity check that all time types are supported
+// and do not panic.
+func TestAllTimestamps(t *testing.T) {
+ n := timeutil.Now()
+ for _, kw := range dchKeywords {
+ t.Run(kw.name, func(t *testing.T) {
+ _, err := TimeToChar(n, kw.name)
+ if err != nil {
+ require.True(t, pgerror.HasCandidateCode(err))
+ }
+ })
+ }
+}
+
+// TestAllIntervals does a basic sanity check that all time types are supported
+// and do not panic.
+func TestAllIntervals(t *testing.T) {
+ d := duration.MakeDuration(0, 10, 15)
+ for _, kw := range dchKeywords {
+ t.Run(kw.name, func(t *testing.T) {
+ _, err := DurationToChar(d, kw.name)
+ if err != nil {
+ require.True(t, pgerror.HasCandidateCode(err))
+ }
+ })
+ }
+}
+
+func parseArgs(t *testing.T, cmdArgs []datadriven.CmdArg) *time.Location {
+ tz := time.UTC
+ for _, arg := range cmdArgs {
+ switch arg.Key {
+ case "tz":
+ var err error
+ tz, err = timeutil.LoadLocation(arg.Vals[0])
+ require.NoError(t, err)
+ default:
+ t.Errorf("unknown arg %s", arg.Key)
+ }
+ }
+ return tz
+}
|