diff --git a/backend/services/dbmigration/portable_data.go b/backend/services/dbmigration/portable_data.go index 16ff9f8..4b497b8 100644 --- a/backend/services/dbmigration/portable_data.go +++ b/backend/services/dbmigration/portable_data.go @@ -182,12 +182,18 @@ func copyPortableTables(ctx context.Context, source *sql.Conn, target *sql.DB, p return err } if plan.Target.Type == config.DatabaseTypePostgreSQL && types["id"] != "" { - // 显式复制主键后必须推进序列,否则重启后的新增记录会冲突。 quoted := quotePostgreSQLIdentifier(table) - query := "SELECT setval(pg_get_serial_sequence($1, 'id'), COALESCE(MAX(id), 1), MAX(id) IS NOT NULL) FROM " + quoted - if _, err := transaction.ExecContext(ctx, query, quoted); err != nil { + var sequence sql.NullString + if err := transaction.QueryRowContext(ctx, "SELECT pg_get_serial_sequence($1, 'id')", quoted).Scan(&sequence); err != nil { return ErrSQLDataVerification } + // 只有绑定序列的主键需要推进;日志等文本主键不能参与数值聚合。 + if sequence.Valid { + query := "SELECT setval($1::regclass, COALESCE(MAX(id), 1), MAX(id) IS NOT NULL) FROM " + quoted + if _, err := transaction.ExecContext(ctx, query, sequence.String); err != nil { + return ErrSQLDataVerification + } + } } } if err := synchronizeSequences(ctx, transaction, plan, sequences); err != nil { diff --git a/backend/services/dbmigration/portable_integration_test.go b/backend/services/dbmigration/portable_integration_test.go index 05e3c8d..aa83362 100644 --- a/backend/services/dbmigration/portable_integration_test.go +++ b/backend/services/dbmigration/portable_integration_test.go @@ -94,9 +94,11 @@ func runPortableIntegrationChain(t *testing.T, inputs []TargetInput) { } var subject string queryErr := connection.QueryRowContext(ctx, "SELECT subject FROM email WHERE id=82").Scan(&subject) + var logPayload string + logErr := connection.QueryRowContext(ctx, "SELECT payload FROM mail_log WHERE id='migration-log'").Scan(&logPayload) _, insertErr := connection.ExecContext(ctx, "INSERT INTO "+quoteDatabaseIdentifier(target.Type, "user")+" (account,name,password) VALUES ('next_hop_"+string(rune('a'+index))+"','sequence','test-only')") _ = connection.Close() - if queryErr != nil || subject != "中文主题" || insertErr != nil { + if queryErr != nil || subject != "中文主题" || logErr != nil || logPayload != `{"message":"中文日志"}` || insertErr != nil { t.Fatal("target content or identity sequence was not preserved") } source = target diff --git a/backend/services/dbmigration/portable_test.go b/backend/services/dbmigration/portable_test.go index f5133c0..0af2244 100644 --- a/backend/services/dbmigration/portable_test.go +++ b/backend/services/dbmigration/portable_test.go @@ -34,6 +34,7 @@ func portableFixture(t *testing.T) (string, config.DatabaseConfig) { `INSERT INTO "user" (id,account,name,password) VALUES (40,'review','测试','hash')`, `INSERT INTO email (id,subject,text,create_time) VALUES (82,'中文主题','正文😀','2026-09-05 12:30:00.123456')`, `INSERT INTO sessions (token,data,expiry) VALUES ('token',x'0001ff',julianday('2026-09-12 12:00:00'))`, + `INSERT INTO mail_log (id,created_at,kind,actor_id,action,section,result,payload) VALUES ('migration-log',1,'mail','40','迁移验证','','success','{"message":"中文日志"}')`, `INSERT INTO plugin_user_setting (plugin_id,user_id,schema_version,settings_ciphertext,settings_hash,enabled,created_at,updated_at) VALUES ('test',40,1,'cipher','hash',1,'2026-09-05 12:00:00','2026-09-05 12:00:00')`, } { if _, err := database.Exec(query); err != nil { diff --git a/test/backend/log_store_integration_test.go b/test/backend/log_store_integration_test.go index 0e3b508..e6e5afd 100644 --- a/test/backend/log_store_integration_test.go +++ b/test/backend/log_store_integration_test.go @@ -73,7 +73,17 @@ func TestLogStoreServerDatabaseIntegration(t *testing.T) { t.Fatal("dedicated test database is unavailable") } migrations := migration.DefaultMigrations() - for _, statement := range migrations[len(migrations)-2].StatementsFor(test.dialect) { + var logSchema []string + for _, item := range migrations { + if item.Name == "create_managed_logs" { + logSchema = item.StatementsFor(test.dialect) + break + } + } + if len(logSchema) == 0 { + t.Fatal("managed log schema migration is missing") + } + for _, statement := range logSchema { if strings.HasPrefix(statement, "CREATE TABLE IF NOT EXISTS ") { statement = strings.Replace(statement, "CREATE TABLE IF NOT EXISTS ", "CREATE TEMPORARY TABLE ", 1) }