MemberJunction / MemberJunction/MJ
UserSetting: no uniqueness on (UserID, Setting); duplicates accumulate and GetSetting becomes non-deterministic
- Dominant language
- TSQL
- Stars
- 29
- Forks
- 6
- Avg merge
- 2d 1h
- Merged PRs (30d)
- 323
Description
## Summary
`__mj.UserSetting` has no uniqueness constraint on `(UserID, Setting)`, so duplicate rows accumulate, and `UserInfoEngine.GetSetting()` (a `find()` over the loaded array) then returns whichever duplicate loads first.
## Evidence (a 5.51.0 install, read-only query against a staging DB, 2026-09-04)
Indexes on `__mj.UserSetting`: `PK_UserSetting` (clustered, ID) and `IDX_AUTO_MJ_FKEY_UserSetting_UserID` (non-unique). No check constraints.
Duplicate `(UserID, Setting)` pairs:
| UserID | Setting | rows |
|---|---|---|
| 23BABF74-… | `Explorer.Theme` | **437** |
| 27BABF74-… | `mj.chat.drafts.v1` | 10 |
| 501495F6-… | `Explorer.Theme` | 8 |
| 7E19082F-… | `Explorer.Theme` | 3 |
| 27BABF74-… | `HomeApp.PinnedItems` | 2 |
551 rows for 22 users in total; `Explorer.Theme` alone accounts for 454.
## Where it comes from
`UserInfoEngine.SetSetting` / `SetSettingDebounced` create-or-update against the engine's in-memory cache (`GetSettingEntity` → create if absent). Anything that writes before the cache reflects the row (a second tab, a debounced write racing a fresh `Config()`, a write on the server where the load is unfiltered) creates another row instead of updating the existing one. Nothing in the schema stops it.
## Suggested fix
1. A unique index on `(UserID, Setting)` (migration), with a one-off dedupe that keeps the most recently updated row per pair.
2. `SetSetting` upserts against the database key `(UserID, Setting)`, not only against the cached entity, so a cache miss cannot mint a duplicate.
3. `GetSetting` / `GetSettingEntity` deterministic if duplicates exist during the transition (newest `__mj_UpdatedAt` wins).
## Context
Found while planning to move an app's user-level prefs onto `UserInfoEngine` per its documented role as the standard for user settings. Two related questions we will raise separately: the server-side `Config()` loads every user's settings (the `UserID` filter applies only on the Network provider), and `MJ: User Settings` has no `ApplicationID` column in 5.51.0 although app-level scoping is described for it.
Contributor guide
Research direction
Start with UserInfoEngine.SetSetting, SetSettingDebounced, GetSettingEntity, GetSetting, and Config(), then inspect the UserSetting schema and migration conventions. Confirm how duplicate rows are handled and how writes race cache loads; done means existing duplicates are safely addressed, new (UserID, Setting) duplicates are prevented, and lookup behavior is deterministic during transition.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- sql
- Domain
- databases
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Active
- Clarity
- Mostly clear
- Newbie friendliness
- 48/100