multica-ai/multica · error
relation %q exists but is not an index
Error message
relation %q exists but is not an index
What it means
to_regclass resolved the configured index name to an existing relation, but pg_class.relkind says it is not an index (it is a table, view, sequence, or materialized view). The migrate tool refuses to DROP INDEX CONCURRENTLY something that is not an index, because that would either fail or, worse, be the wrong object entirely.
Source
Thrown at server/cmd/migrate/main.go:180
func cleanupInvalidConcurrentIndexHook(indexRegclass string) preMigrationHook {
return func(ctx context.Context, pool *pgxpool.Pool) error {
var schemaName, relationName string
var isIndex, isValid bool
err := pool.QueryRow(ctx, `
SELECT n.nspname, c.relname, c.relkind = 'i', COALESCE(i.indisvalid, FALSE)
FROM pg_class c
JOIN pg_namespace n ON n.oid = c.relnamespace
LEFT JOIN pg_index i ON i.indexrelid = c.oid
WHERE c.oid = to_regclass($1)
`, indexRegclass).Scan(&schemaName, &relationName, &isIndex, &isValid)
if errors.Is(err, pgx.ErrNoRows) {
return nil
}
if err != nil {
return fmt.Errorf("inspect concurrent index %q: %w", indexRegclass, err)
}
if !isIndex {
return fmt.Errorf("relation %q exists but is not an index", indexRegclass)
}
if isValid {
return nil
}
qualifiedName := pgx.Identifier{schemaName, relationName}.Sanitize()
if _, err := pool.Exec(ctx, "DROP INDEX CONCURRENTLY IF EXISTS "+qualifiedName); err != nil {
return fmt.Errorf("drop invalid concurrent index %s: %w", qualifiedName, err)
}
slog.Warn("removed invalid index before migration retry", "index", qualifiedName)
return nil
}
}
func runTaskUsageHourlyHook(ctx context.Context, pool *pgxpool.Pool) error {
res, err := taskusagebackfill.Hook(ctx, pool, taskusagebackfill.HookOptions{})
if err != nil {
return fmt.Errorf("task_usage_hourly pre-103 hook: %w", err)View on GitHub (pinned to 2c0912b6ec)
Solutions
- Inspect the object: psql -c "SELECT c.relkind, n.nspname, c.relname FROM pg_class c JOIN pg_namespace n ON n.oid=c.relnamespace WHERE c.oid = to_regclass('<name from error>')"
- If it is a stale/wrong relation, rename or drop it deliberately (after backing up), then re-run migrate
- If the name is genuinely needed by other code, fix the migration's index name or resolve the collision in the schema
Example fix
-- before: collision blocks migration -- relation 'public.tasks_created_at_idx' is a table -- after ALTER TABLE public.tasks_created_at_idx RENAME TO tasks_created_at_idx_old; -- then re-run: migrate up
Defensive patterns
Strategy: type-guard
Type guard
-- guard: is the named relation actually an index?
SELECT c.relkind = 'i' AS is_index
FROM pg_class c WHERE c.oid = to_regclass('public.my_idx'); Try / catch
if !isIndex {
return fmt.Errorf("relation %q exists but is not an index", indexRegclass)
} Prevention
- Keep self-hosted schemas aligned with shipped migrations; avoid manual DDL that reuses index names
- Audit name collisions with pg_class before migrations on drifted databases
When it happens
Trigger: A table/view/sequence shares the exact name the migration expects for its index — e.g. a leftover table created by an old or hand-run script with the index's name, or a rename collision after schema drift.
Common situations: Self-hosted databases that diverged from the managed schema; someone created a relation named like the index (e.g. tasks_created_at_idx used as a table name); restored dump with renamed objects.
Related errors
- inspect concurrent index %q: %w
- drop invalid concurrent index %s: %w
- task_usage_hourly pre-103 hook: %w
- attribution strict-constraint pre-198 hook: %w
- invalid schema migrations table %q: %w
AI-assisted analysis of multica-ai/multica@2c0912b6ec (2026-08-15).
Data as JSON: /api/errors/b661512fe657ce74.
Report an issue: GitHub.