package store import ( "database/sql/driver" "errors" "fmt" "time" ) // timestampLayout is RFC 3339 in UTC with fixed millisecond precision, so // stored timestamps sort lexicographically in time order. const timestampLayout = "2006-01-02T15:04:05.000Z" // Timestamp is an instant stored as UTC RFC 3339 TEXT with millisecond // precision. Sub-millisecond precision is dropped on write. Use // sql.Null[Timestamp] for a nullable column. type Timestamp struct { time.Time } // Now returns the current instant as a Timestamp, in UTC at millisecond // precision, so it equals its stored form. func Now() Timestamp { return Timestamp{time.Now().UTC().Truncate(time.Millisecond)} } // String returns the stored TEXT form. func (t Timestamp) String() string { return t.UTC().Format(timestampLayout) } // Value implements driver.Valuer. The zero Timestamp is an error, as is a UTC // year outside 0000–9999, which the four-digit stored form cannot represent. func (t Timestamp) Value() (driver.Value, error) { if t.IsZero() { return nil, errors.New("store: zero Timestamp") } if y := t.UTC().Year(); y < 0 || y > 9999 { return nil, fmt.Errorf("store: Timestamp year %d outside 0000-9999", y) } return t.String(), nil } // Scan implements sql.Scanner. It accepts only TEXT in the stored form, and // rejects the zero instant, which Value refuses to write. func (t *Timestamp) Scan(src any) error { s, ok := src.(string) if !ok { return fmt.Errorf("store: scan Timestamp: want TEXT, got %T", src) } parsed, err := time.Parse(timestampLayout, s) if err != nil { return fmt.Errorf("store: scan Timestamp %q: %w", s, err) } if parsed.Format(timestampLayout) != s { return fmt.Errorf("store: scan Timestamp %q: want %s", s, timestampLayout) } if parsed.IsZero() { return fmt.Errorf("store: scan Timestamp %q: zero Timestamp", s) } t.Time = parsed return nil } // localDateLayout is the household-local calendar date form. const localDateLayout = "2006-01-02" // LocalDate is a household-local calendar date with no time or zone, stored // as YYYY-MM-DD TEXT. The zero LocalDate is invalid and cannot be stored. type LocalDate struct { year int month time.Month day int } // ParseLocalDate parses a YYYY-MM-DD date, rejecting any other form and any // date that does not exist on the calendar. func ParseLocalDate(s string) (LocalDate, error) { t, err := time.Parse(localDateLayout, s) if err != nil { return LocalDate{}, fmt.Errorf("store: invalid date %q: %w", s, err) } if t.Format(localDateLayout) != s { return LocalDate{}, fmt.Errorf("store: invalid date %q: want YYYY-MM-DD", s) } return LocalDate{t.Year(), t.Month(), t.Day()}, nil } // LocalDateOf returns the calendar date of t in t's own location. func LocalDateOf(t time.Time) LocalDate { y, m, d := t.Date() return LocalDate{y, m, d} } // Date returns the year, month and day of d. func (d LocalDate) Date() (year int, month time.Month, day int) { return d.year, d.month, d.day } // String returns the YYYY-MM-DD form. func (d LocalDate) String() string { return fmt.Sprintf("%04d-%02d-%02d", d.year, int(d.month), d.day) } // IsZero reports whether d is the zero LocalDate. func (d LocalDate) IsZero() bool { return d == LocalDate{} } // Value implements driver.Valuer. The zero LocalDate is an error, as is a // year outside 0000–9999, which the four-digit stored form cannot represent. func (d LocalDate) Value() (driver.Value, error) { if d.IsZero() { return nil, errors.New("store: zero LocalDate") } if d.year < 0 || d.year > 9999 { return nil, fmt.Errorf("store: LocalDate year %d outside 0000-9999", d.year) } return d.String(), nil } // Scan implements sql.Scanner. It accepts only YYYY-MM-DD TEXT. func (d *LocalDate) Scan(src any) error { s, ok := src.(string) if !ok { return fmt.Errorf("store: scan LocalDate: want TEXT, got %T", src) } parsed, err := ParseLocalDate(s) if err != nil { return fmt.Errorf("store: scan LocalDate: %w", err) } *d = parsed return nil }