gastownhall/beads · error
%s: %w
Error message
%s: %w
What it means
parseOptionalTime wraps any parseTime failure with the name of the optional timestamp field being processed, so the caller can tell WHICH column (e.g. 'closed_at', 'updated_at') had the bad value. It is a pure error-annotation wrapper: the root cause (invalid timestamp or canonicalization failure) is preserved via %w.
Source
Thrown at internal/migration/legacysqlite/reader.go:805
}
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 {
byID[i.ID] = i
}
if err := loadLabels(ctx, db, byID); err != nil {
return err
}
if err := loadDependencies(ctx, db, byID); err != nil {
return err
}
if err := validateDependencyGraph(issues); err != nil {
return err
}View on GitHub (pinned to 71377f2769)
Solutions
- Use the field name prefix in the message to target the exact column, then fix that column's value in the legacy SQLite DB.
- Rewrite the value to RFC3339 or 'YYYY-MM-DD HH:MM:SS' format, or set it to NULL if the timestamp should be absent.
- Check whether an old bd version or external tool wrote that column and normalize it with a one-off UPDATE.
- Re-run the migration after fixing; the wrapper reports fields one at a time, so a pre-pass scan of all datetime columns is faster for many bad rows.
Example fix
// before: closed_at = 'not a date' UPDATE issues SET closed_at = NULL WHERE closed_at = 'not a date'; // after: NULL optional timestamp parses cleanly
Defensive patterns
Strategy: validation
Validate before calling
// validate all optional datetime columns by name before migration
optionalDatetimeCols := []string{"closed_at", "updated_at", "compacted_at"}
for _, col := range optionalDatetimeCols {
rows, _ := legacyDB.Query(fmt.Sprintf("SELECT %s FROM issues WHERE %s IS NOT NULL", col, col))
for rows.Next() {
var s string
rows.Scan(&s)
if !parseableTimestamp(s) {
fmt.Printf("bad %s: %q\n", col, s)
}
}
} Try / catch
if err := migrateLegacy(db); err != nil {
// message is "<field>: invalid timestamp ..."; split on the first colon
if field, _, ok := strings.Cut(err.Error(), ":"); ok {
return fmt.Errorf("fix column %s in legacy DB: %w", field, err)
}
return err
} Prevention
- Treat optional datetime columns with the same format validation as required ones.
- Set unparseable optional timestamps to NULL rather than leaving garbage text.
- Fix one-at-a-time field reports by scanning all datetime columns up front.
When it happens
Trigger: applyOptionalTimestamps encounters a non-NULL optional datetime column (closed_at, updated_at, etc.) whose text cannot be parsed by parseTime, or parses but fails canonicalization. The resulting error reads like: '<field>: invalid timestamp "..."'.
Common situations: Legacy rows where one optional column (often closed_at) was written in a different format than the rest; manually edited rows; epoch integers or locale dates in a single column while others are fine.
Related errors
- timestamp %q: %w
- invalid timestamp %q
- timestamp rounds outside current DATETIME range
- 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/f2662160877c2e61.
Report an issue: GitHub.