Skip to content

Active-only UNIQUE indexes on user.email + user.username (TIGER-274) - #323

Merged
WebTigers merged 2 commits into
mainfrom
fix/active-only-user-unique
Oct 1, 2026
Merged

WebTigers merged 2 commits into
mainfrom
fix/active-only-user-unique

Conversation

@WebTigers

Copy link
Copy Markdown
Owner

What

Migration 0053 moves the UNIQUE indexes on user.email and user.username onto a generated
active-only column, so soft-delete frees the value:

ADD COLUMN email_active VARCHAR(191) GENERATED ALWAYS AS (IF(deleted=0, email, NULL)) VIRTUAL;
DROP INDEX uq_user_email;  ADD UNIQUE KEY uq_user_email_active (email_active);
-- …same for username

Why

A plain UNIQUE(email) spans soft-deleted (deleted=1) rows too (ARCHITECTURE §7a: "a deleted row
still holds its unique value"
), so soft-deleting a user never freed their email/username — a re-signup
with that address collided on the index (an ugly core.api.error.general) instead of gracefully
reusing it. MySQL/MariaDB allow multiple NULLs in a unique index, so deleted rows become NULL and
drop out, while live rows stay unique.

The real email/username columns are untouched and Tiger_Model_User::findByEmail + the finders
already filter deleted=0, so no application code changes.

Convention (TIGER-274)

This is the platform-wide pattern for every unique + soft-deletable column. First instances shipped
alongside this: user.email/user.username (here) and TigerHosting hosting_account.username +
hosting_domain.name. TIGER-274 tracks auditing/converting the rest (org slug, CMS/registry slugs, …).

Proof

Applied + verified on dev (MariaDB 10.5): a soft-deleted email/username is reusable by a new active
row; a second active duplicate is still refused.

🤖 Generated with Claude Code

https://claude.ai/code/session_01ASauLLscjqdsNqBNsx2Typ

WebTigers and others added 2 commits October 1, 2026 12:03
A plain UNIQUE spans soft-deleted rows, so soft-delete never freed an email/username — a
re-signup with a deleted account's address collided on the index. Migration 0053 moves the
unique index onto a generated `<col>_active = IF(deleted=0, col, NULL)`: deleted rows become
NULL (multiple NULLs allowed) and free the value; live rows stay unique. Real columns +
findByEmail/finders unchanged (they already filter deleted=0). The platform-wide convention
for unique + soft-deletable columns (TIGER-274); proven on MariaDB 10.5 via the TigerHosting
sibling migration.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ASauLLscjqdsNqBNsx2Typ
@WebTigers
WebTigers merged commit fc19ab2 into main Oct 1, 2026
14 checks passed
@WebTigers
WebTigers deleted the fix/active-only-user-unique branch October 1, 2026 16:47
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.

1 participant