package db import ( "database/sql/driver" "fmt" "strconv" "time" ) // Timestamp is a type that wraps `time.Time`. It implements `driver.Value` and `json.Marshal` to // store its value as an integer, a Unix timestamp in milliseconds. // // It uses type embedding (`struct { time.Time }`), instead of making it simply a typedef of // `time.Time`, because that keeps methods on `time.Time` (like Add, UnixMilli, etc.) intact and // exposed on instances of Timestamp. type Timestamp struct { time.Time } func TimestampFromUnix(num int64) Timestamp { return Timestamp{time.Unix(num, 0)} } func TimestampFromUnixMilli(num int64) Timestamp { return Timestamp{time.UnixMilli(num)} } // TimestampNow returns a new Timestamp corresponding to the current time, rounded to the nearest millisecond. func TimestampNow() Timestamp { return Timestamp{time.Now().Round(time.Millisecond)} } // ------------ // driver.Value // ------------ func (t Timestamp) Value() (driver.Value, error) { return t.UnixMilli(), nil } func (t *Timestamp) Scan(src any) error { val, isOk := src.(int64) if !isOk { return fmt.Errorf("incompatible type for Timestamp: %#v", src) } *t = TimestampFromUnixMilli(val) return nil } // ------------------------ // json.Marshal / Unmarshal // ------------------------ func (t Timestamp) MarshalJSON() ([]byte, error) { return fmt.Appendf(nil, "%d", t.UnixMilli()), nil } func (t *Timestamp) UnmarshalJSON(b []byte) error { ms, err := strconv.ParseInt(string(b), 10, 64) if err != nil { return fmt.Errorf("invalid timestamp %q: %w", string(b), err) } *t = TimestampFromUnixMilli(ms) return nil }