gastownhall/beads · error
reading migration %s: %w
Error message
reading migration %s: %w
What it means
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.
Source
Thrown at internal/storage/schema/schema.go:1337
func (m migrationSource) pendingMigrationDirtyTables(ctx context.Context, db DBConn, dirtyBefore map[string]dirtyTableState) ([]string, error) {
if len(dirtyBefore) == 0 {
return nil, nil
}
current, err := m.currentVersion(ctx, db)
if err != nil {
return nil, err
}
dirtyNames := sortedDirtyTableNames(dirtyBefore)
touched := make(map[string]struct{})
for _, mf := range m.list() {
if mf.version <= current {
continue
}
data, err := m.files.ReadFile(m.dir + "/" + mf.name)
if err != nil {
return nil, fmt.Errorf("reading migration %s: %w", mf.name, err)
}
sqlText := string(data)
for _, table := range dirtyNames {
if migrationSQLTouchesTable(sqlText, table) {
touched[table] = struct{}{}
}
}
}
names := make([]string, 0, len(touched))
for table := range touched {
names = append(names, table)
}
sort.Strings(names)
return names, nil
}
func migrationSQLTouchesTable(sqlText, table string) bool {View on GitHub (pinned to 71377f2769)
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
Example fix
// before (embed misses new migration dir)
//go:embed migrations/*.sql
var files embed.FS
m := migrationSource{files: files, dir: "sqlmigrations"}
// after (dir matches embed root)
//go:embed migrations/*.sql
var files embed.FS
m := migrationSource{files: files, dir: "migrations"} Defensive patterns
Strategy: try-catch
Validate before calling
// Before calling into migration code, verify every listed migration is readable.
for _, mf := range src.list() {
if _, err := src.files.ReadFile(src.dir + "/" + mf.name); err != nil {
return fmt.Errorf("migration %s unreadable at startup: %w", mf.name, err)
}
} Type guard
func migrationReadable(src migrationSource, mf migrationFile) bool {
f, err := src.files.ReadFile(src.dir + "/" + mf.name)
return err == nil && len(f) > 0
} Try / catch
names, err := m.pendingMigrationDirtyTables(ctx, db, dirtyBefore)
if err != nil {
var pathErr *fs.PathError
if errors.As(err, &pathErr) {
// embedded FS mismatch: rebuild binary or fix embed directive
log.Printf("migration file unreadable: %s", pathErr.Path)
}
return err
} Prevention
- 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()
When it happens
Trigger: 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).
Common situations: 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.
Related errors
- ErrTransaction
- ErrQuery
- ErrScan
- db: Exists: id must not be empty
- db: CountForPrefix: prefix must not be empty
AI-assisted analysis of gastownhall/beads@71377f2769 (2026-08-30).
Data as JSON: /api/errors/dacb9d742179f7e8.
Report an issue: GitHub.