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

  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.