Skip to content

Commit b88e77c

Browse files
easyCZroboquat
authored andcommitted
[usage] Harden parsing of time from VarChar field
1 parent df7ed58 commit b88e77c

File tree

2 files changed

+86
-31
lines changed

2 files changed

+86
-31
lines changed

components/usage/pkg/db/types.go

Lines changed: 42 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -6,54 +6,83 @@ package db
66

77
import (
88
"database/sql/driver"
9-
"errors"
109
"fmt"
1110
"github.com/relvacode/iso8601"
1211
"time"
1312
)
1413

1514
func NewVarcharTime(t time.Time) VarcharTime {
16-
return VarcharTime(t.UTC())
15+
return VarcharTime{
16+
t: t,
17+
valid: true,
18+
}
1719
}
1820

1921
func NewVarcharTimeFromStr(s string) (VarcharTime, error) {
20-
parsed, err := iso8601.ParseString(string(s))
21-
if err != nil {
22-
return VarcharTime{}, fmt.Errorf("failed to parse as ISO 8601: %w", err)
23-
}
24-
return VarcharTime(parsed), nil
22+
var vt VarcharTime
23+
err := vt.Scan(s)
24+
return vt, err
2525
}
2626

2727
// VarcharTime exists for cases where records are inserted into the DB as VARCHAR but actually contain a timestamp which is time.RFC3339
28-
type VarcharTime time.Time
28+
type VarcharTime struct {
29+
t time.Time
30+
valid bool
31+
}
2932

3033
// Scan implements the Scanner interface.
3134
func (n *VarcharTime) Scan(value interface{}) error {
3235
if value == nil {
33-
return fmt.Errorf("nil value")
36+
n.valid = false
37+
return nil
3438
}
3539

3640
switch s := value.(type) {
3741
case []uint8:
42+
// Null value - empty string mean value is not set
3843
if len(s) == 0 {
39-
return errors.New("failed to parse empty varchar time")
44+
n.valid = false
45+
return nil
4046
}
4147

4248
parsed, err := iso8601.ParseString(string(s))
4349
if err != nil {
4450
return fmt.Errorf("failed to parse %v into ISO8601: %w", string(s), err)
4551
}
46-
*n = VarcharTime(parsed.UTC())
52+
n.valid = true
53+
n.t = parsed.UTC()
54+
return nil
55+
case string:
56+
if len(s) == 0 {
57+
n.valid = false
58+
return nil
59+
}
60+
61+
parsed, err := iso8601.ParseString(s)
62+
if err != nil {
63+
return fmt.Errorf("failed to parse %v into ISO8601: %w", s, err)
64+
}
65+
66+
n.valid = true
67+
n.t = parsed.UTC()
4768
return nil
4869
}
4970
return fmt.Errorf("unknown scan value for VarcharTime with value: %v", value)
5071
}
5172

73+
func (n VarcharTime) Time() time.Time {
74+
return n.t
75+
}
76+
77+
func (n VarcharTime) IsSet() bool {
78+
return n.valid
79+
}
80+
5281
// Value implements the driver Valuer interface.
5382
func (n VarcharTime) Value() (driver.Value, error) {
54-
return time.Time(n).UTC().Format(time.RFC3339Nano), nil
83+
return n.t.UTC().Format(time.RFC3339Nano), nil
5584
}
5685

5786
func (n VarcharTime) String() string {
58-
return time.Time(n).Format(time.RFC3339Nano)
87+
return n.t.Format(time.RFC3339Nano)
5988
}

components/usage/pkg/db/types_test.go

Lines changed: 44 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -11,43 +11,69 @@ import (
1111
)
1212

1313
func TestVarcharTime_Scan(t *testing.T) {
14+
type Expectation struct {
15+
Time VarcharTime
16+
Error bool
17+
}
18+
1419
for _, scenario := range []struct {
1520
Name string
1621

1722
Input interface{}
18-
Expected time.Time
19-
Error bool
23+
Expected Expectation
2024
}{
2125
{
22-
Name: "nil value errors",
26+
Name: "nil value does not error and sets invalid",
2327
Input: nil,
24-
Error: true,
28+
Expected: Expectation{
29+
Error: false,
30+
},
2531
},
2632
{
27-
Name: "empty uint8 slice errors",
33+
Name: "empty uint8 slice does not error and sets invalid",
2834
Input: []uint8{},
29-
Error: true,
35+
Expected: Expectation{
36+
Error: false,
37+
},
3038
},
3139
{
32-
Name: "fails with string",
33-
Input: "2019-05-10T09:54:28.185Z",
34-
Error: true,
40+
Name: "parses valid ISO 8601 from TypeScript from []uint8",
41+
Input: []uint8("2019-05-10T09:54:28.185Z"),
42+
Expected: Expectation{
43+
Time: VarcharTime{
44+
t: time.Date(2019, 05, 10, 9, 54, 28, 185000000, time.UTC),
45+
valid: true,
46+
},
47+
Error: false,
48+
},
3549
},
3650
{
37-
Name: "parses valid ISO 8601 from TypeScript",
38-
Input: []uint8("2019-05-10T09:54:28.185Z"),
39-
Expected: time.Date(2019, 05, 10, 9, 54, 28, 185000000, time.UTC),
51+
Name: "invalid string errors",
52+
Input: "2019-05-10T09:54:28.185Z-not-a-datetime",
53+
Expected: Expectation{
54+
Error: true,
55+
},
56+
},
57+
{
58+
Name: "string is parsed",
59+
Input: "2019-05-10T09:54:28.185Z",
60+
Expected: Expectation{
61+
Time: VarcharTime{
62+
t: time.Date(2019, 05, 10, 9, 54, 28, 185000000, time.UTC),
63+
valid: true,
64+
},
65+
Error: false,
66+
},
4067
},
4168
} {
4269
t.Run(scenario.Name, func(t *testing.T) {
4370
var vt VarcharTime
4471
err := vt.Scan(scenario.Input)
45-
if scenario.Error {
46-
require.Error(t, err)
47-
} else {
48-
require.NoError(t, err)
49-
require.Equal(t, scenario.Expected, time.Time(vt))
50-
}
72+
73+
require.Equal(t, scenario.Expected, Expectation{
74+
Time: vt,
75+
Error: err != nil,
76+
})
5177
})
5278
}
5379
}

0 commit comments

Comments
 (0)