From d6526af30211335feb6b268c96692dfeec4167cb Mon Sep 17 00:00:00 2001 From: "Abdullah." <125115953+mabdullahabaid@users.noreply.github.com> Date: Sun, 12 Oct 2025 21:25:45 +0500 Subject: [PATCH] Move search vector command from 1-7 to 1-10 and make it less verbose. (#14892) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Made the command more explicit for idempotency. - Drop index explicitly (even though PostgreSQL does it automatically when a column is dropped, but it might be good practice to account for any unforeseen failures). - Drop column. - Create column. - Create index (if we reach this point, column has already been created and no error was thrown). Figured we do not need extra queries to check ObjectMetadata for the existence of person table on a workspace - we can use the IF EXISTS syntax instead. --------- Co-authored-by: Félix Malfait --- ...erson-search-vector-with-phones.command.ts | 170 ++++++++++++------ .../1-10-upgrade-version-command.module.ts | 4 +- .../1-7/1-7-upgrade-version-command.module.ts | 3 +- 3 files changed, 116 insertions(+), 61 deletions(-) diff --git a/packages/twenty-server/src/database/commands/upgrade-version-command/1-10/1-10-regenerate-person-search-vector-with-phones.command.ts b/packages/twenty-server/src/database/commands/upgrade-version-command/1-10/1-10-regenerate-person-search-vector-with-phones.command.ts index bc5878d10e..3fda227939 100644 --- a/packages/twenty-server/src/database/commands/upgrade-version-command/1-10/1-10-regenerate-person-search-vector-with-phones.command.ts +++ b/packages/twenty-server/src/database/commands/upgrade-version-command/1-10/1-10-regenerate-person-search-vector-with-phones.command.ts @@ -1,7 +1,7 @@ import { InjectDataSource, InjectRepository } from '@nestjs/typeorm'; import { Command } from 'nest-commander'; -import { DataSource, Repository } from 'typeorm'; +import { DataSource, Repository, type QueryRunner } from 'typeorm'; import { ActiveOrSuspendedWorkspacesMigrationCommandRunner, @@ -9,6 +9,7 @@ import { } from 'src/database/commands/command-runners/active-or-suspended-workspaces-migration.command-runner'; import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity'; import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager'; +import { WorkspaceSchemaManagerService } from 'src/engine/twenty-orm/workspace-schema-manager/workspace-schema-manager.service'; import { getWorkspaceSchemaName } from 'src/engine/workspace-datasource/utils/get-workspace-schema-name.util'; import { getTsVectorColumnExpressionFromFields } from 'src/engine/workspace-manager/workspace-sync-metadata/utils/get-ts-vector-column-expression.util'; import { SEARCH_FIELDS_FOR_PERSON } from 'src/modules/person/standard-objects/person.workspace-entity'; @@ -25,94 +26,149 @@ export class RegeneratePersonSearchVectorWithPhonesCommand extends ActiveOrSuspe @InjectDataSource() private readonly coreDataSource: DataSource, protected readonly twentyORMGlobalManager: TwentyORMGlobalManager, + private readonly workspaceSchemaManager: WorkspaceSchemaManagerService, ) { super(workspaceRepository, twentyORMGlobalManager); } override async runOnWorkspace({ workspaceId, + options, }: RunOnWorkspaceArgs): Promise { - const schemaName = getWorkspaceSchemaName(workspaceId); + await this.ensureUnaccentFunction(); this.logger.log( - `Regenerating person search vector for workspace ${workspaceId} in schema ${schemaName}`, + `Regenerating person search vector for workspace ${workspaceId}`, ); - const personTableExists = await this.coreDataSource.query(` - SELECT EXISTS ( - SELECT FROM information_schema.tables - WHERE table_schema = '${schemaName}' - AND table_name = 'person' - ); - `); + const queryRunner = this.coreDataSource.createQueryRunner(); - if (!personTableExists[0]?.exists) { - this.logger.log( - `Person table does not exist in workspace ${workspaceId}, skipping`, - ); - - return; - } - - const searchVectorColumnExists = await this.coreDataSource.query(` - SELECT EXISTS ( - SELECT FROM information_schema.columns - WHERE table_schema = '${schemaName}' - AND table_name = 'person' - AND column_name = 'searchVector' - ); - `); - - if (!searchVectorColumnExists[0]?.exists) { - this.logger.log( - `searchVector column does not exist in workspace ${workspaceId}, skipping`, - ); - - return; - } + await queryRunner.connect(); try { + const schemaName = getWorkspaceSchemaName(workspaceId); + const newSearchVectorExpression = getTsVectorColumnExpressionFromFields( SEARCH_FIELDS_FOR_PERSON, ); - this.logger.log( - `Dropping existing searchVector column for workspace ${workspaceId}`, + const isDryRun = Boolean(options.dryRun); + + await queryRunner.startTransaction(); + + await this.applySearchVectorChanges( + schemaName, + workspaceId, + newSearchVectorExpression, + queryRunner, ); - await this.coreDataSource.query(` - ALTER TABLE "${schemaName}"."person" - DROP COLUMN "searchVector" - `); + if (isDryRun) { + await queryRunner.rollbackTransaction(); - this.logger.log( - `Creating new searchVector column with phone indexing for workspace ${workspaceId}`, - ); + this.logger.log( + `DRY RUN: Would regenerate person search vector for workspace ${workspaceId}`, + ); - await this.coreDataSource.query(` - ALTER TABLE "${schemaName}"."person" - ADD COLUMN "searchVector" tsvector - GENERATED ALWAYS AS (${newSearchVectorExpression}) STORED - `); + return; + } - this.logger.log( - `Recreating GIN index on searchVector for workspace ${workspaceId}`, - ); - - await this.coreDataSource.query(` - CREATE INDEX "IDX_person_searchVector" - ON "${schemaName}"."person" - USING GIN ("searchVector") - `); + await queryRunner.commitTransaction(); this.logger.log( `Successfully regenerated person search vector for workspace ${workspaceId}`, ); } catch (error) { + if (queryRunner.isTransactionActive) { + await queryRunner.rollbackTransaction(); + } + this.logger.error( `Failed to regenerate person search vector for workspace ${workspaceId}: ${error.message}`, ); + throw error; + } finally { + await queryRunner.release(); + } + } + + private async applySearchVectorChanges( + schemaName: string, + workspaceId: string, + newSearchVectorExpression: string, + queryRunner: QueryRunner, + ): Promise { + this.logger.log( + `Dropping existing searchVector index for workspace ${workspaceId}`, + ); + + await this.workspaceSchemaManager.indexManager.dropIndex({ + queryRunner, + schemaName, + indexName: 'IDX_person_searchVector', + }); + + this.logger.log( + `Dropping existing searchVector column for workspace ${workspaceId}`, + ); + + await this.workspaceSchemaManager.columnManager.dropColumns({ + queryRunner, + schemaName, + tableName: 'person', + columnNames: ['searchVector'], + }); + + this.logger.log( + `Creating new searchVector column with phone indexing for workspace ${workspaceId}`, + ); + + await this.workspaceSchemaManager.columnManager.addColumns({ + queryRunner, + schemaName, + tableName: 'person', + columnDefinitions: [ + { + name: 'searchVector', + type: 'tsvector', + isNullable: true, + asExpression: newSearchVectorExpression, + generatedType: 'STORED', + }, + ], + }); + + this.logger.log( + `Recreating GIN index on searchVector for workspace ${workspaceId}`, + ); + + await this.workspaceSchemaManager.indexManager.createIndex({ + queryRunner, + schemaName, + tableName: 'person', + index: { + name: 'IDX_person_searchVector', + columns: ['searchVector'], + type: 'GIN', + }, + }); + } + + private async ensureUnaccentFunction(): Promise { + const result = await this.coreDataSource.query(` + SELECT EXISTS ( + SELECT 1 FROM pg_proc p + JOIN pg_namespace n ON p.pronamespace = n.oid + WHERE n.nspname = 'public' + AND p.proname = 'unaccent_immutable' + ) as function_exists + `); + + if (!result[0]?.function_exists) { + throw new Error( + 'The public.unaccent_immutable() function is required but not found. Please run database migrations first.', + ); } } } diff --git a/packages/twenty-server/src/database/commands/upgrade-version-command/1-10/1-10-upgrade-version-command.module.ts b/packages/twenty-server/src/database/commands/upgrade-version-command/1-10/1-10-upgrade-version-command.module.ts index 2a29911057..4cd0cc4988 100644 --- a/packages/twenty-server/src/database/commands/upgrade-version-command/1-10/1-10-upgrade-version-command.module.ts +++ b/packages/twenty-server/src/database/commands/upgrade-version-command/1-10/1-10-upgrade-version-command.module.ts @@ -11,7 +11,7 @@ import { IndexMetadataEntity } from 'src/engine/metadata-modules/index-metadata/ import { IndexMetadataModule } from 'src/engine/metadata-modules/index-metadata/index-metadata.module'; import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; import { WorkspaceMigrationModule } from 'src/engine/metadata-modules/workspace-migration/workspace-migration.module'; -import { WorkspaceDataSourceModule } from 'src/engine/workspace-datasource/workspace-datasource.module'; +import { WorkspaceSchemaManagerModule } from 'src/engine/twenty-orm/workspace-schema-manager/workspace-schema-manager.module'; import { WorkspaceMigrationRunnerModule } from 'src/engine/workspace-manager/workspace-migration-runner/workspace-migration-runner.module'; @Module({ @@ -22,10 +22,10 @@ import { WorkspaceMigrationRunnerModule } from 'src/engine/workspace-manager/wor ObjectMetadataEntity, IndexMetadataEntity, ]), - WorkspaceDataSourceModule, IndexMetadataModule, WorkspaceMigrationRunnerModule, WorkspaceMigrationModule, + WorkspaceSchemaManagerModule, ], providers: [ MigrateWorkflowStepFilterOperandValueCommand, diff --git a/packages/twenty-server/src/database/commands/upgrade-version-command/1-7/1-7-upgrade-version-command.module.ts b/packages/twenty-server/src/database/commands/upgrade-version-command/1-7/1-7-upgrade-version-command.module.ts index 7a56ecf139..5fcd03dffe 100644 --- a/packages/twenty-server/src/database/commands/upgrade-version-command/1-7/1-7-upgrade-version-command.module.ts +++ b/packages/twenty-server/src/database/commands/upgrade-version-command/1-7/1-7-upgrade-version-command.module.ts @@ -3,10 +3,9 @@ import { TypeOrmModule } from '@nestjs/typeorm'; import { BackfillWorkflowManualTriggerAvailabilityCommand } from 'src/database/commands/upgrade-version-command/1-7/1-7-backfill-workflow-manual-trigger-availability.command'; import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity'; -import { WorkspaceDataSourceModule } from 'src/engine/workspace-datasource/workspace-datasource.module'; @Module({ - imports: [TypeOrmModule.forFeature([Workspace]), WorkspaceDataSourceModule], + imports: [TypeOrmModule.forFeature([Workspace])], providers: [BackfillWorkflowManualTriggerAvailabilityCommand], exports: [BackfillWorkflowManualTriggerAvailabilityCommand], })