0db4ddd46e
## 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.
35 lines
1.2 KiB
TypeScript
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;
|
|
};
|