feat: 实现 SQLite 迁移备份、完整表结构与简单写入队列
This commit is contained in:
+144
-15
@@ -1,36 +1,165 @@
|
||||
package store
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestOpenAndMigrate(t *testing.T) {
|
||||
func TestOpenEmptyDirCreatesTables(t *testing.T) {
|
||||
t.Parallel()
|
||||
dir := t.TempDir()
|
||||
db, err := OpenWriter(dir, "FULL")
|
||||
|
||||
db, err := Open(dir, "FULL")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer func() { _ = db.Close() }()
|
||||
|
||||
if migErr := Migrate(db); migErr != nil {
|
||||
t.Fatal(migErr)
|
||||
assertMigrationCount(t, db.Write, 2)
|
||||
for _, table := range []string{
|
||||
"endpoints", "settings", "talk_grants", "groups", "group_members",
|
||||
"messages", "message_bodies", "deliveries", "receipts", "send_keys",
|
||||
"admin_sessions", "api_tokens", "schema_migrations",
|
||||
} {
|
||||
assertTableExists(t, db.Write, table)
|
||||
}
|
||||
if migErr := Migrate(db); migErr != nil {
|
||||
t.Fatal(migErr)
|
||||
// 空目录首次启动不应产生迁移备份。
|
||||
if entries, readErr := os.ReadDir(filepath.Join(dir, "backup")); readErr == nil && len(entries) > 0 {
|
||||
t.Fatalf("unexpected backup files on empty start: %d", len(entries))
|
||||
}
|
||||
}
|
||||
|
||||
func TestOpenIdempotentNoRemigrate(t *testing.T) {
|
||||
t.Parallel()
|
||||
dir := t.TempDir()
|
||||
|
||||
db1, err := Open(dir, "FULL")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
assertMigrationCount(t, db1.Write, 2)
|
||||
_ = db1.Close()
|
||||
|
||||
db2, err := Open(dir, "FULL")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer func() { _ = db2.Close() }()
|
||||
assertMigrationCount(t, db2.Write, 2)
|
||||
|
||||
if entries, readErr := os.ReadDir(filepath.Join(dir, "backup")); readErr == nil && len(entries) > 0 {
|
||||
t.Fatalf("idempotent reopen should not backup: %v", entries)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMigrateBackupWhenNewVersion(t *testing.T) {
|
||||
t.Parallel()
|
||||
dir := t.TempDir()
|
||||
|
||||
// 模拟仅应用了 0001 的旧库。
|
||||
w, err := OpenWriter(dir, "FULL")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, execErr := w.Exec(`
|
||||
CREATE TABLE schema_migrations (
|
||||
version INTEGER PRIMARY KEY,
|
||||
applied_at INTEGER NOT NULL
|
||||
)`); execErr != nil {
|
||||
t.Fatal(execErr)
|
||||
}
|
||||
if _, execErr := w.Exec(`INSERT INTO schema_migrations(version, applied_at) VALUES(1, ?)`, time.Now().UnixMilli()); execErr != nil {
|
||||
t.Fatal(execErr)
|
||||
}
|
||||
_ = w.Close()
|
||||
|
||||
db, err := Open(dir, "FULL")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer func() { _ = db.Close() }()
|
||||
|
||||
assertMigrationCount(t, db.Write, 2)
|
||||
assertTableExists(t, db.Write, "endpoints")
|
||||
assertTableExists(t, db.Write, "api_tokens")
|
||||
|
||||
backupDir := filepath.Join(dir, "backup")
|
||||
entries, err := os.ReadDir(backupDir)
|
||||
if err != nil {
|
||||
t.Fatalf("backup dir missing: %v", err)
|
||||
}
|
||||
found := false
|
||||
for _, e := range entries {
|
||||
if strings.HasPrefix(e.Name(), "pre-migrate-") && strings.HasSuffix(e.Name(), ".db") {
|
||||
found = true
|
||||
info, statErr := e.Info()
|
||||
if statErr != nil {
|
||||
t.Fatal(statErr)
|
||||
}
|
||||
if info.Size() == 0 {
|
||||
t.Fatalf("backup file empty: %s", e.Name())
|
||||
}
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
t.Fatal("expected pre-migrate-*.db backup")
|
||||
}
|
||||
}
|
||||
|
||||
func TestWriteQueueOneOpOneTx(t *testing.T) {
|
||||
t.Parallel()
|
||||
dir := t.TempDir()
|
||||
db, err := Open(dir, "FULL")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer func() { _ = db.Close() }()
|
||||
|
||||
ctx := context.Background()
|
||||
err = db.Queue.Do(ctx, func(tx *sql.Tx) error {
|
||||
_, execErr := tx.Exec(
|
||||
`INSERT INTO settings(key, value, updated_at) VALUES(?, ?, ?)`,
|
||||
"registration_enabled", "0", time.Now().UnixMilli(),
|
||||
)
|
||||
return execErr
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
var n int
|
||||
if scanErr := db.QueryRow(`SELECT COUNT(*) FROM schema_migrations`).Scan(&n); scanErr != nil {
|
||||
var value string
|
||||
if scanErr := db.Read.QueryRow(`SELECT value FROM settings WHERE key = ?`, "registration_enabled").Scan(&value); scanErr != nil {
|
||||
t.Fatal(scanErr)
|
||||
}
|
||||
if n != 1 {
|
||||
t.Fatalf("want 1 migration row, got %d", n)
|
||||
if value != "0" {
|
||||
t.Fatalf("want 0, got %q", value)
|
||||
}
|
||||
}
|
||||
|
||||
func assertMigrationCount(t *testing.T, db *sql.DB, want int) {
|
||||
t.Helper()
|
||||
var n int
|
||||
if err := db.QueryRow(`SELECT COUNT(*) FROM schema_migrations`).Scan(&n); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if n != want {
|
||||
t.Fatalf("migration rows: want %d got %d", want, n)
|
||||
}
|
||||
}
|
||||
|
||||
func assertTableExists(t *testing.T, db *sql.DB, name string) {
|
||||
t.Helper()
|
||||
var got string
|
||||
err := db.QueryRow(
|
||||
`SELECT name FROM sqlite_master WHERE type='table' AND name=?`,
|
||||
name,
|
||||
).Scan(&got)
|
||||
if err != nil {
|
||||
t.Fatalf("table %s missing: %v", name, err)
|
||||
}
|
||||
nested, openErr := OpenWriter(filepath.Join(dir, "nested"), "NORMAL")
|
||||
if openErr != nil {
|
||||
t.Fatal(openErr)
|
||||
}
|
||||
_ = nested.Close()
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user