mirror of
https://github.com/go-admin-team/go-admin.git
synced 2026-09-20 17:57:54 +00:00
sys_user.username, sys_role.role_key and sys_dict_type.dict_type had no unique index. Uniqueness was a SELECT COUNT followed by an INSERT, which two concurrent requests both pass — and login resolves a username with First, so which of the two accounts answers is whichever the database returns. The index cannot be on the key alone, because a soft-deleted row keeps occupying the name and a deleted user's username could never be used again. It has to include the delete marker, and the marker has to be non-null: two live rows are (alice, NULL) and (alice, NULL), and NULL is not equal to NULL, so an index over a nullable marker admits both. That is the worst of the three states — a constraint that reads as protection and binds nothing — and there is a test that demonstrates it rather than asserting it. ModelTime.DeletedAt is milliseconds since the epoch now, zero while the row is live. Sixteen tables carry it; the migration converts each one, preserving when each deleted row was deleted, then adds the three indexes. Written to be re-runnable rather than transactional, because DDL does not roll back on MySQL and an operator whose first attempt failed halfway should have nothing to do but run it again. It refuses before altering anything if a table already holds duplicates, naming them, rather than letting the index fail and leaving the operator to guess. The timestamp conversion happens in Go: turning a timestamp into epoch milliseconds is spelled differently by every dialect this supports, and these row counts do not justify four versions of it.