From 78b30928869df32cd15feb8b93a055984f596e46 Mon Sep 17 00:00:00 2001 From: Charles Bochet Date: Thu, 14 May 2026 18:30:38 +0200 Subject: [PATCH] fix(server): batch upgrade migration inserts to stay under PG param limit (#20588) ## Summary Prod deploy of v2.5.0 fails with a query failure inserting into `core.upgradeMigration`: ``` query failed: INSERT INTO "core"."upgradeMigration" ("id", "name", "status", "attempt", "executedByVersion", "errorMessage", "isInitial", "workspaceId", "createdAt") VALUES (DEFAULT, $1, $2, $3, $4, $5, DEFAULT, $6, DEFAULT), (DEFAULT, $7, $8, $9, $10, $11, DEFAULT, $12, DEFAULT), ... (continues past $2515) ... ``` ### Root cause `UpgradeMigrationService.recordUpgradeMigration` writes one row per workspace via a single `repository.save([...rows])` call. `UpgradeMigrationEntity` has **6 user-provided columns** per row (`name`, `status`, `attempt`, `executedByVersion`, `errorMessage`, `workspaceId`), so the multi-row INSERT binds `6 * (1 + N_workspaces)` parameters. Postgres' wire protocol caps a single statement at **65,535 bind parameters** (16-bit count). That gives a hard ceiling of ~10,920 rows per call. Production has enough workspaces to overflow. --- .../services/upgrade-migration.service.ts | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/packages/twenty-server/src/engine/core-modules/upgrade/services/upgrade-migration.service.ts b/packages/twenty-server/src/engine/core-modules/upgrade/services/upgrade-migration.service.ts index 2fd618d2b94..2077516ad9b 100644 --- a/packages/twenty-server/src/engine/core-modules/upgrade/services/upgrade-migration.service.ts +++ b/packages/twenty-server/src/engine/core-modules/upgrade/services/upgrade-migration.service.ts @@ -1,6 +1,7 @@ import { Injectable } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; +import chunk from 'lodash.chunk'; import { isDefined } from 'twenty-shared/utils'; import { In, IsNull, type QueryRunner, Repository } from 'typeorm'; @@ -21,6 +22,8 @@ export type WorkspaceLastAttemptedCommand = { isInitial: boolean; }; +const UPGRADE_MIGRATION_SAVE_BATCH_SIZE = 1000; + @Injectable() export class UpgradeMigrationService { constructor( @@ -95,7 +98,7 @@ export class UpgradeMigrationService { where: { name, workspaceId: IsNull() }, }); - await repository.save([ + const instanceRows = [ { name, status, @@ -112,7 +115,14 @@ export class UpgradeMigrationService { workspaceId, errorMessage, })), - ]); + ]; + + for (const batch of chunk( + instanceRows, + UPGRADE_MIGRATION_SAVE_BATCH_SIZE, + )) { + await repository.save(batch); + } return; } @@ -134,7 +144,9 @@ export class UpgradeMigrationService { }); } - await repository.save(rows); + for (const batch of chunk(rows, UPGRADE_MIGRATION_SAVE_BATCH_SIZE)) { + await repository.save(batch); + } } async markAsWorkspaceInitial({