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
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user