From c1b62334b75f60e7562a776d7da57f0d31c7cfd4 Mon Sep 17 00:00:00 2001 From: Thomas Trompette Date: Fri, 10 Jul 2026 14:48:33 +0200 Subject: [PATCH] fix(workflow): scope one-active-per-workflow index to workspace (#22795) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Problem The Phase 0 core index \`IDX_WORKFLOW_VERSION_ONE_ACTIVE_PER_WORKFLOW\` is on \`(workflowId) WHERE status='ACTIVE'\`, with **no \`workspaceId\`**. But \`core.workflowVersion\` is a shared multi-tenant table, so this enforces "one active version per workflowId **globally across all workspaces**" instead of per workspace. The version backfill fails on staging with: \`\`\` duplicate key value violates unique constraint "IDX_WORKFLOW_VERSION_ONE_ACTIVE_PER_WORKFLOW" Detail: Key ("workflowId")=(8b213cac-...) already exists. \`\`\` across several different workspaces that share the same workflowId (seeded/cloned data): workspace A's active version claims the workflowId, and every other workspace's insert collides. Every other index on this table includes \`workspaceId\`; this one dropped it when copied from the per-tenant workspace entity. ## Fix Index becomes \`(workspaceId, workflowId) WHERE status='ACTIVE'\` — one active version per workflow **per workspace**, matching the table's multi-tenant design and the intended invariant. New 2-20 fast instance command drops and recreates the index (Phase 0's command is merged/append-only). ## Test Reset + reproduce the exact scenario against the fixed index: - two workspaces with the same workflowId, both ACTIVE → **insert succeeds** (previously collided) - a second ACTIVE version for the same workflow within one workspace → **still blocked** (invariant preserved) Zero \`migrate:generate\` drift, typecheck + lint clean. After this deploys, re-run \`upgrade:2-20:backfill-workflow-version-to-core\`. Review in cubic --- ...83512000000-create-workflow-version-core-table.ts | 2 +- .../workflow/entities/workflow-version.entity.ts | 12 ++++++++---- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/packages/twenty-server/src/database/commands/upgrade-version-command/2-20/2-20-instance-command-fast-1783512000000-create-workflow-version-core-table.ts b/packages/twenty-server/src/database/commands/upgrade-version-command/2-20/2-20-instance-command-fast-1783512000000-create-workflow-version-core-table.ts index e9412e808a..f611d4b02b 100644 --- a/packages/twenty-server/src/database/commands/upgrade-version-command/2-20/2-20-instance-command-fast-1783512000000-create-workflow-version-core-table.ts +++ b/packages/twenty-server/src/database/commands/upgrade-version-command/2-20/2-20-instance-command-fast-1783512000000-create-workflow-version-core-table.ts @@ -31,7 +31,7 @@ export class CreateWorkflowVersionCoreTableFastInstanceCommand ); await queryRunner.query( `CREATE UNIQUE INDEX IF NOT EXISTS "IDX_WORKFLOW_VERSION_ONE_ACTIVE_PER_WORKFLOW" - ON "core"."workflowVersion" ("workflowId") + ON "core"."workflowVersion" ("workspaceId", "workflowId") WHERE "status" = 'ACTIVE'`, ); } diff --git a/packages/twenty-server/src/engine/core-modules/workflow/entities/workflow-version.entity.ts b/packages/twenty-server/src/engine/core-modules/workflow/entities/workflow-version.entity.ts index 00265e725e..02835d0981 100644 --- a/packages/twenty-server/src/engine/core-modules/workflow/entities/workflow-version.entity.ts +++ b/packages/twenty-server/src/engine/core-modules/workflow/entities/workflow-version.entity.ts @@ -25,10 +25,14 @@ export enum WorkflowVersionStatus { upgradeCommandName: CREATE_WORKFLOW_VERSION_CORE_TABLE_UPGRADE_COMMAND_NAME, }) @Index('IDX_WORKFLOW_VERSION_WORKSPACE_ID', ['workspaceId']) -@Index('IDX_WORKFLOW_VERSION_ONE_ACTIVE_PER_WORKFLOW', ['workflowId'], { - unique: true, - where: `"status" = 'ACTIVE'`, -}) +@Index( + 'IDX_WORKFLOW_VERSION_ONE_ACTIVE_PER_WORKFLOW', + ['workspaceId', 'workflowId'], + { + unique: true, + where: `"status" = 'ACTIVE'`, + }, +) @Index('IDX_WORKFLOW_VERSION_APPLICATION_ID', ['applicationId']) export class WorkflowVersionEntity extends SyncableEntity { @PrimaryGeneratedColumn('uuid')