gastownhall/beads · error
timestamp rounds outside current DATETIME range
Error message
timestamp rounds outside current DATETIME range
What it means
canonicalCurrentDatetime converts a parsed timestamp to UTC rounded to the second, then rejects it if the resulting year falls outside 0..9999 — the representable range of the current DATETIME columns. This prevents values that parse fine as Go time.Time but cannot be stored in the target schema.
Source
Thrown at internal/migration/legacysqlite/reader.go:794
return ""
}
func parseTime(s string) (time.Time, error) {
for _, layout := range []string{time.RFC3339Nano, "2006-01-02 15:04:05.999999999-07:00", "2006-01-02 15:04:05.999999999"} {
if t, e := time.Parse(layout, s); e == nil {
canonical, err := canonicalCurrentDatetime(t)
if err != nil {
return time.Time{}, fmt.Errorf("timestamp %q: %w", s, err)
}
return canonical, nil
}
}
return time.Time{}, fmt.Errorf("invalid timestamp %q", s)
}
func canonicalCurrentDatetime(t time.Time) (time.Time, error) {
canonical := t.UTC().Round(time.Second)
if year := canonical.Year(); year < 0 || year > 9999 {
return time.Time{}, fmt.Errorf("timestamp rounds outside current DATETIME range")
}
return canonical, nil
}
func parseOptionalTime(name string, raw sql.NullString) (*time.Time, error) {
if !raw.Valid {
return nil, nil
}
parsed, err := parseTime(raw.String)
if err != nil {
return nil, fmt.Errorf("%s: %w", name, err)
}
return &parsed, nil
}
func loadChildren(ctx context.Context, db *sql.Tx, issues []*types.Issue) error {
byID := map[string]*types.Issue{}
for _, i := range issues {View on GitHub (pinned to 71377f2769)
Solutions
- Clamp the offending timestamp to a safe range (e.g. year 9999 or below) in the legacy SQLite DB before migrating.
- If the value is 9999-12-31T23:59:59.5+, reduce it slightly so second-rounding stays within year 9999.
- Search the source of the extreme value (test fixtures, scripts) and fix the generator.
- If 'infinity-like' sentinel dates were intentional, replace them with NULL or a documented max date.
Example fix
// before: '9999-12-31 23:59:59.9' rounds to year 10000 UPDATE issues SET updated_at = '9999-12-31 23:59:59' WHERE updated_at > '9999-12-31 23:59:58'; // after: rounds within DATETIME range
Defensive patterns
Strategy: validation
Validate before calling
func yearInDatetimeRange(s string) bool {
for _, layout := range []string{time.RFC3339Nano, "2006-01-02 15:04:05.999999999"} {
if t, err := time.Parse(layout, s); err == nil {
y := t.UTC().Round(time.Second).Year()
return y >= 0 && y <= 9999
}
}
return false
} Type guard
func fitsCurrentDatetime(t time.Time) bool {
y := t.UTC().Round(time.Second).Year()
return y >= 0 && y <= 9999
} Prevention
- Clamp timestamps to years 1..9999 at write time in any tool touching the legacy DB.
- Keep sentinel 'max' dates safely below 9999-12-31 23:59:58 so second-rounding cannot overflow the year.
- Validate timestamps after UTC conversion, not just in local time.
When it happens
Trigger: A timestamp parses successfully in parseTime but after UTC conversion and second-rounding its year is negative or greater than 9999 — e.g. year-10000+ dates, extreme negative years, or 9999-12-31T23:59:59.x that rounds up to year 10000. Called from applyCanonicalTimestamps and parseTime.
Common situations: Synthetic test data with far-future dates; buggy generators writing year 10000+ timestamps; dates near the DATETIME ceiling that cross the boundary only after rounding; corrupt rows with negative years.
Related errors
- timestamp %q: %w
- invalid timestamp %q
- %s: %w
- dependency created_at is zero for %s -> %s
- comment created_at is zero for issue %s
AI-assisted analysis of gastownhall/beads@71377f2769 (2026-08-30).
Data as JSON: /api/errors/df83f7b5fa73d778.
Report an issue: GitHub.