package main import ( "database/sql" _ "modernc.org/sqlite" ) var db *sql.DB 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 session ( id INTEGER PRIMARY KEY AUTOINCREMENT, uid INTEGER, value TEXT UNIQUE, created_at INTEGER, expires INTEGER, description TEXT ); 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 ); CREATE TABLE IF NOT EXISTS vote ( id INTEGER PRIMARY KEY AUTOINCREMENT, uid INTEGER, pid INTEGER, mode TEXT, 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 INTEGER NOT NULL DEFAULT 0, PRIMARY KEY (day, anon_ip, path) ); -- 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); ` 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)") if err != nil { return err } if err = db.Ping(); err != nil { return err } _, err = db.Exec(schema) return err }