fix: make 1.19 upgrade commands resilient to pre-existing data (#18716)

## Summary

- **BackfillMissingStandardViewsCommand**: When view validation fails
(e.g. a viewField references a field metadata that doesn't exist in the
workspace), log a warning and skip instead of throwing — so the
workspace upgrade continues with the remaining commands.
- **AddMissingSystemFieldsToStandardObjectsCommand**: Wrap both the
non-tsVector batch migration and each individual tsVector migration in
try-catch. If a field already exists (e.g. duplicate key on `name +
objectMetadataId + workspaceId`), the error is logged as a warning and
the command moves on to the next field.

These errors were observed during the 1.18 → 1.19 production upgrade for
workspaces with non-standard state (missing "owner" field metadata on
Opportunity, or searchVector fields already present with a different
universalIdentifier).

## Test plan

- [ ] Re-run upgrade on the affected production workspaces
- [ ] Verify upgrade completes successfully with warnings instead of
failures
- [ ] Confirm that workspaces which were already upgrading cleanly are
unaffected


Made with [Cursor](https://cursor.com)
This commit is contained in:
Charles Bochet
2026-03-17 23:57:40 +01:00
committed by GitHub
parent a82739cdb1
commit 2d4f6f8f6f
2 changed files with 103 additions and 28 deletions
@@ -8,6 +8,7 @@ import { Repository } from 'typeorm';
import { ActiveOrSuspendedWorkspacesMigrationCommandRunner } from 'src/database/commands/command-runners/active-or-suspended-workspaces-migration.command-runner';
import { RunOnWorkspaceArgs } from 'src/database/commands/command-runners/workspaces-migration.command-runner';
import { ADD_MISSING_SYSTEM_FIELDS_TO_STANDARD_OBJECTS_1771420702241 } from 'src/database/commands/upgrade-version-command/workspace-migrations/1771420702241-add-missing-system-fields-to-standard-objects';
import { WorkspaceMigrationRunnerException } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/exceptions/workspace-migration-runner.exception';
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service';
import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager';
@@ -37,9 +38,44 @@ const TS_VECTOR_INDIVIDUAL_MIGRATIONS = allActions
} satisfies WorkspaceMigration,
}));
const NON_TS_VECTOR_INDIVIDUAL_MIGRATIONS = allActions
.filter((action) => action.flatEntity.type !== FieldMetadataType.TS_VECTOR)
.map((action) => ({
universalIdentifier: action.flatEntity.universalIdentifier,
fieldName: action.flatEntity.name,
objectIdentifier: action.flatEntity.objectMetadataUniversalIdentifier,
migration: {
applicationUniversalIdentifier,
actions: [action],
} satisfies WorkspaceMigration,
}));
const FIRST_NON_TS_VECTOR_UNIVERSAL_IDENTIFIER =
allActions[0].flatEntity.universalIdentifier;
const DUPLICATE_KEY_MESSAGE = 'duplicate key value violates unique constraint';
const isUniqueViolationError = (error: Error): boolean => {
if (error.message.includes(DUPLICATE_KEY_MESSAGE)) {
return true;
}
if (error instanceof WorkspaceMigrationRunnerException) {
const nestedErrors = [
error.errors?.metadata,
error.errors?.workspaceSchema,
error.errors?.actionTranspilation,
];
return nestedErrors.some(
(nestedError) =>
nestedError?.message?.includes(DUPLICATE_KEY_MESSAGE) === true,
);
}
return false;
};
@Command({
name: 'upgrade:1-19:add-missing-system-fields-to-standard-objects',
description:
@@ -57,6 +93,46 @@ export class AddMissingSystemFieldsToStandardObjectsCommand extends ActiveOrSusp
super(workspaceRepository, twentyORMGlobalManager, dataSourceService);
}
private async runIndividualMigrations(
workspaceId: string,
migrations: {
universalIdentifier: string;
fieldName: string;
objectIdentifier: string;
migration: WorkspaceMigration;
}[],
): Promise<void> {
for (const entry of migrations) {
const alreadyCreated = await this.hasFieldBeenCreated(
workspaceId,
entry.universalIdentifier,
);
if (alreadyCreated) {
continue;
}
this.logger.log(
`Adding field ${entry.fieldName} on object ${entry.objectIdentifier} in workspace ${workspaceId}`,
);
try {
await this.workspaceMigrationRunnerService.run({
workspaceId,
workspaceMigration: entry.migration,
});
} catch (error) {
if (!isUniqueViolationError(error)) {
throw error;
}
this.logger.warn(
`Field ${entry.fieldName} on object ${entry.objectIdentifier} already exists in workspace ${workspaceId}, skipping`,
);
}
}
}
private async hasFieldBeenCreated(
workspaceId: string,
universalIdentifier: string,
@@ -99,35 +175,35 @@ export class AddMissingSystemFieldsToStandardObjectsCommand extends ActiveOrSusp
`Adding ${NON_TS_VECTOR_MIGRATION.actions.length} non-tsVector fields (position, createdBy, updatedBy) in workspace ${workspaceId}`,
);
await this.workspaceMigrationRunnerService.run({
workspaceId,
workspaceMigration: NON_TS_VECTOR_MIGRATION,
});
try {
await this.workspaceMigrationRunnerService.run({
workspaceId,
workspaceMigration: NON_TS_VECTOR_MIGRATION,
});
} catch (error) {
if (!isUniqueViolationError(error)) {
throw error;
}
this.logger.warn(
`Batch non-tsVector migration hit a duplicate field in workspace ${workspaceId}, falling back to individual field migrations`,
);
await this.runIndividualMigrations(
workspaceId,
NON_TS_VECTOR_INDIVIDUAL_MIGRATIONS,
);
}
} else {
this.logger.log(
`Non-tsVector fields already exist in workspace ${workspaceId}, skipping.`,
);
}
for (const tsVectorEntry of TS_VECTOR_INDIVIDUAL_MIGRATIONS) {
const alreadyCreated = await this.hasFieldBeenCreated(
workspaceId,
tsVectorEntry.universalIdentifier,
);
if (alreadyCreated) {
continue;
}
this.logger.log(
`Adding tsVector field ${tsVectorEntry.fieldName} on object ${tsVectorEntry.objectIdentifier} in workspace ${workspaceId}`,
);
await this.workspaceMigrationRunnerService.run({
workspaceId,
workspaceMigration: tsVectorEntry.migration,
});
}
await this.runIndividualMigrations(
workspaceId,
TS_VECTOR_INDIVIDUAL_MIGRATIONS,
);
this.logger.log(
`Successfully added missing system fields to standard objects in workspace ${workspaceId}`,
@@ -171,12 +171,11 @@ export class BackfillMissingStandardViewsCommand extends ActiveOrSuspendedWorksp
);
if (validateAndBuildResult.status === 'fail') {
this.logger.error(
`Failed to backfill missing standard views:\n${JSON.stringify(validateAndBuildResult, null, 2)}`,
);
throw new Error(
`Failed to backfill missing standard views for workspace ${workspaceId}`,
this.logger.warn(
`Failed to backfill missing standard views for workspace ${workspaceId}, skipping:\n${JSON.stringify(validateAndBuildResult)}`,
);
return;
}
this.logger.log(