Files
KrisHD_AI/src/db/schema.ts
T

511 lines
25 KiB
TypeScript

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();
}
}