Clone
1
Datenbank-Schema
ScriptSorcerer edited this page 2026-09-23 23:35:07 +02:00

Datenbank-Schema

← Zurück zur Übersicht · Funktionen: Datenbank-Repositories

Diese Seite beschreibt alle Tabellen und wie sie zusammenhängen.

Bereich Tabellen Angelegt in
Benutzer users, user_settings schema.ts
Chats chat_sessions, chat_messages, chat_message_versions, chat_memories, persona_chat_state schema.ts
Personas personas, persona_versions, prompt_templates schema.ts
Marktplatz persona_market, persona_market_favorites, persona_market_ratings, persona_market_reports schema.ts
Anhänge attachments, message_attachments, attachment_vision_analyses, upload_deletions attachments.ts

Beziehungsdiagramm

Durchgezogene Linie = echter Fremdschlüssel (SQLite prüft ihn). Gestrichelte Linie = nur logische Beziehung (Spalte verweist auf eine andere Tabelle, wird aber nicht von der Datenbank geprüft, sondern vom Code).

erDiagram
    users ||--o| user_settings : "hat"
    users ||--o{ chat_memories : "besitzt"
    users ||--o{ persona_chat_state : "besitzt"
    users ||--o{ attachments : "lädt hoch"
    users ||..o{ chat_sessions : "besitzt"
    users ||..o{ personas : "besitzt"
    users ||..o{ prompt_templates : "besitzt"
    users ||..o{ persona_market : "veröffentlicht"
    users ||..o{ persona_market_favorites : "favorisiert"
    users ||..o{ persona_market_ratings : "bewertet"
    users ||..o{ persona_market_reports : "meldet"

    chat_sessions ||--o{ chat_messages : "enthält"
    chat_sessions ||--o{ chat_memories : "hat"
    chat_sessions ||--o| persona_chat_state : "hat"
    chat_sessions |o--o{ chat_sessions : "Abzweigung von"
    chat_messages |o--o{ chat_sessions : "Abzweigpunkt"
    chat_messages |o..o{ chat_sessions : "aktives Blatt"
    chat_messages |o--o{ chat_messages : "Parent"
    chat_messages ||--o{ chat_message_versions : "Versionen"
    chat_messages ||--o{ message_attachments : "hat"

    attachments ||--o{ message_attachments : "verwendet in"
    attachments ||--o{ attachment_vision_analyses : "Analysen"
    attachments ||..o| upload_deletions : "Trigger"

    personas |o--o{ chat_sessions : "Persona im Chat"
    personas |o--o{ chat_memories : "bezieht sich auf"
    personas |o--o{ persona_chat_state : "bezieht sich auf"
    personas |o--o{ user_settings : "aktive Persona"
    personas ||--o{ persona_versions : "Versionen"
    personas |o--o{ prompt_templates : "Vorlagen"
    personas |o..o{ persona_market : "veröffentlicht als"
    persona_market |o..o{ personas : "gesammelt als"
    persona_market ||--o{ persona_market_favorites : "hat"
    persona_market ||--o{ persona_market_ratings : "hat"
    persona_market ||--o{ persona_market_reports : "hat"

Lösch- und Umbenennungsverhalten

Aufräumen passiert an zwei Stellen: teils automatisch durch Fremdschlüssel in der Datenbank, teils von Hand in repositories.js.

Automatisch durch die Datenbank (ON DELETE)

Wird gelöscht Folge
chat_sessions Löscht alle chat_messages, chat_memories und persona_chat_state des Chats. Abzweigungen dieses Chats behalten ihren Inhalt, source_chat_id wird NULL
chat_messages Löscht chat_message_versions und message_attachments. Kind-Nachrichten bleiben, ihr parent_message_id wird NULL
personas Löscht persona_versions und prompt_templates der Persona. In Chats, Memories, Chat-State und Settings wird der Verweis NULL
persona_market Löscht Favoriten, Bewertungen und Meldungen
attachments Löscht message_attachments und attachment_vision_analyses, trägt die Datei in upload_deletions ein (Trigger)
users Löscht user_settings, chat_memories, persona_chat_state, attachments

Von Hand im Code

Aktion Funktion Was passiert
Benutzer löschen deleteUserTransaction Löscht nacheinander Marktplatz-Favoriten, -Bewertungen, -Meldungen, veröffentlichte Personas, Vorlagen, Persona-Versionen, Memories, Chat-State, Chats, Settings, Personas und zuletzt den Benutzer. Anhänge werden danach automatisch per CASCADE entfernt
Benutzer umbenennen renameUserTransaction Ändert den Namen in users und in allen Tabellen mit Benutzerbezug. attachments wird per ON UPDATE CASCADE automatisch angepasst
Persona löschen clearPersonaReferences + deletePersona Entfernt die Persona aus allen Chats und löscht ihren Marktplatz-Eintrag, danach die Persona selbst

Wird eine neue Tabelle mit Benutzerbezug angelegt, muss sie in beiden Transaktionen ergänzt werden.


Tabellen

Benutzer

users

Spalte Typ Hinweis
username TEXT PK
password TEXT Passwort-Hash, nie Klartext
session_version INTEGER Default 0. Wird erhöht, um alle Sessions des Benutzers ungültig zu machen (z. B. nach Passwortänderung)

user_settings (1:1 zu users)

Spalte Typ Hinweis
username TEXT PK, FK → users (CASCADE)
active_persona_id INTEGER FK → personas (SET NULL)
active_user_persona_id INTEGER FK → personas (SET NULL), die aktuell gewählte User-Persona

Chats

chat_sessions

Spalte Typ Hinweis
id INTEGER PK
username TEXT Besitzer (logisch → users)
title TEXT Gespeicherter Titel, siehe Hinweis unten
assistant_persona_id INTEGER FK → personas (SET NULL)
user_persona_id INTEGER FK → personas (SET NULL)
folder_name TEXT Ordner in der Seitenleiste
is_pinned INTEGER Boolean
archived_at DATETIME NULL = nicht archiviert
scenario_prompt, scenario_summary TEXT Rollenspiel-Szenario und Kurzfassung
context_summary TEXT Automatische Zusammenfassung älterer Nachrichten
context_summary_message_id INTEGER Bis zu dieser Nachricht reicht die Zusammenfassung (logisch → chat_messages)
active_leaf_message_id INTEGER Letzte Nachricht des sichtbaren Zweigs (logisch → chat_messages), siehe Nachrichtenbaum
source_chat_id INTEGER FK → chat_sessions (SET NULL). Gesetzt, wenn dieser Chat eine Abzweigung eines anderen ist
branched_from_message_id INTEGER FK → chat_messages (SET NULL). Nachricht, an der abgezweigt wurde
preferred_model, temperature, context_length, response_length, system_instruction Modell-Einstellungen pro Chat
created_at, updated_at DATETIME updated_at bestimmt die Sortierung in der Liste

Angezeigter Titel: Hat ein Chat eine Assistant-Persona und ist keine Abzweigung, wird der Name der Persona als Titel angezeigt. Der gespeicherte Titel steht dann in stored_title. Diese Logik steckt in den SELECT-Abfragen in repositories.js.

Hauptchat: Pro Kombination aus Assistant- und User-Persona gibt es einen Hauptchat (source_chat_id IS NULL), den getChatByParticipants findet.

chat_messages

Spalte Typ Hinweis
id INTEGER PK
chat_id INTEGER FK → chat_sessions (CASCADE)
role TEXT user oder bot
content TEXT Aktuell angezeigter Text
thinking TEXT Reasoning-Ausgabe des Modells
sources TEXT Websuche-Quellen als JSON
model_name TEXT Modell, das die Antwort erzeugt hat
retry_variants TEXT JSON-Array aller Antwortvarianten (Kopie aus chat_message_versions)
retry_active_index INTEGER Welche Variante angezeigt wird
retry_retries_used INTEGER Anzahl genutzter Wiederholungen
retry_prompt_message_id INTEGER Zugehörige User-Nachricht (logisch → chat_messages)
delivery_status TEXT complete (Default), failed, sowie ein Zwischenstatus während der Generierung (wird im Chat-Service gesetzt)
error_message TEXT Fehlertext bei failed
parent_message_id INTEGER FK → chat_messages (SET NULL), Vorgänger im Baum
created_at DATETIME

chat_message_versions

Alle Fassungen einer Nachricht (Regenerieren, Bearbeiten).

Spalte Typ Hinweis
id INTEGER PK
message_id INTEGER FK → chat_messages (CASCADE), indiziert
content, thinking, sources, model_name TEXT Inhalt dieser Fassung
created_at DATETIME

chat_memories

Fakten, die sich das Modell in einem Chat merkt.

Spalte Typ Hinweis
id INTEGER PK
username TEXT FK → users (CASCADE)
chat_id INTEGER FK → chat_sessions (CASCADE)
assistant_persona_id, user_persona_id INTEGER FK → personas (SET NULL)
fact TEXT Der Fakt
created_at, updated_at DATETIME

persona_chat_state (1:1 zu chat_sessions)

Rollenspiel-Zustand eines Chats. chat_id ist UNIQUE, Änderungen laufen per Upsert.

Spalte Typ Hinweis
id INTEGER PK
username TEXT FK → users (CASCADE)
chat_id INTEGER FK → chat_sessions (CASCADE), UNIQUE
assistant_persona_id, user_persona_id INTEGER FK → personas (SET NULL)
relationship_notes, tone, current_location, goals, unresolved_threads, boundaries TEXT Zustand der Unterhaltung
created_at, updated_at DATETIME

Personas

personas

Spalte Typ Hinweis
id INTEGER PK
username TEXT Besitzer (logisch → users)
name TEXT
pronouns, appearance, background, details, example_dialogues TEXT Beschreibung
custom_fields TEXT Eigene Felder als JSON
persona_type TEXT assistant (Default) oder user
source_market_id INTEGER Gesetzt, wenn aus dem Marktplatz gesammelt (logisch → persona_market)
source_creator_username TEXT Ursprünglicher Ersteller
created_at, updated_at DATETIME

persona_versions

Bei jeder Änderung einer Persona wird der vorherige Stand als Version gespeichert.

Spalte Typ Hinweis
id INTEGER PK
persona_id INTEGER FK → personas (CASCADE)
username TEXT
version_number INTEGER Fortlaufend ab 1
snapshot_json TEXT Stand der Persona als JSON (erzeugt von buildPersonaSnapshot)
created_at DATETIME

prompt_templates

Spalte Typ Hinweis
id INTEGER PK
username TEXT Besitzer. NULL = System-Vorlage, sichtbar für alle, bearbeitbar nur als Benutzer system
persona_id INTEGER FK → personas (CASCADE). NULL = für alle Personas
name, description, category TEXT
prompt_text TEXT Der Prompt
starter_text TEXT Optionaler Einstiegstext
is_shared INTEGER Boolean. Geteilte Vorlagen sehen alle Benutzer
created_at, updated_at DATETIME

Marktplatz

persona_market

Eine Kopie der Persona zum Zeitpunkt der Veröffentlichung. Änderungen am Original wirken erst nach erneutem Veröffentlichen.

Spalte Typ Hinweis
id INTEGER PK
persona_id INTEGER Original-Persona (logisch → personas)
creator_username TEXT Ersteller
name, pronouns, appearance, background, details, example_dialogues TEXT Kopie der Persona
persona_type TEXT assistant oder user
tags TEXT JSON-Array
usage_count INTEGER Wie oft gesammelt
favorite_count INTEGER Zwischengespeicherte Anzahl, wird nach jeder Änderung neu gezählt
rating_total, rating_count INTEGER Summe und Anzahl der Bewertungen. Durchschnitt wird in Abfragen als rating_average berechnet
moderation_status TEXT Default approved. rejected wird öffentlich ausgeblendet
moderation_notes TEXT
soft_deleted_at DATETIME NULL = sichtbar
created_at, updated_at DATETIME

UNIQUE (persona_id, creator_username): Jede Persona kann nur einmal veröffentlicht werden, erneutes Veröffentlichen aktualisiert den Eintrag.

persona_market_favorites · persona_market_ratings

Spalte Typ Hinweis
market_id INTEGER PK, FK → persona_market (CASCADE)
username TEXT PK
rating INTEGER nur in _ratings
created_at (updated_at) DATETIME

Zusammengesetzter Primärschlüssel: ein Favorit bzw. eine Bewertung pro Benutzer und Eintrag.

persona_market_reports

Spalte Typ Hinweis
id INTEGER PK
market_id INTEGER FK → persona_market (CASCADE)
reporter_username TEXT
reason, details TEXT
status TEXT Default open. Offene Meldungen werden zuerst gelistet
created_at, updated_at DATETIME

Anhänge

attachments

Metadaten hochgeladener Bilder. Die Datei selbst liegt im Upload-Verzeichnis.

Spalte Typ Hinweis
id TEXT PK
username TEXT FK → users (CASCADE, auch ON UPDATE)
original_name TEXT Ursprünglicher Dateiname
storage_key TEXT UNIQUE, Dateiname im Speicher, nie ans Frontend geben
mime_type TEXT
size_bytes, width, height INTEGER
status TEXT temporary (Default) oder attached, per CHECK erzwungen
created_at TEXT
vision_model, vision_analysis, vision_analyzed_at TEXT Ältere Einzelanalyse, siehe Auffälligkeiten

message_attachments (n:m Nachricht ↔ Anhang)

Spalte Typ Hinweis
message_id INTEGER PK, FK → chat_messages (CASCADE)
attachment_id TEXT PK, FK → attachments (CASCADE), indiziert
position INTEGER Reihenfolge der Bilder

attachment_vision_analyses

Zwischenspeicher für Bildanalysen, damit dasselbe Bild nicht mehrfach analysiert wird.

Spalte Typ Hinweis
attachment_id TEXT PK, FK → attachments (CASCADE)
vision_model TEXT PK
prompt_version INTEGER PK
question TEXT PK
analysis TEXT Ergebnis
analyzed_at TEXT

upload_deletions

Warteschlange für Dateien, die die App vom Speicher löschen muss. Einzige Spalte: storage_key (PK).

Lebenszyklus eines Anhangs

flowchart TD
    A["Bild hochgeladen"] --> B["attachments<br/>status = temporary"]
    B -->|"Nachricht gesendet"| C["status = attached<br/>+ Zeile in message_attachments"]
    C -->|"Nachricht oder Chat gelöscht"| D["message_attachments-Zeile gelöscht"]
    D -->|"Trigger release_message_attachment<br/>(keine weiteren Verweise)"| E["attachments-Zeile gelöscht"]
    B -->|"temporärer Upload verworfen"| E
    E -->|"Trigger queue_attachment_file_delete"| F["storage_key in upload_deletions"]
    F --> G["App löscht die Datei vom Speicher"]

Nachrichtenbaum

Nachrichten eines Chats sind keine flache Liste, sondern ein Baum. Jede Nachricht verweist über parent_message_id auf ihren Vorgänger. So können aus einer Stelle mehrere Verläufe entstehen, z. B. durch Bearbeiten einer Frage.

flowchart TD
    M1["1 · user: Hallo"] --> M2["2 · bot: Hi!"]
    M2 --> M3["3 · user: Erzähl einen Witz"]
    M3 --> M4["4 · bot: Witz"]
    M2 --> M5["5 · user: Erzähl ein Rätsel<br/>(bearbeitete Frage)"]
    M5 --> M6["6 · bot: Rätsel"]
  • Aktiver Zweig: chat_sessions.active_leaf_message_id zeigt auf das sichtbare Ende, z. B. 6. Die App läuft von dort über die Parents zurück zur Wurzel: 6 → 5 → 2 → 1. Das machen die rekursiven Abfragen (WITH RECURSIVE branch ...) in repositories.js.
  • Geschwister: Nachrichten mit gleichem Parent und gleicher Rolle (hier 3 und 5) sind Alternativen, zwischen denen der Benutzer wechseln kann.
  • Fehlgeschlagene Antworten (delivery_status = 'failed') bleiben im Baum, werden aber beim Aufbau des Modellkontexts übersprungen.

Drei Arten von „Alternativen“

Mechanismus Wo gespeichert Wofür
Versionen einer Nachricht chat_message_versions (vollständig) und retry_variants (Kopie der Inhalte für die Anzeige) Regenerieren einer Antwort, Bearbeiten. saveMessageVersion hält beide synchron
Zweige im Baum parent_message_id + active_leaf_message_id Verschiedene Gesprächsverläufe im selben Chat
Chat-Abzweigung neuer Chat mit source_chat_id + branched_from_message_id Eigener Chat, der an einer bestimmten Nachricht eines anderen Chats beginnt

Migrationen

runMigrations bringt ältere Datenbanken auf den aktuellen Stand. Die Funktion läuft bei jedem Start, ändert aber nur, was noch fehlt (idempotent).

Schritt Was passiert Wann aktiv
chat_message_versions anlegen Tabelle + Index Immer, auch bei neuen DBs (existiert nur hier)
ensureColumn(...) Liest Spalten per PRAGMA table_info, führt ALTER TABLE ... ADD COLUMN nur bei fehlender Spalte aus Nur bei alten DBs. Ausnahme: chat_messages.sources und thinking existieren nur hier, werden also auch bei neuen DBs angelegt
UPDATE ... WHERE ... IS NULL Setzt Default-Werte in alten Zeilen Läuft jedes Mal (liest die ganze Tabelle)
Nachrichtenbaum aufbauen Verknüpft jede Nachricht mit ihrer Vorgängerin (LAG), setzt das aktive Blatt Einmalig, wenn parent_message_id neu angelegt wurde
Alte Tabelle chats übernehmen Pro Benutzer ein Chat „Imported chat“ mit allen alten Nachrichten, in einer Transaktion Einmalig, wenn chats existiert und chat_messages leer ist
chats löschen DROP TABLE chats Wenn dropLegacyChats (Config DROP_LEGACY_CHATS, Default true)

Geplante Verbesserung: versionierte Migrationen mit PRAGMA user_version, damit jede Migration genau einmal läuft (siehe TODO in schema.ts).


Bekannte Auffälligkeiten

Punkt Details
Fehlende Fremdschlüssel auf users chat_sessions, personas, prompt_templates, persona_versions und alle Marktplatz-Tabellen verweisen nur logisch auf users. Das Aufräumen übernimmt deleteUserTransaction. SQLite kann Fremdschlüssel nicht per ALTER TABLE nachrüsten, dafür muss die Tabelle neu erstellt werden
Umbenennen trotz Fremdschlüssel (zu prüfen) renameUserTransaction ändert zuerst users.username. user_settings, chat_memories und persona_chat_state haben einen Fremdschlüssel auf users ohne ON UPDATE CASCADE. Hat der Benutzer dort Einträge, schlägt das Umbenennen vermutlich mit FOREIGN KEY constraint failed fehl. Mit einem Test prüfen
sources, thinking, chat_message_versions Nur in runMigrations definiert, nicht in initializeSchema
user_settings.active_persona_id Wird gelesen, aber in repositories.js nirgends gesetzt
Zwei Orte für Bildanalysen attachments.vision_* und attachment_vision_analyses. Prüfen, ob die Spalten noch verwendet werden
Uneinheitliche Zeitstempel DATETIME in schema.ts, TEXT in attachments.ts. In SQLite gleichwertig, aber inkonsistent
Bewertungen ohne Wertebereich persona_market_ratings.rating hat keinen CHECK, der Bereich wird nur im Code geprüft