From 3e53a16b2701bffbf4c6e2b0920c526045697916 Mon Sep 17 00:00:00 2001
From: Paul Rastoin <45004772+prastoin@users.noreply.github.com>
Date: Tue, 30 Jun 2026 14:03:30 +0200
Subject: [PATCH] Clear orphan search field metadata backfill
tsVectorFieldMetadataId (#22353)
Instead of invariant throw in instance slow, auto recover by deleting
orphan search field metadata as in the end they would just end up as
dead metadata
---
...ld-metadata-id-on-search-field-metadata.ts | 30 +++++++++++++------
1 file changed, 21 insertions(+), 9 deletions(-)
diff --git a/packages/twenty-server/src/database/commands/upgrade-version-command/2-18/2-18-instance-command-slow-1810000003000-backfill-ts-vector-field-metadata-id-on-search-field-metadata.ts b/packages/twenty-server/src/database/commands/upgrade-version-command/2-18/2-18-instance-command-slow-1810000003000-backfill-ts-vector-field-metadata-id-on-search-field-metadata.ts
index ec627aa8c3..9664063ac0 100644
--- a/packages/twenty-server/src/database/commands/upgrade-version-command/2-18/2-18-instance-command-slow-1810000003000-backfill-ts-vector-field-metadata-id-on-search-field-metadata.ts
+++ b/packages/twenty-server/src/database/commands/upgrade-version-command/2-18/2-18-instance-command-slow-1810000003000-backfill-ts-vector-field-metadata-id-on-search-field-metadata.ts
@@ -1,3 +1,5 @@
+import { Logger } from '@nestjs/common';
+
import { DataSource, QueryRunner } from 'typeorm';
import { RegisteredInstanceCommand } from 'src/engine/core-modules/upgrade/decorators/registered-instance-command.decorator';
@@ -6,12 +8,18 @@ import { SlowInstanceCommand } from 'src/engine/core-modules/upgrade/interfaces/
// Backfills the tsVectorFieldMetadataId FK by pointing every existing
// searchFieldMetadata row at its object's system searchVector (TS_VECTOR) field,
// then enforces NOT NULL. Runs after the fast command that adds the column (and makes
-// the FKs deferrable). Any row left without a resolvable searchVector
-// aborts the upgrade rather than silently relaxing the NOT NULL guarantee.
+// the FKs deferrable). Rows left without a resolvable searchVector are orphans: they
+// index into a TS_VECTOR column that no longer exists (e.g. the object's searchVector
+// field was deleted while these rows survived, since their FK only cascades on the
+// indexed field). They are dead data, so we delete them rather than abort the upgrade.
@RegisteredInstanceCommand('2.18.0', 1810000003000, { type: 'slow' })
export class BackfillTsVectorFieldMetadataIdOnSearchFieldMetadataSlowInstanceCommand
implements SlowInstanceCommand
{
+ private readonly logger = new Logger(
+ BackfillTsVectorFieldMetadataIdOnSearchFieldMetadataSlowInstanceCommand.name,
+ );
+
async runDataMigration(dataSource: DataSource): Promise {
await dataSource.query(
`UPDATE "core"."searchFieldMetadata" "searchFieldMetadata"
@@ -23,16 +31,20 @@ export class BackfillTsVectorFieldMetadataIdOnSearchFieldMetadataSlowInstanceCom
AND "searchFieldMetadata"."tsVectorFieldMetadataId" IS NULL`,
);
- const unresolvedRows: { count: string }[] = await dataSource.query(
- `SELECT COUNT(*) AS "count" FROM "core"."searchFieldMetadata"
- WHERE "tsVectorFieldMetadataId" IS NULL`,
+ const deletedRows: { count: string }[] = await dataSource.query(
+ `WITH "deleted" AS (
+ DELETE FROM "core"."searchFieldMetadata"
+ WHERE "tsVectorFieldMetadataId" IS NULL
+ RETURNING "id"
+ )
+ SELECT COUNT(*) AS "count" FROM "deleted"`,
);
- const unresolvedCount = Number(unresolvedRows[0]?.count ?? 0);
+ const deletedCount = Number(deletedRows[0]?.count ?? 0);
- if (unresolvedCount > 0) {
- throw new Error(
- `Cannot enforce searchFieldMetadata.tsVectorFieldMetadataId NOT NULL: ${unresolvedCount} row(s) reference an object without a searchVector (TS_VECTOR) field`,
+ if (deletedCount > 0) {
+ this.logger.warn(
+ `Deleted ${deletedCount} orphaned searchFieldMetadata row(s) referencing an object without a searchVector (TS_VECTOR) field`,
);
}
}