From d88ecfdac597fce81be2e211fbbeed42a2b10d26 Mon Sep 17 00:00:00 2001 From: ScriptSorcerer Date: Wed, 23 Sep 2026 23:34:41 +0200 Subject: [PATCH] Datenbank aktualisiert --- ...8KI-%C3%BCberpr%C3%BCfung-n%C3%B6tig%29.md | 491 ------------------ Datenbank.md | 239 +++++++++ 2 files changed, 239 insertions(+), 491 deletions(-) delete mode 100644 Datenbank-Schema-%28KI-%C3%BCberpr%C3%BCfung-n%C3%B6tig%29.md create mode 100644 Datenbank.md diff --git a/Datenbank-Schema-%28KI-%C3%BCberpr%C3%BCfung-n%C3%B6tig%29.md b/Datenbank-Schema-%28KI-%C3%BCberpr%C3%BCfung-n%C3%B6tig%29.md deleted file mode 100644 index 5e4a0cb..0000000 --- a/Datenbank-Schema-%28KI-%C3%BCberpr%C3%BCfung-n%C3%B6tig%29.md +++ /dev/null @@ -1,491 +0,0 @@ -# Datenbank - -Die App verwendet **SQLite** (über `better-sqlite3`). Das Schema wird an drei Stellen aufgebaut: - -| Datei | Funktion | Aufgabe | -|---|---|---| -| `src/db/schema.js` | `initializeSchema(db)` | Legt die Grundtabellen an | -| `src/db/schema.js` | `runMigrations(db, { dropLegacyChats })` | Ergänzt fehlende Spalten/Tabellen in bestehenden Datenbanken, migriert Altdaten | -| `src/db/attachments.js` | `initializeAttachments(db)` | Tabellen und Trigger für Bild-Uploads | - -**Wichtig:** Das vollständige Schema ergibt sich erst aus allen drei Funktionen zusammen. -Die Tabelle `chat_message_versions` und die Spalten `chat_messages.sources` und -`chat_messages.thinking` existieren z. B. nur in `runMigrations`. - -## Allgemeine Konventionen - -- **`PRAGMA foreign_keys = ON`**: SQLite prüft Fremdschlüssel nur mit dieser Einstellung. - Sie muss bei jeder Verbindung gesetzt werden. -- **`PRAGMA journal_mode = WAL`**: Erlaubt gleichzeitiges Lesen während geschrieben wird. -- **Booleans** werden als `INTEGER` gespeichert (`0` = false, `1` = true), - z. B. `is_pinned`, `is_shared`. -- **JSON-Daten** werden als `TEXT` gespeichert (z. B. `retry_variants`, `sources`, - `custom_fields`, `tags`, `snapshot_json`) und im Code mit `JSON.parse` gelesen. -- **Zeitstempel** werden mit `DEFAULT CURRENT_TIMESTAMP` in UTC gesetzt. -- Spaltennamen in der DB sind `snake_case`, im JavaScript-Code `camelCase` - (siehe z. B. `attachmentMetadata`). - -## Übersicht der Bereiche - -| Bereich | Tabellen | -|---|---| -| Benutzer | `users`, `user_settings` | -| Chats | `chat_sessions`, `chat_messages`, `chat_message_versions`, `chat_memories`, `persona_chat_state` | -| Personas | `personas`, `persona_versions`, `prompt_templates` | -| Marktplatz | `persona_market`, `persona_market_favorites`, `persona_market_ratings`, `persona_market_reports` | -| Anhänge | `attachments`, `message_attachments`, `attachment_vision_analyses`, `upload_deletions` | - -## Beziehungsdiagramm - -Legende: **durchgezogene Linie** = echter Fremdschlüssel (von SQLite geprüft), -**gestrichelte Linie** = nur logische Beziehung (Spalte verweist auf eine andere Tabelle, -aber ohne `FOREIGN KEY`, wird also nicht von der DB geprüft). - -```mermaid -erDiagram - users { - TEXT username PK - } - user_settings { - TEXT username PK, FK - INTEGER active_persona_id FK - INTEGER active_user_persona_id FK - } - chat_sessions { - INTEGER id PK - TEXT username - INTEGER assistant_persona_id FK - INTEGER user_persona_id FK - INTEGER source_chat_id FK - INTEGER branched_from_message_id FK - INTEGER active_leaf_message_id - } - chat_messages { - INTEGER id PK - INTEGER chat_id FK - INTEGER parent_message_id FK - INTEGER retry_prompt_message_id - } - chat_message_versions { - INTEGER id PK - INTEGER message_id FK - } - chat_memories { - INTEGER id PK - TEXT username FK - INTEGER chat_id FK - INTEGER assistant_persona_id FK - INTEGER user_persona_id FK - } - persona_chat_state { - INTEGER id PK - TEXT username FK - INTEGER chat_id FK, UK - INTEGER assistant_persona_id FK - INTEGER user_persona_id FK - } - personas { - INTEGER id PK - TEXT username - INTEGER source_market_id - } - persona_versions { - INTEGER id PK - INTEGER persona_id FK - } - prompt_templates { - INTEGER id PK - TEXT username - INTEGER persona_id FK - } - persona_market { - INTEGER id PK - INTEGER persona_id - TEXT creator_username - } - persona_market_favorites { - INTEGER market_id PK, FK - TEXT username PK - } - persona_market_ratings { - INTEGER market_id PK, FK - TEXT username PK - } - persona_market_reports { - INTEGER id PK - INTEGER market_id FK - TEXT reporter_username - } - attachments { - TEXT id PK - TEXT username FK - TEXT storage_key UK - } - message_attachments { - INTEGER message_id PK, FK - TEXT attachment_id PK, FK - } - attachment_vision_analyses { - TEXT attachment_id PK, FK - TEXT vision_model PK - INTEGER prompt_version PK - TEXT question PK - } - upload_deletions { - TEXT storage_key PK - } - - 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{ persona_market : "veröffentlicht" - users ||..o{ prompt_templates : "besitzt" - - 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 : "Quelle für Branch" - chat_messages |o--o{ chat_sessions : "Abzweigpunkt" - chat_messages |o--o{ chat_messages : "Parent" - chat_messages ||--o{ chat_message_versions : "hat Versionen" - chat_messages ||--o{ message_attachments : "hat" - attachments ||--o{ message_attachments : "wird verwendet in" - attachments ||--o{ attachment_vision_analyses : "hat Analysen" - attachments ||..o| upload_deletions : "Trigger" - - personas |o--o{ chat_sessions : "Assistant-/User-Persona" - 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 : "hat Versionen" - personas ||--o{ prompt_templates : "hat" - personas |o..o{ persona_market : "veröffentlicht als" - persona_market |o..o{ personas : "importiert als" - persona_market ||--o{ persona_market_favorites : "hat" - persona_market ||--o{ persona_market_ratings : "hat" - persona_market ||--o{ persona_market_reports : "hat" -``` - -### Lösch-Verhalten (ON DELETE) - -| Wird gelöscht | Folge | -|---|---| -| `users` | Löscht `user_settings`, `chat_memories`, `persona_chat_state`, `attachments` (CASCADE). **Nicht** automatisch: `chat_sessions`, `personas`, `prompt_templates`, Marktplatz-Einträge (kein FK) | -| `chat_sessions` | Löscht alle `chat_messages`, `chat_memories`, `persona_chat_state` des Chats | -| `chat_messages` | Löscht `chat_message_versions` und `message_attachments`. Kind-Nachrichten behalten ihren Platz, `parent_message_id` wird `NULL` | -| `personas` | Löscht `persona_versions`, `prompt_templates`. In Chats, Memories und Settings wird der Verweis auf `NULL` gesetzt | -| `persona_market` | Löscht Favoriten, Bewertungen und Meldungen | -| `attachments` | Löscht `message_attachments`, `attachment_vision_analyses` und trägt die Datei in `upload_deletions` ein (Trigger) | - -## Tabellen - -### Benutzer - -#### `users` -Registrierte Benutzer. - -| Spalte | Typ | Hinweis | -|---|---|---| -| `username` | TEXT | **PK** | -| `password` | TEXT | Passwort-Hash | -| `session_version` | INTEGER | Default `0`. Wird erhöht, um alle Sessions eines Benutzers ungültig zu machen | - -#### `user_settings` -Einstellungen pro Benutzer (1:1 zu `users`). - -| Spalte | Typ | Hinweis | -|---|---|---| -| `username` | TEXT | **PK**, FK → `users` (CASCADE) | -| `active_persona_id` | INTEGER | FK → `personas` (SET NULL), aktive Assistant-Persona | -| `active_user_persona_id` | INTEGER | FK → `personas` (SET NULL), aktive User-Persona | - -### Chats - -#### `chat_sessions` -Ein Chat bzw. eine Unterhaltung. - -| Spalte | Typ | Hinweis | -|---|---|---| -| `id` | INTEGER | **PK**, Autoincrement | -| `username` | TEXT | Besitzer (logisch → `users`, kein FK) | -| `title` | TEXT | Chat-Titel | -| `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, Default `0` | -| `archived_at` | DATETIME | `NULL` = nicht archiviert | -| `scenario_prompt` | TEXT | Szenario-Beschreibung | -| `scenario_summary` | TEXT | Kurzfassung des Szenarios | -| `context_summary` | TEXT | Automatische Zusammenfassung des bisherigen Verlaufs | -| `context_summary_message_id` | INTEGER | Bis zu welcher Nachricht die Zusammenfassung reicht. Default `0` | -| `active_leaf_message_id` | INTEGER | Letzte Nachricht des aktuell sichtbaren Zweigs (logisch → `chat_messages`) | -| `source_chat_id` | INTEGER | FK → `chat_sessions` (SET NULL), Ursprungs-Chat bei Branches | -| `branched_from_message_id` | INTEGER | FK → `chat_messages` (SET NULL), Nachricht, an der abgezweigt wurde | -| `preferred_model` | TEXT | Bevorzugtes Modell für diesen Chat | -| `temperature` | REAL | Modell-Parameter | -| `context_length` | INTEGER | Modell-Parameter | -| `response_length` | INTEGER | Modell-Parameter | -| `system_instruction` | TEXT | Zusätzliche System-Anweisung | -| `created_at`, `updated_at` | DATETIME | | - -#### `chat_messages` -Einzelne Nachrichten. Nachrichten bilden über `parent_message_id` einen **Baum**, damit -Verzweigungen (z. B. durch Regenerieren oder Bearbeiten) möglich sind. - -| Spalte | Typ | Hinweis | -|---|---|---| -| `id` | INTEGER | **PK**, Autoincrement | -| `chat_id` | INTEGER | FK → `chat_sessions` (CASCADE) | -| `role` | TEXT | z. B. `user`, `bot` | -| `content` | TEXT | Nachrichtentext | -| `thinking` | TEXT | Reasoning-Ausgabe des Modells (nur via Migration) | -| `sources` | TEXT | Websuche-Quellen als JSON (nur via Migration) | -| `model_name` | TEXT | Verwendetes Modell | -| `retry_variants` | TEXT | Alternative Antworten als JSON | -| `retry_active_index` | INTEGER | Aktuell angezeigte Variante. Default `0` | -| `retry_retries_used` | INTEGER | Anzahl genutzter Wiederholungen. Default `0` | -| `retry_prompt_message_id` | INTEGER | Zugehörige User-Nachricht (logisch → `chat_messages`) | -| `delivery_status` | TEXT | Default `'complete'` | -| `error_message` | TEXT | Fehlertext bei fehlgeschlagener Antwort | -| `parent_message_id` | INTEGER | FK → `chat_messages` (SET NULL), Vorgänger im Baum | -| `created_at` | DATETIME | | - -#### `chat_message_versions` -Frühere Fassungen einer Nachricht (z. B. nach Bearbeiten oder Regenerieren). - -| Spalte | Typ | Hinweis | -|---|---|---| -| `id` | INTEGER | **PK** | -| `message_id` | INTEGER | FK → `chat_messages` (CASCADE), indiziert | -| `content`, `thinking`, `sources`, `model_name` | TEXT | Stand der Nachricht zu diesem Zeitpunkt | -| `created_at` | DATETIME | | - -#### `chat_memories` -Fakten, die sich das Modell innerhalb eines Chats 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 gemerkte Fakt | -| `created_at`, `updated_at` | DATETIME | | - -#### `persona_chat_state` -Rollenspiel-Zustand eines Chats (1:1 zu `chat_sessions`, wegen `UNIQUE` auf `chat_id`). - -| 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` -Charaktere, entweder für den Assistenten oder für den Benutzer selbst. - -| Spalte | Typ | Hinweis | -|---|---|---| -| `id` | INTEGER | **PK** | -| `username` | TEXT | Besitzer (logisch → `users`, kein FK) | -| `name` | TEXT | | -| `pronouns`, `appearance`, `background`, `details` | TEXT | Beschreibung | -| `custom_fields` | TEXT | Eigene Felder als JSON | -| `persona_type` | TEXT | `'assistant'` (Default) oder User-Persona | -| `source_market_id` | INTEGER | Falls aus dem Marktplatz importiert (logisch → `persona_market`) | -| `source_creator_username` | TEXT | Ursprünglicher Ersteller | -| `example_dialogues` | TEXT | Beispieldialoge | -| `created_at`, `updated_at` | DATETIME | | - -#### `persona_versions` -Versionsverlauf einer Persona. - -| Spalte | Typ | Hinweis | -|---|---|---| -| `id` | INTEGER | **PK** | -| `persona_id` | INTEGER | FK → `personas` (CASCADE) | -| `username` | TEXT | Wer die Version erstellt hat | -| `version_number` | INTEGER | Fortlaufende Nummer | -| `snapshot_json` | TEXT | Vollständiger Stand der Persona als JSON | -| `created_at` | DATETIME | | - -#### `prompt_templates` -Wiederverwendbare Prompt-Vorlagen. - -| Spalte | Typ | Hinweis | -|---|---|---| -| `id` | INTEGER | **PK** | -| `username` | TEXT | Besitzer, darf `NULL` sein (z. B. System-Vorlagen) | -| `persona_id` | INTEGER | FK → `personas` (CASCADE), optional | -| `name`, `description`, `category` | TEXT | | -| `prompt_text` | TEXT | Der eigentliche Prompt | -| `starter_text` | TEXT | Optionaler Einstiegstext | -| `is_shared` | INTEGER | Boolean, Default `0` | -| `created_at`, `updated_at` | DATETIME | | - -### Marktplatz - -#### `persona_market` -Öffentlich geteilte Personas. Eine Kopie der Persona, damit spätere Änderungen am Original -den Marktplatz-Eintrag nicht automatisch verändern. - -| Spalte | Typ | Hinweis | -|---|---|---| -| `id` | INTEGER | **PK** | -| `persona_id` | INTEGER | Original-Persona (logisch → `personas`, kein FK) | -| `creator_username` | TEXT | Ersteller | -| `name`, `pronouns`, `appearance`, `background`, `details`, `example_dialogues` | TEXT | Kopie der Persona-Daten | -| `persona_type` | TEXT | Default `'assistant'` | -| `tags` | TEXT | Schlagwörter | -| `usage_count` | INTEGER | Wie oft importiert. Default `0` | -| `favorite_count` | INTEGER | Zwischengespeicherte Anzahl Favoriten | -| `rating_total`, `rating_count` | INTEGER | Summe und Anzahl der Bewertungen (Durchschnitt = total / count) | -| `moderation_status` | TEXT | Default `'approved'` | -| `moderation_notes` | TEXT | | -| `soft_deleted_at` | DATETIME | `NULL` = sichtbar. Einträge werden nicht wirklich gelöscht | -| `created_at`, `updated_at` | DATETIME | | - -**UNIQUE** (`persona_id`, `creator_username`): Eine Persona kann pro Ersteller nur einmal veröffentlicht werden. - -#### `persona_market_favorites` -| Spalte | Typ | Hinweis | -|---|---|---| -| `market_id` | INTEGER | **PK**, FK → `persona_market` (CASCADE) | -| `username` | TEXT | **PK** | -| `created_at` | DATETIME | | - -Zusammengesetzter PK: Jeder Benutzer kann einen Eintrag nur einmal favorisieren. - -#### `persona_market_ratings` -| Spalte | Typ | Hinweis | -|---|---|---| -| `market_id` | INTEGER | **PK**, FK → `persona_market` (CASCADE) | -| `username` | TEXT | **PK** | -| `rating` | INTEGER | Bewertung | -| `created_at`, `updated_at` | DATETIME | | - -Zusammengesetzter PK: Eine Bewertung pro Benutzer und Eintrag (kann aktualisiert werden). - -#### `persona_market_reports` -Meldungen von Benutzern zur Moderation. - -| Spalte | Typ | Hinweis | -|---|---|---| -| `id` | INTEGER | **PK** | -| `market_id` | INTEGER | FK → `persona_market` (CASCADE) | -| `reporter_username` | TEXT | | -| `reason` | TEXT | | -| `details` | TEXT | | -| `status` | TEXT | Default `'open'` | -| `created_at`, `updated_at` | DATETIME | | - -### Anhänge - -#### `attachments` -Hochgeladene Bilder. Die Datei selbst liegt im Upload-Verzeichnis, die DB speichert nur Metadaten. - -| 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 | -| `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 | Einzelne Bildanalyse (siehe auch `attachment_vision_analyses`) | - -#### `message_attachments` -Verknüpfungstabelle Nachricht ↔ Anhang (n:m). - -| Spalte | Typ | Hinweis | -|---|---|---| -| `message_id` | INTEGER | **PK**, FK → `chat_messages` (CASCADE) | -| `attachment_id` | TEXT | **PK**, FK → `attachments` (CASCADE), indiziert | -| `position` | INTEGER | Reihenfolge der Bilder in der Nachricht | - -#### `attachment_vision_analyses` -Zwischengespeicherte Bildanalysen. Pro Bild, Modell, Prompt-Version und Frage wird -das Ergebnis nur einmal berechnet. - -| 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 vom Speicher gelöscht werden müssen. - -| Spalte | Typ | Hinweis | -|---|---|---| -| `storage_key` | TEXT | **PK** | - -### Lebenszyklus eines Anhangs - -Die Datenbank kann keine Dateien löschen. Deshalb tragen Trigger die zu löschenden -Dateien in `upload_deletions` ein, und die App räumt sie danach im Dateisystem auf. - -```mermaid -flowchart TD - A[Bild hochgeladen] --> B["attachments
status = temporary"] - B -->|Nachricht gesendet| C["status = attached
+ Eintrag in message_attachments"] - C -->|Nachricht oder Chat gelöscht| D[message_attachments-Zeile gelöscht] - D -->|"Trigger release_message_attachment
(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 Datei vom Speicher] -``` - -**Trigger:** - -- **`release_message_attachment`**: Nach dem Löschen einer Verknüpfung wird der Anhang gelöscht, - wenn er `attached` ist und von keiner anderen Nachricht mehr verwendet wird. -- **`queue_attachment_file_delete`**: Nach dem Löschen eines Anhangs wird dessen - `storage_key` in `upload_deletions` eingetragen. - -## Migrationen - -`runMigrations` ist so geschrieben, dass sie **beliebig oft** laufen kann: - -- `CREATE TABLE IF NOT EXISTS` legt nur fehlende Tabellen an. -- `ensureColumn` liest die vorhandenen Spalten per `PRAGMA table_info` und führt - `ALTER TABLE ... ADD COLUMN` nur aus, wenn die Spalte fehlt. -- `UPDATE ... WHERE ... IS NULL` füllt Default-Werte für alte Zeilen nach. - -**Einmalige Datenmigrationen:** - -- **Nachrichtenbaum:** Wenn `parent_message_id` neu angelegt wurde, wird jede Nachricht mit - ihrer Vorgängernachricht im selben Chat verknüpft (`LAG`-Fensterfunktion), und - `active_leaf_message_id` wird auf die letzte Nachricht gesetzt. -- **Alte `chats`-Tabelle:** Existiert die frühere Tabelle `chats` und ist `chat_messages` - leer, werden alle alten Nachrichten pro Benutzer in einen Chat „Imported chat“ übernommen. - Mit `dropLegacyChats = true` (Config `DROP_LEGACY_CHATS`) wird `chats` danach gelöscht. - -## Bekannte Auffälligkeiten - -- **Fehlende Fremdschlüssel auf `users`:** `chat_sessions`, `personas`, `prompt_templates`, - `persona_versions` und die Marktplatz-Tabellen verweisen nur logisch auf `users`. - Beim Löschen eines Benutzers bleiben diese Daten erhalten, sofern der Code sie nicht - selbst löscht. -- **Doppelte Tabellendefinitionen:** Mehrere Tabellen werden sowohl in `initializeSchema` - als auch in `runMigrations` angelegt. Wegen `IF NOT EXISTS` ist das harmlos, muss aber - bei Änderungen an beiden Stellen gepflegt werden. -- **Zwei Orte für Bildanalysen:** `attachments.vision_analysis` und die Tabelle - `attachment_vision_analyses`. Prüfen, ob die Spalten in `attachments` noch verwendet werden. -- **Uneinheitliche Zeitstempel-Typen:** Meist `DATETIME`, in den Anhang-Tabellen `TEXT`. - In SQLite technisch gleichwertig, aber inkonsistent. -- **Bewertungen ohne Wertebereich:** `persona_market_ratings.rating` hat keinen `CHECK`, - der gültige Bereich wird nur im Code geprüft. \ No newline at end of file diff --git a/Datenbank.md b/Datenbank.md new file mode 100644 index 0000000..2a9c191 --- /dev/null +++ b/Datenbank.md @@ -0,0 +1,239 @@ +# 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](Datenbank-Schema) | Alle Tabellen, Beziehungen, Lösch-Verhalten, Nachrichtenbaum, Migrationen | +| [Datenbank-Repositories](Datenbank-Repositories) | Alle Funktionen aus `repositories.js` zum Nachschlagen | + +Siehe auch: [TypeScript](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`.** + +```mermaid +flowchart TD + Browser["Browser"] --> Routes["routes/
HTTP-Anfragen, Eingabeprüfung"] + Routes --> Services["services/
Geschäftslogik"] + Services --> Repos["db/repositories.js
SQL-Abfragen"] + Repos --> DB[("SQLite
app.db")] + Start["App-Start"] -.-> Create["db/index.ts
createDatabase()"] + Create -.-> DB +``` + +## Startablauf + +```mermaid +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)
PRAGMAs + 13 Grundtabellen"] + E --> F["runMigrations(db, config)
fehlende Spalten, Altdaten"] + F --> G["initializeAttachments(db)
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` + +```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](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` | + +```ts +// 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](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: + +```ts +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: `/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: + +```bash +sqlite3 data/app.db +``` + +```sql +.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.** + +```bash +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](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). \ No newline at end of file