gastownhall/beads · error

timestamp %q: %w

Error message

timestamp %q: %w

What it means

parseTime successfully parsed a timestamp string with one of the supported layouts (RFC3339Nano or SQLite-style datetime formats), but canonicalizing it to the current DATETIME representation failed. The original string is included in the message and the underlying cause (from canonicalCurrentDatetime) is wrapped with %w, so this error typically indicates a timestamp that parses but is unrepresentable (see the wrapped year-range error).

Source

Thrown at internal/migration/legacysqlite/reader.go:783

	for _, v := range values {
		if v.Valid && v.String != "" {
			return true
		}
	}
	return false
}
func nullString(v sql.NullString) string {
	if v.Valid {
		return v.String
	}
	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

View on GitHub (pinned to 71377f2769)

Solutions

  1. Read the wrapped cause in the message chain to see why canonicalization failed (usually year out of range).
  2. Fix the offending timestamp in the legacy SQLite DB to a realistic date before migrating (e.g. UPDATE dependencies SET created_at = ...).
  3. Ensure timestamps are written in RFC3339 or '2006-01-02 15:04:05' format with years 0001-9999.
  4. If seconds rounding pushes the value out of range (e.g. 9999-12-31T23:59:59.9), nudge the value back slightly.

Example fix

// before: created_at = '10000-01-01 00:00:00'
UPDATE dependencies SET created_at = '2024-01-01 00:00:00' WHERE created_at = '10000-01-01 00:00:00';
// after: canonicalizable timestamp
Defensive patterns

Strategy: validation

Validate before calling

func timestampCanonicalizable(s string) bool {
    for _, layout := range []string{time.RFC3339Nano, "2006-01-02 15:04:05.999999999-07:00", "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 isStorableDatetime(t time.Time) bool {
    y := t.UTC().Round(time.Second).Year()
    return y >= 0 && y <= 9999
}

Try / catch

// Go: wrap the migration call and inspect the wrapped cause
if err := migrateLegacy(db); err != nil {
    var inner error
    if errors.As(err, &inner) || strings.Contains(err.Error(), "timestamp") {
        log.Fatalf("fix timestamp in legacy DB: %v", err)
    }
    return err
}

Prevention

When it happens

Trigger: A dependency row's created_at or a comment/issue timestamp parses under one of the three accepted layouts, but t.UTC().Round(time.Second) yields a year outside 0..9999 (or another canonicalization failure). Raised from parseTime for callers parseOptionalTime, appendLegacyDependencyRow, and loadComments.

Common situations: Timestamps with extreme years (e.g. year 10000+) written by buggy tools; hand-edited SQLite rows with nonsense datetimes; dates far in the future like 9999-12-31T23:59:60 that round up past the representable range.

Related errors


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