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
+56 -67
View File
@@ -2,54 +2,58 @@ package main
import (
"database/sql"
"strings"
"errors"
_ "modernc.org/sqlite"
"github.com/jackc/pgx/v5/pgconn"
_ "github.com/jackc/pgx/v5/stdlib"
)
var db *sql.DB
// Zeitstempel sind Unix-Sekunden (BIGINT) statt timestamptz: das JSON-Format
// der API liefert sie so aus, und die Bump-Rechnung arbeitet direkt damit.
// "user" ist in Postgres ein reserviertes Wort, daher heißt die Tabelle account.
const schema = `
CREATE TABLE IF NOT EXISTS user (
id INTEGER PRIMARY KEY AUTOINCREMENT,
uid INTEGER UNIQUE,
username TEXT UNIQUE,
password BLOB,
created_at INTEGER,
last_login INTEGER,
avatar TEXT NOT NULL DEFAULT ''
CREATE TABLE IF NOT EXISTS account (
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
uid BIGINT NOT NULL UNIQUE,
username TEXT NOT NULL UNIQUE,
password BYTEA NOT NULL,
created_at BIGINT NOT NULL,
last_login BIGINT,
avatar TEXT NOT NULL DEFAULT ''
);
CREATE TABLE IF NOT EXISTS session (
id INTEGER PRIMARY KEY AUTOINCREMENT,
uid INTEGER,
value TEXT UNIQUE,
created_at INTEGER,
expires INTEGER,
description TEXT
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
uid BIGINT NOT NULL,
value TEXT NOT NULL UNIQUE,
created_at BIGINT NOT NULL,
expires BIGINT NOT NULL,
description TEXT NOT NULL DEFAULT ''
);
CREATE TABLE IF NOT EXISTS entry (
id INTEGER PRIMARY KEY AUTOINCREMENT,
pid INTEGER UNIQUE NOT NULL,
uid INTEGER NOT NULL,
created_at INTEGER NOT NULL,
content TEXT NOT NULL DEFAULT '',
filepath TEXT NOT NULL DEFAULT '',
reply_to INTEGER NOT NULL DEFAULT 0,
reply_count INTEGER NOT NULL DEFAULT 0,
last_activity INTEGER NOT NULL DEFAULT 0,
deleted INTEGER NOT NULL DEFAULT 0,
bump_count INTEGER NOT NULL DEFAULT 0,
last_bump INTEGER NOT NULL DEFAULT 0
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
pid BIGINT NOT NULL UNIQUE,
uid BIGINT NOT NULL,
created_at BIGINT NOT NULL,
content TEXT NOT NULL DEFAULT '',
filepath TEXT NOT NULL DEFAULT '',
reply_to BIGINT NOT NULL DEFAULT 0,
reply_count BIGINT NOT NULL DEFAULT 0,
last_activity BIGINT NOT NULL DEFAULT 0,
deleted BIGINT NOT NULL DEFAULT 0,
bump_count BIGINT NOT NULL DEFAULT 0,
last_bump BIGINT NOT NULL DEFAULT 0
);
CREATE TABLE IF NOT EXISTS vote (
id INTEGER PRIMARY KEY AUTOINCREMENT,
uid INTEGER,
pid INTEGER,
mode TEXT,
UNIQUE(uid, pid)
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
uid BIGINT NOT NULL,
pid BIGINT NOT NULL,
mode TEXT NOT NULL,
UNIQUE (uid, pid)
);
-- Seitenaufrufe, aggregiert pro (Tag, anonymisiertes Netz, Pfad). Es wird nie
@@ -59,11 +63,11 @@ CREATE TABLE IF NOT EXISTS vote (
-- deterministisch. Aggregation statt Eventlog -> die Tabelle wächst nur mit
-- Tag × Netz × Pfad, nicht pro Request.
CREATE TABLE IF NOT EXISTS impression (
day TEXT NOT NULL,
anon_ip TEXT NOT NULL,
path TEXT NOT NULL DEFAULT '',
asn TEXT NOT NULL DEFAULT '',
hits INTEGER NOT NULL DEFAULT 0,
day TEXT NOT NULL,
anon_ip TEXT NOT NULL,
path TEXT NOT NULL DEFAULT '',
asn TEXT NOT NULL DEFAULT '',
hits BIGINT NOT NULL DEFAULT 0,
PRIMARY KEY (day, anon_ip, path)
);
@@ -73,12 +77,12 @@ CREATE TABLE IF NOT EXISTS impression (
-- Absender nachziehen zu können, ohne das Schema zu ändern. handled: 0 offen,
-- 1 von der Moderation erledigt. reason ist ein optionaler Freitext.
CREATE TABLE IF NOT EXISTS report (
id INTEGER PRIMARY KEY AUTOINCREMENT,
pid INTEGER NOT NULL,
uid INTEGER NOT NULL DEFAULT 0,
reason TEXT NOT NULL DEFAULT '',
created_at INTEGER NOT NULL,
handled INTEGER NOT NULL DEFAULT 0
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
pid BIGINT NOT NULL,
uid BIGINT NOT NULL DEFAULT 0,
reason TEXT NOT NULL DEFAULT '',
created_at BIGINT NOT NULL,
handled BIGINT NOT NULL DEFAULT 0
);
-- Indizes für die häufigen Zugriffspfade; ohne sie werden Feed (ORDER BY
@@ -95,35 +99,20 @@ CREATE INDEX IF NOT EXISTS idx_report_open ON report(handled, created_at
func initDB(dsn string) error {
var err error
// SQLite hat genau einen Schreiber: busy_timeout wartet bei Sperren, statt
// sofort mit SQLITE_BUSY zu scheitern; WAL erlaubt parallele Leser.
db, err = sql.Open("sqlite", dsn+"?_pragma=busy_timeout(5000)&_pragma=journal_mode(WAL)&_pragma=foreign_keys(on)")
db, err = sql.Open("pgx", dsn)
if err != nil {
return err
}
if err = db.Ping(); err != nil {
return err
}
if _, err = db.Exec(schema); err != nil {
return err
}
return migrate()
_, err = db.Exec(schema)
return err
}
// migrate ergänzt Spalten, die nach dem ersten Deploy dazukamen. CREATE TABLE IF
// NOT EXISTS fasst eine bestehende Tabelle nicht an -> neue Spalten brauchen ein
// ALTER. SQLite kann ADD COLUMN nicht "IF NOT EXISTS", deshalb schlucken wir den
// "duplicate column"-Fehler: bei einer frischen DB sind die Spalten schon aus
// dem Schema da, bei einer alten werden sie hier nachgezogen.
func migrate() error {
alters := []string{
`ALTER TABLE entry ADD COLUMN bump_count INTEGER NOT NULL DEFAULT 0`,
`ALTER TABLE entry ADD COLUMN last_bump INTEGER NOT NULL DEFAULT 0`,
}
for _, stmt := range alters {
if _, err := db.Exec(stmt); err != nil && !strings.Contains(err.Error(), "duplicate column") {
return err
}
}
return nil
// isUniqueViolation meldet, ob err am UNIQUE-Constraint constraint scheiterte
// (Postgres-Namen nach Schema <tabelle>_<spalte>_key, z. B. account_username_key).
func isUniqueViolation(err error, constraint string) bool {
var pgErr *pgconn.PgError
return errors.As(err, &pgErr) && pgErr.Code == "23505" && pgErr.ConstraintName == constraint
}