{"record":{"id":"1df5fdba1262dc2e","repo":"multica-ai/multica","slug":"pre-migration-hook-for-q-s-w","errorCode":null,"errorMessage":"pre-migration hook for %q (%s): %w","messagePattern":"pre-migration hook for %q \\((.+?)\\): %w","errorType":"console","errorClass":null,"httpStatus":null,"severity":"error","filePath":"server/cmd/migrate/main.go","lineNumber":436,"sourceCode":"\t\t\t\tcontinue\n\t\t\t}\n\t\t}\n\n\t\tsql, err := os.ReadFile(file)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"read migration %q: %w\", file, err)\n\t\t}\n\n\t\t// Run any pre-migration hook before the SQL file. Hooks\n\t\t// receive the *pgxpool.Pool (not the loop's pinned conn), so\n\t\t// they can acquire other session-level locks without\n\t\t// colliding with migrationAdvisoryLockKey. Hook failures\n\t\t// abort the run before schema_migrations is updated, so the\n\t\t// same version retries cleanly on the next invocation.\n\t\tif hook, ok := opts.Hooks[version]; ok && hook != nil {\n\t\t\tslog.Info(\"running pre-migration hook\", \"version\", version, \"direction\", opts.Direction)\n\t\t\tif err := hook(ctx, pool); err != nil {\n\t\t\t\treturn fmt.Errorf(\"pre-migration hook for %q (%s): %w\", version, opts.Direction, err)\n\t\t\t}\n\t\t}\n\n\t\tif _, err := conn.Exec(ctx, string(sql)); err != nil {\n\t\t\treturn fmt.Errorf(\"apply migration %q: %w\", file, err)\n\t\t}\n\n\t\tif opts.Direction == \"up\" {\n\t\t\t_, err = conn.Exec(ctx, insertSQL, version)\n\t\t} else {\n\t\t\t_, err = conn.Exec(ctx, deleteSQL, version)\n\t\t}\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"record migration %q: %w\", version, err)\n\t\t}\n\n\t\tfmt.Printf(\"  %s  %s\\n\", opts.Direction, version)\n\t}","sourceCodeStart":418,"sourceCodeEnd":454,"githubUrl":"https://github.com/multica-ai/multica/blob/2c0912b6ec764b373d44eeea1e80f0d9f11ab417/server/cmd/migrate/main.go#L418-L454","documentation":"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.","triggerScenarios":"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.","commonSituations":"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).","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."],"exampleFix":"// before: hook assumes column exists\nhook := func(ctx context.Context, pool *pgxpool.Pool) error {\n    _, err := pool.Exec(ctx, \"UPDATE issues SET rank = 0\")\n    return err\n}\n\n// after: make the hook idempotent and tolerant of partial state\nhook := func(ctx context.Context, pool *pgxpool.Pool) error {\n    if _, err := pool.Exec(ctx, \"ALTER TABLE issues ADD COLUMN IF NOT EXISTS rank int NOT NULL DEFAULT 0\"); err != nil {\n        return fmt.Errorf(\"prepare rank column: %w\", err)\n    }\n    _, err := pool.Exec(ctx, \"UPDATE issues SET rank = 0 WHERE rank IS NULL\")\n    return err\n}","handlingStrategy":"retry","validationCode":"// Before registering, assert the hook only touches state it owns and is idempotent,\n// so a failed run can be retried without manual repair.\nfor v, hook := range opts.Hooks {\n    if hook == nil {\n        return fmt.Errorf(\"nil pre-migration hook registered for version %s\", v)\n    }\n    if !slices.Contains(versions, v) {\n        return fmt.Errorf(\"hook registered for unknown version %s\", v)\n    }\n}","typeGuard":null,"tryCatchPattern":"err := runMigrations(ctx, pool, opts)\nif err != nil {\n    var hookErr *HookError // if you introduce one; otherwise string-match the prefix\n    if errors.As(err, &hookErr) {\n        // schema_migrations untouched: safe to fix the hook and re-run\n        log.Printf(\"hook failed, version not recorded; retry after fix: %v\", err)\n    }\n    return err\n}","preventionTips":["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."],"tags":["go","database","migration","hooks","postgres"],"backgroundTag":null,"analyzedSha":"2c0912b6ec764b373d44eeea1e80f0d9f11ab417","analyzedAt":"2026-08-15T13:25:18.241Z","schemaVersion":2},"datasetVersion":"2026-08-15T17:31:12.345Z"}