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.
This commit is contained in:
Charles Bochet
2026-05-14 18:30:38 +02:00
committed by GitHub
parent 5a1d3841f4
commit 78b3092886

View File

@@ -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({