Files
twenty/packages/twenty-server/src/database/commands/upgrade-version-command/2-18/utils/does-physical-index-exist.util.ts
T
Paul Rastoin 0db4ddd46e fix(server): tolerate metadata-physical index drift in legacy index name normalization (#22472)
## Context

The 2.18 `NormalizeLegacyIndexNames` workspace command (`1799200000000`,
introduced in #22053) fails several production workspaces with `42P01
relation … does not exist`, rolling back the whole per-workspace upgrade
transaction and marking the workspace `Failed`.

## Root cause

The command assumes the physical index in the workspace schema is named
exactly as recorded in `core."indexMetadata"."name"`. For workspaces
where the physical index was already rebuilt/renamed under the v2
deterministic name (only targeted phone/relation rebuilds got new names
after #14567) while metadata kept the legacy hash, the rename source no
longer exists, so `ALTER INDEX … RENAME` aborts the entire workspace
upgrade. (The duplicate-drop path uses `DROP INDEX IF EXISTS` and is
unaffected.)

## Fix

- **`WorkspaceSchemaIndexManagerService`**: new `doesIndexExist` and
`getIndexDefinition` helpers querying `pg_indexes` for a `(schema,
index)` pair. `renameIndexWithoutRebuild` keeps its strict semantics (no
`IF EXISTS`) — drift tolerance lives in the command, which is the only
caller that expects it.
- **`NormalizeLegacyIndexNamesCommand`** — the rename operation now
reconciles drift instead of blindly renaming:
- Target name already exists physically, source gone → skip the rename,
just point `indexMetadata.name` at it (the common "physical already v2,
metadata still legacy" case).
- Both source and target exist physically → compare their
`pg_indexes.indexdef` ignoring the name: if identical, drop the legacy
duplicate (it would otherwise be orphaned forever since metadata stops
referencing it, adding permanent write/maintenance cost); if the
definitions differ, keep it in place and log a warning.
  - Source exists, target free → rename as before, then update metadata.
- Neither exists → log a warning and update metadata so a future rebuild
recreates the index under the expected v2 name.

In every branch the metadata name ends up on the recomputed v2 name, and
no missing physical index can abort the workspace transaction anymore.

## Tests

- Regression tests on the command spec for the four drift cases
(target-already-renamed, both-missing, both-present-identical → drop,
both-present-different → keep); existing
rename/duplicate/dry-run/rollback tests updated to declare the physical
indexes present.
- New spec for `WorkspaceSchemaIndexManagerService` covering the rename
SQL, the `pg_indexes` existence check, and the definition lookup.
- New spec for `areIndexDefinitionsEquivalent` (name-only diff,
uniqueness, columns, where clause, malformed input).

`npx jest` on all three specs (20 passed), `lint:diff-with-main` and
`typecheck` green.
2026-07-03 13:00:48 +00:00

35 lines
1.2 KiB
TypeScript

import { type QueryRunner } from 'typeorm';
// Physical catalog introspection is an upgrade-only concern: in normal
// operation metadata is the source of truth and migrations never need to ask
// Postgres whether an index actually exists. Keep this helper scoped to the
// legacy index name normalization command instead of the shared schema
// manager.
//
// Query pg_class/pg_namespace directly instead of the pg_indexes view: the
// view joins pg_index against pg_class twice and derives schemaname from the
// table's namespace, which prevents an index lookup and can scan the whole
// catalog on large multi-tenant clusters. This lookup hits the unique
// (relname, relnamespace) index on pg_class.
export const doesPhysicalIndexExist = async ({
queryRunner,
schemaName,
indexName,
}: {
queryRunner: QueryRunner;
schemaName: string;
indexName: string;
}): Promise<boolean> => {
const result: { exists: boolean }[] = await queryRunner.query(
`SELECT EXISTS (
SELECT 1
FROM pg_class c
JOIN pg_namespace n ON n.oid = c.relnamespace
WHERE c.relname = $2 AND n.nspname = $1 AND c.relkind IN ('i', 'I')
) AS "exists"`,
[schemaName, indexName],
);
return result[0]?.exists === true;
};