feat(db): enforce globally unique issue prefixes and identifiers

Derive issue_prefix from first 3 letters of company name with
deterministic suffixes on collision. Migration rebuilds existing
prefixes, reassigns issue numbers, and adds unique indexes.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Forgotten
2026-02-23 16:08:10 -06:00
parent 09e64b5b03
commit d2d927bd2f
6 changed files with 5342 additions and 24 deletions

View File

@@ -0,0 +1,51 @@
DROP INDEX "issues_company_identifier_idx";--> statement-breakpoint
-- Rebuild issue prefixes to be company-specific and globally unique.
-- Base prefix is first 3 letters of company name (A-Z only), fallback "CMP".
-- Duplicate bases receive deterministic letter suffixes: PAP, PAPA, PAPAA, ...
WITH ranked_companies AS (
SELECT
c.id,
COALESCE(NULLIF(SUBSTRING(REGEXP_REPLACE(UPPER(c.name), '[^A-Z]', '', 'g') FROM 1 FOR 3), ''), 'CMP') AS base_prefix,
ROW_NUMBER() OVER (
PARTITION BY COALESCE(NULLIF(SUBSTRING(REGEXP_REPLACE(UPPER(c.name), '[^A-Z]', '', 'g') FROM 1 FOR 3), ''), 'CMP')
ORDER BY c.created_at, c.id
) AS prefix_rank
FROM companies c
)
UPDATE companies c
SET issue_prefix = CASE
WHEN ranked_companies.prefix_rank = 1 THEN ranked_companies.base_prefix
ELSE ranked_companies.base_prefix || REPEAT('A', ranked_companies.prefix_rank - 1)
END
FROM ranked_companies
WHERE c.id = ranked_companies.id;--> statement-breakpoint
-- Reassign issue numbers sequentially per company to guarantee uniqueness.
WITH numbered_issues AS (
SELECT
i.id,
ROW_NUMBER() OVER (PARTITION BY i.company_id ORDER BY i.created_at, i.id) AS issue_number
FROM issues i
)
UPDATE issues i
SET issue_number = numbered_issues.issue_number
FROM numbered_issues
WHERE i.id = numbered_issues.id;--> statement-breakpoint
-- Rebuild identifiers from normalized prefix + issue number.
UPDATE issues i
SET identifier = c.issue_prefix || '-' || i.issue_number
FROM companies c
WHERE c.id = i.company_id;--> statement-breakpoint
-- Sync counters to the largest issue number currently assigned per company.
UPDATE companies c
SET issue_counter = COALESCE((
SELECT MAX(i.issue_number)
FROM issues i
WHERE i.company_id = c.id
), 0);--> statement-breakpoint
CREATE UNIQUE INDEX "companies_issue_prefix_idx" ON "companies" USING btree ("issue_prefix");--> statement-breakpoint
CREATE UNIQUE INDEX "issues_identifier_idx" ON "issues" USING btree ("identifier");

File diff suppressed because it is too large Load Diff

View File

@@ -120,6 +120,13 @@
"when": 1771955900000,
"tag": "0016_agent_icon",
"breakpoints": true
},
{
"idx": 17,
"version": "7",
"when": 1771883888199,
"tag": "0017_tiresome_gabe_jones",
"breakpoints": true
}
]
}

View File

@@ -1,17 +1,23 @@
import { pgTable, uuid, text, integer, timestamp, boolean } from "drizzle-orm/pg-core";
import { pgTable, uuid, text, integer, timestamp, boolean, uniqueIndex } from "drizzle-orm/pg-core";
export const companies = pgTable("companies", {
id: uuid("id").primaryKey().defaultRandom(),
name: text("name").notNull(),
description: text("description"),
status: text("status").notNull().default("active"),
issuePrefix: text("issue_prefix").notNull().default("PAP"),
issueCounter: integer("issue_counter").notNull().default(0),
budgetMonthlyCents: integer("budget_monthly_cents").notNull().default(0),
spentMonthlyCents: integer("spent_monthly_cents").notNull().default(0),
requireBoardApprovalForNewAgents: boolean("require_board_approval_for_new_agents")
.notNull()
.default(true),
createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow(),
});
export const companies = pgTable(
"companies",
{
id: uuid("id").primaryKey().defaultRandom(),
name: text("name").notNull(),
description: text("description"),
status: text("status").notNull().default("active"),
issuePrefix: text("issue_prefix").notNull().default("PAP"),
issueCounter: integer("issue_counter").notNull().default(0),
budgetMonthlyCents: integer("budget_monthly_cents").notNull().default(0),
spentMonthlyCents: integer("spent_monthly_cents").notNull().default(0),
requireBoardApprovalForNewAgents: boolean("require_board_approval_for_new_agents")
.notNull()
.default(true),
createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow(),
},
(table) => ({
issuePrefixUniqueIdx: uniqueIndex("companies_issue_prefix_idx").on(table.issuePrefix),
}),
);

View File

@@ -59,6 +59,6 @@ export const issues = pgTable(
),
parentIdx: index("issues_company_parent_idx").on(table.companyId, table.parentId),
projectIdx: index("issues_company_project_idx").on(table.companyId, table.projectId),
identifierIdx: uniqueIndex("issues_company_identifier_idx").on(table.companyId, table.identifier),
identifierIdx: uniqueIndex("issues_identifier_idx").on(table.identifier),
}),
);