fix(server): recreate searchVector GIN index on rebuild (#22349)
## Summary Fixes a pre-existing regression where rebuilding a `TS_VECTOR` (`searchVector`) generated column drops its GIN index without recreating it, leaving search correct but **unindexed** (sequential scan). Changing a generated column's expression requires `DROP COLUMN` + `ADD COLUMN` (Postgres can't `ALTER` a generated expression). The `searchVector`'s GIN index is a separate index-metadata entity built on that column, so the `DROP COLUMN` cascade-drops the physical index — and the rebuild branch never re-issued `CREATE INDEX`. This existed on `main` (triggered by `asExpression`/`generatedType` settings changes) and was inherited by the `rebuildSearchVector` refactor in #22287. This is the first, self-contained part of https://github.com/twentyhq/core-team-issues/issues/2620. The 2.18 recompute/backfill workspace command is intentionally left for a follow-up PR. ## What changed ### Runner loads the maps a rebuild needs `workspace-migration-runner.service.ts` — `fieldMetadata` declares neither `searchFieldMetadata` nor `index` as a related metadata name, so a `fieldMetadata`-only rebuild action had neither `flatSearchFieldMetadataMaps` (needed by the expression derivation) nor `flatIndexMaps` (needed to recreate the index) in context. The runner now detects `update` actions carrying `rebuildSearchVector === true` and loads those two maps — **only** when a rebuild is present, so ordinary field operations are unaffected. ### Handler recreates the index after re-adding the column `update-field-action-handler.service.ts` — in the rebuild branch, after `addColumns`, recreate the field's single GIN index: ```ts const [searchVectorFlatIndexMetadata] = findFieldRelatedIndexes({ flatFieldMetadata: optimisticFlatFieldMetadata, flatObjectMetadata, flatIndexMaps, }); if (isDefined(searchVectorFlatIndexMetadata)) { await createIndexInWorkspaceSchema({ flatIndexMetadata: searchVectorFlatIndexMetadata, ... }); } ``` - **Narrow lookup, not a workspace-wide scan.** The flat field has no index back-reference (`fieldMetadata.indexFieldMetadatas` is `null` in `ALL_ONE_TO_MANY_METADATA_RELATIONS`). The *object* does aggregate its indexes (`indexMetadataIds`), so we reuse the existing `findFieldRelatedIndexes` helper — already used by `handle-index-changes-during-field-update.util.ts` and the morph-rename path — which resolves only this object's indexes and filters to the one on the field. - A `TS_VECTOR` field has exactly one index (the standard `searchVectorGinIndex`), so we retrieve that single index rather than iterating. `createIndex` emits `CREATE INDEX IF NOT EXISTS` (idempotent). This makes the rebuild self-contained (column + index move together) and fixes every rebuild path: rename, label-identifier change, and `searchFieldMetadata` changes. ### Regression test Extends `update-one-field-metadata-search-vector-side-effect.integration-spec.ts` to query `pg_indexes` before and after the rename and assert the GIN index on the `searchVector` column persists (not just that search still returns the record). Fails without the fix, passes with it. ## Test plan - [x] `npx nx lint:diff-with-main twenty-server` — 0 warnings, 0 errors - [x] `npx nx typecheck twenty-server` — clean for changed files - [ ] Integration: extended rename-rebuild spec (GIN index present post-rebuild) Part of https://github.com/twentyhq/core-team-issues/issues/2620
This commit is contained in:
+20
@@ -9,6 +9,7 @@ import { isDefined } from 'twenty-shared/utils';
|
||||
import { ColumnType, type QueryRunner } from 'typeorm';
|
||||
|
||||
import { computeMorphOrRelationFieldJoinColumnName } from 'src/engine/metadata-modules/field-metadata/utils/compute-morph-or-relation-field-join-column-name.util';
|
||||
import { createIndexInWorkspaceSchema } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/index/utils/index-action-handler.utils';
|
||||
import { WorkspaceMigrationRunnerActionHandler } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/interfaces/workspace-migration-runner-action-handler-service.interface';
|
||||
|
||||
import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
|
||||
@@ -17,6 +18,7 @@ import { getCompositeTypeOrThrow } from 'src/engine/metadata-modules/field-metad
|
||||
import { findFlatEntityByIdInFlatEntityMapsOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps-or-throw.util';
|
||||
import { findManyFlatEntityByIdInFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/utils/find-many-flat-entity-by-id-in-flat-entity-maps.util';
|
||||
import { findFlatEntityByUniversalIdentifierOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-universal-identifier-or-throw.util';
|
||||
import { findFieldRelatedIndexes } from 'src/engine/metadata-modules/flat-field-metadata/utils/find-field-related-index.util';
|
||||
import { FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type';
|
||||
import { isCompositeFlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/utils/is-composite-flat-field-metadata.util';
|
||||
import { isEnumFlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/utils/is-enum-flat-field-metadata.util';
|
||||
@@ -168,6 +170,7 @@ export class UpdateFieldActionHandlerService extends WorkspaceMigrationRunnerAct
|
||||
flatObjectMetadataMaps,
|
||||
flatFieldMetadataMaps,
|
||||
flatSearchFieldMetadataMaps,
|
||||
flatIndexMaps,
|
||||
},
|
||||
workspaceId,
|
||||
} = context;
|
||||
@@ -368,6 +371,23 @@ export class UpdateFieldActionHandlerService extends WorkspaceMigrationRunnerAct
|
||||
tableName,
|
||||
columnDefinitions,
|
||||
});
|
||||
|
||||
const [searchVectorFlatIndexMetadata] = findFieldRelatedIndexes({
|
||||
flatFieldMetadata: optimisticFlatFieldMetadata,
|
||||
flatObjectMetadata,
|
||||
flatIndexMaps,
|
||||
});
|
||||
|
||||
if (isDefined(searchVectorFlatIndexMetadata)) {
|
||||
await createIndexInWorkspaceSchema({
|
||||
flatIndexMetadata: searchVectorFlatIndexMetadata,
|
||||
flatObjectMetadata,
|
||||
flatFieldMetadataMaps,
|
||||
workspaceSchemaManagerService: this.workspaceSchemaManagerService,
|
||||
queryRunner,
|
||||
workspaceId,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+12
@@ -230,6 +230,17 @@ export class WorkspaceMigrationRunnerService {
|
||||
const actionMetadataNames = [
|
||||
...new Set(actions.flatMap((action) => action.metadataName)),
|
||||
];
|
||||
|
||||
const hasSearchVectorRebuildAction = actions.some(
|
||||
(action) =>
|
||||
action.metadataName === 'fieldMetadata' &&
|
||||
action.type === 'update' &&
|
||||
action.rebuildSearchVector === true,
|
||||
);
|
||||
|
||||
const searchVectorRebuildMetadataNames: AllMetadataName[] =
|
||||
hasSearchVectorRebuildAction ? ['index'] : [];
|
||||
|
||||
const actionsMetadataAndRelatedMetadataNames: AllMetadataName[] = [
|
||||
...new Set([
|
||||
...actionMetadataNames,
|
||||
@@ -238,6 +249,7 @@ export class WorkspaceMigrationRunnerService {
|
||||
...actionMetadataNames.flatMap(
|
||||
getMetadataRelatedMetadataNamesForValidation,
|
||||
),
|
||||
...searchVectorRebuildMetadataNames,
|
||||
]),
|
||||
];
|
||||
const allFlatEntityMapsKeys = actionsMetadataAndRelatedMetadataNames.map(
|
||||
|
||||
Reference in New Issue
Block a user