From 2d4f6f8f6f822b24e4245bbd80150e1aa16ec372 Mon Sep 17 00:00:00 2001 From: Charles Bochet Date: Tue, 17 Mar 2026 23:57:40 +0100 Subject: [PATCH] fix: make 1.19 upgrade commands resilient to pre-existing data (#18716) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 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) --- ...stem-fields-to-standard-objects.command.ts | 122 ++++++++++++++---- ...backfill-missing-standard-views.command.ts | 9 +- 2 files changed, 103 insertions(+), 28 deletions(-) diff --git a/packages/twenty-server/src/database/commands/upgrade-version-command/1-19/1-19-add-missing-system-fields-to-standard-objects.command.ts b/packages/twenty-server/src/database/commands/upgrade-version-command/1-19/1-19-add-missing-system-fields-to-standard-objects.command.ts index 9606b4794a..3a9a89504f 100644 --- a/packages/twenty-server/src/database/commands/upgrade-version-command/1-19/1-19-add-missing-system-fields-to-standard-objects.command.ts +++ b/packages/twenty-server/src/database/commands/upgrade-version-command/1-19/1-19-add-missing-system-fields-to-standard-objects.command.ts @@ -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 { + 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}`, diff --git a/packages/twenty-server/src/database/commands/upgrade-version-command/1-19/1-19-backfill-missing-standard-views.command.ts b/packages/twenty-server/src/database/commands/upgrade-version-command/1-19/1-19-backfill-missing-standard-views.command.ts index b7f6da20e9..5542f04348 100644 --- a/packages/twenty-server/src/database/commands/upgrade-version-command/1-19/1-19-backfill-missing-standard-views.command.ts +++ b/packages/twenty-server/src/database/commands/upgrade-version-command/1-19/1-19-backfill-missing-standard-views.command.ts @@ -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(