fix(server): normalize legacy index names (command) (#22053)

## TL;DR

Adds a workspace upgrade command that normalizes index names to the
current v2 deterministic naming convention (IDX_ prefix). This will
close https://github.com/twentyhq/twenty/issues/21383 : uniqueness
constraint cannot be disabled (for users who set it before v2
determinist naming is enforced).

### Background

The deterministic index name embeds the table name, columns, uniqueness
and where clause. The naming convention changed on **2025-09-23**
(#14567 - added the `IDX_`/`IDX_UNIQUE_` prefix and folded the table
name into the hash). Index rows created before that kept their old name
in `core."indexMetadata"`, and nothing rewrites it (only targeted
phone/relation rebuilds got new names).

Code paths that locate an index by recomputing its expected name then
miss these legacy-named rows. The most visible symptom is #21383:
toggling a field's `isUnique` from `true` → `false` recomputes the
expected unique-index name, fails to find the legacy-named index, and
silently no-ops — so uniqueness can't be disabled.

### What the command does

Per workspace, for each index:
- recomputes the expected name with the same generator the app uses
(`generateFlatIndexMetadataWithNameOrThrow`);
- if the stored name differs → **rename** it (metadata `UPDATE` + a new
metadata-only `ALTER INDEX … RENAME`, which preserves the unique
constraint with no rebuild/lock);
- if a correctly-named twin already exists (the legacy +
freshly-generated duplicate case) → **drop the redundant** one (physical
+ metadata, field rows cascade) and keep the canonical;
- invalidates the metadata cache so the running app + `isUnique`
derivation reflect the new names.

Honors `--dry-run`, wraps writes in a transaction (rolls back on error),
and skips (with a warning) any single index whose name can't be
recomputed so it can't abort the whole workspace.

### Notable changes
- New `renameIndex` on `WorkspaceSchemaIndexManagerService` (`ALTER
INDEX IF EXISTS … RENAME`).
- Planning logic extracted into a pure, unit-tested util
(`planIndexNameNormalization`).
This commit is contained in:
Marie
2026-06-29 16:04:40 +02:00
committed by GitHub
parent 6fa803edc2
commit 4eb28b73c7
7 changed files with 773 additions and 0 deletions
@@ -0,0 +1,16 @@
import { Module } from '@nestjs/common';
import { WorkspaceIteratorModule } from 'src/database/commands/command-runners/workspace-iterator.module';
import { NormalizeLegacyIndexNamesCommand } from 'src/database/commands/upgrade-version-command/2-18/2-18-workspace-command-1799200000000-normalize-legacy-index-names.command';
import { WorkspaceSchemaManagerModule } from 'src/engine/twenty-orm/workspace-schema-manager/workspace-schema-manager.module';
import { WorkspaceCacheModule } from 'src/engine/workspace-cache/workspace-cache.module';
@Module({
imports: [
WorkspaceCacheModule,
WorkspaceIteratorModule,
WorkspaceSchemaManagerModule,
],
providers: [NormalizeLegacyIndexNamesCommand],
})
export class V2_18_UpgradeVersionCommandModule {}
@@ -0,0 +1,206 @@
import { Command } from 'nest-commander';
import { isDefined } from 'twenty-shared/utils';
import { ActiveOrSuspendedWorkspaceCommandRunner } from 'src/database/commands/command-runners/active-or-suspended-workspace.command-runner';
import { WorkspaceIteratorService } from 'src/database/commands/command-runners/workspace-iterator.service';
import { type RunOnWorkspaceArgs } from 'src/database/commands/command-runners/workspace.command-runner';
import {
type FlatIndexNameStatus,
planIndexNameNormalization,
} from 'src/database/commands/upgrade-version-command/2-18/utils/plan-index-name-normalization.util';
import { RegisteredWorkspaceCommand } from 'src/engine/core-modules/upgrade/decorators/registered-workspace-command.decorator';
import { findManyFlatEntityByUniversalIdentifierInUniversalFlatEntityMapsOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/find-many-flat-entity-by-universal-identifier-in-universal-flat-entity-maps-or-throw.util';
import { generateFlatIndexMetadataWithNameOrThrow } from 'src/engine/metadata-modules/index-metadata/utils/generate-flat-index.util';
import { WorkspaceSchemaManagerService } from 'src/engine/twenty-orm/workspace-schema-manager/workspace-schema-manager.service';
import { WorkspaceCacheService } from 'src/engine/workspace-cache/services/workspace-cache.service';
import { getWorkspaceSchemaName } from 'src/engine/workspace-datasource/utils/get-workspace-schema-name.util';
import {
deleteIndexMetadata,
dropIndexFromWorkspaceSchema,
} from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/index/utils/index-action-handler.utils';
@RegisteredWorkspaceCommand('2.18.0', 1799200000000)
@Command({
name: 'upgrade:2-18:normalize-legacy-index-names',
description:
'Rename indexes whose stored name predates the v2 deterministic naming convention to their recomputed name, and drop redundant duplicates.',
})
export class NormalizeLegacyIndexNamesCommand extends ActiveOrSuspendedWorkspaceCommandRunner {
constructor(
protected readonly workspaceIteratorService: WorkspaceIteratorService,
private readonly workspaceCacheService: WorkspaceCacheService,
private readonly workspaceSchemaManagerService: WorkspaceSchemaManagerService,
) {
super(workspaceIteratorService);
}
override async runOnWorkspace({
workspaceId,
dataSource,
options,
}: RunOnWorkspaceArgs): Promise<void> {
if (!isDefined(dataSource)) {
this.logger.log(`No data source for workspace ${workspaceId}, skipping`);
return;
}
const { flatIndexMaps, flatObjectMetadataMaps, flatFieldMetadataMaps } =
await this.workspaceCacheService.getOrRecompute(workspaceId, [
'flatIndexMaps',
'flatObjectMetadataMaps',
'flatFieldMetadataMaps',
]);
const indexStatuses: FlatIndexNameStatus[] = [];
for (const flatObjectMetadata of Object.values(
flatObjectMetadataMaps.byUniversalIdentifier,
)) {
if (!isDefined(flatObjectMetadata)) {
continue;
}
const objectIndexes =
findManyFlatEntityByUniversalIdentifierInUniversalFlatEntityMapsOrThrow(
{
flatEntityMaps: flatIndexMaps,
universalIdentifiers:
flatObjectMetadata.indexMetadataUniversalIdentifiers,
},
);
if (objectIndexes.length === 0) {
continue;
}
const objectFlatFieldMetadatas =
findManyFlatEntityByUniversalIdentifierInUniversalFlatEntityMapsOrThrow(
{
flatEntityMaps: flatFieldMetadataMaps,
universalIdentifiers: flatObjectMetadata.fieldUniversalIdentifiers,
},
);
for (const flatIndex of objectIndexes) {
try {
const expectedName = generateFlatIndexMetadataWithNameOrThrow({
flatObjectMetadata,
objectFlatFieldMetadatas,
flatIndex,
}).name;
indexStatuses.push({
indexMetadataId: flatIndex.id,
objectMetadataId: flatIndex.objectMetadataId,
currentName: flatIndex.name,
expectedName,
});
} catch (error) {
this.logger.warn(
`Could not recompute expected name for index ${flatIndex.name} (${flatIndex.id}) in workspace ${workspaceId}, skipping: ${error instanceof Error ? error.message : String(error)}`,
);
}
}
}
const operations = planIndexNameNormalization(indexStatuses);
if (operations.length === 0) {
this.logger.log(
`No legacy index names to normalize for workspace ${workspaceId}, skipping`,
);
return;
}
if (options.dryRun) {
for (const operation of operations) {
if (operation.type === 'rename') {
this.logger.log(
`[DRY RUN] Would rename index ${operation.fromName} -> ${operation.toName} (workspace ${workspaceId})`,
);
} else {
this.logger.log(
`[DRY RUN] Would drop redundant duplicate index ${operation.redundantName} (kept ${operation.keptName}) (workspace ${workspaceId})`,
);
}
}
return;
}
const schemaName = getWorkspaceSchemaName(workspaceId);
const queryRunner = dataSource.createQueryRunner();
let isQueryRunnerConnected = false;
let isTransactionStarted = false;
try {
await queryRunner.connect();
isQueryRunnerConnected = true;
await queryRunner.startTransaction();
isTransactionStarted = true;
for (const operation of operations) {
if (operation.type === 'rename') {
await this.workspaceSchemaManagerService.indexManager.renameIndexWithoutRebuild(
{
queryRunner,
schemaName,
fromIndexName: operation.fromName,
toIndexName: operation.toName,
},
);
await queryRunner.query(
`UPDATE "core"."indexMetadata"
SET "name" = $1
WHERE "id" = $2
AND "workspaceId" = $3`,
[operation.toName, operation.indexMetadataId, workspaceId],
);
this.logger.log(
`Renamed index ${operation.fromName} -> ${operation.toName} (workspace ${workspaceId})`,
);
} else {
await dropIndexFromWorkspaceSchema({
indexName: operation.redundantName,
workspaceSchemaManagerService: this.workspaceSchemaManagerService,
queryRunner,
schemaName,
});
await deleteIndexMetadata({
entityId: operation.indexMetadataId,
queryRunner,
workspaceId,
});
this.logger.log(
`Dropped redundant duplicate index ${operation.redundantName} (kept ${operation.keptName}) (workspace ${workspaceId})`,
);
}
}
await queryRunner.commitTransaction();
} catch (error) {
if (isTransactionStarted) {
await queryRunner.rollbackTransaction();
}
throw error;
} finally {
if (isQueryRunnerConnected) {
await queryRunner.release();
}
}
await this.workspaceCacheService.invalidateAndRecompute(workspaceId, [
'flatIndexMaps',
'flatObjectMetadataMaps',
'flatFieldMetadataMaps',
]);
}
}
@@ -0,0 +1,292 @@
import { type DataSource, type QueryRunner } from 'typeorm';
import { NormalizeLegacyIndexNamesCommand } from 'src/database/commands/upgrade-version-command/2-18/2-18-workspace-command-1799200000000-normalize-legacy-index-names.command';
import { type WorkspaceCacheService } from 'src/engine/workspace-cache/services/workspace-cache.service';
import { type WorkspaceIteratorService } from 'src/database/commands/command-runners/workspace-iterator.service';
import { type WorkspaceSchemaManagerService } from 'src/engine/twenty-orm/workspace-schema-manager/workspace-schema-manager.service';
import { generateFlatIndexMetadataWithNameOrThrow } from 'src/engine/metadata-modules/index-metadata/utils/generate-flat-index.util';
import {
deleteIndexMetadata,
dropIndexFromWorkspaceSchema,
} from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/index/utils/index-action-handler.utils';
jest.mock(
'src/engine/metadata-modules/index-metadata/utils/generate-flat-index.util',
);
jest.mock(
'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/index/utils/index-action-handler.utils',
);
const generateFlatIndexMetadataWithNameOrThrowMock =
generateFlatIndexMetadataWithNameOrThrow as jest.Mock;
const deleteIndexMetadataMock = deleteIndexMetadata as jest.Mock;
const dropIndexFromWorkspaceSchemaMock = dropIndexFromWorkspaceSchema as jest.Mock;
const WORKSPACE_ID = '20202020-0000-0000-0000-000000000001';
type IndexFixture = {
universalIdentifier: string;
id: string;
name: string;
expectedName: string;
};
const buildFlatEntityMaps = (indexes: IndexFixture[]) => {
const flatIndexMaps = {
byUniversalIdentifier: Object.fromEntries(
indexes.map((index) => [
index.universalIdentifier,
{
universalIdentifier: index.universalIdentifier,
id: index.id,
name: index.name,
objectMetadataId: 'object-1',
},
]),
),
};
const flatObjectMetadataMaps = {
byUniversalIdentifier: {
'object-uid': {
universalIdentifier: 'object-uid',
nameSingular: 'myObject',
indexMetadataUniversalIdentifiers: indexes.map(
(index) => index.universalIdentifier,
),
fieldUniversalIdentifiers: [],
},
},
};
const flatFieldMetadataMaps = { byUniversalIdentifier: {} };
// Drive expected names per index via the mocked generator.
generateFlatIndexMetadataWithNameOrThrowMock.mockImplementation(
({ flatIndex }: { flatIndex: { id: string } }) => {
const match = indexes.find((index) => index.id === flatIndex.id);
return { name: match?.expectedName ?? flatIndex.id };
},
);
// eslint-disable-next-line @typescript-eslint/no-explicit-any
return { flatIndexMaps, flatObjectMetadataMaps, flatFieldMetadataMaps } as any;
};
const buildQueryRunner = (): {
queryRunner: QueryRunner;
query: jest.Mock;
commit: jest.Mock;
rollback: jest.Mock;
} => {
const query = jest.fn();
const commit = jest.fn();
const rollback = jest.fn();
const queryRunner = {
connect: jest.fn(),
startTransaction: jest.fn(),
commitTransaction: commit,
rollbackTransaction: rollback,
release: jest.fn(),
query,
} as unknown as QueryRunner;
return { queryRunner, query, commit, rollback };
};
describe('NormalizeLegacyIndexNamesCommand', () => {
let command: NormalizeLegacyIndexNamesCommand;
let renameIndexMock: jest.Mock;
let dropIndexMock: jest.Mock;
let getOrRecomputeMock: jest.Mock;
let invalidateAndRecomputeMock: jest.Mock;
beforeEach(() => {
jest.clearAllMocks();
renameIndexMock = jest.fn();
dropIndexMock = jest.fn();
getOrRecomputeMock = jest.fn();
invalidateAndRecomputeMock = jest.fn();
const workspaceIteratorService = {} as WorkspaceIteratorService;
const workspaceCacheService = {
getOrRecompute: getOrRecomputeMock,
invalidateAndRecompute: invalidateAndRecomputeMock,
} as unknown as WorkspaceCacheService;
const workspaceSchemaManagerService = {
indexManager: {
renameIndexWithoutRebuild: renameIndexMock,
dropIndex: dropIndexMock,
},
} as unknown as WorkspaceSchemaManagerService;
command = new NormalizeLegacyIndexNamesCommand(
workspaceIteratorService,
workspaceCacheService,
workspaceSchemaManagerService,
);
});
const runOnWorkspace = (dataSource: DataSource, dryRun = false) =>
command.runOnWorkspace({
workspaceId: WORKSPACE_ID,
dataSource: dataSource as never,
options: { dryRun },
index: 0,
total: 1,
});
it('renames a legacy-named index and updates its metadata row, then invalidates the cache', async () => {
getOrRecomputeMock.mockResolvedValue(
buildFlatEntityMaps([
{
universalIdentifier: 'idx-uid',
id: 'index-1',
name: 'legacyhash',
expectedName: 'IDX_UNIQUE_new',
},
]),
);
const { queryRunner, query, commit } = buildQueryRunner();
const dataSource = {
createQueryRunner: () => queryRunner,
} as unknown as DataSource;
await runOnWorkspace(dataSource);
expect(renameIndexMock).toHaveBeenCalledWith(
expect.objectContaining({
fromIndexName: 'legacyhash',
toIndexName: 'IDX_UNIQUE_new',
}),
);
expect(query).toHaveBeenCalledWith(expect.stringContaining('UPDATE'), [
'IDX_UNIQUE_new',
'index-1',
WORKSPACE_ID,
]);
expect(dropIndexFromWorkspaceSchemaMock).not.toHaveBeenCalled();
expect(commit).toHaveBeenCalled();
expect(invalidateAndRecomputeMock).toHaveBeenCalledWith(WORKSPACE_ID, [
'flatIndexMaps',
'flatObjectMetadataMaps',
'flatFieldMetadataMaps',
]);
});
it('drops the redundant duplicate and renames the survivor when two indexes collide', async () => {
getOrRecomputeMock.mockResolvedValue(
buildFlatEntityMaps([
{
universalIdentifier: 'idx-a',
id: 'index-a',
name: 'legacyA',
expectedName: 'IDX_UNIQUE_shared',
},
{
universalIdentifier: 'idx-b',
id: 'index-b',
name: 'legacyB',
expectedName: 'IDX_UNIQUE_shared',
},
]),
);
const { queryRunner } = buildQueryRunner();
const dataSource = {
createQueryRunner: () => queryRunner,
} as unknown as DataSource;
await runOnWorkspace(dataSource);
expect(renameIndexMock).toHaveBeenCalledTimes(1);
expect(renameIndexMock).toHaveBeenCalledWith(
expect.objectContaining({
fromIndexName: 'legacyA',
toIndexName: 'IDX_UNIQUE_shared',
}),
);
expect(dropIndexFromWorkspaceSchemaMock).toHaveBeenCalledTimes(1);
expect(dropIndexFromWorkspaceSchemaMock).toHaveBeenCalledWith(
expect.objectContaining({ indexName: 'legacyB' }),
);
expect(deleteIndexMetadataMock).toHaveBeenCalledWith(
expect.objectContaining({ entityId: 'index-b', workspaceId: WORKSPACE_ID }),
);
});
it('does nothing when all index names already match', async () => {
getOrRecomputeMock.mockResolvedValue(
buildFlatEntityMaps([
{
universalIdentifier: 'idx-uid',
id: 'index-1',
name: 'IDX_ok',
expectedName: 'IDX_ok',
},
]),
);
const createQueryRunner = jest.fn();
const dataSource = { createQueryRunner } as unknown as DataSource;
await runOnWorkspace(dataSource);
expect(createQueryRunner).not.toHaveBeenCalled();
expect(renameIndexMock).not.toHaveBeenCalled();
expect(invalidateAndRecomputeMock).not.toHaveBeenCalled();
});
it('does not write anything in dry-run mode', async () => {
getOrRecomputeMock.mockResolvedValue(
buildFlatEntityMaps([
{
universalIdentifier: 'idx-uid',
id: 'index-1',
name: 'legacyhash',
expectedName: 'IDX_UNIQUE_new',
},
]),
);
const createQueryRunner = jest.fn();
const dataSource = { createQueryRunner } as unknown as DataSource;
await runOnWorkspace(dataSource, true);
expect(createQueryRunner).not.toHaveBeenCalled();
expect(renameIndexMock).not.toHaveBeenCalled();
expect(dropIndexFromWorkspaceSchemaMock).not.toHaveBeenCalled();
expect(invalidateAndRecomputeMock).not.toHaveBeenCalled();
});
it('rolls back the transaction when an operation fails', async () => {
getOrRecomputeMock.mockResolvedValue(
buildFlatEntityMaps([
{
universalIdentifier: 'idx-uid',
id: 'index-1',
name: 'legacyhash',
expectedName: 'IDX_UNIQUE_new',
},
]),
);
renameIndexMock.mockRejectedValueOnce(new Error('boom'));
const { queryRunner, commit, rollback } = buildQueryRunner();
const dataSource = {
createQueryRunner: () => queryRunner,
} as unknown as DataSource;
await expect(runOnWorkspace(dataSource)).rejects.toThrow('boom');
expect(rollback).toHaveBeenCalled();
expect(commit).not.toHaveBeenCalled();
expect(invalidateAndRecomputeMock).not.toHaveBeenCalled();
});
});
@@ -0,0 +1,169 @@
import {
type FlatIndexNameStatus,
planIndexNameNormalization,
} from 'src/database/commands/upgrade-version-command/2-18/utils/plan-index-name-normalization.util';
const buildStatus = (
overrides: Partial<FlatIndexNameStatus> &
Pick<FlatIndexNameStatus, 'indexMetadataId' | 'currentName' | 'expectedName'>,
): FlatIndexNameStatus => ({
objectMetadataId: 'object-1',
...overrides,
});
describe('planIndexNameNormalization', () => {
it('returns no operations for an empty input', () => {
expect(planIndexNameNormalization([])).toEqual([]);
});
it('returns no operations when every index name already matches', () => {
const statuses = [
buildStatus({
indexMetadataId: 'index-1',
currentName: 'IDX_UNIQUE_aaa',
expectedName: 'IDX_UNIQUE_aaa',
}),
buildStatus({
indexMetadataId: 'index-2',
currentName: 'IDX_bbb',
expectedName: 'IDX_bbb',
}),
];
expect(planIndexNameNormalization(statuses)).toEqual([]);
});
it('renames a single legacy-named index to its expected name', () => {
const statuses = [
buildStatus({
indexMetadataId: 'index-1',
currentName: 'legacyhash000000000000000000',
expectedName: 'IDX_UNIQUE_newhash',
}),
];
expect(planIndexNameNormalization(statuses)).toEqual([
{
type: 'rename',
indexMetadataId: 'index-1',
objectMetadataId: 'object-1',
fromName: 'legacyhash000000000000000000',
toName: 'IDX_UNIQUE_newhash',
},
]);
});
it('renames the survivor and drops the duplicate when two legacy indexes resolve to the same name', () => {
const statuses = [
buildStatus({
indexMetadataId: 'index-legacy-a',
currentName: 'legacyhashA',
expectedName: 'IDX_UNIQUE_shared',
}),
buildStatus({
indexMetadataId: 'index-legacy-b',
currentName: 'legacyhashB',
expectedName: 'IDX_UNIQUE_shared',
}),
];
expect(planIndexNameNormalization(statuses)).toEqual([
{
type: 'rename',
indexMetadataId: 'index-legacy-a',
objectMetadataId: 'object-1',
fromName: 'legacyhashA',
toName: 'IDX_UNIQUE_shared',
},
{
type: 'dropRedundant',
indexMetadataId: 'index-legacy-b',
objectMetadataId: 'object-1',
redundantName: 'legacyhashB',
keptName: 'IDX_UNIQUE_shared',
},
]);
});
it('keeps the already-correct twin and only drops the legacy duplicate (no rename)', () => {
const statuses = [
buildStatus({
indexMetadataId: 'index-legacy',
currentName: 'legacyhash',
expectedName: 'IDX_UNIQUE_shared',
}),
buildStatus({
indexMetadataId: 'index-correct',
currentName: 'IDX_UNIQUE_shared',
expectedName: 'IDX_UNIQUE_shared',
}),
];
expect(planIndexNameNormalization(statuses)).toEqual([
{
type: 'dropRedundant',
indexMetadataId: 'index-legacy',
objectMetadataId: 'object-1',
redundantName: 'legacyhash',
keptName: 'IDX_UNIQUE_shared',
},
]);
});
it('handles a mix of correct, legacy, and duplicate indexes across objects', () => {
const statuses = [
// already correct -> ignored
buildStatus({
indexMetadataId: 'ok',
objectMetadataId: 'object-a',
currentName: 'IDX_ok',
expectedName: 'IDX_ok',
}),
// legacy single -> rename
buildStatus({
indexMetadataId: 'legacy-single',
objectMetadataId: 'object-a',
currentName: 'oldname',
expectedName: 'IDX_renamed',
}),
// duplicate pair on another object -> rename + drop
buildStatus({
indexMetadataId: 'dup-1',
objectMetadataId: 'object-b',
currentName: 'oldDup1',
expectedName: 'IDX_UNIQUE_dup',
}),
buildStatus({
indexMetadataId: 'dup-2',
objectMetadataId: 'object-b',
currentName: 'oldDup2',
expectedName: 'IDX_UNIQUE_dup',
}),
];
const operations = planIndexNameNormalization(statuses);
expect(operations).toHaveLength(3);
expect(operations).toContainEqual({
type: 'rename',
indexMetadataId: 'legacy-single',
objectMetadataId: 'object-a',
fromName: 'oldname',
toName: 'IDX_renamed',
});
expect(operations).toContainEqual({
type: 'rename',
indexMetadataId: 'dup-1',
objectMetadataId: 'object-b',
fromName: 'oldDup1',
toName: 'IDX_UNIQUE_dup',
});
expect(operations).toContainEqual({
type: 'dropRedundant',
indexMetadataId: 'dup-2',
objectMetadataId: 'object-b',
redundantName: 'oldDup2',
keptName: 'IDX_UNIQUE_dup',
});
});
});
@@ -0,0 +1,72 @@
export type FlatIndexNameStatus = {
indexMetadataId: string;
objectMetadataId: string;
currentName: string;
expectedName: string;
};
export type RenameIndexNameOperation = {
type: 'rename';
indexMetadataId: string;
objectMetadataId: string;
fromName: string;
toName: string;
};
export type DropRedundantIndexOperation = {
type: 'dropRedundant';
indexMetadataId: string;
objectMetadataId: string;
redundantName: string;
keptName: string;
};
export type IndexNameNormalizationOperation =
| RenameIndexNameOperation
| DropRedundantIndexOperation;
export const planIndexNameNormalization = (
indexStatuses: FlatIndexNameStatus[],
): IndexNameNormalizationOperation[] => {
const operations: IndexNameNormalizationOperation[] = [];
const statusesByExpectedName = new Map<string, FlatIndexNameStatus[]>();
for (const status of indexStatuses) {
const group = statusesByExpectedName.get(status.expectedName) ?? [];
group.push(status);
statusesByExpectedName.set(status.expectedName, group);
}
for (const [expectedName, group] of statusesByExpectedName) {
const survivor =
group.find((status) => status.currentName === expectedName) ?? group[0];
if (survivor.currentName !== expectedName) {
operations.push({
type: 'rename',
indexMetadataId: survivor.indexMetadataId,
objectMetadataId: survivor.objectMetadataId,
fromName: survivor.currentName,
toName: expectedName,
});
}
for (const status of group) {
if (status.indexMetadataId === survivor.indexMetadataId) {
continue;
}
operations.push({
type: 'dropRedundant',
indexMetadataId: status.indexMetadataId,
objectMetadataId: status.objectMetadataId,
redundantName: status.currentName,
keptName: expectedName,
});
}
}
return operations;
};
@@ -18,6 +18,7 @@ import { V2_14_UpgradeVersionCommandModule } from 'src/database/commands/upgrade
import { V2_15_UpgradeVersionCommandModule } from 'src/database/commands/upgrade-version-command/2-15/2-15-upgrade-version-command.module';
import { V2_16_UpgradeVersionCommandModule } from 'src/database/commands/upgrade-version-command/2-16/2-16-upgrade-version-command.module';
import { V2_17_UpgradeVersionCommandModule } from 'src/database/commands/upgrade-version-command/2-17/2-17-upgrade-version-command.module';
import { V2_18_UpgradeVersionCommandModule } from 'src/database/commands/upgrade-version-command/2-18/2-18-upgrade-version-command.module';
@Module({
imports: [
@@ -39,6 +40,7 @@ import { V2_17_UpgradeVersionCommandModule } from 'src/database/commands/upgrade
V2_15_UpgradeVersionCommandModule,
V2_16_UpgradeVersionCommandModule,
V2_17_UpgradeVersionCommandModule,
V2_18_UpgradeVersionCommandModule,
],
})
export class WorkspaceCommandProviderModule {}
@@ -90,4 +90,20 @@ export class WorkspaceSchemaIndexManagerService {
await queryRunner.query(sql);
}
async renameIndexWithoutRebuild({
queryRunner,
schemaName,
fromIndexName,
toIndexName,
}: {
queryRunner: QueryRunner;
schemaName: string;
fromIndexName: string;
toIndexName: string;
}): Promise<void> {
const sql = `ALTER INDEX ${escapeIdentifier(schemaName)}.${escapeIdentifier(fromIndexName)} RENAME TO ${escapeIdentifier(toIndexName)}`;
await queryRunner.query(sql);
}
}