multica-ai/multica · error
pre-migration hook for %q (%s): %w
Error message
pre-migration hook for %q (%s): %w
What it means
Returned by the migration runner when a registered pre-migration hook (opts.Hooks[version]) fails before the migration's SQL file is executed. Hooks run against the pgxpool.Pool so they can take their own session-level locks without colliding with migrationAdvisoryLockKey. Because the failure aborts before schema_migrations is updated, the same version retries cleanly on the next invocation.
Source
Thrown at server/cmd/migrate/main.go:436
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)
}
if opts.Direction == "up" {
_, err = conn.Exec(ctx, insertSQL, version)
} else {
_, err = conn.Exec(ctx, deleteSQL, version)
}
if err != nil {
return fmt.Errorf("record migration %q: %w", version, err)
}
fmt.Printf(" %s %s\n", opts.Direction, version)
}View on GitHub (pinned to 2c0912b6ec)
Solutions
- Read the wrapped error (%w) — the root cause is the hook's own failure, not the migration framework.
- Fix the hook's SQL/logic (constraint, timeout, missing object) in the code that registers opts.Hooks.
- Re-run the migrate command; the version was not recorded in schema_migrations, so it retries from the start cleanly.
- If the hook repeatedly cannot acquire a lock, verify it is not taking the same advisory lock as migrationAdvisoryLockKey.
Example fix
// before: hook assumes column exists
hook := func(ctx context.Context, pool *pgxpool.Pool) error {
_, err := pool.Exec(ctx, "UPDATE issues SET rank = 0")
return err
}
// after: make the hook idempotent and tolerant of partial state
hook := func(ctx context.Context, pool *pgxpool.Pool) error {
if _, err := pool.Exec(ctx, "ALTER TABLE issues ADD COLUMN IF NOT EXISTS rank int NOT NULL DEFAULT 0"); err != nil {
return fmt.Errorf("prepare rank column: %w", err)
}
_, err := pool.Exec(ctx, "UPDATE issues SET rank = 0 WHERE rank IS NULL")
return err
} Defensive patterns
Strategy: retry
Validate before calling
// Before registering, assert the hook only touches state it owns and is idempotent,
// so a failed run can be retried without manual repair.
for v, hook := range opts.Hooks {
if hook == nil {
return fmt.Errorf("nil pre-migration hook registered for version %s", v)
}
if !slices.Contains(versions, v) {
return fmt.Errorf("hook registered for unknown version %s", v)
}
} Try / catch
err := runMigrations(ctx, pool, opts)
if err != nil {
var hookErr *HookError // if you introduce one; otherwise string-match the prefix
if errors.As(err, &hookErr) {
// schema_migrations untouched: safe to fix the hook and re-run
log.Printf("hook failed, version not recorded; retry after fix: %v", err)
}
return err
} Prevention
- Write pre-migration hooks to be idempotent (IF NOT EXISTS, WHERE NOT EXISTS) since a failure leaves the version unrecorded and it will re-run.
- Keep hooks off migrationAdvisoryLockKey so they cannot self-deadlock with the runner.
- Test hooks against a schema snapshot fixture in CI, not just fresh databases.
When it happens
Trigger: Running `migrate` (up or down) on a version that has an entry in opts.Hooks, and the hook function returns a non-nil error — e.g. a data-backfill hook that fails a constraint, times out, or cannot acquire its own lock.
Common situations: A pre-migration data-copy/backfill hook hitting a NOT NULL or unique violation; hook SQL timing out on a large table; a hook expecting a table/column that a previous migration did not create (version skew between environments).
Related errors
- apply migration %q: %w
- record migration %q: %w
- inspect concurrent index %q: %w
- relation %q exists but is not an index
- drop invalid concurrent index %s: %w
AI-assisted analysis of multica-ai/multica@2c0912b6ec (2026-08-15).
Data as JSON: /api/errors/1df5fdba1262dc2e.
Report an issue: GitHub.