Datenbank von SQLite auf Postgres umgestellt

Verbindung über KVER_DSN; Tabelle user heißt jetzt account (reserviertes
Wort in Postgres). kver import-sqlite übernimmt die alte Datei einmalig in
einer Transaktion.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TiXsPUqw7oeomZ8wZrQW5q
This commit is contained in:
irrlicht
2026-09-26 22:22:07 +02:00
co-authored by Claude Opus 5.5
parent c6ab082f8c
commit 2714fdb1a5
15 changed files with 412 additions and 301 deletions
+135
View File
@@ -0,0 +1,135 @@
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()
}