Entropic 2.3.8
Local-first agentic inference engine
Loading...
Searching...
No Matches
database.cpp
Go to the documentation of this file.
1// SPDX-License-Identifier: Apache-2.0
9
11#include <sqlite3.h>
12
13#include <array>
14#include <cstring>
15
16namespace entropic {
17
18namespace {
19auto logger = entropic::log::get("storage.database");
20} // anonymous namespace
21
22// ── Migration definitions ─────────────────────────────────
23
29struct Migration {
30 const char* name;
31 const char* sql;
32};
33
35static constexpr const char* MIGRATION_001_INITIAL = R"sql(
36CREATE TABLE IF NOT EXISTS conversations (
37 id TEXT PRIMARY KEY,
38 title TEXT,
39 created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
40 updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
41 project_path TEXT,
42 model_id TEXT,
43 metadata TEXT DEFAULT '{}'
44);
45
46CREATE TABLE IF NOT EXISTS messages (
47 id TEXT PRIMARY KEY,
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,
56 identity_tier TEXT,
57 FOREIGN KEY (conversation_id) REFERENCES conversations(id) ON DELETE CASCADE
58);
59
60CREATE TABLE IF NOT EXISTS tool_executions (
61 id TEXT PRIMARY KEY,
62 message_id TEXT,
63 server_name TEXT NOT NULL,
64 tool_name TEXT NOT NULL,
65 arguments TEXT DEFAULT '{}',
66 result TEXT,
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
71);
72
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);
79)sql";
80
82static constexpr const char* MIGRATION_002_FTS = R"sql(
83CREATE VIRTUAL TABLE IF NOT EXISTS messages_fts USING fts5(
84 content,
85 content='messages',
86 content_rowid='rowid'
87);
88
89CREATE TRIGGER IF NOT EXISTS messages_ai AFTER INSERT ON messages BEGIN
90 INSERT INTO messages_fts(rowid, content) VALUES (NEW.rowid, NEW.content);
91END;
92
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);
96END;
97
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);
102END;
103)sql";
104
106static constexpr const char* MIGRATION_003_DELEGATIONS = R"sql(
107CREATE TABLE IF NOT EXISTS delegations (
108 id TEXT PRIMARY KEY,
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,
113 task TEXT NOT NULL,
114 max_turns INTEGER,
115 status TEXT NOT NULL DEFAULT 'pending'
116 CHECK(status IN ('pending', 'running', 'completed', 'failed')),
117 result_summary TEXT,
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
124);
125
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);
130)sql";
131
133static constexpr const char* MIGRATION_004_COMPACTION_SNAPSHOTS = R"sql(
134CREATE TABLE IF NOT EXISTS compaction_snapshots (
135 id TEXT PRIMARY KEY,
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
143);
144
145CREATE INDEX IF NOT EXISTS idx_snapshots_conversation
146 ON compaction_snapshots(conversation_id);
147)sql";
148
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},
155}};
156
157// ── SqliteDatabase implementation ─────────────────────────
158
165SqliteDatabase::SqliteDatabase(const std::filesystem::path& db_path)
166 : db_path_(db_path) {}
167
176
184 std::lock_guard lock(mutex_);
185
186 // Ensure parent directory exists
187 auto parent = db_path_.parent_path();
188 if (!parent.empty()) {
189 std::filesystem::create_directories(parent);
190 }
191
192 int rc = sqlite3_open_v2(
193 db_path_.c_str(), &db_,
194 SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE | SQLITE_OPEN_FULLMUTEX,
195 nullptr);
196
197 if (rc != SQLITE_OK) {
198 logger->error("Failed to open database {}: {}",
199 db_path_.string(), sqlite3_errmsg(db_));
200 return false;
201 }
202
203 // Enable foreign keys and WAL mode
204 sqlite3_exec(db_, "PRAGMA foreign_keys = ON", nullptr, nullptr, nullptr);
205 sqlite3_exec(db_, "PRAGMA journal_mode = WAL", nullptr, nullptr, nullptr);
206
207 if (!run_migrations()) {
208 logger->error("Migration failed for {}", db_path_.string());
209 return false;
210 }
211
212 logger->info("Database initialized: {}", db_path_.string());
213 return true;
214}
215
222 std::lock_guard lock(mutex_);
223 if (db_) {
224 sqlite3_close(db_);
225 db_ = nullptr;
226 }
227}
228
236 std::lock_guard lock(mutex_);
237 return db_ != nullptr;
238}
239
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()),
251 &stmt, nullptr);
252
253 if (rc != SQLITE_OK) {
254 logger->error("SQL prepare failed: {} — {}",
255 sqlite3_errmsg(db_), std::string(sql));
256 return nullptr;
257 }
258 return stmt;
259}
260
269bool SqliteDatabase::execute(std::string_view sql,
270 std::function<void(sqlite3_stmt*)> binder) {
271 std::lock_guard lock(mutex_);
272 if (!db_) return false;
273
274 auto* stmt = prepare(sql);
275 if (!stmt) return false;
276
277 if (binder) binder(stmt);
278
279 int rc = sqlite3_step(stmt);
280 sqlite3_finalize(stmt);
281 bool ok = (rc == SQLITE_DONE);
282 if (!ok) {
283 logger->error("SQL execute failed: {}", sqlite3_errmsg(db_));
284 }
285 return ok;
286}
287
295bool SqliteDatabase::execute_raw(std::string_view sql) {
296 std::lock_guard lock(mutex_);
297 if (!db_) return false;
298
299 char* err_msg = nullptr;
300 int rc = sqlite3_exec(
301 db_, std::string(sql).c_str(),
302 nullptr, nullptr, &err_msg);
303
304 if (rc != SQLITE_OK) {
305 logger->error("SQL exec failed: {}",
306 err_msg ? err_msg : "unknown error");
307 sqlite3_free(err_msg);
308 return false;
309 }
310 return true;
311}
312
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;
328
329 auto* stmt = prepare(sql);
330 if (!stmt) return false;
331
332 if (binder) binder(stmt);
333
334 int rc = sqlite3_step(stmt);
335 bool found = (rc == SQLITE_ROW);
336 if (found && extractor) {
337 extractor(stmt);
338 }
339
340 sqlite3_finalize(stmt);
341 return found;
342}
343
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_);
358 if (!db_) return 0;
359
360 auto* stmt = prepare(sql);
361 if (!stmt) return 0;
362
363 if (binder) binder(stmt);
364
365 size_t count = 0;
366 while (sqlite3_step(stmt) == SQLITE_ROW) {
367 if (row_handler) row_handler(stmt);
368 ++count;
369 }
370
371 sqlite3_finalize(stmt);
372 return count;
373}
374
382 return db_;
383}
384
392static std::vector<std::string> get_applied_migrations(sqlite3* db) {
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;
398
399 while (sqlite3_step(stmt) == SQLITE_ROW) {
400 auto* text = sqlite3_column_text(stmt, 0);
401 if (text) {
402 applied.emplace_back(reinterpret_cast<const char*>(text));
403 }
404 }
405 sqlite3_finalize(stmt);
406 return applied;
407}
408
417static bool apply_migration(sqlite3* db, const Migration& mig) {
418 logger->info("Running migration: {}", mig.name);
419
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);
426 return false;
427 }
428
429 sqlite3_stmt* stmt = nullptr;
430 rc = sqlite3_prepare_v2(db,
431 "INSERT INTO migrations (name) VALUES (?)",
432 -1, &stmt, nullptr);
433 if (rc == SQLITE_OK) {
434 sqlite3_bind_text(stmt, 1, mig.name, -1, SQLITE_STATIC);
435 sqlite3_step(stmt);
436 sqlite3_finalize(stmt);
437 }
438 return true;
439}
440
449static bool is_applied(const std::vector<std::string>& applied,
450 const char* name) {
451 for (const auto& n : applied) {
452 if (n == name) return true;
453 }
454 return false;
455}
456
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,"
468 " name TEXT UNIQUE,"
469 " applied_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP"
470 ")",
471 nullptr, nullptr, &err_msg);
472
473 if (rc != SQLITE_OK) {
474 logger->error("Failed to create migrations table: {}",
475 err_msg ? err_msg : "unknown");
476 sqlite3_free(err_msg);
477 return false;
478 }
479
480 auto applied = get_applied_migrations(db_);
481 for (const auto& mig : MIGRATIONS) {
482 if (is_applied(applied, mig.name)) continue;
483 if (!apply_migration(db_, mig)) return false;
484 }
485 return true;
486}
487
488} // namespace entropic
bool execute(std::string_view sql, std::function< void(sqlite3_stmt *)> binder=nullptr)
Execute a write statement (INSERT, UPDATE, DELETE).
Definition database.cpp:269
bool execute_raw(std::string_view sql)
Execute raw SQL (multiple statements, no binding).
Definition database.cpp:295
bool is_open() const
Check if database is open.
Definition database.cpp:235
void close()
Close database connection.
Definition database.cpp:221
~SqliteDatabase()
Destructor — closes connection if open.
Definition database.cpp:173
sqlite3 * raw_handle() const
Get the underlying sqlite3 handle (for advanced use).
Definition database.cpp:381
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.
Definition database.cpp:353
bool fetch_one(std::string_view sql, std::function< void(sqlite3_stmt *)> binder, std::function< void(sqlite3_stmt *)> extractor)
Fetch a single row.
Definition database.cpp:322
bool initialize()
Initialize database and run pending migrations.
Definition database.cpp:183
SqliteDatabase(const std::filesystem::path &db_path)
Construct with database file path.
Definition database.cpp:165
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.
Definition logging.cpp:211
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.
Definition database.cpp:417
static std::vector< std::string > get_applied_migrations(sqlite3 *db)
Get names of already-applied migrations.
Definition database.cpp:392
static bool is_applied(const std::vector< std::string > &applied, const char *name)
Check if a migration name is in the applied list.
Definition database.cpp:449
A single named migration.
Definition database.cpp:29
const char * name
Migration name (e.g., "001_initial")
Definition database.cpp:30
const char * sql
SQL to execute.
Definition database.cpp:31