From c0a41b3976fa269e8555c3ae859c16017827af52 Mon Sep 17 00:00:00 2001 From: "Beau Beauchamp, WebTigers" Date: Thu, 1 Oct 2026 12:03:01 -0400 Subject: [PATCH 1/2] Active-only UNIQUE indexes on user.email + user.username (TIGER-274) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 `_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 Claude-Session: https://claude.ai/code/session_01ASauLLscjqdsNqBNsx2Typ --- migrations/0053_active_only_user_unique.php | 33 +++++++++++++++++++++ 1 file changed, 33 insertions(+) create mode 100644 migrations/0053_active_only_user_unique.php diff --git a/migrations/0053_active_only_user_unique.php b/migrations/0053_active_only_user_unique.php new file mode 100644 index 0000000..914e7d9 --- /dev/null +++ b/migrations/0053_active_only_user_unique.php @@ -0,0 +1,33 @@ +_active = IF(deleted=0, col, NULL)` instead: MySQL/MariaDB allow multiple NULLs in a unique + * index, so deleted rows drop out (NULL) and free the value, while live rows stay unique. The real + * `email`/`username` columns are untouched — `Tiger_Model_User::findByEmail`/finders already filter + * `deleted=0`, so no application code changes. This is the convention for every unique + soft-deletable + * column platform-wide (see TIGER-274). + */ +return [ + 'up' => [ + "ALTER TABLE `user` DROP INDEX `uq_user_email`", + "ALTER TABLE `user` ADD COLUMN `email_active` VARCHAR(191) GENERATED ALWAYS AS (IF(`deleted`=0, `email`, NULL)) VIRTUAL", + "ALTER TABLE `user` ADD UNIQUE KEY `uq_user_email_active` (`email_active`)", + "ALTER TABLE `user` DROP INDEX `uq_user_username`", + "ALTER TABLE `user` ADD COLUMN `username_active` VARCHAR(64) GENERATED ALWAYS AS (IF(`deleted`=0, `username`, NULL)) VIRTUAL", + "ALTER TABLE `user` ADD UNIQUE KEY `uq_user_username_active` (`username_active`)", + ], + 'down' => [ + "ALTER TABLE `user` DROP INDEX `uq_user_email_active`", + "ALTER TABLE `user` DROP COLUMN `email_active`", + "ALTER TABLE `user` ADD UNIQUE KEY `uq_user_email` (`email`)", + "ALTER TABLE `user` DROP INDEX `uq_user_username_active`", + "ALTER TABLE `user` DROP COLUMN `username_active`", + "ALTER TABLE `user` ADD UNIQUE KEY `uq_user_username` (`username`)", + ], +]; From a5af331de11cbadaf10c53919008b372653a6c8a Mon Sep 17 00:00:00 2001 From: "Beau Beauchamp, WebTigers" Date: Thu, 1 Oct 2026 12:44:02 -0400 Subject: [PATCH 2/2] =?UTF-8?q?Release=201.19.0=20=E2=80=94=20active-only?= =?UTF-8?q?=20unique=20indexes=20(TIGER-274)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01ASauLLscjqdsNqBNsx2Typ --- CHANGELOG.md | 13 +++++++++++++ library/Tiger/Version.php | 2 +- 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 656cd66..98e7472 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,19 @@ All notable changes to **Tiger Core** (`webtigers/tiger-core`). Format follows ## [Unreleased] +## [1.19.0] — 2026-10-01 + +### Changed + +- **Active-only UNIQUE indexes on `user.email` + `user.username` — soft-delete now frees the value + (TIGER-274).** A plain `UNIQUE(col)` spans soft-deleted (`deleted=1`) rows, so soft-deleting a user + never freed their email/username and a re-signup with that address collided on the index. Migration + `0053` moves each unique index onto a generated `_active = IF(deleted=0, col, NULL)` column — + MySQL/MariaDB allow multiple NULLs in a unique index, so soft-deleted rows drop out and the value is + reusable, while live rows stay unique. The real `email`/`username` columns are untouched and the + finders already filter `deleted=0`, so there is no behavioral change for live rows. This is the + platform-wide convention for every unique + soft-deletable column (TIGER-274). + ## [1.18.1] — 2026-09-27 ### Fixed diff --git a/library/Tiger/Version.php b/library/Tiger/Version.php index ab5241b..90367db 100644 --- a/library/Tiger/Version.php +++ b/library/Tiger/Version.php @@ -9,5 +9,5 @@ class Tiger_Version { /** Current Tiger Core version. Keep in lockstep with the git tag cut for a release. */ - const VERSION = '1.18.1'; + const VERSION = '1.19.0'; }