From 3d00dd4066edccbee3a6b4c80e79e156b5f2374e Mon Sep 17 00:00:00 2001 From: Paul Rastoin <45004772+prastoin@users.noreply.github.com> Date: Tue, 30 Jun 2026 14:48:26 +0200 Subject: [PATCH] feat(server): add 2.18 recompute-search-vectors upgrade command (#22355) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary closes https://github.com/twentyhq/core-team-issues/issues/2620 Adds the 2.18 `recompute-search-vectors` workspace upgrade command — Part 2 of https://github.com/twentyhq/core-team-issues/issues/2620 (Part 1, the GIN-index rebuild fix, merged in #22349). It uniformizes every workspace's `TS_VECTOR` (`searchVector`) columns onto the new derive-from-`searchFieldMetadata` model and drops the now-dead cached settings: - **Recomputes** every searchVector column (re-derives its generated expression from the `searchFieldMetadata` rows) and **recreates its GIN index** — relying on the Part 1 rebuild fix. - **Clears** the deprecated cached `TS_VECTOR` settings (`asExpression` / `generatedType`), which nothing reads anymore. ## How New command `RecomputeSearchVectorsCommand` (`@RegisteredWorkspaceCommand('2.18.0', 1799200001000)`), per active/suspended workspace: 1. Load `flatFieldMetadataMaps`, enumerate every `FieldMetadataType.TS_VECTOR` field. Skip (log) if none. 2. Support `--dry-run` (log the count, no writes). 3. Build one `update-field` action per TS_VECTOR field and run them in a **single migration** via `workspaceMigrationRunnerService.run(...)`: ```ts update: { universalSettings: null }, // clears the deprecated cached settings rebuildSearchVector: true, // re-derive column + recreate GIN index ``` `universalSettings: null` transpiles to `settings: null`, so one atomic action both clears settings and triggers the rebuild; `runner.run` invalidates the field-metadata cache afterward. ## Why these choices - **Single migration under the standard app** covers standard, custom, and installed-app search vectors at once — the runner operates per workspace-schema table regardless of a field's owning application, and the update-field handler doesn't use `flatApplication`. - **Ordering is safe**: the upgrade sequence runs fast-instance → slow-instance → workspace commands per version, so the 2.18 `tsVectorFieldMetadataId` backfill (which the expression derivation depends on) is guaranteed to have run first. - **Cost**: this is a deliberate full rebuild — it drops/re-adds every searchVector STORED column (table rewrite per searchable object) and recreates each GIN index, per workspace, under the workspace iterator. Intentional ("uniformize for everyone"), as noted in the issue. ## Test plan - [x] `npx nx typecheck twenty-server` - [x] `npx nx lint:diff-with-main twenty-server` Closes Part 2 of https://github.com/twentyhq/core-team-issues/issues/2620 Review in cubic --- .../2-18-upgrade-version-command.module.ts | 9 +- ...001000-recompute-search-vectors.command.ts | 90 +++++++++++++++++++ 2 files changed, 98 insertions(+), 1 deletion(-) create mode 100644 packages/twenty-server/src/database/commands/upgrade-version-command/2-18/2-18-workspace-command-1799200001000-recompute-search-vectors.command.ts diff --git a/packages/twenty-server/src/database/commands/upgrade-version-command/2-18/2-18-upgrade-version-command.module.ts b/packages/twenty-server/src/database/commands/upgrade-version-command/2-18/2-18-upgrade-version-command.module.ts index 515548774f..a184edc960 100644 --- a/packages/twenty-server/src/database/commands/upgrade-version-command/2-18/2-18-upgrade-version-command.module.ts +++ b/packages/twenty-server/src/database/commands/upgrade-version-command/2-18/2-18-upgrade-version-command.module.ts @@ -3,10 +3,12 @@ import { Module } from '@nestjs/common'; import { WorkspaceIteratorModule } from 'src/database/commands/command-runners/workspace-iterator.module'; import { AddMessageIsDraftFieldCommand } from 'src/database/commands/upgrade-version-command/2-18/2-18-workspace-command-1810000005000-add-message-is-draft-field.command'; import { NormalizeLegacyIndexNamesCommand } from 'src/database/commands/upgrade-version-command/2-18/2-18-workspace-command-1799200000000-normalize-legacy-index-names.command'; +import { RecomputeSearchVectorsCommand } from 'src/database/commands/upgrade-version-command/2-18/2-18-workspace-command-1799200001000-recompute-search-vectors.command'; import { ApplicationModule } from 'src/engine/core-modules/application/application.module'; import { WorkspaceSchemaManagerModule } from 'src/engine/twenty-orm/workspace-schema-manager/workspace-schema-manager.module'; import { WorkspaceCacheModule } from 'src/engine/workspace-cache/workspace-cache.module'; import { WorkspaceMigrationModule } from 'src/engine/workspace-manager/workspace-migration/workspace-migration.module'; +import { WorkspaceMigrationRunnerModule } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/workspace-migration-runner.module'; @Module({ imports: [ @@ -15,7 +17,12 @@ import { WorkspaceMigrationModule } from 'src/engine/workspace-manager/workspace WorkspaceIteratorModule, WorkspaceMigrationModule, WorkspaceSchemaManagerModule, + WorkspaceMigrationRunnerModule, + ], + providers: [ + AddMessageIsDraftFieldCommand, + NormalizeLegacyIndexNamesCommand, + RecomputeSearchVectorsCommand, ], - providers: [AddMessageIsDraftFieldCommand, NormalizeLegacyIndexNamesCommand], }) export class V2_18_UpgradeVersionCommandModule {} diff --git a/packages/twenty-server/src/database/commands/upgrade-version-command/2-18/2-18-workspace-command-1799200001000-recompute-search-vectors.command.ts b/packages/twenty-server/src/database/commands/upgrade-version-command/2-18/2-18-workspace-command-1799200001000-recompute-search-vectors.command.ts new file mode 100644 index 0000000000..3099400d74 --- /dev/null +++ b/packages/twenty-server/src/database/commands/upgrade-version-command/2-18/2-18-workspace-command-1799200001000-recompute-search-vectors.command.ts @@ -0,0 +1,90 @@ +import { Command } from 'nest-commander'; +import { FieldMetadataType } from 'twenty-shared/types'; +import { isDefined } from 'twenty-shared/utils'; + +import { ActiveOrSuspendedWorkspaceCommandRunner } from 'src/database/commands/command-runners/active-or-suspended-workspace.command-runner'; +import { WorkspaceIteratorService } from 'src/database/commands/command-runners/workspace-iterator.service'; +import { type RunOnWorkspaceArgs } from 'src/database/commands/command-runners/workspace.command-runner'; +import { RegisteredWorkspaceCommand } from 'src/engine/core-modules/upgrade/decorators/registered-workspace-command.decorator'; +import { TWENTY_STANDARD_APPLICATION } from 'src/engine/workspace-manager/twenty-standard-application/constants/twenty-standard-applications'; +import { WorkspaceCacheService } from 'src/engine/workspace-cache/services/workspace-cache.service'; +import { type UniversalUpdateFieldAction } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/field/types/workspace-migration-field-action'; +import { WORKSPACE_MIGRATION_ACTION_TYPE } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/constants/workspace-migration-action-type.constant'; +import { WorkspaceMigrationRunnerService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/services/workspace-migration-runner.service'; + +@RegisteredWorkspaceCommand('2.18.0', 1799200001000) +@Command({ + name: 'upgrade:2-18:recompute-search-vectors', + description: + 'Recompute every TS_VECTOR (searchVector) column from its searchFieldMetadata rows, recreate its GIN index, and clear the deprecated cached TS_VECTOR settings. Idempotent.', +}) +export class RecomputeSearchVectorsCommand extends ActiveOrSuspendedWorkspaceCommandRunner { + constructor( + protected readonly workspaceIteratorService: WorkspaceIteratorService, + private readonly workspaceCacheService: WorkspaceCacheService, + private readonly workspaceMigrationRunnerService: WorkspaceMigrationRunnerService, + ) { + super(workspaceIteratorService); + } + + override async runOnWorkspace({ + workspaceId, + options, + }: RunOnWorkspaceArgs): Promise { + const isDryRun = options.dryRun ?? false; + + const { flatFieldMetadataMaps } = + await this.workspaceCacheService.getOrRecompute(workspaceId, [ + 'flatFieldMetadataMaps', + ]); + + const tsVectorFlatFieldMetadatas = Object.values( + flatFieldMetadataMaps.byUniversalIdentifier, + ) + .filter(isDefined) + .filter( + (flatFieldMetadata) => + flatFieldMetadata.type === FieldMetadataType.TS_VECTOR, + ); + + if (tsVectorFlatFieldMetadatas.length === 0) { + this.logger.log( + `No TS_VECTOR fields for workspace ${workspaceId}, skipping`, + ); + + return; + } + + this.logger.log( + `${isDryRun ? '[DRY RUN] ' : ''}Recomputing ${tsVectorFlatFieldMetadatas.length} search vector(s) and clearing their settings for workspace ${workspaceId}`, + ); + + if (isDryRun) { + return; + } + + const actions: UniversalUpdateFieldAction[] = tsVectorFlatFieldMetadatas.map( + (flatFieldMetadata) => ({ + type: WORKSPACE_MIGRATION_ACTION_TYPE.update, + metadataName: 'fieldMetadata', + universalIdentifier: flatFieldMetadata.universalIdentifier, + update: { universalSettings: null }, + rebuildSearchVector: true, + }), + ); + + await this.workspaceMigrationRunnerService.run({ + workspaceMigration: { + // Cross-app actions; this is only the runner's existence gate, not a scope filter. + applicationUniversalIdentifier: + TWENTY_STANDARD_APPLICATION.universalIdentifier, + actions, + }, + workspaceId, + }); + + this.logger.log( + `Successfully recomputed ${tsVectorFlatFieldMetadatas.length} search vector(s) for workspace ${workspaceId}`, + ); + } +}