{"record":{"id":"df83f7b5fa73d778","repo":"gastownhall/beads","slug":"timestamp-rounds-outside-current-datetime-range","errorCode":null,"errorMessage":"timestamp rounds outside current DATETIME range","messagePattern":"timestamp rounds outside current DATETIME range","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"internal/migration/legacysqlite/reader.go","lineNumber":794,"sourceCode":"\treturn \"\"\n}\nfunc parseTime(s string) (time.Time, error) {\n\tfor _, layout := range []string{time.RFC3339Nano, \"2006-01-02 15:04:05.999999999-07:00\", \"2006-01-02 15:04:05.999999999\"} {\n\t\tif t, e := time.Parse(layout, s); e == nil {\n\t\t\tcanonical, err := canonicalCurrentDatetime(t)\n\t\t\tif err != nil {\n\t\t\t\treturn time.Time{}, fmt.Errorf(\"timestamp %q: %w\", s, err)\n\t\t\t}\n\t\t\treturn canonical, nil\n\t\t}\n\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 {","sourceCodeStart":776,"sourceCodeEnd":812,"githubUrl":"https://github.com/gastownhall/beads/blob/71377f276968b452ee607177637970a4ff888584/internal/migration/legacysqlite/reader.go#L776-L812","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","solutions":["Clamp the offending timestamp to a safe range (e.g. year 9999 or below) in the legacy SQLite DB before migrating.","If the value is 9999-12-31T23:59:59.5+, reduce it slightly so second-rounding stays within year 9999.","Search the source of the extreme value (test fixtures, scripts) and fix the generator.","If 'infinity-like' sentinel dates were intentional, replace them with NULL or a documented max date."],"exampleFix":"// before: '9999-12-31 23:59:59.9' rounds to year 10000\nUPDATE issues SET updated_at = '9999-12-31 23:59:59' WHERE updated_at > '9999-12-31 23:59:58';\n// after: rounds within DATETIME range","handlingStrategy":"validation","validationCode":"func yearInDatetimeRange(s string) bool {\n    for _, layout := range []string{time.RFC3339Nano, \"2006-01-02 15:04:05.999999999\"} {\n        if t, err := time.Parse(layout, s); err == nil {\n            y := t.UTC().Round(time.Second).Year()\n            return y >= 0 && y <= 9999\n        }\n    }\n    return false\n}","typeGuard":"func fitsCurrentDatetime(t time.Time) bool {\n    y := t.UTC().Round(time.Second).Year()\n    return y >= 0 && y <= 9999\n}","tryCatchPattern":null,"preventionTips":["Clamp timestamps to years 1..9999 at write time in any tool touching the legacy DB.","Keep sentinel 'max' dates safely below 9999-12-31 23:59:58 so second-rounding cannot overflow the year.","Validate timestamps after UTC conversion, not just in local time."],"tags":["migration","sqlite","timestamp","datetime-range"],"backgroundTag":"datetime-out-of-range","analyzedSha":"71377f276968b452ee607177637970a4ff888584","analyzedAt":"2026-08-30T18:55:39.744Z","schemaVersion":2},"datasetVersion":"2026-08-30T23:17:21.991Z"}