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], })