1
Datenbank
ScriptSorcerer edited this page 2026-09-23 23:34:41 +02:00
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.

Datenbank

Die App speichert alle Daten in einer SQLite-Datenbank und greift über die Bibliothek better-sqlite3 darauf zu.

Seite Inhalt
Datenbank (diese Seite) Überblick, Dateien, Startablauf, Grundlagen, Notfall-Hilfe
Datenbank-Schema Alle Tabellen, Beziehungen, Lösch-Verhalten, Nachrichtenbaum, Migrationen
Datenbank-Repositories Alle Funktionen aus repositories.js zum Nachschlagen

Siehe auch: TypeScript


Die Dateien auf einen Blick

Datei Sprache Exportiert Aufgabe
src/db/index.ts TypeScript createDatabase Öffnet die Datenbank und baut das Schema auf
src/db/schema.ts TypeScript initializeSchema, runMigrations, MigrationOptions Grundtabellen anlegen, ältere Datenbanken aktualisieren
src/db/attachments.ts TypeScript initializeAttachments, attachmentMetadata Tabellen und Trigger für Bild-Anhänge, Umwandlung für die API
src/db/repositories.js JavaScript createRepositories Alle Lese- und Schreibzugriffe der App (ca. 90 Funktionen)

Wo die Datenbank in der App steht

Die App ist in Schichten aufgebaut. Jede Schicht spricht nur mit der darunter. SQL steht im laufenden Betrieb ausschließlich in repositories.js.

flowchart TD
    Browser["Browser"] --> Routes["routes/<br/>HTTP-Anfragen, Eingabeprüfung"]
    Routes --> Services["services/<br/>Geschäftslogik"]
    Services --> Repos["db/repositories.js<br/>SQL-Abfragen"]
    Repos --> DB[("SQLite<br/>app.db")]
    Start["App-Start"] -.-> Create["db/index.ts<br/>createDatabase()"]
    Create -.-> DB

Startablauf

flowchart TD
    A["createDatabase(config)"] --> B{"dbPath ist :memory: ?"}
    B -- nein --> C["Ordner für die DB-Datei anlegen"]
    B -- ja --> D
    C --> D["new Database(dbPath)"]
    D --> E["initializeSchema(db)<br/>PRAGMAs + 13 Grundtabellen"]
    E --> F["runMigrations(db, config)<br/>fehlende Spalten, Altdaten"]
    F --> G["initializeAttachments(db)<br/>Anhang-Tabellen + Trigger"]
    G --> H["Verbindung zurückgeben"]
    H --> I["createRepositories(db, config)"]

Die Reihenfolge ist wichtig:

  • runMigrations erwartet, dass die Grundtabellen bereits existieren.
  • initializeAttachments verweist per Fremdschlüssel auf users und chat_messages, diese müssen also vorher angelegt sein.

Die Dateien im Detail

src/db/index.ts

export function createDatabase(config: Config): Database.Database

Einstiegspunkt beim App-Start. Verwendet aus der Config:

Config-Feld Verwendung
dbPath Pfad zur Datenbankdatei. ":memory:" = Datenbank nur im Arbeitsspeicher (für Tests, weg nach dem Schließen)
dropLegacyChats Wird an runMigrations weitergereicht

Der Ordner wird mit path.dirname(config.dbPath) angelegt, funktioniert also auch mit einem eigenen DB_PATH außerhalb von data/.

runMigrations(db, config) bekommt die ganze Config, liest aber nur dropLegacyChats heraus. TypeScript erlaubt das, weil Config alle Felder von MigrationOptions enthält (strukturelle Typisierung).

src/db/schema.ts

Funktion Aufgabe
initializeSchema(db) Setzt die PRAGMAs und legt alle Grundtabellen mit CREATE TABLE IF NOT EXISTS an. Bestehende Tabellen werden nicht verändert
runMigrations(db, options) Ergänzt fehlende Spalten und Tabellen in älteren Datenbanken, übernimmt Altdaten. Details: Datenbank-Schema → Migrationen

Enthaltene Typen:

  • ColumnInfo: eine Zeile aus PRAGMA table_info(...) (intern)
  • MigrationOptions: { dropLegacyChats?: boolean } (exportiert)

src/db/attachments.ts

Funktion Aufgabe
initializeAttachments(db) Legt attachments, message_attachments, attachment_vision_analyses, upload_deletions, einen Index und zwei Trigger an
attachmentMetadata(row) Wandelt eine DB-Zeile (snake_case) in das API-Format (camelCase) um. Gibt nur öffentliche Felder zurück, nie storage_key oder username
// Eingabe: Zeile aus der Datenbank
interface AttachmentRow {
    id: string; original_name: string; mime_type: string;
    size_bytes: number; width: number; height: number;
}

// Ausgabe: an das Frontend
interface AttachmentMetadata {
    id: string; type: "image"; originalName: string; mimeType: string;
    sizeBytes: number; width: number; height: number;
}

AttachmentRow enthält nur die Spalten, die attachmentMetadata braucht. Eine vollständige Zeile aus SELECT * passt trotzdem, weil TypeScript zusätzliche Felder erlaubt.

src/db/repositories.js

Die Datenzugriffsschicht (Repository-Pattern). createRepositories(db, config) gibt ein Objekt mit allen Funktionen zurück, die der Rest der App benutzt, z. B. repositories.listChats(username).

Vollständige Referenz: Datenbank-Repositories

Konventionen

