From 8567ebffd94277478ce73f8f79c2b0cef75b98db Mon Sep 17 00:00:00 2001 From: Bohan Jiang <52446949+Bohan-J@users.noreply.github.com> Date: Mon, 13 Jul 2026 19:31:27 +0800 Subject: [PATCH] fix(migrations): rebuild legacy label index CONCURRENTLY on rollback (MUL-4479) (#5357) The 162_resource_labels down migration recreated issue_label_workspace_name_lower_idx with a plain CREATE UNIQUE INDEX, which takes a blocking lock on the existing issue_label table during a rollback to v0.3.43 and violates the online-migration rule that every CREATE INDEX must be CONCURRENTLY. CREATE INDEX CONCURRENTLY cannot run in a transaction or a multi-command migration, so the rebuild is split out of 162.down: - 171.down now rebuilds the legacy index as a single CREATE UNIQUE INDEX CONCURRENTLY (the natural inverse of 171.up, which drops it). - 174 (new, no-op up) deletes the agent/skill label rows in its down so they are gone before 171.down rebuilds the workspace-wide unique index. Down migrations apply high->low, so 174.down -> 171.down -> 162.down runs in the required order. - 162.down keeps only the transaction-safe structural teardown. Validated on an isolated Postgres: full up->down->up chain with a colliding issue/agent label pair; the concurrent rebuild succeeds after the rows are deleted, restores the valid pre-162 unique index, and a negative control (rebuild before the delete) fails on the exact collision. Co-authored-by: J Co-authored-by: multica-agent --- .../migrations/162_resource_labels.down.sql | 19 ++++++++++++------- ...drop_legacy_label_namespace_index.down.sql | 13 ++++++++++--- ..._legacy_label_index_rollback_prep.down.sql | 6 ++++++ ...74_legacy_label_index_rollback_prep.up.sql | 8 ++++++++ 4 files changed, 36 insertions(+), 10 deletions(-) create mode 100644 server/migrations/174_legacy_label_index_rollback_prep.down.sql create mode 100644 server/migrations/174_legacy_label_index_rollback_prep.up.sql diff --git a/server/migrations/162_resource_labels.down.sql b/server/migrations/162_resource_labels.down.sql index d0d93b0526..e1cff1558c 100644 --- a/server/migrations/162_resource_labels.down.sql +++ b/server/migrations/162_resource_labels.down.sql @@ -1,16 +1,21 @@ +-- Revert issue_label to its pre-162 issue-only shape and drop the resource +-- label junction tables. The two rollback steps that each need their own +-- transaction live in earlier down migrations, because a single migration +-- file cannot mix DML/DDL with CREATE INDEX CONCURRENTLY: +-- * 174.down removes the agent/skill rows, and +-- * 171.down rebuilds the legacy workspace-wide unique name index +-- CONCURRENTLY. +-- Both run before this file (down migrations apply high->low), so the +-- non-issue rows are already gone by the time we drop resource_type here. DROP TABLE IF EXISTS skill_to_label; DROP TABLE IF EXISTS agent_to_label; +-- Defensive: a short-lived pre-release deploy may have applied the original +-- 162 that created these indexes inline. On the normal down chain 167/168 +-- already dropped them CONCURRENTLY, so these are no-ops. DROP INDEX IF EXISTS issue_label_workspace_type_idx; DROP INDEX IF EXISTS issue_label_workspace_type_name_lower_idx; --- The pre-162 schema can only represent issue labels. Resource-scoped rows --- must be removed before restoring its workspace-wide unique name index. -DELETE FROM issue_label WHERE resource_type <> 'issue'; - ALTER TABLE issue_label DROP COLUMN IF EXISTS description, DROP COLUMN IF EXISTS resource_type; - -CREATE UNIQUE INDEX issue_label_workspace_name_lower_idx - ON issue_label (workspace_id, LOWER(name)); diff --git a/server/migrations/171_drop_legacy_label_namespace_index.down.sql b/server/migrations/171_drop_legacy_label_namespace_index.down.sql index ba913d50d2..f21f5970fa 100644 --- a/server/migrations/171_drop_legacy_label_namespace_index.down.sql +++ b/server/migrations/171_drop_legacy_label_namespace_index.down.sql @@ -1,3 +1,10 @@ --- Migration 162 restores the legacy index after it removes non-issue labels. --- Recreating it here could fail while those rows still exist during rollback. -SELECT 1; +-- Inverse of 171.up, which drops the pre-162 workspace-wide unique name +-- index. Recreated as a single CREATE INDEX CONCURRENTLY statement so the +-- rollback stays online-safe (it cannot be combined with other statements +-- or run inside a transaction block). +-- +-- The agent/skill rows that would violate this workspace-wide uniqueness are +-- removed earlier in the down chain by 174.down (down migrations apply +-- high->low), so only issue rows remain when this index is rebuilt. +CREATE UNIQUE INDEX CONCURRENTLY IF NOT EXISTS issue_label_workspace_name_lower_idx + ON issue_label (workspace_id, LOWER(name)); diff --git a/server/migrations/174_legacy_label_index_rollback_prep.down.sql b/server/migrations/174_legacy_label_index_rollback_prep.down.sql new file mode 100644 index 0000000000..21fbcd9bc9 --- /dev/null +++ b/server/migrations/174_legacy_label_index_rollback_prep.down.sql @@ -0,0 +1,6 @@ +-- Remove the resource-scoped (agent/skill) labels before 171.down rebuilds +-- the pre-162 workspace-wide unique name index, which cannot coexist with +-- rows that share (workspace_id, LOWER(name)) across resource types. This +-- runs first in the down chain (high->low), and resource_type still exists +-- here because 162.down (which drops it) runs later. +DELETE FROM issue_label WHERE resource_type <> 'issue'; diff --git a/server/migrations/174_legacy_label_index_rollback_prep.up.sql b/server/migrations/174_legacy_label_index_rollback_prep.up.sql new file mode 100644 index 0000000000..bfb5c49cee --- /dev/null +++ b/server/migrations/174_legacy_label_index_rollback_prep.up.sql @@ -0,0 +1,8 @@ +-- Forward no-op. This migration exists only to sequence a rollback step: +-- its down removes the agent/skill rows from issue_label before 171.down +-- rebuilds the workspace-wide unique name index. Because down migrations +-- apply high->low, keeping this delete in the highest-numbered resource +-- label migration guarantees the non-issue rows are gone before both +-- 171.down (index rebuild) and 162.down (drops resource_type). There is +-- nothing to do on the way up. +SELECT 1;