multica-ai/multica · error

read migration %q: %w

Error message

read migration %q: %w

What it means

os.ReadFile failed for a migration SQL file listed in opts.Files. The runner needs the file's bytes to exec it; failure means the path does not exist, is a directory, or the process lacks read permission. The path is exactly what was passed in the file list, so this usually indicates the files were moved/deleted between listing and reading, or the list was built against a different root.

Source

Thrown at server/cmd/migrate/main.go:424

		if err := conn.QueryRow(ctx, existsSQL, version).Scan(&exists); err != nil {
			return fmt.Errorf("check migration %q: %w", version, err)
		}

		if opts.Direction == "up" {
			if exists {
				fmt.Printf("  skip  %s (already applied)\n", version)
				continue
			}
		} else {
			if !exists {
				fmt.Printf("  skip  %s (not applied)\n", version)
				continue
			}
		}

		sql, err := os.ReadFile(file)
		if err != nil {
			return fmt.Errorf("read migration %q: %w", file, err)
		}

		// Run any pre-migration hook before the SQL file. Hooks
		// receive the *pgxpool.Pool (not the loop's pinned conn), so
		// they can acquire other session-level locks without
		// colliding with migrationAdvisoryLockKey. Hook failures
		// abort the run before schema_migrations is updated, so the
		// same version retries cleanly on the next invocation.
		if hook, ok := opts.Hooks[version]; ok && hook != nil {
			slog.Info("running pre-migration hook", "version", version, "direction", opts.Direction)
			if err := hook(ctx, pool); err != nil {
				return fmt.Errorf("pre-migration hook for %q (%s): %w", version, opts.Direction, err)
			}
		}

		if _, err := conn.Exec(ctx, string(sql)); err != nil {
			return fmt.Errorf("apply migration %q: %w", file, err)
		}

View on GitHub (pinned to 2c0912b6ec)

Solutions

  1. Verify the exact path from the error exists at runtime: ls -l <path>
  2. Use absolute paths or embed the migrations via go:embed so the binary is self-contained
  3. If containerized, confirm the migrations dir is COPYed into the image and readable by the runtime user

Example fix

# before
migrate -files=./migrations/*.sql   # relative to cwd, wrong cwd at runtime

# after
migrate -files=/app/migrations/*.sql
# or embed in Go:
//go:embed migrations/*.sql
var migrationFS embed.FS
Defensive patterns

Strategy: validation

Validate before calling

for _, f := range files {
    if _, err := os.Stat(f); err != nil {
        log.Fatalf("migration file missing: %s", f)
    }
}

Try / catch

sql, err := os.ReadFile(file)
if err != nil {
    return fmt.Errorf("read migration %q: %w", file, err)
}

Prevention

When it happens

Trigger: Embedded-vs-disk mismatch: opts.Files contains absolute/relative paths from a build step but the binary runs where those paths do not exist; file deleted between discovery and read; permission changes (e.g. container user differs from file owner).

Common situations: Running the migrate binary from a different working directory with relative file globs; CI cache stale; container image built without copying the migrations directory.

Related errors


AI-assisted analysis of multica-ai/multica@2c0912b6ec (2026-08-15). Data as JSON: /api/errors/155e6e7f573041d1. Report an issue: GitHub.