{"record":{"id":"dacb9d742179f7e8","repo":"gastownhall/beads","slug":"reading-migration-s-w","errorCode":null,"errorMessage":"reading migration %s: %w","messagePattern":"reading migration (.+?): %w","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"internal/storage/schema/schema.go","lineNumber":1337,"sourceCode":"\nfunc (m migrationSource) pendingMigrationDirtyTables(ctx context.Context, db DBConn, dirtyBefore map[string]dirtyTableState) ([]string, error) {\n\tif len(dirtyBefore) == 0 {\n\t\treturn nil, nil\n\t}\n\tcurrent, err := m.currentVersion(ctx, db)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tdirtyNames := sortedDirtyTableNames(dirtyBefore)\n\ttouched := make(map[string]struct{})\n\tfor _, mf := range m.list() {\n\t\tif mf.version <= current {\n\t\t\tcontinue\n\t\t}\n\t\tdata, err := m.files.ReadFile(m.dir + \"/\" + mf.name)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"reading migration %s: %w\", mf.name, err)\n\t\t}\n\t\tsqlText := string(data)\n\t\tfor _, table := range dirtyNames {\n\t\t\tif migrationSQLTouchesTable(sqlText, table) {\n\t\t\t\ttouched[table] = struct{}{}\n\t\t\t}\n\t\t}\n\t}\n\n\tnames := make([]string, 0, len(touched))\n\tfor table := range touched {\n\t\tnames = append(names, table)\n\t}\n\tsort.Strings(names)\n\treturn names, nil\n}\n\nfunc migrationSQLTouchesTable(sqlText, table string) bool {","sourceCodeStart":1319,"sourceCodeEnd":1355,"githubUrl":"https://github.com/gastownhall/beads/blob/71377f276968b452ee607177637970a4ff888584/internal/storage/schema/schema.go#L1319-L1355","documentation":"This error wraps an os/fs.ReadError that occurs when the schema package tries to read a pending migration file from its embedded filesystem (m.files.ReadFile at internal/storage/schema/schema.go:1337) while computing which dirty tables pending migrations will touch. The library throws it because a migration that list() reported as available could not actually be read from the migration source, so the dirty-table analysis cannot proceed safely. The wrapped underlying error (in the %w) names the real cause.","triggerScenarios":"Calling pendingMigrationDirtyTables (via the migrate path) when the embedded FS entry at m.dir/mf.name is missing or unreadable — e.g. m.dir is misregistered in the embed directive, the migration file was renamed/deleted without rebuilding, or list() and files disagree (stale binary, build cache issue).","commonSituations":"Adding a migration file but forgetting the //go:embed directive; deleting or renaming a migration while a stale build still lists it; case-sensitivity mismatches (works on macOS, fails on Linux CI); embedding from the wrong directory so dir prefix doesn't match.","solutions":["Rebuild the binary (go build ./...) so the embedded filesystem matches the migration files on disk","Verify the //go:embed pattern covers the migration directory and every .sql file","Confirm m.dir matches the embedded directory path exactly (case-sensitive)","Inspect the wrapped error in the message for the exact OS cause (no such file vs permission)","If the file was intentionally removed, delete or supersede its migration record instead of leaving a dangling entry"],"exampleFix":"// before (embed misses new migration dir)\n//go:embed migrations/*.sql\nvar files embed.FS\nm := migrationSource{files: files, dir: \"sqlmigrations\"}\n// after (dir matches embed root)\n//go:embed migrations/*.sql\nvar files embed.FS\nm := migrationSource{files: files, dir: \"migrations\"}","handlingStrategy":"try-catch","validationCode":"// Before calling into migration code, verify every listed migration is readable.\nfor _, mf := range src.list() {\n    if _, err := src.files.ReadFile(src.dir + \"/\" + mf.name); err != nil {\n        return fmt.Errorf(\"migration %s unreadable at startup: %w\", mf.name, err)\n    }\n}","typeGuard":"func migrationReadable(src migrationSource, mf migrationFile) bool {\n    f, err := src.files.ReadFile(src.dir + \"/\" + mf.name)\n    return err == nil && len(f) > 0\n}","tryCatchPattern":"names, err := m.pendingMigrationDirtyTables(ctx, db, dirtyBefore)\nif err != nil {\n    var pathErr *fs.PathError\n    if errors.As(err, &pathErr) {\n        // embedded FS mismatch: rebuild binary or fix embed directive\n        log.Printf(\"migration file unreadable: %s\", pathErr.Path)\n    }\n    return err\n}","preventionTips":["Keep //go:embed patterns up to date when adding migration directories","Always rebuild (not just re-run) after adding or renaming migration files","Treat applied migrations as immutable: never delete or rename them","Test migrations on a case-sensitive filesystem (Linux CI) to catch case mismatches","Add a startup smoke test that reads every migration listed by list()"],"tags":["database","migration","embedded-fs","go"],"backgroundTag":"migration-file-not-found","analyzedSha":"71377f276968b452ee607177637970a4ff888584","analyzedAt":"2026-08-30T18:55:39.744Z","schemaVersion":2},"datasetVersion":"2026-08-30T23:17:21.991Z"}