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

  1. Read the wrapped error (%w) — the root cause is the hook's own failure, not the migration framework.
  2. Fix the hook's SQL/logic (constraint, timeout, missing object) in the code that registers opts.Hooks.
  3. Re-run the migrate command; the version was not recorded in schema_migrations, so it retries from the start cleanly.
  4. 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

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


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