//! Persistent Storage Service for Capsule Core //! Wraps SQLite to store peer discovery data and QVL trust graph. const std = @import("std"); const c = @cImport({ @cInclude("sqlite3.h"); }); const dht = @import("dht.zig"); pub const RemoteNode = dht.RemoteNode; pub const ID_LEN = dht.ID_LEN; pub const StorageError = error{ DbOpenFailed, ExecFailed, PrepareFailed, StepFailed, }; pub const StorageService = struct { db: ?*c.sqlite3 = null, allocator: std.mem.Allocator, pub fn init(allocator: std.mem.Allocator, db_path: []const u8) !*StorageService { const self = try allocator.create(StorageService); self.* = .{ .allocator = allocator, .db = null, }; const db_path_c = try allocator.dupeZ(u8, db_path); defer allocator.free(db_path_c); if (c.sqlite3_open(db_path_c, &self.db) != c.SQLITE_OK) { std.log.err("SQLite: Failed to open database {s}: {s}", .{ db_path, c.sqlite3_errmsg(self.db) }); return error.DbOpenFailed; } try self.initSchema(); std.log.info("SQLite: Database initialized at {s}", .{db_path}); return self; } pub fn deinit(self: *StorageService) void { if (self.db) |db| { _ = c.sqlite3_close(db); } self.allocator.destroy(self); } fn initSchema(self: *StorageService) !void { const sql = \\ PRAGMA journal_mode = WAL; \\ CREATE TABLE IF NOT EXISTS peers ( \\ id BLOB PRIMARY KEY, \\ address TEXT NOT NULL, \\ last_seen INTEGER NOT NULL, \\ seen_count INTEGER DEFAULT 1 \\ ); \\ CREATE TABLE IF NOT EXISTS qvl_nodes ( \\ did BLOB PRIMARY KEY, \\ trust_score REAL DEFAULT 0.0 \\ ); \\ CREATE TABLE IF NOT EXISTS qvl_edges ( \\ source BLOB, \\ target BLOB, \\ weight REAL, \\ PRIMARY KEY(source, target) \\ ); ; var err_msg: [*c]u8 = null; if (c.sqlite3_exec(self.db, sql, null, null, &err_msg) != c.SQLITE_OK) { std.log.err("SQLite: Schema init failed: {s}", .{err_msg}); c.sqlite3_free(err_msg); return error.ExecFailed; } } pub fn savePeer(self: *StorageService, node: RemoteNode) !void { const sql = "INSERT INTO peers (id, address, last_seen) VALUES (?, ?, ?) " ++ "ON CONFLICT(id) DO UPDATE SET address=excluded.address, last_seen=excluded.last_seen, seen_count=seen_count+1;"; var stmt: ?*c.sqlite3_stmt = null; if (c.sqlite3_prepare_v2(self.db, sql, -1, &stmt, null) != c.SQLITE_OK) return error.PrepareFailed; defer _ = c.sqlite3_finalize(stmt); // Bind ID _ = c.sqlite3_bind_blob(stmt, 1, &node.id, @intCast(node.id.len), null); // Bind Address var addr_buf: [64]u8 = undefined; const addr_str = try std.fmt.bufPrintZ(&addr_buf, "{any}", .{node.address}); _ = c.sqlite3_bind_text(stmt, 2, addr_str.ptr, -1, null); // Bind Last Seen _ = c.sqlite3_bind_int64(stmt, 3, node.last_seen); if (c.sqlite3_step(stmt) != c.SQLITE_DONE) return error.StepFailed; } pub fn loadPeers(self: *StorageService, allocator: std.mem.Allocator) ![]RemoteNode { const sql = "SELECT id, address, last_seen FROM peers;"; var stmt: ?*c.sqlite3_stmt = null; if (c.sqlite3_prepare_v2(self.db, sql, -1, &stmt, null) != c.SQLITE_OK) return error.PrepareFailed; defer _ = c.sqlite3_finalize(stmt); var list = std.ArrayList(RemoteNode){}; defer list.deinit(allocator); while (c.sqlite3_step(stmt) == c.SQLITE_ROW) { const id_ptr = c.sqlite3_column_blob(stmt, 0); const id_len = c.sqlite3_column_bytes(stmt, 0); const addr_ptr = c.sqlite3_column_text(stmt, 1); const last_seen = c.sqlite3_column_int64(stmt, 2); if (id_len != ID_LEN) continue; var node: RemoteNode = undefined; @memcpy(&node.id, @as([*]const u8, @ptrCast(id_ptr))[0..ID_LEN]); const addr_str = std.mem.span(addr_ptr); node.address = try std.net.Address.parseIp(addr_str, 0); // Port logic handled via federation later node.last_seen = last_seen; try list.append(allocator, node); } const out = try allocator.alloc(RemoteNode, list.items.len); @memcpy(out, list.items); return out; } };