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

  1. Use the field name prefix in the message to target the exact column, then fix that column's value in the legacy SQLite DB.
  2. Rewrite the value to RFC3339 or 'YYYY-MM-DD HH:MM:SS' format, or set it to NULL if the timestamp should be absent.
  3. Check whether an old bd version or external tool wrote that column and normalize it with a one-off UPDATE.
  4. 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

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


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