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 Review in cubic --- ...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`, ); } }