Files
Kontrollverlust/db.go
T
irrlichtandClaude Opus 5.5 2714fdb1a5 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
2026-09-26 22:22:07 +02:00

119 lines
4.3 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package main
import (
"database/sql"
"errors"
"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 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 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 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 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
-- eine volle IP gespeichert: anon_ip ist auf /16 (IPv4) bzw. /32 (IPv6) gekürzt
-- (siehe maskIP). asn ist das autonome System (Netzbetreiber), aufgelöst aus dem
-- bereits anonymisierten Netz (siehe lookupASN) -- nicht im PK, da pro Netz
-- 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 BIGINT NOT NULL DEFAULT 0,
PRIMARY KEY (day, anon_ip, path)
);
-- Meldungen zu Beiträgen (DSA-Melde-/Abhilfeverfahren). Bewusst ohne Auth:
-- jeder darf melden, weil es eine Moderationshilfe ist. uid ist der Melder,
-- vorerst immer 0 (anonym) -- die Spalte existiert nur, um bei Spam später den
-- 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 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
-- last_activity), Thread (WHERE reply_to) und Vote-Zähler (WHERE pid) mit
-- wachsender Tabelle zu Full-Table-Scans. username/session.value/vote(uid,pid)
-- sind über UNIQUE bereits indiziert.
CREATE INDEX IF NOT EXISTS idx_entry_last_activity ON entry(last_activity);
CREATE INDEX IF NOT EXISTS idx_entry_reply_to ON entry(reply_to);
CREATE INDEX IF NOT EXISTS idx_entry_uid ON entry(uid);
CREATE INDEX IF NOT EXISTS idx_vote_pid ON vote(pid);
CREATE INDEX IF NOT EXISTS idx_session_uid ON session(uid);
CREATE INDEX IF NOT EXISTS idx_report_open ON report(handled, created_at);
`
func initDB(dsn string) error {
var err error
db, err = sql.Open("pgx", dsn)
if err != nil {
return err
}
if err = db.Ping(); err != nil {
return err
}
_, err = db.Exec(schema)
return err
}
// 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
}