diff --git a/components/usage/pkg/db/types.go b/components/usage/pkg/db/types.go index c710959f8a8a8f..60dd9ae10a0559 100644 --- a/components/usage/pkg/db/types.go +++ b/components/usage/pkg/db/types.go @@ -39,37 +39,36 @@ func (n *VarcharTime) Scan(value interface{}) error { switch s := value.(type) { case []uint8: - // Null value - empty string mean value is not set - if len(s) == 0 { - n.valid = false - return nil - } - - parsed, err := iso8601.ParseString(string(s)) - if err != nil { - return fmt.Errorf("failed to parse %v into ISO8601: %w", string(s), err) - } - n.valid = true - n.t = parsed.UTC() - return nil + return n.parseString(string(s)) case string: - if len(s) == 0 { - n.valid = false - return nil - } - - parsed, err := iso8601.ParseString(s) - if err != nil { - return fmt.Errorf("failed to parse %v into ISO8601: %w", s, err) - } - - n.valid = true - n.t = parsed.UTC() - return nil + return n.parseString(s) } return fmt.Errorf("unknown scan value for VarcharTime with value: %v", value) } +func (n *VarcharTime) parseString(s string) error { + // Null value - empty string mean value is not set + if len(s) == 0 { + n.valid = false + return nil + } + + parsed, err := iso8601.ParseString(s) + if err != nil { + return fmt.Errorf("failed to parse %v into ISO8601: %w", s, err) + } + + if parsed.UTC().IsZero() { + n.t = time.Time{}.UTC() + n.valid = false + return nil + } + + n.valid = true + n.t = parsed.UTC() + return nil +} + func (n VarcharTime) Time() time.Time { return n.t } @@ -80,9 +79,21 @@ func (n VarcharTime) IsSet() bool { // Value implements the driver Valuer interface. func (n VarcharTime) Value() (driver.Value, error) { - return n.t.UTC().Format(time.RFC3339Nano), nil + if n.IsSet() { + return TimeToISO8601(n.t), nil + } + return "", nil } func (n VarcharTime) String() string { - return n.t.Format(time.RFC3339Nano) + if n.IsSet() { + return TimeToISO8601(n.t) + } + return "" +} + +const ISO8601Format = "2006-01-02T15:04:05.000Z" + +func TimeToISO8601(t time.Time) string { + return t.UTC().Format(ISO8601Format) } diff --git a/components/usage/pkg/db/types_test.go b/components/usage/pkg/db/types_test.go index 50fadfb3539982..73f02f47e417e1 100644 --- a/components/usage/pkg/db/types_test.go +++ b/components/usage/pkg/db/types_test.go @@ -6,6 +6,7 @@ package db import ( "github.com/stretchr/testify/require" + "gorm.io/gorm" "testing" "time" ) @@ -77,3 +78,81 @@ func TestVarcharTime_Scan(t *testing.T) { }) } } + +func TestVarcharTime_Value_ISO8601(t *testing.T) { + for _, scenario := range []struct { + Time VarcharTime + Expected string + }{ + { + Time: NewVarcharTime(time.Date(2019, 05, 10, 9, 54, 28, 185000000, time.UTC)), + Expected: "2019-05-10T09:54:28.185Z", + }, + { + Time: VarcharTime{}, + Expected: "", + }, + } { + wireFormat, err := scenario.Time.Value() + require.NoError(t, err) + require.Equal(t, scenario.Expected, wireFormat) + } +} + +func TestVarcharTime_String_ISO8601(t *testing.T) { + for _, scenario := range []struct { + Time VarcharTime + Expected string + }{ + { + Time: NewVarcharTime(time.Date(2019, 05, 10, 9, 54, 28, 185000000, time.UTC)), + Expected: "2019-05-10T09:54:28.185Z", + }, + { + Time: VarcharTime{}, + Expected: "", + }, + } { + require.Equal(t, scenario.Expected, scenario.Time.String()) + } +} + +func TestVarcharTime_SerializeAndDeserialize(t *testing.T) { + // Custom table to be able to exercise serialization easily, independent of other models + type VarcharModel struct { + ID int `gorm:"primaryKey"` + Time VarcharTime `gorm:"column:time;type:varchar(255);"` + } + + conn := ConnectForTests(t) + require.NoError(t, conn.AutoMigrate(&VarcharModel{})) + + conn.Session(&gorm.Session{AllowGlobalUpdate: true}).Delete(&VarcharModel{}) + + for _, scenario := range []struct { + Description string + Input VarcharModel + Expected VarcharModel + }{ + { + Description: "empty value for VarcharTime", + Input: VarcharModel{ + ID: 1, + Time: VarcharTime{}, + }, + Expected: VarcharModel{ + ID: 1, + Time: VarcharTime{}, + }, + }, + } { + tx := conn.Create(scenario.Input) + require.NoError(t, tx.Error) + + var read VarcharModel + tx = conn.First(&read, scenario.Input.ID) + require.NoError(t, tx.Error) + + require.Equal(t, scenario.Expected, read) + } +}