Skip to content

Commit bcb26f9

Browse files
authored
SNOW-1256926 Add converter from snowflake date/time format to go (#1077)
1 parent 656ba61 commit bcb26f9

File tree

2 files changed

+195
-0
lines changed

2 files changed

+195
-0
lines changed

datetime.go

+62
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
// Copyright (c) 2024 Snowflake Computing Inc. All rights reserved.
2+
3+
package gosnowflake
4+
5+
import (
6+
"errors"
7+
"regexp"
8+
"strconv"
9+
"strings"
10+
)
11+
12+
var incorrectSecondsFractionRegex = regexp.MustCompile(`[^.,]FF`)
13+
var correctSecondsFractionRegex = regexp.MustCompile(`FF(?P<fraction>\d?)`)
14+
15+
type formatReplacement struct {
16+
input string
17+
output string
18+
}
19+
20+
var formatReplacements = []formatReplacement{
21+
{input: "YYYY", output: "2006"},
22+
{input: "YY", output: "06"},
23+
{input: "MMMM", output: "January"},
24+
{input: "MM", output: "01"},
25+
{input: "MON", output: "Jan"},
26+
{input: "DD", output: "02"},
27+
{input: "DY", output: "Mon"},
28+
{input: "HH24", output: "15"},
29+
{input: "HH12", output: "03"},
30+
{input: "AM", output: "PM"},
31+
{input: "MI", output: "04"},
32+
{input: "SS", output: "05"},
33+
{input: "TZH", output: "Z07"},
34+
{input: "TZM", output: "00"},
35+
}
36+
37+
func snowflakeFormatToGoFormat(sfFormat string) (string, error) {
38+
res := sfFormat
39+
for _, replacement := range formatReplacements {
40+
res = strings.Replace(res, replacement.input, replacement.output, -1)
41+
}
42+
43+
if incorrectSecondsFractionRegex.MatchString(res) {
44+
return "", errors.New("incorrect second fraction - golang requires fraction to be preceded by comma or decimal point")
45+
}
46+
for {
47+
submatch := correctSecondsFractionRegex.FindStringSubmatch(res)
48+
if submatch == nil {
49+
break
50+
}
51+
fractionNumbers := 9
52+
if submatch[1] != "" {
53+
var err error
54+
fractionNumbers, err = strconv.Atoi(submatch[1])
55+
if err != nil {
56+
return "", err
57+
}
58+
}
59+
res = strings.Replace(res, submatch[0], strings.Repeat("0", fractionNumbers), -1)
60+
}
61+
return res, nil
62+
}

datetime_test.go

+133
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
package gosnowflake
2+
3+
import (
4+
"testing"
5+
"time"
6+
)
7+
8+
func TestSnowflakeFormatToGoFormatUnitTest(t *testing.T) {
9+
location, err := time.LoadLocation("Europe/Warsaw")
10+
assertNilF(t, err)
11+
someTime1 := time.Date(2024, time.January, 19, 3, 42, 33, 123456789, location)
12+
someTime2 := time.Date(1973, time.December, 5, 13, 5, 3, 987000000, location)
13+
testcases := []struct {
14+
inputFormat string
15+
output string
16+
formatted1 string
17+
formatted2 string
18+
}{
19+
{
20+
inputFormat: "YYYY-MM-DD HH24:MI:SS.FF TZH:TZM",
21+
output: "2006-01-02 15:04:05.000000000 Z07:00",
22+
formatted1: "2024-01-19 03:42:33.123456789 +01:00",
23+
formatted2: "1973-12-05 13:05:03.987000000 +01:00",
24+
},
25+
{
26+
inputFormat: "YY-MM-DD HH12:MI:SS,FF5AM TZHTZM",
27+
output: "06-01-02 03:04:05,00000PM Z0700",
28+
formatted1: "24-01-19 03:42:33,12345AM +0100",
29+
formatted2: "73-12-05 01:05:03,98700PM +0100",
30+
},
31+
{
32+
inputFormat: "MMMM DD, YYYY DY HH24:MI:SS.FF9 TZH:TZM",
33+
output: "January 02, 2006 Mon 15:04:05.000000000 Z07:00",
34+
formatted1: "January 19, 2024 Fri 03:42:33.123456789 +01:00",
35+
formatted2: "December 05, 1973 Wed 13:05:03.987000000 +01:00",
36+
},
37+
{
38+
inputFormat: "MON DD, YYYY HH12:MI:SS,FF9PM TZH:TZM",
39+
output: "Jan 02, 2006 03:04:05,000000000PM Z07:00",
40+
formatted1: "Jan 19, 2024 03:42:33,123456789AM +01:00",
41+
formatted2: "Dec 05, 1973 01:05:03,987000000PM +01:00",
42+
},
43+
{
44+
inputFormat: "HH24:MI:SS.FF3 HH12:MI:SS,FF9",
45+
output: "15:04:05.000 03:04:05,000000000",
46+
formatted1: "03:42:33.123 03:42:33,123456789",
47+
formatted2: "13:05:03.987 01:05:03,987000000",
48+
},
49+
}
50+
for _, tc := range testcases {
51+
t.Run(tc.inputFormat, func(t *testing.T) {
52+
goFormat, err := snowflakeFormatToGoFormat(tc.inputFormat)
53+
assertNilF(t, err)
54+
assertEqualE(t, tc.output, goFormat)
55+
assertEqualE(t, tc.formatted1, someTime1.Format(goFormat))
56+
assertEqualE(t, tc.formatted2, someTime2.Format(goFormat))
57+
})
58+
}
59+
}
60+
61+
func TestIncorrectSecondsFraction(t *testing.T) {
62+
_, err := snowflakeFormatToGoFormat("HH24 MI SS FF")
63+
assertHasPrefixE(t, err.Error(), "incorrect second fraction")
64+
}
65+
66+
func TestSnowflakeFormatToGoFormatIntegrationTest(t *testing.T) {
67+
runDBTest(t, func(dbt *DBTest) {
68+
dbt.mustExec("ALTER SESSION SET TIME_OUTPUT_FORMAT = 'HH24:MI:SS.FF'")
69+
for _, forceFormat := range []string{forceJSON, forceARROW} {
70+
dbt.mustExec(forceFormat)
71+
72+
for _, tc := range []struct {
73+
sfType string
74+
formatParamName string
75+
sfFunction string
76+
}{
77+
{
78+
sfType: "TIMESTAMPLTZ",
79+
formatParamName: "TIMESTAMP_OUTPUT_FORMAT",
80+
sfFunction: "CURRENT_TIMESTAMP",
81+
},
82+
{
83+
sfType: "TIMESTAMPTZ",
84+
formatParamName: "TIMESTAMP_OUTPUT_FORMAT",
85+
sfFunction: "CURRENT_TIMESTAMP",
86+
},
87+
{
88+
sfType: "TIMESTAMPNTZ",
89+
formatParamName: "TIMESTAMP_NTZ_OUTPUT_FORMAT",
90+
sfFunction: "CURRENT_TIMESTAMP",
91+
},
92+
{
93+
sfType: "DATE",
94+
formatParamName: "DATE_OUTPUT_FORMAT",
95+
sfFunction: "CURRENT_DATE",
96+
},
97+
{
98+
sfType: "TIME",
99+
formatParamName: "TIME_OUTPUT_FORMAT",
100+
sfFunction: "CURRENT_TIME",
101+
},
102+
} {
103+
t.Run(tc.sfType+"___"+forceFormat, func(t *testing.T) {
104+
params := dbt.mustQuery("show parameters like '" + tc.formatParamName + "'")
105+
defer params.Close()
106+
params.Next()
107+
defaultTimestampOutputFormat, err := ScanSnowflakeParameter(params.rows)
108+
assertNilF(t, err)
109+
110+
rows := dbt.mustQuery("SELECT " + tc.sfFunction + "()::" + tc.sfType + ", " + tc.sfFunction + "()::" + tc.sfType + "::varchar")
111+
defer rows.Close()
112+
var t1 time.Time
113+
var t2 string
114+
rows.Next()
115+
err = rows.Scan(&t1, &t2)
116+
assertNilF(t, err)
117+
goFormat, err := snowflakeFormatToGoFormat(defaultTimestampOutputFormat.Value)
118+
assertNilF(t, err)
119+
assertEqualE(t, t1.Format(goFormat), t2)
120+
parseResult, err := time.Parse(goFormat, t2)
121+
assertNilF(t, err)
122+
if tc.sfType != "TIME" {
123+
assertEqualE(t, t1.UTC(), parseResult.UTC())
124+
} else {
125+
assertEqualE(t, t1.Hour(), parseResult.Hour())
126+
assertEqualE(t, t1.Minute(), parseResult.Minute())
127+
assertEqualE(t, t1.Second(), parseResult.Second())
128+
}
129+
})
130+
}
131+
}
132+
})
133+
}

0 commit comments

Comments
 (0)