multica-ai/multica · critical

record migration %q: %w

Error message

record migration %q: %w

What it means

Returned when updating the schema_migrations bookkeeping row fails after the migration SQL applied successfully: the INSERT (up) or DELETE (down) of the version could not be executed. The schema change is applied but not recorded, leaving the version in a half-recorded state that a re-run must reconcile.

Source

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

		// 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)
	}

	return nil
}

// quoteQualifiedIdentifier safely quotes either an unqualified table
// name ("foo") or a schema-qualified name ("schema.foo") for embedding
// into a SQL statement. Postgres does not let parametrized queries
// supply identifiers, so we have to interpolate, but pgx.Identifier
// does the right escaping (double-quotes, embedded-quote handling).
//
// The accepted shape is exactly one or two dot-separated components.
// Names containing more than one dot are rejected outright rather than
// silently sanitized into a "schema"."b.c" reference, which is valid
// SQL but almost certainly not what the caller meant.

View on GitHub (pinned to 2c0912b6ec)

Solutions

  1. Check the wrapped error: a unique violation on insert means the version row already exists — inspect schema_migrations and reconcile manually.
  2. If schema_migrations is missing, recreate it and repopulate applied versions before re-running.
  3. If the connection dropped, verify Postgres logs, then re-run migrate — up will re-apply SQL, so confirm the migration body is idempotent (IF NOT EXISTS) or manually mark the version.
  4. Never delete version rows for migrations whose changes are live; only for verified rolled-back ones.

Example fix

-- before: version applied but row missing, re-run would double-apply
-- inspect, then record manually:
SELECT * FROM schema_migrations ORDER BY version;
INSERT INTO schema_migrations (version) VALUES ('20260815000000');

-- after: migrate now skips the already-applied version
Defensive patterns

Strategy: validation

Validate before calling

-- Pre-flight before migrating: confirm the bookkeeping table is sane.
SELECT EXISTS (
  SELECT 1 FROM information_schema.tables
  WHERE table_name = 'schema_migrations'
) AS has_table;
-- If false, recreate it and backfill applied versions before running migrate.

Try / catch

err := runMigrations(ctx, pool, opts)
if err != nil {
    var pgErr *pgconn.PgError
    if errors.As(err, &pgErr) && pgErr.Code == "23505" {
        // version row already inserted: schema is applied, bookkeeping is fine
        log.Printf("version already recorded; treating as applied")
        return nil // only after verifying the schema change really is present
    }
    return err
}

Prevention

When it happens

Trigger: The insertSQL/deleteSQL statement against schema_migrations errors — the table is missing/corrupt, the pinned connection was killed mid-run, a unique violation on re-inserting an already-present version, or the transaction/permissions on schema_migrations are wrong.

Common situations: schema_migrations was dropped or recreated manually; a prior crash left the version row inserted while the operator assumed it was absent; a DBA revoked INSERT on the migrations table from the migrating role.

Related errors


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