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:
co-authored by
Claude Opus 5.5
parent
c6ab082f8c
commit
2714fdb1a5
+135
@@ -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()
|
||||
}
|
||||
Reference in New Issue
Block a user