package main import ( "database/sql" "fmt" "strings" _ "modernc.org/sqlite" ) // importTable beschreibt eine Tabelle für den einmaligen Umzug SQLite -> Postgres. // Die Spalten heißen in beiden Datenbanken gleich; nur user heißt jetzt account. type importTable struct { src, dst string cols []string identity bool // hat eine id-Identity-Spalte, deren Sequenz nachgezogen werden muss } var importTables = []importTable{ {"user", "account", []string{"id", "uid", "username", "password", "created_at", "last_login", "avatar"}, true}, {"session", "session", []string{"id", "uid", "value", "created_at", "expires", "description"}, true}, {"entry", "entry", []string{"id", "pid", "uid", "created_at", "content", "filepath", "reply_to", "reply_count", "last_activity", "deleted", "bump_count", "last_bump"}, true}, {"vote", "vote", []string{"id", "uid", "pid", "mode"}, true}, {"impression", "impression", []string{"day", "anon_ip", "path", "asn", "hits"}, false}, {"report", "report", []string{"id", "pid", "uid", "reason", "created_at", "handled"}, true}, } // importSQLite kopiert alle Daten aus der SQLite-Datei path in die (per initDB // bereits verbundene und mit Schema versehene) Postgres-Datenbank. Alles in einer // Transaktion: entweder ist der Umzug vollständig oder es ist nichts passiert. // Bricht ab, wenn Zieltabellen schon Daten enthalten -- ein zweiter Lauf soll // nichts doppeln. func importSQLite(path string) error { src, err := sql.Open("sqlite", "file:"+path+"?mode=ro&immutable=1") if err != nil { return err } defer src.Close() if err := src.Ping(); err != nil { return fmt.Errorf("sqlite öffnen: %w", err) } var check string if err := src.QueryRow(`PRAGMA integrity_check`).Scan(&check); err != nil || check != "ok" { return fmt.Errorf("sqlite integrity_check: %q %v", check, err) } for _, t := range importTables { var n int64 if err := db.QueryRow(`SELECT COUNT(*) FROM ` + t.dst).Scan(&n); err != nil { return err } if n > 0 { return fmt.Errorf("zieltabelle %s ist nicht leer (%d zeilen) -- abbruch", t.dst, n) } } tx, err := db.Begin() if err != nil { return err } defer tx.Rollback() for _, t := range importTables { var exists int src.QueryRow(`SELECT COUNT(*) FROM sqlite_master WHERE type = 'table' AND name = ?`, t.src).Scan(&exists) if exists == 0 { fmt.Printf("%-10s -> %-10s fehlt in sqlite, übersprungen\n", t.src, t.dst) continue } n, err := copyTable(src, tx, t) if err != nil { return fmt.Errorf("%s: %w", t.src, err) } var got int64 if err := tx.QueryRow(`SELECT COUNT(*) FROM ` + t.dst).Scan(&got); err != nil { return err } if got != n { return fmt.Errorf("%s: %d gelesen, aber %d in postgres", t.src, n, got) } if t.identity { if _, err := tx.Exec(fmt.Sprintf( `SELECT setval(pg_get_serial_sequence('%s', 'id'), COALESCE(MAX(id), 0) + 1, false) FROM %s`, t.dst, t.dst)); err != nil { return fmt.Errorf("%s: sequenz: %w", t.dst, err) } } fmt.Printf("%-10s -> %-10s %6d zeilen\n", t.src, t.dst, n) } return tx.Commit() } func copyTable(src *sql.DB, tx *sql.Tx, t importTable) (int64, error) { cols := strings.Join(t.cols, ", ") rows, err := src.Query(`SELECT ` + cols + ` FROM "` + t.src + `"`) if err != nil { return 0, err } defer rows.Close() ph := make([]string, len(t.cols)) for i := range ph { ph[i] = fmt.Sprintf("$%d", i+1) } override := "" if t.identity { override = " OVERRIDING SYSTEM VALUE" } stmt, err := tx.Prepare(`INSERT INTO ` + t.dst + ` (` + cols + `)` + override + ` VALUES (` + strings.Join(ph, ", ") + `)`) if err != nil { return 0, err } defer stmt.Close() var n int64 vals := make([]any, len(t.cols)) ptrs := make([]any, len(t.cols)) for i := range vals { ptrs[i] = &vals[i] } for rows.Next() { if err := rows.Scan(ptrs...); err != nil { return n, err } if _, err := stmt.Exec(vals...); err != nil { return n, fmt.Errorf("zeile %d: %w", n+1, err) } n++ } return n, rows.Err() }