mirror of
https://github.com/nestriness/nestri.git
synced 2026-09-19 17:25:19 +03:00
feat(core): make one address one account, on rows that never had one
Runs against a database where every user was created by a gaming sign-in, so most rows have no email at all and nothing has ever stopped two rows from sharing one. The address is normalized first, duplicates are separated before the unique index exists — the older row keeps the address, the newer one is asked for a new one and loses nothing else — and the index is partial so that accounts with no address do not collide with each other. Verified against a database built to contain the awkward rows rather than against an empty schema, by the script alongside it: an account with no address, one with both, one with two connections, a duplicated address in two different cases, an account already over the connection cap, and a deleted row holding an address a live row also holds. Removing the de-duplication makes the index creation fail, which is how we know the fixtures are load-bearing. Also adds a nullable column recording which attempt holds a session run. It is not part of the change above and carries no reason of its own; the endpoint that reads and writes it arrives separately, and it is here because a schema change has one owner at a time.
This commit is contained in:
66
packages/core/migrations/0009_email_is_the_root_identity.sql
Normal file
66
packages/core/migrations/0009_email_is_the_root_identity.sql
Normal file
@@ -0,0 +1,66 @@
|
||||
-- One address, one account. ref(d-0048)
|
||||
--
|
||||
-- Runs against a database in which every user was created by signing in with a
|
||||
-- gaming account, which means most rows have no email at all and nothing has
|
||||
-- ever stopped two rows from sharing one. Three consequences, in order:
|
||||
--
|
||||
-- 1. The column is normalized first. The address is about to become an
|
||||
-- identity, so `Ada@Example.com ` and `ada@example.com` have to stop
|
||||
-- being two of them. Trimming and lower-casing happens here once; the
|
||||
-- code that writes the column does the same thing on the way in.
|
||||
-- 2. Duplicates are separated before the index exists, because
|
||||
-- `CREATE UNIQUE INDEX` fails outright on the first pair it meets, and a
|
||||
-- migration that dies half way through is worse than one that decides.
|
||||
-- 3. The index is partial. A null email is not a value, so the accounts that
|
||||
-- have none do not collide with each other — which is the only reason a
|
||||
-- unique index can land on these rows at all.
|
||||
|
||||
UPDATE "user"
|
||||
SET "email" = lower(btrim("email"))
|
||||
WHERE "email" IS NOT NULL
|
||||
AND "email" <> lower(btrim("email"));--> statement-breakpoint
|
||||
|
||||
-- Where two accounts claim one address, the older keeps it.
|
||||
--
|
||||
-- Nothing is deleted: both accounts survive, with their games, their hardware
|
||||
-- and their team. What the newer one loses is the address, and `email_verified`
|
||||
-- goes back to false to say so — the next sign-in asks for an address and the
|
||||
-- person supplies one, which is a prompt rather than a loss.
|
||||
--
|
||||
-- The older row wins because its address has been in use longest, so it is the
|
||||
-- one a receipt or a reset was most likely sent to. `id` breaks a tie on
|
||||
-- `time_created`, so the choice is total and re-running this changes nothing.
|
||||
UPDATE "user" u
|
||||
SET "email" = NULL, "email_verified" = false
|
||||
WHERE u."email" IS NOT NULL
|
||||
AND u."time_deleted" IS NULL
|
||||
AND EXISTS (
|
||||
SELECT 1 FROM "user" older
|
||||
WHERE older."email" = u."email"
|
||||
AND older."time_deleted" IS NULL
|
||||
AND (older."time_created", older."id") < (u."time_created", u."id")
|
||||
);--> statement-breakpoint
|
||||
|
||||
CREATE UNIQUE INDEX "user_email_unique" ON "user" USING btree ("email") WHERE email is not null and time_deleted is null;--> statement-breakpoint
|
||||
|
||||
-- There is no constraint here for the cap on how many gaming accounts one
|
||||
-- person may connect, and there cannot be one.
|
||||
--
|
||||
-- A unique index makes a value unique; it cannot count the rows that share a
|
||||
-- foreign key, so no index shape says "at most four of these". The cap is
|
||||
-- enforced in application code, and a direct write to `linked_account` can
|
||||
-- exceed it. This is written where the schema is read so that nobody looks for
|
||||
-- the rule here, fails to find it, and concludes there is not one. Rows
|
||||
-- already over the cap are left alone: the limit governs connecting another,
|
||||
-- not keeping what is already connected.
|
||||
|
||||
-- Below is not part of the above, and carries no reason of its own.
|
||||
--
|
||||
-- It records which attempt holds a run: the agent generates an opaque value
|
||||
-- per claim, the row remembers the first one to arrive, and every later write
|
||||
-- has to present it. Nullable and unbackfilled, because a run nobody has
|
||||
-- claimed genuinely has no holder, and never cleared, because a finished run
|
||||
-- still has to say which attempt ran it. The endpoint that reads and writes it
|
||||
-- arrives separately; it is here because a schema change has one owner at a
|
||||
-- time.
|
||||
ALTER TABLE "session" ADD COLUMN "claim_token" text;
|
||||
2316
packages/core/migrations/meta/0009_snapshot.json
Normal file
2316
packages/core/migrations/meta/0009_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -64,6 +64,13 @@
|
||||
"when": 1788547836146,
|
||||
"tag": "0008_session_one_active_run_per_box",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 9,
|
||||
"version": "7",
|
||||
"when": 1788555252186,
|
||||
"tag": "0009_email_is_the_root_identity",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
||||
Reference in New Issue
Block a user