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.

Solutions

  1. Clamp the offending timestamp to a safe range (e.g. year 9999 or below) in the legacy SQLite DB before migrating.
  2. If the value is 9999-12-31T23:59:59.5+, reduce it slightly so second-rounding stays within year 9999.
  3. Search the source of the extreme value (test fixtures, scripts) and fix the generator.
  4. 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

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


AI-assisted analysis of gastownhall/beads@71377f2769 (2026-08-30). Data as JSON: /api/errors/df83f7b5fa73d778. Report an issue: GitHub.

Appendix: 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)