Compare commits

...
Author SHA1 Message Date
Sonarly Claude Code a8d37db19c fix: add indexes on timelineActivity FK columns for custom object morph relations
https://sonarly.com/issue/16678?type=bug

The `timelineActivity` table lacks database indexes on foreign key columns created for custom object morph relations, causing a 3,491ms sequential scan when loading a single custom object record's timeline activities.

Fix: **What changed:** When creating a custom object, Twenty automatically creates default relations to standard objects like `timelineActivity`, `noteTarget`, `taskTarget`, `attachment`, and `favorite`. The function `generateMorphOrRelationFlatFieldMetadataPair` already generates both the relation field metadata AND a BTREE index on the FK join column for the MANY_TO_ONE side. However, in `buildDefaultRelationFlatFieldMetadatasForCustomObject`, the destructuring `const { flatFieldMetadatas } = generateMorphOrRelationFlatFieldMetadataPair(...)` was silently discarding the `indexMetadatas` return value.

**The fix** (2 files, ~10 lines changed):

1. **`build-default-relation-flat-field-metadatas-for-custom-object.util.ts`**: Added `standardRelationIndexMetadatas` to the accumulator record type. Now destructures `indexMetadatas` alongside `flatFieldMetadatas` and accumulates them across all default relation objects.

2. **`from-create-object-input-to-flat-object-metadata-and-flat-field-metadatas-to-create.util.ts`**: Spreads the new `standardRelationIndexMetadatas` into `flatIndexMetadataToCreate` alongside the existing search vector GIN index.

**Why this is the right fix:** The index generation logic was already correct in `generateMorphOrRelationFlatFieldMetadataPair` (used successfully for user-created relation fields). The bug was purely that the custom object creation pipeline discarded the generated indexes. This was explicitly noted as a TODO in commit `fbd3286792` ("Index v2 side effects"): *"Will handle morph indexes in a new dedicated PR."*

**Note for existing workspaces:** This fix only affects NEW custom object creation. Existing workspaces with custom objects will still have missing indexes on their standard object tables. A one-time migration or `workspace:sync-metadata` run would be needed to backfill those indexes for existing custom objects.
2026-03-19 20:04:22 +00:00
2 changed files with 13 additions and 4 deletions
@@ -104,6 +104,7 @@ export const fromCreateObjectInputToFlatObjectMetadataAndFlatFieldMetadatasToCre
const {
standardSourceFlatFieldMetadatas,
standardTargetFlatFieldMetadatas,
standardRelationIndexMetadatas,
} = buildDefaultRelationFlatFieldMetadatasForCustomObject({
existingFlatObjectMetadataMaps,
sourceFlatObjectMetadata: universalFlatObjectMetadataToCreate,
@@ -124,9 +125,10 @@ export const fromCreateObjectInputToFlatObjectMetadataAndFlatFieldMetadatasToCre
return {
flatObjectMetadataToCreate: universalFlatObjectMetadataToCreate,
flatIndexMetadataToCreate: Object.values(
defaultIndexesForCustomObject.indexes,
),
flatIndexMetadataToCreate: [
...Object.values(defaultIndexesForCustomObject.indexes),
...standardRelationIndexMetadatas,
],
relationTargetFlatFieldMetadataToCreate: standardTargetFlatFieldMetadatas,
flatFieldMetadataToCreateOnObject: objectFlatFieldMetadatas,
};
@@ -20,6 +20,7 @@ import {
} from 'src/engine/metadata-modules/object-metadata/object-metadata.exception';
import { STANDARD_OBJECT_ICONS } from 'src/engine/workspace-manager/workspace-migration/constant/standard-object-icons';
import { type UniversalFlatFieldMetadata } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-field-metadata.type';
import { type UniversalFlatIndexMetadata } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-index-metadata.type';
import { type UniversalFlatObjectMetadata } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-object-metadata.type';
const morphIdByRelationObjectNameSingular = {
@@ -50,11 +51,13 @@ export type BuildDefaultRelationFieldsForCustomObjectArgs = {
type SourceAndTargetFlatFieldMetadatasRecord = {
standardSourceFlatFieldMetadatas: UniversalFlatFieldMetadata[];
standardTargetFlatFieldMetadatas: UniversalFlatFieldMetadata[];
standardRelationIndexMetadatas: UniversalFlatIndexMetadata[];
};
const EMPTY_SOURCE_AND_TARGET_FLAT_FIELD_METADATAS_RECORD: SourceAndTargetFlatFieldMetadatasRecord =
{
standardSourceFlatFieldMetadatas: [],
standardTargetFlatFieldMetadatas: [],
standardRelationIndexMetadatas: [],
};
export const buildDefaultRelationFlatFieldMetadatasForCustomObject = ({
@@ -127,7 +130,7 @@ export const buildDefaultRelationFlatFieldMetadatasForCustomObject = ({
const morphId =
morphIdByRelationObjectNameSingular[objectMetadataNameSingular];
const { flatFieldMetadatas } =
const { flatFieldMetadatas, indexMetadatas } =
generateMorphOrRelationFlatFieldMetadataPair({
sourceFlatObjectMetadata,
targetFlatObjectMetadata,
@@ -164,6 +167,10 @@ export const buildDefaultRelationFlatFieldMetadatasForCustomObject = ({
...sourceAndTargetFlatFieldMetadatasRecord.standardTargetFlatFieldMetadatas,
flatFieldMetadatas[1],
],
standardRelationIndexMetadatas: [
...sourceAndTargetFlatFieldMetadatasRecord.standardRelationIndexMetadatas,
...indexMetadatas,
],
};
},
EMPTY_SOURCE_AND_TARGET_FLAT_FIELD_METADATAS_RECORD,