multica-ai/multica · error

create migrations table

Error message

create migrations table: %w

What it means

CREATE TABLE IF NOT EXISTS for the schema_migrations tracking table failed. IF EXISTS-style idempotence only guards against the table already existing properly; errors arise from insufficient CREATE privilege on the schema, an invalid table identifier at the SQL level (after quoting), disk/filesystem issues, or a cancelled context.

Solutions

  1. Grant the migration role CREATE on the schema (or use the schema owner)
  2. Create the target schema first if using schema-qualified tracking table names
  3. Re-run after fixing privileges; the run is resumable

Example fix

-- before: migrate role lacks CREATE
GRANT USAGE ON SCHEMA public TO migrate_user;  -- insufficient

-- after
GRANT USAGE, CREATE ON SCHEMA public TO migrate_user;
Defensive patterns

Strategy: validation

Validate before calling

-- verify the role can create tables before running migrations
SELECT has_schema_privilege(current_user, 'public', 'CREATE');

Try / catch

if _, err := conn.Exec(ctx, createTableSQL); err != nil {
    return fmt.Errorf("create migrations table: %w", err)
}

Prevention

When it happens

Trigger: Running the migration role without CREATE on the target schema; the custom -schema-migrations-table targets a schema that does not exist; ctx cancelled right after lock acquisition.

Common situations: Least-privilege DB users missing DDL grants; pointing at a schema-qualified tracking table whose schema was never created.

Related errors


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

Appendix: source

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

	// Best-effort explicit unlock on the success path. On error returns
	// the defer still runs; on os.Exit error paths in main() it does not,
	// but session-level advisory locks are released automatically when
	// the connection closes at process exit, so the next runner is never
	// permanently blocked.
	defer func() {
		if _, err := conn.Exec(ctx, "SELECT pg_advisory_unlock($1)", lockKey); err != nil {
			slog.Warn("failed to release migration advisory lock", "error", err)
		}
	}()

	// Create migrations tracking table.
	if _, err := conn.Exec(ctx, fmt.Sprintf(`
		CREATE TABLE IF NOT EXISTS %s (
			version TEXT PRIMARY KEY,
			applied_at TIMESTAMPTZ NOT NULL DEFAULT now()
		)
	`, tableIdent)); err != nil {
		return fmt.Errorf("create migrations table: %w", err)
	}

	existsSQL := fmt.Sprintf("SELECT EXISTS(SELECT 1 FROM %s WHERE version = $1)", tableIdent)
	insertSQL := fmt.Sprintf("INSERT INTO %s (version) VALUES ($1)", tableIdent)
	deleteSQL := fmt.Sprintf("DELETE FROM %s WHERE version = $1", tableIdent)

	for _, file := range opts.Files {
		version := migrations.ExtractVersion(file)

		var exists bool
		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

View on GitHub (pinned to 2c0912b6ec)