Skip to content

fix(db): migrate legacy AList role values on startup - #3262

Closed
juhuan wants to merge 1 commit into
OpenListTeam:mainfrom
juhuan:fix/migrate-alist-legacy-user-role
Closed

juhuan wants to merge 1 commit into
OpenListTeam:mainfrom
juhuan:fix/migrate-alist-legacy-user-role

Conversation

@juhuan

@juhuan juhuan commented Oct 25, 2026

Copy link
Copy Markdown

Summary / 摘要

Fixes a startup crash loop for everyone upgrading from AList v3.46.0+ (and restores the admin role that silently disappeared on such databases).

  • This PR changes public API, config, storage format, or migration behavior. — it adds a one-off data normalization of x_users.role; no schema change and no user action required.
  • This PR has breaking changes.
  • This PR requires corresponding changes in related repositories.

AList v3.46.0 and later model the role as a list of ints stored in a text column ("[1]", "[2]"), OpenList has always used a single int. AutoMigrate rebuilds the column as integer but the legacy rows keep their textual value, so WHERE role = 2 never matches '[2]':

  • GetGuest() reports ErrRecordNotFound → initGuest() inserts a second guest row → UNIQUE constraint failed: x_users.username (2067) → Fatalf and a restart loop. The error only appears in data/log/log.log, docker logs just stops after init logrus..., which makes it very hard to diagnose.
  • GetAdmin() fails too → the administrator keeps the username but silently loses admin rights, because IsAdmin() is Role == ADMIN.

What this PR does:

  1. internal/db: migrateUserRoles() runs right after AutoMigrate and rewrites every legacy [N] / [N,M] value into the single-int form, preferring the most privileged role when a row lists several. Already-normalized rows are skipped, so it is idempotent and a no-op on fresh installs. Scanning role into a string keeps it portable across SQLite (text and blob), MySQL and PostgreSQL.
  2. internal/bootstrap/data: initGuest() no longer calls Fatalf when the insert fails because a row named guest already exists — it takes that row over as the disabled guest and logs a warning instead, so a mismatched role can never brick the instance again.
  3. internal/db/user_role_test.go: table test for the legacy value conversion.

Related Issues / 关联 Issue

Closes #3261

Testing / 测试

  • go test ./... — ran the new package test only: go test ./internal/db/ -run TestConvertLegacyRole → PASS
  • Manual test / 手动测试:

Reproduced on a real AList v3.52.0 SQLite database (x_users.role = b'[2]' / b'[1]'), run with a locally built v4.2.8 binary:

Before — exits during init:

INFO[...] init logrus...
FATA[...] [init user] Failed to create guest user: constraint failed: UNIQUE constraint failed: x_users.username (2067)

After:

INFO[...] [migrate user role] converted 2 legacy AList role value(s)
# boot continues
1|juhuan|2|integer|disabled=0   <- admin role recognised again
2|guest |1|integer|disabled=1

Also verified:

  • second start on the already-migrated database logs no conversion and still boots (idempotent);
  • a database where role is already integer but the guest row has GENERAL still boots, with the takeover warning: [init user] Username "guest" was already taken by user id=2 and has been taken over as the disabled guest account.

Not tested: MySQL and PostgreSQL backends (no instance available); the SQL used is a plain SELECT/UPDATE with values bound through GORM, no dialect specific syntax.

Checklist / 检查清单

  • I have read CONTRIBUTING.
  • I confirm this contribution follows the repository license, contribution policy, and code of conduct.
  • I have formatted the changed code with gofmt, go fmt, or prettier where applicable.

AI Disclosure / AI 使用声明

  • This PR includes AI-assisted content.

Tools used / 使用工具:

  • Other (please specify) / 其他(请注明): WorkBuddy AI Agent

Usage scope / 使用范围:

  • Code generation / 代码生成

  • Tests / 测试

  • Review assistance / 审查辅助

  • I have reviewed and validated all AI-assisted content included in this PR.

  • I have ensured that all AI-assisted commits include Co-Authored-By attribution.

  • I can reproduce all AI-assisted content included in this PR without any AI tools.

AList v3.46.0 and later store the user role as a list of ints serialized
into a text column (`"[1]"`, `"[2]"`), while OpenList has always used a
single int. AutoMigrate rebuilds the column as integer but leaves the old
values textual, so `WHERE role = 2` never matches `[2]` and both
GetAdmin() and GetGuest() return gorm.ErrRecordNotFound.

Because the guest lookup fails, initGuest inserts a second `guest` row,
violates uni_x_users_username and kills the process with Fatalf: anyone
upgrading from AList v3.46+ gets a restart loop whose only error lives in
data/log/log.log, not in docker logs. Even without the crash the
administrator silently loses admin rights.

Normalize the legacy values right after AutoMigrate, keeping the most
privileged role when a row lists several, and let initGuest take over an
existing `guest` row (forced disabled) instead of refusing to boot.

Fixes OpenListTeam#3261

Co-Authored-By: WorkBuddy AI Agent <noreply@workbuddy.cn>
@jyxjjj

jyxjjj commented Oct 25, 2026

Copy link
Copy Markdown
Member

NOT COMPATIBLE

@jyxjjj jyxjjj closed this Oct 25, 2026
@jyxjjj

jyxjjj commented Oct 25, 2026

Copy link
Copy Markdown
Member

If you have these requirements, please perform manual modifications to the JSON from the import/export feature.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[BUG] Upgrading from AList v3.46+ crash-loops on startup: UNIQUE constraint failed: x_users.username (2067)

2 participants