MemberJunction / MemberJunction/MJ

UserSetting: no uniqueness on (UserID, Setting); duplicates accumulate and GetSetting becomes non-deterministic

Open
#4,224 0 comments 0 reactions 0 assignees View on GitHub
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

Open the contributing 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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.