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

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22353?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->
This commit is contained in:
Paul Rastoin
2026-06-30 14:03:30 +02:00
committed by GitHub
parent fe644a0630
commit 3e53a16b27
@@ -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<void> {
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`,
);
}
}