ControlBy, Model, ModelTime, ActiveRecord, BaseUser, Response, Page,
Migration and the menu type constants now read `type X = pkg.X` /
`const X = pkg.X` against go-admin-core's sdk/contract/models instead
of defining these shapes locally. Every embed, GORM tag and JSON tag
is unchanged - a type alias is the same type, not a new one - and
every existing import of go-admin/common/models keeps compiling with
no changes of its own (verified with `git diff --exit-code` over the
70 files that import common/models, common/dto or common/actions).
The menu type constants (Directory/Menu/Button) are declared as direct
references rather than restated literals: an independently written
copy of the same value can be edited out of step with go-admin-core's,
where a direct reference cannot (PRD 006 hard constraint 4).
PRD 006 F1/F5.
Claude-Session: https://claude.ai/code/session_01HPTAw8b8tAdFNFn8rKdPYx
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.