{"record":{"id":"f2662160877c2e61","repo":"gastownhall/beads","slug":"s-w-f26621","errorCode":null,"errorMessage":"%s: %w","messagePattern":"%s: %w","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"internal/migration/legacysqlite/reader.go","lineNumber":805,"sourceCode":"\t}\n\treturn time.Time{}, fmt.Errorf(\"invalid timestamp %q\", s)\n}\n\nfunc canonicalCurrentDatetime(t time.Time) (time.Time, error) {\n\tcanonical := t.UTC().Round(time.Second)\n\tif year := canonical.Year(); year < 0 || year > 9999 {\n\t\treturn time.Time{}, fmt.Errorf(\"timestamp rounds outside current DATETIME range\")\n\t}\n\treturn canonical, nil\n}\n\nfunc parseOptionalTime(name string, raw sql.NullString) (*time.Time, error) {\n\tif !raw.Valid {\n\t\treturn nil, nil\n\t}\n\tparsed, err := parseTime(raw.String)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"%s: %w\", name, err)\n\t}\n\treturn &parsed, nil\n}\n\nfunc loadChildren(ctx context.Context, db *sql.Tx, issues []*types.Issue) error {\n\tbyID := map[string]*types.Issue{}\n\tfor _, i := range issues {\n\t\tbyID[i.ID] = i\n\t}\n\tif err := loadLabels(ctx, db, byID); err != nil {\n\t\treturn err\n\t}\n\tif err := loadDependencies(ctx, db, byID); err != nil {\n\t\treturn err\n\t}\n\tif err := validateDependencyGraph(issues); err != nil {\n\t\treturn err\n\t}","sourceCodeStart":787,"sourceCodeEnd":823,"githubUrl":"https://github.com/gastownhall/beads/blob/71377f276968b452ee607177637970a4ff888584/internal/migration/legacysqlite/reader.go#L787-L823","documentation":"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.","triggerScenarios":"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 \"...\"'.","commonSituations":"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.","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."],"exampleFix":"// before: closed_at = 'not a date'\nUPDATE issues SET closed_at = NULL WHERE closed_at = 'not a date';\n// after: NULL optional timestamp parses cleanly","handlingStrategy":"validation","validationCode":"// validate all optional datetime columns by name before migration\noptionalDatetimeCols := []string{\"closed_at\", \"updated_at\", \"compacted_at\"}\nfor _, col := range optionalDatetimeCols {\n    rows, _ := legacyDB.Query(fmt.Sprintf(\"SELECT %s FROM issues WHERE %s IS NOT NULL\", col, col))\n    for rows.Next() {\n        var s string\n        rows.Scan(&s)\n        if !parseableTimestamp(s) {\n            fmt.Printf(\"bad %s: %q\\n\", col, s)\n        }\n    }\n}","typeGuard":null,"tryCatchPattern":"if err := migrateLegacy(db); err != nil {\n    // message is \"<field>: invalid timestamp ...\"; split on the first colon\n    if field, _, ok := strings.Cut(err.Error(), \":\"); ok {\n        return fmt.Errorf(\"fix column %s in legacy DB: %w\", field, err)\n    }\n    return err\n}","preventionTips":["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."],"tags":["migration","sqlite","timestamp","error-wrapping"],"backgroundTag":"invalid-timestamp","analyzedSha":"71377f276968b452ee607177637970a4ff888584","analyzedAt":"2026-08-30T18:55:39.744Z","schemaVersion":2},"datasetVersion":"2026-08-30T23:17:21.991Z"}