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).
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
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.
- 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 |