From dd98fc579fc7fd2e6c86caf9c1c99a97b4af5e05 Mon Sep 17 00:00:00 2001 From: Abhishek Sah Date: Wed, 16 Sep 2026 13:32:07 +0530 Subject: [PATCH] feat(store): enforce unique names and emails over live rows only Organization name, project name, user email, user name, domain name per org, and group name per org were plain unique constraints. Once rows are soft-deleted they keep their values, so a deleted row would block the name or email forever. Each constraint is replaced by a unique index that only covers rows where deleted_at is null. A live row still cannot share a name or email with another live row. Deleted rows no longer take part in the check. Roles, resources, relations, and policies keep their plain constraints for now. Their repositories upsert with ON CONFLICT on those columns, so they need a code change alongside the index and will follow separately. --- .../20260916120000_live_unique_indexes.down.sql | 17 +++++++++++++++++ .../20260916120000_live_unique_indexes.up.sql | 17 +++++++++++++++++ 2 files changed, 34 insertions(+) create mode 100644 internal/store/postgres/migrations/20260916120000_live_unique_indexes.down.sql create mode 100644 internal/store/postgres/migrations/20260916120000_live_unique_indexes.up.sql diff --git a/internal/store/postgres/migrations/20260916120000_live_unique_indexes.down.sql b/internal/store/postgres/migrations/20260916120000_live_unique_indexes.down.sql new file mode 100644 index 000000000..380f20bbb --- /dev/null +++ b/internal/store/postgres/migrations/20260916120000_live_unique_indexes.down.sql @@ -0,0 +1,17 @@ +ALTER TABLE organizations ADD CONSTRAINT organizations_name_key UNIQUE (name); +DROP INDEX IF EXISTS uq_organizations_name_live; + +ALTER TABLE projects ADD CONSTRAINT projects_name_key UNIQUE (name); +DROP INDEX IF EXISTS uq_projects_name_live; + +ALTER TABLE users ADD CONSTRAINT users_email_key UNIQUE (email); +DROP INDEX IF EXISTS uq_users_email_live; + +ALTER TABLE users ADD CONSTRAINT users_name_key UNIQUE (name); +DROP INDEX IF EXISTS uq_users_name_live; + +ALTER TABLE domains ADD CONSTRAINT org_id_name_unique UNIQUE (org_id, name); +DROP INDEX IF EXISTS uq_domains_org_id_name_live; + +ALTER TABLE groups ADD CONSTRAINT groups_org_id_name_key UNIQUE (org_id, name); +DROP INDEX IF EXISTS uq_groups_org_id_name_live; diff --git a/internal/store/postgres/migrations/20260916120000_live_unique_indexes.up.sql b/internal/store/postgres/migrations/20260916120000_live_unique_indexes.up.sql new file mode 100644 index 000000000..b7d5e4208 --- /dev/null +++ b/internal/store/postgres/migrations/20260916120000_live_unique_indexes.up.sql @@ -0,0 +1,17 @@ +CREATE UNIQUE INDEX IF NOT EXISTS uq_organizations_name_live ON organizations (name) WHERE deleted_at IS NULL; +ALTER TABLE organizations DROP CONSTRAINT IF EXISTS organizations_name_key; + +CREATE UNIQUE INDEX IF NOT EXISTS uq_projects_name_live ON projects (name) WHERE deleted_at IS NULL; +ALTER TABLE projects DROP CONSTRAINT IF EXISTS projects_name_key; + +CREATE UNIQUE INDEX IF NOT EXISTS uq_users_email_live ON users (email) WHERE deleted_at IS NULL; +ALTER TABLE users DROP CONSTRAINT IF EXISTS users_email_key; + +CREATE UNIQUE INDEX IF NOT EXISTS uq_users_name_live ON users (name) WHERE deleted_at IS NULL; +ALTER TABLE users DROP CONSTRAINT IF EXISTS users_name_key; + +CREATE UNIQUE INDEX IF NOT EXISTS uq_domains_org_id_name_live ON domains (org_id, name) WHERE deleted_at IS NULL; +ALTER TABLE domains DROP CONSTRAINT IF EXISTS org_id_name_unique; + +CREATE UNIQUE INDEX IF NOT EXISTS uq_groups_org_id_name_live ON groups (org_id, name) WHERE deleted_at IS NULL; +ALTER TABLE groups DROP CONSTRAINT IF EXISTS groups_org_id_name_key;