Move search vector command from 1-7 to 1-10 and make it less verbose. (#14892)
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 <felix.malfait@gmail.com>
This commit is contained in:
+113
-57
@@ -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<void> {
|
||||
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<void> {
|
||||
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<void> {
|
||||
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.',
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+2
-2
@@ -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,
|
||||
|
||||
+1
-2
@@ -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],
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user