import type Database from "better-sqlite3"; /* * TODO: Offene Punkte in Schema und Migrationen * * 1. Fehlendes in initializeSchema ergänzen. * Tabelle chat_message_versions und die Spalten chat_messages.sources und * chat_messages.thinking existieren nur in runMigrations. Nach initializeSchema * verschieben, damit neue Datenbanken nicht von den Migrationen abhängen. * * 2. Doppelte CREATE TABLE-Blöcke am Ende von runMigrations entfernen. * Sie sind identisch zu initializeSchema. Vorher prüfen, ob runMigrations * irgendwo (z. B. in Tests) ohne vorheriges initializeSchema aufgerufen wird. * * 3. Versionierte Migrationen mit PRAGMA user_version einführen. * Jede Migration läuft dann genau einmal. Danach können die ensureColumn-Aufrufe * und die UPDATE ... WHERE ... IS NULL-Zeilen (Full Table Scan bei jedem Start) * in eine einmalige Migration wandern. * * 4. Fehlende Fremdschlüssel auf users prüfen: * chat_sessions, personas, prompt_templates, persona_versions, persona_market, * persona_market_favorites/_ratings/_reports. * Aktuell erledigt deleteUserTransaction in repositories.js das Aufräumen von Hand. * Achtung: SQLite kann Fremdschlüssel nicht per ALTER TABLE hinzufügen, * dafür muss die Tabelle neu erstellt und die Daten kopiert werden. * * 5. Kleinigkeiten: * - ensureColumn: Parameter tableName wird nicht benutzt. * - persona_market_ratings.rating hat keinen CHECK auf den gültigen Wertebereich. * - Zeitstempel uneinheitlich (DATETIME hier, TEXT in attachments.ts). * - attachments.vision_* vs. Tabelle attachment_vision_analyses: * prüfen, ob die Spalten noch gebraucht werden. * * Doku und Beziehungsdiagramm: Wiki-Seite "Datenbank-Schema". */ /** Eine Zeile aus `PRAGMA table_info(...)`: Beschreibung einer Spalte. */ interface ColumnInfo { cid: number; name: string; type: string; notnull: number; dflt_value: string | null; pk: number; } /** Optionen für {@link runMigrations}. */ export interface MigrationOptions { /** Alte Tabelle `chats` nach der Übernahme löschen. Default: `true`. */ dropLegacyChats?: boolean; } /** * Setzt die Datenbank-Einstellungen und legt alle Grundtabellen an. * * Bestehende Tabellen werden nicht verändert (`IF NOT EXISTS`). * Fehlende Spalten in älteren Datenbanken ergänzt {@link runMigrations}. * * @param db - Offene Datenbankverbindung */ export function initializeSchema(db: Database.Database): void { // WAL: Lesen bleibt möglich, während geschrieben wird (sqlite spezifisch) db.pragma("journal_mode = WAL"); // SQLite prüft Fremdschlüssel nur mit dieser Einstellung (gilt pro Verbindung) db.pragma("foreign_keys = ON"); // Registrierte Benutzer // Chats (Unterhaltungen) inkl. Modell-Einstellungen und Branch-Informationen // Nachrichten eines Chats, als Baum über parent_message_id verknüpft // Fakten, die sich das Modell pro Chat merkt // Rollenspiel-Zustand eines Chats (1:1 zu chat_sessions) // Personas des Benutzers (für den Assistenten oder den Benutzer selbst) // Einstellungen pro Benutzer (1:1 zu users) // Öffentlich geteilte Personas im Marktplatz // Versionsverlauf einer Persona // Favoriten im Marktplatz (ein Eintrag pro Benutzer und Persona) // Bewertungen im Marktplatz (eine pro Benutzer und Persona) // Meldungen zur Moderation im Marktplatz // Wiederverwendbare Prompt-Vorlagen db.prepare(` CREATE TABLE IF NOT EXISTS users ( username TEXT PRIMARY KEY, password TEXT NOT NULL, session_version INTEGER DEFAULT 0 ) `).run(); db.prepare(` CREATE TABLE IF NOT EXISTS chat_sessions ( id INTEGER PRIMARY KEY AUTOINCREMENT, username TEXT NOT NULL, title TEXT NOT NULL, assistant_persona_id INTEGER, user_persona_id INTEGER, folder_name TEXT, is_pinned INTEGER DEFAULT 0, archived_at DATETIME, scenario_prompt TEXT, scenario_summary TEXT, context_summary TEXT, context_summary_message_id INTEGER DEFAULT 0, active_leaf_message_id INTEGER, source_chat_id INTEGER, branched_from_message_id INTEGER, preferred_model TEXT, temperature REAL, context_length INTEGER, response_length INTEGER, system_instruction TEXT, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (assistant_persona_id) REFERENCES personas (id) ON DELETE SET NULL, FOREIGN KEY (user_persona_id) REFERENCES personas (id) ON DELETE SET NULL, FOREIGN KEY (source_chat_id) REFERENCES chat_sessions (id) ON DELETE SET NULL, FOREIGN KEY (branched_from_message_id) REFERENCES chat_messages (id) ON DELETE SET NULL ) `).run(); db.prepare(` CREATE TABLE IF NOT EXISTS chat_messages ( id INTEGER PRIMARY KEY AUTOINCREMENT, chat_id INTEGER NOT NULL, role TEXT NOT NULL, content TEXT NOT NULL, model_name TEXT, retry_variants TEXT, retry_active_index INTEGER DEFAULT 0, retry_retries_used INTEGER DEFAULT 0, retry_prompt_message_id INTEGER, delivery_status TEXT DEFAULT 'complete', error_message TEXT, parent_message_id INTEGER, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (chat_id) REFERENCES chat_sessions (id) ON DELETE CASCADE, FOREIGN KEY (parent_message_id) REFERENCES chat_messages (id) ON DELETE SET NULL ) `).run(); db.prepare(` CREATE TABLE IF NOT EXISTS chat_memories ( id INTEGER PRIMARY KEY AUTOINCREMENT, username TEXT NOT NULL, chat_id INTEGER NOT NULL, assistant_persona_id INTEGER, user_persona_id INTEGER, fact TEXT NOT NULL, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (username) REFERENCES users (username) ON DELETE CASCADE, FOREIGN KEY (chat_id) REFERENCES chat_sessions (id) ON DELETE CASCADE, FOREIGN KEY (assistant_persona_id) REFERENCES personas (id) ON DELETE SET NULL, FOREIGN KEY (user_persona_id) REFERENCES personas (id) ON DELETE SET NULL ) `).run(); db.prepare(` CREATE TABLE IF NOT EXISTS persona_chat_state ( id INTEGER PRIMARY KEY AUTOINCREMENT, username TEXT NOT NULL, chat_id INTEGER NOT NULL UNIQUE, assistant_persona_id INTEGER, user_persona_id INTEGER, relationship_notes TEXT, tone TEXT, current_location TEXT, goals TEXT, unresolved_threads TEXT, boundaries TEXT, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (username) REFERENCES users (username) ON DELETE CASCADE, FOREIGN KEY (chat_id) REFERENCES chat_sessions (id) ON DELETE CASCADE, FOREIGN KEY (assistant_persona_id) REFERENCES personas (id) ON DELETE SET NULL, FOREIGN KEY (user_persona_id) REFERENCES personas (id) ON DELETE SET NULL ) `).run(); db.prepare(` CREATE TABLE IF NOT EXISTS personas ( id INTEGER PRIMARY KEY AUTOINCREMENT, username TEXT NOT NULL, name TEXT NOT NULL, pronouns TEXT, appearance TEXT, background TEXT, details TEXT, custom_fields TEXT, persona_type TEXT DEFAULT 'assistant', source_market_id INTEGER, source_creator_username TEXT, example_dialogues TEXT, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ) `).run(); db.prepare(` CREATE TABLE IF NOT EXISTS user_settings ( username TEXT PRIMARY KEY, active_persona_id INTEGER, active_user_persona_id INTEGER, FOREIGN KEY (username) REFERENCES users (username) ON DELETE CASCADE, FOREIGN KEY (active_persona_id) REFERENCES personas (id) ON DELETE SET NULL, FOREIGN KEY (active_user_persona_id) REFERENCES personas (id) ON DELETE SET NULL ) `).run(); db.prepare(` CREATE TABLE IF NOT EXISTS persona_market ( id INTEGER PRIMARY KEY AUTOINCREMENT, persona_id INTEGER, creator_username TEXT NOT NULL, name TEXT NOT NULL, pronouns TEXT, appearance TEXT, background TEXT, details TEXT, example_dialogues TEXT, persona_type TEXT DEFAULT 'assistant', tags TEXT, usage_count INTEGER DEFAULT 0, favorite_count INTEGER DEFAULT 0, rating_total INTEGER DEFAULT 0, rating_count INTEGER DEFAULT 0, moderation_status TEXT DEFAULT 'approved', moderation_notes TEXT, soft_deleted_at DATETIME, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME DEFAULT CURRENT_TIMESTAMP, UNIQUE (persona_id, creator_username) ) `).run(); db.prepare(` CREATE TABLE IF NOT EXISTS persona_versions ( id INTEGER PRIMARY KEY AUTOINCREMENT, persona_id INTEGER NOT NULL, username TEXT NOT NULL, version_number INTEGER NOT NULL, snapshot_json TEXT NOT NULL, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (persona_id) REFERENCES personas (id) ON DELETE CASCADE ) `).run(); db.prepare(` CREATE TABLE IF NOT EXISTS persona_market_favorites ( market_id INTEGER NOT NULL, username TEXT NOT NULL, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (market_id, username), FOREIGN KEY (market_id) REFERENCES persona_market (id) ON DELETE CASCADE ) `).run(); db.prepare(` CREATE TABLE IF NOT EXISTS persona_market_ratings ( market_id INTEGER NOT NULL, username TEXT NOT NULL, rating INTEGER NOT NULL, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (market_id, username), FOREIGN KEY (market_id) REFERENCES persona_market (id) ON DELETE CASCADE ) `).run(); db.prepare(` CREATE TABLE IF NOT EXISTS persona_market_reports ( id INTEGER PRIMARY KEY AUTOINCREMENT, market_id INTEGER NOT NULL, reporter_username TEXT NOT NULL, reason TEXT NOT NULL, details TEXT, status TEXT DEFAULT 'open', created_at DATETIME DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (market_id) REFERENCES persona_market (id) ON DELETE CASCADE ) `).run(); db.prepare(` CREATE TABLE IF NOT EXISTS prompt_templates ( id INTEGER PRIMARY KEY AUTOINCREMENT, username TEXT, persona_id INTEGER, name TEXT NOT NULL, description TEXT, category TEXT, prompt_text TEXT NOT NULL, starter_text TEXT, is_shared INTEGER DEFAULT 0, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (persona_id) REFERENCES personas (id) ON DELETE CASCADE ) `).run(); } /** * Bringt eine bestehende Datenbank auf den aktuellen Stand. * * Ergänzt fehlende Tabellen und Spalten aus älteren Versionen der App und * übernimmt Daten aus der alten Tabelle `chats`. Läuft bei jedem Start, ändert * aber nur, was noch fehlt (idempotent). * * Muss nach {@link initializeSchema} aufgerufen werden. * * @param db - Offene Datenbankverbindung * @param options - Siehe {@link MigrationOptions} */ export function runMigrations( db: Database.Database, { dropLegacyChats = true }: MigrationOptions = {} ): void { // Versionsverlauf von Nachrichten. // Existiert nur hier, wird also auch bei neuen Datenbanken hier angelegt. db.exec(` CREATE TABLE IF NOT EXISTS chat_message_versions ( id INTEGER PRIMARY KEY AUTOINCREMENT, message_id INTEGER NOT NULL REFERENCES chat_messages(id) ON DELETE CASCADE, content TEXT NOT NULL, thinking TEXT, sources TEXT, model_name TEXT, created_at DATETIME DEFAULT CURRENT_TIMESTAMP ); CREATE INDEX IF NOT EXISTS chat_message_versions_message ON chat_message_versions(message_id); `); /** * Liest die vorhandenen Spalten einer Tabelle. * Der Tabellenname wird direkt eingesetzt, da PRAGMA keine Parameter (?) erlaubt. * Deshalb nur mit festen Tabellennamen aus dem Code aufrufen, nie mit Benutzereingaben. */ const getColumns = (tableName: string): ColumnInfo[] => db.prepare<[], ColumnInfo>(`PRAGMA table_info(${tableName})`).all(); /** Führt `sql` aus, wenn die Spalte `name` in `columns` noch fehlt. */ const ensureColumn = (tableName: string, columns: ColumnInfo[], name: string, sql: string): void => { if (!columns.some((column) => column.name === name)) { db.prepare(sql).run(); } }; // --- users --- const userColumns = getColumns("users"); ensureColumn("users", userColumns, "session_version", "ALTER TABLE users ADD COLUMN session_version INTEGER DEFAULT 0"); db.prepare("UPDATE users SET session_version = 0 WHERE session_version IS NULL").run(); // --- personas --- const personaColumns = getColumns("personas"); ensureColumn("personas", personaColumns, "custom_fields", "ALTER TABLE personas ADD COLUMN custom_fields TEXT"); ensureColumn("personas", personaColumns, "persona_type", "ALTER TABLE personas ADD COLUMN persona_type TEXT DEFAULT 'assistant'"); ensureColumn("personas", personaColumns, "source_market_id", "ALTER TABLE personas ADD COLUMN source_market_id INTEGER"); ensureColumn("personas", personaColumns, "source_creator_username", "ALTER TABLE personas ADD COLUMN source_creator_username TEXT"); ensureColumn("personas", personaColumns, "example_dialogues", "ALTER TABLE personas ADD COLUMN example_dialogues TEXT"); // --- user_settings --- const userSettingsColumns = getColumns("user_settings"); ensureColumn("user_settings", userSettingsColumns, "active_user_persona_id", "ALTER TABLE user_settings ADD COLUMN active_user_persona_id INTEGER"); // --- persona_market --- const marketColumns = getColumns("persona_market"); ensureColumn("persona_market", marketColumns, "persona_id", "ALTER TABLE persona_market ADD COLUMN persona_id INTEGER"); ensureColumn("persona_market", marketColumns, "usage_count", "ALTER TABLE persona_market ADD COLUMN usage_count INTEGER DEFAULT 0"); ensureColumn("persona_market", marketColumns, "example_dialogues", "ALTER TABLE persona_market ADD COLUMN example_dialogues TEXT"); ensureColumn("persona_market", marketColumns, "tags", "ALTER TABLE persona_market ADD COLUMN tags TEXT"); ensureColumn("persona_market", marketColumns, "favorite_count", "ALTER TABLE persona_market ADD COLUMN favorite_count INTEGER DEFAULT 0"); ensureColumn("persona_market", marketColumns, "rating_total", "ALTER TABLE persona_market ADD COLUMN rating_total INTEGER DEFAULT 0"); ensureColumn("persona_market", marketColumns, "rating_count", "ALTER TABLE persona_market ADD COLUMN rating_count INTEGER DEFAULT 0"); ensureColumn("persona_market", marketColumns, "moderation_status", "ALTER TABLE persona_market ADD COLUMN moderation_status TEXT DEFAULT 'approved'"); ensureColumn("persona_market", marketColumns, "moderation_notes", "ALTER TABLE persona_market ADD COLUMN moderation_notes TEXT"); ensureColumn("persona_market", marketColumns, "soft_deleted_at", "ALTER TABLE persona_market ADD COLUMN soft_deleted_at DATETIME"); // NULL-Werte auf den Default setzen (z. B. aus Versionen, in denen explizit NULL gespeichert wurde) db.prepare("UPDATE persona_market SET usage_count = 0 WHERE usage_count IS NULL").run(); db.prepare("UPDATE persona_market SET favorite_count = 0 WHERE favorite_count IS NULL").run(); db.prepare("UPDATE persona_market SET rating_total = 0 WHERE rating_total IS NULL").run(); db.prepare("UPDATE persona_market SET rating_count = 0 WHERE rating_count IS NULL").run(); // --- chat_messages --- const chatMessageColumns = getColumns("chat_messages"); // Nur hier definiert, nicht in initializeSchema: wird also auch bei neuen DBs gebraucht ensureColumn("chat_messages", chatMessageColumns, "sources", "ALTER TABLE chat_messages ADD COLUMN sources TEXT"); ensureColumn("chat_messages", chatMessageColumns, "thinking", "ALTER TABLE chat_messages ADD COLUMN thinking TEXT"); // Muss VOR dem ensureColumn für parent_message_id geprüft werden: // Nur wenn die Spalte neu ist, wird unten der Nachrichtenbaum aufgebaut. const hadParentMessageId = chatMessageColumns.some((column) => column.name === "parent_message_id"); ensureColumn("chat_messages", chatMessageColumns, "model_name", "ALTER TABLE chat_messages ADD COLUMN model_name TEXT"); ensureColumn("chat_messages", chatMessageColumns, "retry_variants", "ALTER TABLE chat_messages ADD COLUMN retry_variants TEXT"); ensureColumn("chat_messages", chatMessageColumns, "retry_active_index", "ALTER TABLE chat_messages ADD COLUMN retry_active_index INTEGER DEFAULT 0"); ensureColumn("chat_messages", chatMessageColumns, "retry_retries_used", "ALTER TABLE chat_messages ADD COLUMN retry_retries_used INTEGER DEFAULT 0"); ensureColumn("chat_messages", chatMessageColumns, "retry_prompt_message_id", "ALTER TABLE chat_messages ADD COLUMN retry_prompt_message_id INTEGER"); ensureColumn("chat_messages", chatMessageColumns, "delivery_status", "ALTER TABLE chat_messages ADD COLUMN delivery_status TEXT DEFAULT 'complete'"); ensureColumn("chat_messages", chatMessageColumns, "error_message", "ALTER TABLE chat_messages ADD COLUMN error_message TEXT"); ensureColumn("chat_messages", chatMessageColumns, "parent_message_id", "ALTER TABLE chat_messages ADD COLUMN parent_message_id INTEGER"); db.prepare("UPDATE chat_messages SET retry_active_index = 0 WHERE retry_active_index IS NULL").run(); db.prepare("UPDATE chat_messages SET retry_retries_used = 0 WHERE retry_retries_used IS NULL").run(); db.prepare("UPDATE chat_messages SET delivery_status = 'complete' WHERE delivery_status IS NULL").run(); // --- chat_sessions --- const chatSessionColumns = getColumns("chat_sessions"); ensureColumn("chat_sessions", chatSessionColumns, "assistant_persona_id", "ALTER TABLE chat_sessions ADD COLUMN assistant_persona_id INTEGER"); ensureColumn("chat_sessions", chatSessionColumns, "user_persona_id", "ALTER TABLE chat_sessions ADD COLUMN user_persona_id INTEGER"); ensureColumn("chat_sessions", chatSessionColumns, "folder_name", "ALTER TABLE chat_sessions ADD COLUMN folder_name TEXT"); ensureColumn("chat_sessions", chatSessionColumns, "is_pinned", "ALTER TABLE chat_sessions ADD COLUMN is_pinned INTEGER DEFAULT 0"); ensureColumn("chat_sessions", chatSessionColumns, "archived_at", "ALTER TABLE chat_sessions ADD COLUMN archived_at DATETIME"); ensureColumn("chat_sessions", chatSessionColumns, "scenario_prompt", "ALTER TABLE chat_sessions ADD COLUMN scenario_prompt TEXT"); ensureColumn("chat_sessions", chatSessionColumns, "scenario_summary", "ALTER TABLE chat_sessions ADD COLUMN scenario_summary TEXT"); ensureColumn("chat_sessions", chatSessionColumns, "context_summary", "ALTER TABLE chat_sessions ADD COLUMN context_summary TEXT"); ensureColumn("chat_sessions", chatSessionColumns, "context_summary_message_id", "ALTER TABLE chat_sessions ADD COLUMN context_summary_message_id INTEGER DEFAULT 0"); ensureColumn("chat_sessions", chatSessionColumns, "active_leaf_message_id", "ALTER TABLE chat_sessions ADD COLUMN active_leaf_message_id INTEGER"); ensureColumn("chat_sessions", chatSessionColumns, "source_chat_id", "ALTER TABLE chat_sessions ADD COLUMN source_chat_id INTEGER"); ensureColumn("chat_sessions", chatSessionColumns, "branched_from_message_id", "ALTER TABLE chat_sessions ADD COLUMN branched_from_message_id INTEGER"); ensureColumn("chat_sessions", chatSessionColumns, "preferred_model", "ALTER TABLE chat_sessions ADD COLUMN preferred_model TEXT"); ensureColumn("chat_sessions", chatSessionColumns, "temperature", "ALTER TABLE chat_sessions ADD COLUMN temperature REAL"); ensureColumn("chat_sessions", chatSessionColumns, "context_length", "ALTER TABLE chat_sessions ADD COLUMN context_length INTEGER"); ensureColumn("chat_sessions", chatSessionColumns, "response_length", "ALTER TABLE chat_sessions ADD COLUMN response_length INTEGER"); ensureColumn("chat_sessions", chatSessionColumns, "system_instruction", "ALTER TABLE chat_sessions ADD COLUMN system_instruction TEXT"); db.prepare("UPDATE chat_sessions SET is_pinned = 0 WHERE is_pinned IS NULL").run(); db.prepare("UPDATE chat_sessions SET context_summary_message_id = 0 WHERE context_summary_message_id IS NULL").run(); // Einmalig: Nachrichtenbaum für alte Datenbanken aufbauen. // Früher waren Nachrichten eine flache Liste. Jede Nachricht bekommt jetzt ihre // Vorgängernachricht im selben Chat als Parent (LAG = "Wert der vorherigen Zeile"), // und jeder Chat zeigt auf seine letzte Nachricht als aktives Blatt. if (!hadParentMessageId) { db.exec(` WITH ordered AS ( SELECT id, LAG(id) OVER (PARTITION BY chat_id ORDER BY id) AS previous_id FROM chat_messages ) UPDATE chat_messages SET parent_message_id = (SELECT previous_id FROM ordered WHERE ordered.id = chat_messages.id) WHERE parent_message_id IS NULL AND EXISTS (SELECT 1 FROM ordered WHERE ordered.id = chat_messages.id AND previous_id IS NOT NULL); UPDATE chat_sessions SET active_leaf_message_id = (SELECT MAX(id) FROM chat_messages WHERE chat_id = chat_sessions.id) WHERE active_leaf_message_id IS NULL; `); } // Tabellen, die in älteren Versionen fehlten. // Identisch zu initializeSchema und dort bereits angelegt, daher eigentlich überflüssig. // [HIER die sechs CREATE TABLE-Blöcke unverändert einfügen: // chat_memories, persona_chat_state, persona_versions, // persona_market_favorites, persona_market_ratings, // persona_market_reports, prompt_templates] // --- Einmalig: Übernahme der alten Tabelle "chats" --- // Früher gab es pro Benutzer nur eine flache Nachrichtenliste ohne Chat-Sessions. const legacyChatsExist = db .prepare("SELECT name FROM sqlite_master WHERE type = 'table' AND name = 'chats'") .get(); if (!legacyChatsExist) { return; } // Nur übernehmen, wenn es noch keine neuen Nachrichten gibt, // damit bei einem erneuten Start nichts doppelt importiert wird. const migrationNeeded = db .prepare<[], { count: number }>("SELECT COUNT(*) AS count FROM chat_messages") .get(); if (migrationNeeded?.count === 0) { const insertChatSessionStmt = db.prepare( "INSERT INTO chat_sessions (username, title, assistant_persona_id, user_persona_id, scenario_prompt, scenario_summary) VALUES (?, ?, ?, ?, ?, ?)" ); const getLegacyUsersStmt = db.prepare<[], { username: string }>( "SELECT DISTINCT username FROM chats" ); const getLegacyChatsStmt = db.prepare<[string], { role: string; content: string }>(` SELECT role, content FROM chats WHERE username = ? ORDER BY datetime(created_at) `); const insertChatMessageStmt = db.prepare( "INSERT INTO chat_messages (chat_id, role, content, retry_variants, retry_active_index, retry_retries_used, retry_prompt_message_id) VALUES (?, ?, ?, ?, ?, ?, ?)" ); // In einer Transaktion: Entweder wird alles übernommen oder gar nichts. // Pro Benutzer entsteht ein Chat "Imported chat" mit allen alten Nachrichten. const migrateLegacyChats = db.transaction(() => { for (const { username } of getLegacyUsersStmt.all()) { const chatId = insertChatSessionStmt.run(username, "Imported chat", null, null, null, null).lastInsertRowid; for (const row of getLegacyChatsStmt.all(username)) { insertChatMessageStmt.run(chatId, row.role, row.content, null, 0, 0, null); } } }); migrateLegacyChats(); } if (dropLegacyChats) { db.prepare("DROP TABLE chats").run(); } }