Skip to content

feat(store): enforce unique names and emails over live rows only - #1928

Merged
whoAbhishekSah merged 1 commit into
mainfrom
soft-delete-live-unique-indexes
Sep 17, 2026
Merged

whoAbhishekSah merged 1 commit into
mainfrom
soft-delete-live-unique-indexes

Conversation

@whoAbhishekSah

Copy link
Copy Markdown
Member

What

Replaces six plain unique constraints with unique indexes that only cover live rows, meaning rows where deleted_at IS NULL:

Table Columns Old constraint New index
organizations name organizations_name_key uq_organizations_name_live
projects name projects_name_key uq_projects_name_live
users email users_email_key uq_users_email_live
users name users_name_key uq_users_name_live
domains org_id, name org_id_name_unique uq_domains_org_id_name_live
groups org_id, name groups_org_id_name_key uq_groups_org_id_name_live

Why

Soft-deleted rows keep their values. With a plain unique constraint, a deleted organization would hold its name forever, and a person who deleted their account could never sign up again with the same email. Product confirmed that names and emails should be reusable once the old row is deleted. Uniqueness is still enforced among live rows.

Primary keys are untouched, so ids stay unique across live and deleted rows.

Not in this PR

Roles, resources, relations, and policies keep their plain constraints. Their repositories upsert with ON CONFLICT on those columns. A partial unique index only satisfies ON CONFLICT when the clause repeats the same WHERE predicate, so those four need a code change alongside the index. They will follow separately.

Behaviour change

None today, because no row has deleted_at set yet. Once soft delete starts writing, a deleted name or email can be reused by a new row. Duplicate detection in the repositories keys off SQL state 23505, which a unique index raises the same way a constraint does.

Rollback note

The down migration restores the plain constraints. It will fail if two rows, live or deleted, share a value by then. That is expected and is the reason the read filters must be deployed before any soft-delete write.

Tested

  • Applied the full migration set on an empty local Postgres, rolled back one step, and re-applied. Constraints and indexes appear and disappear as expected.
  • For each of the six cases: insert a row, soft-delete it, insert the same value again (succeeds), insert a third live copy (fails with duplicate key value violates unique constraint "uq_..._live").
  • go test -race ./internal/store/postgres/ passes against Docker Postgres.

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.
@vercel

vercel Bot commented Sep 16, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
frontier Ready Ready Preview Sep 16, 2026 8:04am UTC

@coderabbitai

coderabbitai Bot commented Sep 16, 2026 •

Copy link
Copy Markdown
Contributor

Review Change StackReview Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Advanced

Run ID: 42d0b6cc-1200-4197-8403-e14718bd0aac

📥 Commits

Reviewing files that changed from the base of the PR and between b145dc0 and dd98fc5.

📒 Files selected for processing (2)
  • internal/store/postgres/migrations/20260916120000_live_unique_indexes.down.sql
  • internal/store/postgres/migrations/20260916120000_live_unique_indexes.up.sql

Included review availability: Your plan provides up to 2 included reviews per hour; 1 remains after this review.


📝 Summary

Summary by CodeRabbit

  • Bug Fixes
    • Active organizations, projects, users, domains, and groups now maintain consistent uniqueness rules.
    • Names and contact details can be reused when the previous record has been deleted, while duplicate values remain prevented among active records.
    • Database updates are applied safely without affecting existing active records.

Walkthrough

The migration enforces uniqueness only for non-deleted records across organizations, projects, users, domains, and groups. The rollback restores standard unique constraints.

Changes

Live uniqueness enforcement

Layer / File(s) Summary
Migration and rollback
internal/store/postgres/migrations/20260916120000_live_unique_indexes.up.sql, internal/store/postgres/migrations/20260916120000_live_unique_indexes.down.sql
The forward migration adds partial unique indexes for live records and removes legacy constraints. The rollback removes the indexes and recreates full unique constraints.

Priority: ⬇️ Low

Estimated code review effort: 2 (Simple) | ~10 minutes

Change: Feature

Merge Risk: ⚪ Minimal · up to dd98f

The migration has no verified merge-blocking issue in the available evidence; rollback behavior with reused values remains unverified.

🚥 Pre-merge checks | ✅ 2
✅ Passed checks (2 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

Warning

Git: CodeRabbit could not clone the repository, so clone-backed analysis was skipped and this review may be incomplete. Verify repository clone access, such as SSH credentials, before requesting another full review. If clone access is intentionally unavailable, use path_filters to narrow the review scope.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coveralls

Copy link
Copy Markdown

Coverage Report for CI Build 35071715356

Coverage remained the same at 50.229%

Details

  • Coverage remained the same as the base build.
  • Patch coverage: No coverable lines changed in this PR.
  • No coverage regressions found.

Uncovered Changes

No uncovered changes found.

Coverage Regressions

No coverage regressions found.


Coverage Stats

Coverage Status
Relevant Lines: 41106
Covered Lines: 20647
Line Coverage: 50.23%
Coverage Strength: 16.0 hits per line

💛 - Coveralls

@whoAbhishekSah
whoAbhishekSah merged commit f3f2094 into main Sep 17, 2026
8 checks passed
@whoAbhishekSah
whoAbhishekSah deleted the soft-delete-live-unique-indexes branch September 17, 2026 06:28

This branch was successfully deployed

1 active deployment
Preview — dd98fc57 Deployed Sep 16, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants