35static constexpr const char* MIGRATION_001_INITIAL = R
"sql(
36CREATE TABLE IF NOT EXISTS conversations (
39 created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
40 updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
43 metadata TEXT DEFAULT '{}'
46CREATE TABLE IF NOT EXISTS messages (
48 conversation_id TEXT NOT NULL,
49 role TEXT NOT NULL CHECK(role IN ('user', 'assistant', 'system', 'tool')),
50 content TEXT NOT NULL,
51 tool_calls TEXT DEFAULT '[]',
52 tool_results TEXT DEFAULT '[]',
53 token_count INTEGER DEFAULT 0,
54 created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
55 is_compacted BOOLEAN DEFAULT FALSE,
57 FOREIGN KEY (conversation_id) REFERENCES conversations(id) ON DELETE CASCADE
60CREATE TABLE IF NOT EXISTS tool_executions (
63 server_name TEXT NOT NULL,
64 tool_name TEXT NOT NULL,
65 arguments TEXT DEFAULT '{}',
67 duration_ms INTEGER DEFAULT 0,
68 status TEXT CHECK(status IN ('success', 'error', 'timeout')),
69 created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
70 FOREIGN KEY (message_id) REFERENCES messages(id) ON DELETE CASCADE
73CREATE INDEX IF NOT EXISTS idx_messages_conversation
74 ON messages(conversation_id);
75CREATE INDEX IF NOT EXISTS idx_messages_created
76 ON messages(created_at);
77CREATE INDEX IF NOT EXISTS idx_conversations_updated
78 ON conversations(updated_at);
82static constexpr const char* MIGRATION_002_FTS = R
"sql(
83CREATE VIRTUAL TABLE IF NOT EXISTS messages_fts USING fts5(
89CREATE TRIGGER IF NOT EXISTS messages_ai AFTER INSERT ON messages BEGIN
90 INSERT INTO messages_fts(rowid, content) VALUES (NEW.rowid, NEW.content);
93CREATE TRIGGER IF NOT EXISTS messages_ad AFTER DELETE ON messages BEGIN
94 INSERT INTO messages_fts(messages_fts, rowid, content)
95 VALUES('delete', OLD.rowid, OLD.content);
98CREATE TRIGGER IF NOT EXISTS messages_au AFTER UPDATE ON messages BEGIN
99 INSERT INTO messages_fts(messages_fts, rowid, content)
100 VALUES('delete', OLD.rowid, OLD.content);
101 INSERT INTO messages_fts(rowid, content) VALUES (NEW.rowid, NEW.content);
106static constexpr const char* MIGRATION_003_DELEGATIONS = R
"sql(
107CREATE TABLE IF NOT EXISTS delegations (
109 parent_conversation_id TEXT NOT NULL,
110 child_conversation_id TEXT NOT NULL,
111 delegating_tier TEXT NOT NULL,
112 target_tier TEXT NOT NULL,
115 status TEXT NOT NULL DEFAULT 'pending'
116 CHECK(status IN ('pending', 'running', 'completed', 'failed')),
118 created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
119 completed_at TIMESTAMP,
120 FOREIGN KEY (parent_conversation_id)
121 REFERENCES conversations(id) ON DELETE CASCADE,
122 FOREIGN KEY (child_conversation_id)
123 REFERENCES conversations(id) ON DELETE CASCADE
126CREATE INDEX IF NOT EXISTS idx_delegations_parent
127 ON delegations(parent_conversation_id);
128CREATE INDEX IF NOT EXISTS idx_delegations_child
129 ON delegations(child_conversation_id);
133static constexpr const char* MIGRATION_004_COMPACTION_SNAPSHOTS = R
"sql(
134CREATE TABLE IF NOT EXISTS compaction_snapshots (
136 conversation_id TEXT NOT NULL,
137 messages_json TEXT NOT NULL,
138 message_count INTEGER NOT NULL,
139 token_count_estimate INTEGER,
140 created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
141 FOREIGN KEY (conversation_id)
142 REFERENCES conversations(id) ON DELETE CASCADE
145CREATE INDEX IF NOT EXISTS idx_snapshots_conversation
146 ON compaction_snapshots(conversation_id);
150static constexpr std::array<Migration, 4> MIGRATIONS = {{
151 {
"001_initial", MIGRATION_001_INITIAL},
152 {
"002_fts", MIGRATION_002_FTS},
153 {
"003_delegations", MIGRATION_003_DELEGATIONS},
154 {
"004_compaction_snapshots", MIGRATION_004_COMPACTION_SNAPSHOTS},
166 : db_path_(db_path) {}
184 std::lock_guard lock(mutex_);
187 auto parent = db_path_.parent_path();
188 if (!parent.empty()) {
189 std::filesystem::create_directories(parent);
192 int rc = sqlite3_open_v2(
193 db_path_.c_str(), &db_,
194 SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE | SQLITE_OPEN_FULLMUTEX,
197 if (rc != SQLITE_OK) {
198 logger->error(
"Failed to open database {}: {}",
199 db_path_.string(), sqlite3_errmsg(db_));
204 sqlite3_exec(db_,
"PRAGMA foreign_keys = ON",
nullptr,
nullptr,
nullptr);
205 sqlite3_exec(db_,
"PRAGMA journal_mode = WAL",
nullptr,
nullptr,
nullptr);
207 if (!run_migrations()) {
208 logger->error(
"Migration failed for {}", db_path_.string());
212 logger->info(
"Database initialized: {}", db_path_.string());
222 std::lock_guard lock(mutex_);
236 std::lock_guard lock(mutex_);
237 return db_ !=
nullptr;
247sqlite3_stmt* SqliteDatabase::prepare(std::string_view sql) {
248 sqlite3_stmt* stmt =
nullptr;
249 int rc = sqlite3_prepare_v2(
250 db_, sql.data(),
static_cast<int>(sql.size()),
253 if (rc != SQLITE_OK) {
254 logger->error(
"SQL prepare failed: {} — {}",
255 sqlite3_errmsg(db_), std::string(sql));
270 std::function<
void(sqlite3_stmt*)> binder) {
271 std::lock_guard lock(mutex_);
272 if (!db_)
return false;
274 auto* stmt = prepare(sql);
275 if (!stmt)
return false;
277 if (binder) binder(stmt);
279 int rc = sqlite3_step(stmt);
280 sqlite3_finalize(stmt);
281 bool ok = (rc == SQLITE_DONE);
283 logger->error(
"SQL execute failed: {}", sqlite3_errmsg(db_));
296 std::lock_guard lock(mutex_);
297 if (!db_)
return false;
299 char* err_msg =
nullptr;
300 int rc = sqlite3_exec(
301 db_, std::string(sql).c_str(),
302 nullptr,
nullptr, &err_msg);
304 if (rc != SQLITE_OK) {
305 logger->error(
"SQL exec failed: {}",
306 err_msg ? err_msg :
"unknown error");
307 sqlite3_free(err_msg);
323 std::string_view sql,
324 std::function<
void(sqlite3_stmt*)> binder,
325 std::function<
void(sqlite3_stmt*)> extractor) {
326 std::lock_guard lock(mutex_);
327 if (!db_)
return false;
329 auto* stmt = prepare(sql);
330 if (!stmt)
return false;
332 if (binder) binder(stmt);
334 int rc = sqlite3_step(stmt);
335 bool found = (rc == SQLITE_ROW);
336 if (found && extractor) {
340 sqlite3_finalize(stmt);
354 std::string_view sql,
355 std::function<
void(sqlite3_stmt*)> binder,
356 std::function<
void(sqlite3_stmt*)> row_handler) {
357 std::lock_guard lock(mutex_);
360 auto* stmt = prepare(sql);
363 if (binder) binder(stmt);
366 while (sqlite3_step(stmt) == SQLITE_ROW) {
367 if (row_handler) row_handler(stmt);
371 sqlite3_finalize(stmt);
393 std::vector<std::string> applied;
394 sqlite3_stmt* stmt =
nullptr;
395 int rc = sqlite3_prepare_v2(db,
396 "SELECT name FROM migrations", -1, &stmt,
nullptr);
397 if (rc != SQLITE_OK)
return applied;
399 while (sqlite3_step(stmt) == SQLITE_ROW) {
400 auto* text = sqlite3_column_text(stmt, 0);
402 applied.emplace_back(
reinterpret_cast<const char*
>(text));
405 sqlite3_finalize(stmt);
418 logger->info(
"Running migration: {}", mig.
name);
420 char* err_msg =
nullptr;
421 int rc = sqlite3_exec(db, mig.
sql,
nullptr,
nullptr, &err_msg);
422 if (rc != SQLITE_OK) {
423 logger->error(
"Migration {} failed: {}",
424 mig.
name, err_msg ? err_msg :
"unknown");
425 sqlite3_free(err_msg);
429 sqlite3_stmt* stmt =
nullptr;
430 rc = sqlite3_prepare_v2(db,
431 "INSERT INTO migrations (name) VALUES (?)",
433 if (rc == SQLITE_OK) {
434 sqlite3_bind_text(stmt, 1, mig.
name, -1, SQLITE_STATIC);
436 sqlite3_finalize(stmt);
449static bool is_applied(
const std::vector<std::string>& applied,
451 for (
const auto& n : applied) {
452 if (n == name)
return true;
463bool SqliteDatabase::run_migrations() {
464 char* err_msg =
nullptr;
465 int rc = sqlite3_exec(db_,
466 "CREATE TABLE IF NOT EXISTS migrations ("
467 " id INTEGER PRIMARY KEY,"
469 " applied_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP"
471 nullptr,
nullptr, &err_msg);
473 if (rc != SQLITE_OK) {
474 logger->error(
"Failed to create migrations table: {}",
475 err_msg ? err_msg :
"unknown");
476 sqlite3_free(err_msg);
481 for (
const auto& mig : MIGRATIONS) {
bool execute(std::string_view sql, std::function< void(sqlite3_stmt *)> binder=nullptr)
Execute a write statement (INSERT, UPDATE, DELETE).
bool execute_raw(std::string_view sql)
Execute raw SQL (multiple statements, no binding).
bool is_open() const
Check if database is open.
void close()
Close database connection.
~SqliteDatabase()
Destructor — closes connection if open.
sqlite3 * raw_handle() const
Get the underlying sqlite3 handle (for advanced use).
size_t fetch_all(std::string_view sql, std::function< void(sqlite3_stmt *)> binder, std::function< void(sqlite3_stmt *)> row_handler)
Fetch all matching rows.
bool fetch_one(std::string_view sql, std::function< void(sqlite3_stmt *)> binder, std::function< void(sqlite3_stmt *)> extractor)
Fetch a single row.
bool initialize()
Initialize database and run pending migrations.
SqliteDatabase(const std::filesystem::path &db_path)
Construct with database file path.
Thread-safe SQLite database wrapper with migration support.
spdlog initialization and logger access.
ENTROPIC_EXPORT std::shared_ptr< spdlog::logger > get(const std::string &name)
Get or create a named logger.
Activate model on GPU (WARM → ACTIVE).
@ ok
Tool dispatched, returned non-empty content.
static bool apply_migration(sqlite3 *db, const Migration &mig)
Apply a single migration and record it.
static std::vector< std::string > get_applied_migrations(sqlite3 *db)
Get names of already-applied migrations.
static bool is_applied(const std::vector< std::string > &applied, const char *name)
Check if a migration name is in the applied list.
A single named migration.
const char * name
Migration name (e.g., "001_initial")
const char * sql
SQL to execute.