libertaria-stack/capsule-core/src/qvl_store.zig

285 lines
11 KiB
Zig

//! Quasar Vector Lattice (QVL) Storage Service
//! Wraps DuckDB to store and analyze the trust graph.
const std = @import("std");
const c = @cImport({
@cInclude("duckdb.h");
});
pub const QvlError = error{
DbOpenFailed,
ConnectionFailed,
QueryFailed,
ExecFailed,
ExtensionLoadFailed,
};
const l1_identity = @import("l1_identity");
const slash_mod = l1_identity.slash;
const SlashReason = slash_mod.SlashReason;
const SlashSeverity = slash_mod.SlashSeverity;
const qvl_types = l1_identity.qvl.types;
pub const NodeId = qvl_types.NodeId;
pub const RiskEdge = qvl_types.RiskEdge;
pub const StoredSlashEvent = struct {
timestamp: u64,
target_did: []const u8,
reason: []const u8,
severity: []const u8,
evidence_hash: []const u8,
};
pub const QvlStore = struct {
db: c.duckdb_database = null,
conn: c.duckdb_connection = null,
allocator: std.mem.Allocator,
pub fn init(allocator: std.mem.Allocator, db_path: []const u8) !*QvlStore {
const self = try allocator.create(QvlStore);
self.* = .{
.allocator = allocator,
.db = null,
.conn = null,
};
const db_path_c = try allocator.dupeZ(u8, db_path);
defer allocator.free(db_path_c);
var err_msg: [*c]u8 = null;
if (c.duckdb_open_ext(db_path_c, &self.db, null, &err_msg) != c.DuckDBSuccess) {
std.log.err("DuckDB: Failed to open database {s}: {s}", .{ db_path, err_msg });
return error.DbOpenFailed;
}
if (c.duckdb_connect(self.db, &self.conn) != c.DuckDBSuccess) {
return error.ConnectionFailed;
}
try self.initExtensions();
try self.initSchema();
std.log.info("DuckDB: QVL Store initialized at {s}", .{db_path});
return self;
}
pub fn deinit(self: *QvlStore) void {
if (self.conn != null) c.duckdb_disconnect(&self.conn);
if (self.db != null) c.duckdb_close(&self.db);
self.allocator.destroy(self);
}
fn initExtensions(self: *QvlStore) !void {
const sql = "INSTALL prql; LOAD prql;";
var res: c.duckdb_result = undefined;
if (c.duckdb_query(self.conn, sql, &res) != c.DuckDBSuccess) {
std.log.warn("DuckDB: PRQL extension not available. Falling back to SQL for analytics. Error: {s}", .{c.duckdb_result_error(&res)});
c.duckdb_destroy_result(&res);
return;
}
c.duckdb_destroy_result(&res);
std.log.info("DuckDB: PRQL extension loaded.", .{});
}
fn initSchema(self: *QvlStore) !void {
const sql =
\\ CREATE TABLE IF NOT EXISTS qvl_vertices (
\\ id INTEGER PRIMARY KEY,
\\ did TEXT,
\\ trust_score REAL DEFAULT 0.0,
\\ last_seen TIMESTAMP DEFAULT CURRENT_TIMESTAMP
\\ );
\\ CREATE TABLE IF NOT EXISTS qvl_edges (
\\ source INTEGER,
\\ target INTEGER,
\\ weight REAL,
\\ nonce UBIGINT,
\\ PRIMARY KEY(source, target)
\\ );
\\ CREATE TABLE IF NOT EXISTS slash_events (
\\ timestamp UBIGINT,
\\ target_did TEXT,
\\ reason TEXT,
\\ severity TEXT,
\\ evidence_hash TEXT
\\ );
;
var res: c.duckdb_result = undefined;
if (c.duckdb_query(self.conn, sql, &res) != c.DuckDBSuccess) {
std.log.err("DuckDB: Schema init failed: {s}", .{c.duckdb_result_error(&res)});
c.duckdb_destroy_result(&res);
return error.ExecFailed;
}
c.duckdb_destroy_result(&res);
}
pub fn syncLattice(self: *QvlStore, nodes: []const NodeId, edges: []const RiskEdge) !void {
// Clear old state (analytical snapshot)
_ = try self.execSql("DELETE FROM qvl_vertices;");
_ = try self.execSql("DELETE FROM qvl_edges;");
// Batch insert vertices
var appender: c.duckdb_appender = null;
if (c.duckdb_appender_create(self.conn, null, "qvl_vertices", &appender) != c.DuckDBSuccess) return error.ExecFailed;
defer _ = c.duckdb_appender_destroy(&appender);
for (nodes) |node| {
_ = c.duckdb_append_int32(appender, @intCast(node));
_ = c.duckdb_append_null(appender); // DID unknown here
_ = c.duckdb_append_double(appender, 0.0);
_ = c.duckdb_appender_end_row(appender);
}
// Batch insert edges
var edge_appender: c.duckdb_appender = null;
if (c.duckdb_appender_create(self.conn, null, "qvl_edges", &edge_appender) != c.DuckDBSuccess) return error.ExecFailed;
defer _ = c.duckdb_appender_destroy(&edge_appender);
for (edges) |edge| {
_ = c.duckdb_append_int32(edge_appender, @intCast(edge.from));
_ = c.duckdb_append_int32(edge_appender, @intCast(edge.to));
_ = c.duckdb_append_double(edge_appender, edge.risk);
_ = c.duckdb_append_uint64(edge_appender, edge.nonce);
_ = c.duckdb_appender_end_row(edge_appender);
}
}
pub fn computeTrustRank(self: *QvlStore) !void {
// Fallback to SQL for trust aggregation
const sql =
\\ SELECT target, AVG(weight) as avg_risk
\\ FROM qvl_edges
\\ GROUP BY target
\\ HAVING AVG(weight) > 0.5;
;
var res: c.duckdb_result = undefined;
if (c.duckdb_query(self.conn, sql, &res) != c.DuckDBSuccess) {
std.log.err("DuckDB Analytics Error: {s}", .{c.duckdb_result_error(&res)});
c.duckdb_destroy_result(&res);
return error.QueryFailed;
}
c.duckdb_destroy_result(&res);
}
fn execSql(self: *QvlStore, sql: []const u8) !void {
var res: c.duckdb_result = undefined;
const sql_z = try self.allocator.dupeZ(u8, sql);
defer self.allocator.free(sql_z);
if (c.duckdb_query(self.conn, sql_z.ptr, &res) != c.DuckDBSuccess) {
std.log.err("DuckDB SQL Error: {s}", .{c.duckdb_result_error(&res)});
c.duckdb_destroy_result(&res);
return error.ExecFailed;
}
c.duckdb_destroy_result(&res);
}
pub fn execPrql(self: *QvlStore, prql_query: []const u8) !void {
const prql_buf = try std.fmt.allocPrintZ(self.allocator, "PRQL '{s}'", .{prql_query});
defer self.allocator.free(prql_buf);
var res: c.duckdb_result = undefined;
if (c.duckdb_query(self.conn, prql_buf.ptr, &res) != c.DuckDBSuccess) {
std.log.err("DuckDB PRQL Error: {s}", .{c.duckdb_result_error(&res)});
c.duckdb_destroy_result(&res);
return error.QueryFailed;
}
c.duckdb_destroy_result(&res);
}
pub fn logSlashEvent(
self: *QvlStore,
timestamp: u64,
target_did: []const u8,
reason: []const u8,
severity: []const u8,
evidence_hash: []const u8,
) !void {
var appender: c.duckdb_appender = null;
if (c.duckdb_appender_create(self.conn, null, "slash_events", &appender) != c.DuckDBSuccess) return error.ExecFailed;
defer _ = c.duckdb_appender_destroy(&appender);
_ = c.duckdb_append_uint64(appender, timestamp);
_ = c.duckdb_append_varchar_length(appender, target_did.ptr, target_did.len);
_ = c.duckdb_append_varchar_length(appender, reason.ptr, reason.len);
_ = c.duckdb_append_varchar_length(appender, severity.ptr, severity.len);
_ = c.duckdb_append_varchar_length(appender, evidence_hash.ptr, evidence_hash.len);
_ = c.duckdb_appender_end_row(appender);
}
pub fn getSlashEvents(self: *QvlStore, limit: usize) ![]StoredSlashEvent {
const sql_slice = try std.fmt.allocPrint(self.allocator, "SELECT timestamp, target_did, reason, severity, evidence_hash FROM slash_events ORDER BY timestamp DESC LIMIT {d};", .{limit});
defer self.allocator.free(sql_slice);
const sql = try self.allocator.dupeZ(u8, sql_slice);
defer self.allocator.free(sql);
var res: c.duckdb_result = undefined;
if (c.duckdb_query(self.conn, sql.ptr, &res) != c.DuckDBSuccess) {
std.log.err("DuckDB Slash Log Error: {s}", .{c.duckdb_result_error(&res)});
c.duckdb_destroy_result(&res);
return error.QueryFailed;
}
defer c.duckdb_destroy_result(&res);
const row_count = c.duckdb_row_count(&res);
var events = try self.allocator.alloc(StoredSlashEvent, row_count);
for (0..row_count) |i| {
// Helper to get string safely
const getStr = struct {
fn get(result: *c.duckdb_result, row: u64, col: u64, allocator: std.mem.Allocator) ![]const u8 {
const val = c.duckdb_value_varchar(result, row, col);
defer c.duckdb_free(val);
return allocator.dupe(u8, std.mem.span(val));
}
}.get;
events[i] = StoredSlashEvent{
.timestamp = c.duckdb_value_uint64(&res, i, 0),
.target_did = try getStr(&res, i, 1, self.allocator),
.reason = try getStr(&res, i, 2, self.allocator),
.severity = try getStr(&res, i, 3, self.allocator),
.evidence_hash = try getStr(&res, i, 4, self.allocator),
};
}
return events;
}
/// Retrieve a list of trusted relay DIDs based on QVL scores.
pub fn getTrustedRelays(self: *QvlStore, min_score: f64, limit: usize) ![][]u8 {
const sql_slice = try std.fmt.allocPrint(self.allocator, "SELECT did FROM qvl_vertices WHERE trust_score >= {d} ORDER BY trust_score DESC LIMIT {d};", .{ min_score, limit });
defer self.allocator.free(sql_slice);
const sql = try self.allocator.dupeZ(u8, sql_slice);
defer self.allocator.free(sql);
var res: c.duckdb_result = undefined;
if (c.duckdb_query(self.conn, sql.ptr, &res) != c.DuckDBSuccess) {
std.log.err("DuckDB Relay Query Error: {s}", .{c.duckdb_result_error(&res)});
c.duckdb_destroy_result(&res);
return error.QueryFailed;
}
defer c.duckdb_destroy_result(&res);
const row_count = c.duckdb_row_count(&res);
// If we found nothing, return empty slice
if (row_count == 0) return &[_][]u8{};
var relays = try self.allocator.alloc([]u8, row_count);
for (0..row_count) |i| {
const val = c.duckdb_value_varchar(&res, i, 0);
defer c.duckdb_free(val);
if (val == null) {
// Should not happen if DB is correct, but handle safely
relays[i] = try self.allocator.dupe(u8, "UNKNOWN");
} else {
relays[i] = try self.allocator.dupe(u8, std.mem.span(val));
}
}
return relays;
}
};