From aa78eedb9c25ccef35792aab3c301341e344b6d1 Mon Sep 17 00:00:00 2001 From: alex-dembele Date: Fri, 2 Oct 2026 13:04:22 +0100 Subject: [PATCH] perf(db): index LOWER(email) for the case-insensitive account lookup (#876) Signed-off-by: alex-dembele --- docs/MIGRATIONS.md | 16 ++++++++++++++ .../0068_users_email_lower_index.down.sql | 1 + .../0068_users_email_lower_index.up.sql | 22 +++++++++++++++++++ 3 files changed, 39 insertions(+) create mode 100644 migrations/0068_users_email_lower_index.down.sql create mode 100644 migrations/0068_users_email_lower_index.up.sql diff --git a/docs/MIGRATIONS.md b/docs/MIGRATIONS.md index 0598618c..63d46d89 100644 --- a/docs/MIGRATIONS.md +++ b/docs/MIGRATIONS.md @@ -78,3 +78,19 @@ vide : schéma de la version précédente, données de tous les âges et de tout NULL, lignes antérieures aux hooks), puis la séquence de boot (`PrepareForAutoMigrate`, AutoMigrate, couche SQL). Modèle : `backend/cmd/server/migration_0060_integration_test.go`. Procédure de montée de version et de retour arrière associée : `docs/runbooks/migration-0060.md`. + +**Adresses e-mail en double à la casse près** (`0068`, #876). `GetByEmail` compare +`LOWER(email)` depuis #688, et `idx_users_email_lower` sert cette recherche. L'index n'est pas +unique, parce que des comptes créés avant #687 peuvent exister en double à la casse près. Avant de +proposer une contrainte unique, lister ces doublons (lecture seule) : +```sql +SELECT LOWER(email) AS adresse, COUNT(*) AS comptes, + array_agg(email ORDER BY created_at) AS graphies + FROM users + WHERE deleted_at IS NULL + GROUP BY LOWER(email) +HAVING COUNT(*) > 1; +``` +Sans ligne, la base est prête pour une contrainte unique. Sinon, la connexion atteint la ligne +écrite exactement comme saisie, puis la plus ancienne : fusionner ou renommer les doublons est une +décision de données, pas une migration. diff --git a/migrations/0068_users_email_lower_index.down.sql b/migrations/0068_users_email_lower_index.down.sql new file mode 100644 index 00000000..bb183c6e --- /dev/null +++ b/migrations/0068_users_email_lower_index.down.sql @@ -0,0 +1 @@ +DROP INDEX IF EXISTS idx_users_email_lower; diff --git a/migrations/0068_users_email_lower_index.up.sql b/migrations/0068_users_email_lower_index.up.sql new file mode 100644 index 00000000..3c684bc9 --- /dev/null +++ b/migrations/0068_users_email_lower_index.up.sql @@ -0,0 +1,22 @@ +-- #876 — index the lookup login actually runs. +-- +-- Since #688, GetByEmail matches LOWER(email) so the casing typed never decides +-- which account is reached. The unique index on users.email is on the raw +-- column and cannot serve that predicate, so every sign-in, sign-up, reset and +-- invitation scanned users. +-- +-- Deliberately NOT unique: rows written before #687 may hold case-variant +-- duplicates, and a unique index would fail this migration, and the boot with +-- it, on such a database. docs/MIGRATIONS.md has the query that lists them. +-- +-- users is created by AutoMigrate, which runs before this layer; the guard only +-- matters for a run against an empty database. + +DO $$ +BEGIN + IF to_regclass('users') IS NULL THEN + RETURN; + END IF; + CREATE INDEX IF NOT EXISTS idx_users_email_lower ON users (LOWER(email)); +END +$$;