fix(migration): backfill users.group_users before schema copy (#2934)
Upgrading a v3 database panicked in Schema.Create: v3 bound user groups through a nullable group_id column (or left groupless users NULL), while v4 requires NOT NULL group_users, so the copy to the rebuilt table died on the constraint. A pre-migration repair now adds the column if absent and backfills it - preserving legacy group_id bindings when present and falling back to the first non-admin group - so the copy succeeds. The statements are dialect-portable and no-op on fresh installs where the users table does not yet exist. Authored By: TDvorak <info@tdvorak.dev> Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>pull/3582/head
parent
63066db78a
commit
8d226d2d07
@ -0,0 +1,97 @@
|
|||||||
|
package inventory
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
rawsql "database/sql"
|
||||||
|
"path/filepath"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/cloudreve/Cloudreve/v4/ent"
|
||||||
|
"github.com/cloudreve/Cloudreve/v4/pkg/logging"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
// legacyV3DB builds a minimal v3-shaped sqlite database: users bind their
|
||||||
|
// group via a nullable group_id column, and groupless users carry NULL.
|
||||||
|
func legacyV3DB(t *testing.T, dbPath string) {
|
||||||
|
t.Helper()
|
||||||
|
raw, err := rawsql.Open("sqlite3", "file:"+dbPath)
|
||||||
|
require.NoError(t, err)
|
||||||
|
defer raw.Close()
|
||||||
|
|
||||||
|
stmts := []string{
|
||||||
|
`CREATE TABLE groups (id INTEGER PRIMARY KEY, created_at DATETIME, updated_at DATETIME, name TEXT, permissions BLOB)`,
|
||||||
|
`INSERT INTO groups (id, created_at, updated_at, name, permissions) VALUES
|
||||||
|
(1, '2024-01-01 00:00:00', '2024-01-01 00:00:00', 'Admin', X'00'),
|
||||||
|
(2, '2024-01-01 00:00:00', '2024-01-01 00:00:00', 'User', X'00')`,
|
||||||
|
`CREATE TABLE users (
|
||||||
|
id INTEGER PRIMARY KEY,
|
||||||
|
created_at DATETIME,
|
||||||
|
updated_at DATETIME,
|
||||||
|
deleted_at DATETIME,
|
||||||
|
email VARCHAR(100),
|
||||||
|
nick VARCHAR(100),
|
||||||
|
password VARCHAR(255),
|
||||||
|
status VARCHAR(20),
|
||||||
|
storage BIGINT,
|
||||||
|
group_id INTEGER
|
||||||
|
)`,
|
||||||
|
`INSERT INTO users (created_at, updated_at, email, nick, status, storage, group_id) VALUES
|
||||||
|
('2024-01-01 00:00:00', '2024-01-01 00:00:00', 'admin@x', 'admin', 'active', 0, 1),
|
||||||
|
('2024-01-01 00:00:00', '2024-01-01 00:00:00', 'user@x', 'user', 'active', 0, 2),
|
||||||
|
('2024-01-01 00:00:00', '2024-01-01 00:00:00', 'groupless@x', 'groupless', 'active', 0, NULL)`,
|
||||||
|
}
|
||||||
|
for _, q := range stmts {
|
||||||
|
_, err := raw.Exec(q)
|
||||||
|
require.NoError(t, err, q)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestRepairLegacyUserGroupColumn reproduces upstream #2934: upgrading a v3
|
||||||
|
// database fails inside ent's copy-migration because group_users is NOT NULL
|
||||||
|
// in v4 while v3 rows can be groupless. The repair backfills the column so
|
||||||
|
// Schema.Create succeeds.
|
||||||
|
func TestRepairLegacyUserGroupColumn(t *testing.T) {
|
||||||
|
ctx := context.Background()
|
||||||
|
l := logging.NewConsoleLogger(logging.LevelError)
|
||||||
|
|
||||||
|
openClient := func(t *testing.T) *ent.Client {
|
||||||
|
dbPath := filepath.Join(t.TempDir(), "legacy.db")
|
||||||
|
legacyV3DB(t, dbPath)
|
||||||
|
client, err := ent.Open("sqlite3", "file:"+dbPath)
|
||||||
|
require.NoError(t, err)
|
||||||
|
return client
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Run("schema create fails without repair", func(t *testing.T) {
|
||||||
|
client := openClient(t)
|
||||||
|
defer client.Close()
|
||||||
|
err := client.Schema.Create(ctx)
|
||||||
|
require.Error(t, err)
|
||||||
|
require.Contains(t, err.Error(), "group_users")
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("repair backfills and migration succeeds", func(t *testing.T) {
|
||||||
|
client := openClient(t)
|
||||||
|
defer client.Close()
|
||||||
|
|
||||||
|
repairLegacyUserGroupColumn(l, client, ctx)
|
||||||
|
require.NoError(t, client.Schema.Create(ctx))
|
||||||
|
|
||||||
|
rows, err := client.QueryContext(ctx, `SELECT id, group_users FROM users ORDER BY id`)
|
||||||
|
require.NoError(t, err)
|
||||||
|
defer rows.Close()
|
||||||
|
|
||||||
|
got := map[int]int{}
|
||||||
|
for rows.Next() {
|
||||||
|
var id, gid int
|
||||||
|
require.NoError(t, rows.Scan(&id, &gid))
|
||||||
|
got[id] = gid
|
||||||
|
}
|
||||||
|
require.NoError(t, rows.Err())
|
||||||
|
|
||||||
|
// group_id bindings preserved; groupless user lands on the
|
||||||
|
// non-admin group (id 2), not the admin group.
|
||||||
|
require.Equal(t, map[int]int{1: 1, 2: 2, 3: 2}, got)
|
||||||
|
})
|
||||||
|
}
|
||||||
Loading…
Reference in new issue