Thema Konvention
Benutzer-Identität username (TEXT) ist Primärschlüssel und wird in vielen Tabellen als Verweis gespeichert
Booleans INTEGER mit 0 = false, 1 = true (z. B. is_pinned, is_shared)
JSON Als TEXT gespeichert, im Code mit JSON.parse / JSON.stringify (z. B. tags, sources, retry_variants)
Zeitstempel DEFAULT CURRENT_TIMESTAMP, in UTC
Namen Datenbank: snake_case. JavaScript/API: meist camelCase
Soft Delete Marktplatz-Einträge werden über soft_deleted_at ausgeblendet statt gelöscht

PRAGMA-Einstellungen

PRAGMA ist ein SQLite-spezifischer Befehl, um Einstellungen der Datenbank selbst zu lesen oder zu setzen.

PRAGMA Wo Bedeutung
journal_mode = WAL initializeSchema Lesen bleibt während Schreibvorgängen möglich. Wird in der Datei gespeichert, es entstehen zusätzlich app.db-wal und app.db-shm
foreign_keys = ON initializeSchema Ohne diese Einstellung ignoriert SQLite alle Fremdschlüssel und ON DELETE CASCADE. Gilt nur pro Verbindung
table_info(tabelle) runMigrations Liefert die Spalten einer Tabelle, damit fehlende ergänzt werden können

better-sqlite3 in Kürze

better-sqlite3 arbeitet synchron: Es gibt kein await, Abfragen blockieren kurz den Event-Loop.

Methode Rückgabe Verwendung
db.prepare(sql) Statement Abfrage einmal vorbereiten, danach beliebig oft ausführen. ? sind Platzhalter (Schutz vor SQL-Injection)
stmt.get(...params) Objekt oder undefined Eine Zeile lesen
stmt.all(...params) Array Alle Zeilen lesen
stmt.run(...params) { changes, lastInsertRowid } INSERT, UPDATE, DELETE
db.exec(sql) – Mehrere SQL-Befehle ohne Parameter ausführen (Schema)
db.transaction(fn) Funktion Alle Befehle in fn gelingen gemeinsam oder werden gemeinsam zurückgerollt
db.pragma(sql) abhängig PRAGMA ausführen

In TypeScript liefern .get() und .all() standardmäßig unknown. Den Ergebnistyp gibt man per Generic an:

db.prepare<[string], { count: number }>("SELECT COUNT(*) AS count FROM personas WHERE username = ?")
//         ^^^^^^^^  ^^^^^^^^^^^^^^^^^
//         Parameter  eine Ergebniszeile

Der Typ der Verbindung heißt Database.Database (nicht nur Database, das ist der Konstruktor).


Notfall-Hilfe

Wo liegt die Datenbank?

Standard: <Projektordner>/data/app.db. Änderbar über die Umgebungsvariable DB_PATH. Im WAL-Modus gehören die Dateien app.db-wal und app.db-shm dazu.

Datenbank ansehen

Mit dem SQLite-Kommandozeilenprogramm:

sqlite3 data/app.db
.tables                      -- alle Tabellen
.schema chat_sessions        -- Aufbau einer Tabelle
PRAGMA table_info(personas); -- Spalten einer Tabelle
PRAGMA foreign_key_check;    -- verwaiste Verweise finden
SELECT * FROM users;
.quit

Alternativ grafisch: DB Browser for SQLite.

Backup

Vor jeder Änderung an Schema oder Migrationen ein Backup machen.

sqlite3 data/app.db ".backup data/app-backup.db"

Nicht einfach nur app.db kopieren, solange die App läuft: Im WAL-Modus können Änderungen noch in app.db-wal stehen. .backup berücksichtigt das.

Typische Fehler

Fehlermeldung Ursache Lösung
SqliteError: no such column: xyz Spalte fehlt in einer älteren Datenbank ensureColumn in runMigrations ergänzen
SqliteError: no such table: xyz Tabelle wird nirgends angelegt oder Startreihenfolge falsch initializeSchema prüfen, Reihenfolge in createDatabase prüfen
SqliteError: FOREIGN KEY constraint failed Verweis auf eine Zeile, die nicht existiert (z. B. Chat-ID eines gelöschten Chats) IDs prüfen, PRAGMA foreign_key_check; ausführen
SqliteError: UNIQUE constraint failed Doppelter Eintrag (z. B. Benutzername existiert schon) Vorher prüfen oder Fehler abfangen
SqliteError: database is locked Ein anderer Prozess schreibt gerade (z. B. offenes DB-Programm) Andere Programme schließen, lange Transaktionen vermeiden
attempt to write a readonly database / unable to open database file Keine Schreibrechte auf Datei oder Ordner (oft Docker-Volume) Rechte des data/-Ordners prüfen

Checkliste: Neue Spalte hinzufügen

  1. Spalte im CREATE TABLE in initializeSchema ergänzen (für neue Datenbanken).
  2. ensureColumn(...) in runMigrations ergänzen (für bestehende Datenbanken).
  3. SELECT-Abfragen in repositories.js ergänzen. Bei chat_sessions an vier Stellen: listChatSessions, getChatSession, getChatSessionByParticipants, searchChats.
  4. Falls die Spalte einen Benutzernamen enthält: renameUserTransaction und deleteUserTransaction ergänzen.
  5. Wiki-Seite Datenbank-Schema aktualisieren.

Checkliste: Neue Tabelle hinzufügen

  1. CREATE TABLE IF NOT EXISTS in initializeSchema (bzw. initializeAttachments für Anhänge).
  2. Fremdschlüssel mit passendem ON DELETE festlegen.
  3. Hat die Tabelle eine username-Spalte ohne Fremdschlüssel auf users: in renameUserTransaction und deleteUserTransaction eintragen.
  4. Statements und Funktionen in repositories.js ergänzen.
  5. Wiki aktualisieren (Tabelle, Diagramm, Lösch-Verhalten).