fetch_ml/cli/src/db.zig
Jeremie Fraeys b1c9bc97fc
fix(cli): CLI structure, manifest, and asset fixes
- Fix commands.zig imports (logs.zig → log.zig, remove missing modules)
- Fix manifest.writeManifest to accept allocator param
- Add db.Stmt type alias for sqlite3_stmt
- Fix rsync placeholder to be valid shell script (#!/bin/sh)
2026-02-21 17:59:20 -05:00

264 lines
8.4 KiB
Zig

const std = @import("std");
// SQLite C bindings
pub const c = @cImport({
@cInclude("sqlite3.h");
});
// Public type alias for prepared statement
pub const Stmt = ?*c.sqlite3_stmt;
// SQLITE_TRANSIENT constant - use C wrapper to avoid Zig 0.15 C translation issue
extern fn fetchml_sqlite_transient() c.sqlite3_destructor_type;
fn sqliteTransient() c.sqlite3_destructor_type {
return fetchml_sqlite_transient();
}
// Schema for ML tracking tables
const SCHEMA =
\\ CREATE TABLE IF NOT EXISTS ml_experiments (
\\ experiment_id TEXT PRIMARY KEY,
\\ name TEXT NOT NULL,
\\ artifact_path TEXT,
\\ lifecycle TEXT DEFAULT 'active',
\\ created_at DATETIME DEFAULT CURRENT_TIMESTAMP
\\ );
\\ CREATE TABLE IF NOT EXISTS ml_runs (
\\ run_id TEXT PRIMARY KEY,
\\ experiment_id TEXT REFERENCES ml_experiments(experiment_id),
\\ name TEXT,
\\ status TEXT, -- RUNNING, FINISHED, FAILED, CANCELLED
\\ start_time DATETIME,
\\ end_time DATETIME,
\\ artifact_uri TEXT,
\\ pid INTEGER DEFAULT NULL,
\\ synced INTEGER DEFAULT 0
\\ );
\\ CREATE TABLE IF NOT EXISTS ml_metrics (
\\ run_id TEXT REFERENCES ml_runs(run_id),
\\ key TEXT,
\\ value REAL,
\\ step INTEGER DEFAULT 0,
\\ timestamp DATETIME DEFAULT CURRENT_TIMESTAMP
\\ );
\\ CREATE TABLE IF NOT EXISTS ml_params (
\\ run_id TEXT REFERENCES ml_runs(run_id),
\\ key TEXT,
\\ value TEXT
\\ );
\\ CREATE TABLE IF NOT EXISTS ml_tags (
\\ run_id TEXT REFERENCES ml_runs(run_id),
\\ key TEXT,
\\ value TEXT
\\ );
;
/// Database connection handle
pub const DB = struct {
handle: ?*c.sqlite3,
path: []const u8,
/// Initialize database with WAL mode and schema
pub fn init(allocator: std.mem.Allocator, db_path: []const u8) !DB {
var db: ?*c.sqlite3 = null;
// Open database
const rc = c.sqlite3_open(db_path.ptr, &db);
if (rc != c.SQLITE_OK) {
std.log.err("Failed to open database: {s}", .{c.sqlite3_errmsg(db)});
return error.DBOpenFailed;
}
// Enable WAL mode - required for concurrent CLI writes and TUI reads
var errmsg: [*c]u8 = null;
_ = c.sqlite3_exec(db, "PRAGMA journal_mode=WAL;", null, null, &errmsg);
if (errmsg != null) {
c.sqlite3_free(errmsg);
}
// Set synchronous=NORMAL for performance under WAL
_ = c.sqlite3_exec(db, "PRAGMA synchronous=NORMAL;", null, null, &errmsg);
if (errmsg != null) {
c.sqlite3_free(errmsg);
}
// Apply schema
_ = c.sqlite3_exec(db, SCHEMA, null, null, &errmsg);
if (errmsg != null) {
std.log.err("Schema creation failed: {s}", .{errmsg});
c.sqlite3_free(errmsg);
_ = c.sqlite3_close(db);
return error.SchemaFailed;
}
const path_copy = try allocator.dupe(u8, db_path);
return DB{
.handle = db,
.path = path_copy,
};
}
/// Close database connection
pub fn close(self: *DB) void {
if (self.handle) |db| {
_ = c.sqlite3_close(db);
self.handle = null;
}
}
/// Checkpoint WAL on clean shutdown
pub fn checkpointOnExit(self: *DB) void {
if (self.handle) |db| {
var errmsg: [*c]u8 = null;
_ = c.sqlite3_exec(db, "PRAGMA wal_checkpoint(TRUNCATE);", null, null, &errmsg);
if (errmsg != null) {
c.sqlite3_free(errmsg);
}
}
}
/// Execute a simple SQL statement
pub fn exec(self: DB, sql: []const u8) !void {
if (self.handle == null) return error.DBNotOpen;
var errmsg: [*c]u8 = null;
const rc = c.sqlite3_exec(self.handle, sql.ptr, null, null, &errmsg);
if (rc != c.SQLITE_OK) {
if (errmsg) |e| {
std.log.err("SQL error: {s}", .{e});
c.sqlite3_free(errmsg);
}
return error.SQLExecFailed;
}
}
/// Prepare a statement
pub fn prepare(self: DB, sql: []const u8) !?*c.sqlite3_stmt {
if (self.handle == null) return error.DBNotOpen;
var stmt: ?*c.sqlite3_stmt = null;
const rc = c.sqlite3_prepare_v2(self.handle, sql.ptr, @intCast(sql.len), &stmt, null);
if (rc != c.SQLITE_OK) {
std.log.err("Prepare failed: {s}", .{c.sqlite3_errmsg(self.handle)});
return error.PrepareFailed;
}
return stmt;
}
/// Finalize a prepared statement
pub fn finalize(stmt: ?*c.sqlite3_stmt) void {
if (stmt) |s| {
_ = c.sqlite3_finalize(s);
}
}
/// Bind text parameter to statement
pub fn bindText(stmt: ?*c.sqlite3_stmt, idx: i32, value: []const u8) !void {
if (stmt == null) return error.InvalidStatement;
const rc = c.sqlite3_bind_text(stmt, idx, value.ptr, @intCast(value.len), sqliteTransient());
if (rc != c.SQLITE_OK) return error.BindFailed;
}
/// Bind int64 parameter to statement
pub fn bindInt64(stmt: ?*c.sqlite3_stmt, idx: i32, value: i64) !void {
if (stmt == null) return error.InvalidStatement;
const rc = c.sqlite3_bind_int64(stmt, idx, value);
if (rc != c.SQLITE_OK) return error.BindFailed;
}
/// Bind double parameter to statement
pub fn bindDouble(stmt: ?*c.sqlite3_stmt, idx: i32, value: f64) !void {
if (stmt == null) return error.InvalidStatement;
const rc = c.sqlite3_bind_double(stmt, idx, value);
if (rc != c.SQLITE_OK) return error.BindFailed;
}
/// Step statement (execute)
pub fn step(stmt: ?*c.sqlite3_stmt) !bool {
if (stmt == null) return error.InvalidStatement;
const rc = c.sqlite3_step(stmt);
return rc == c.SQLITE_ROW; // true if has row, false if done
}
/// Reset statement for reuse
pub fn reset(stmt: ?*c.sqlite3_stmt) !void {
if (stmt == null) return error.InvalidStatement;
_ = c.sqlite3_reset(stmt);
_ = c.sqlite3_clear_bindings(stmt);
}
/// Get column text
pub fn columnText(stmt: ?*c.sqlite3_stmt, idx: i32) []const u8 {
if (stmt == null) return "";
const ptr = c.sqlite3_column_text(stmt, idx);
const len = c.sqlite3_column_bytes(stmt, idx);
if (ptr == null or len == 0) return "";
return ptr[0..@intCast(len)];
}
/// Get column int64
pub fn columnInt64(stmt: ?*c.sqlite3_stmt, idx: i32) i64 {
if (stmt == null) return 0;
return c.sqlite3_column_int64(stmt, idx);
}
/// Get column double
pub fn columnDouble(stmt: ?*c.sqlite3_stmt, idx: i32) f64 {
if (stmt == null) return 0.0;
return c.sqlite3_column_double(stmt, idx);
}
};
/// Generate UUID v4 (simple random-based)
pub fn generateUUID(allocator: std.mem.Allocator) ![]const u8 {
var buf: [36]u8 = undefined;
const hex_chars = "0123456789abcdef";
// Random bytes (simplified - in production use crypto RNG)
var bytes: [16]u8 = undefined;
std.crypto.random.bytes(&bytes);
// Set version (4) and variant bits
bytes[6] = (bytes[6] & 0x0f) | 0x40;
bytes[8] = (bytes[8] & 0x3f) | 0x80;
// Format as UUID string
var idx: usize = 0;
for (0..16) |i| {
if (i == 4 or i == 6 or i == 8 or i == 10) {
buf[idx] = '-';
idx += 1;
}
buf[idx] = hex_chars[bytes[i] >> 4];
buf[idx + 1] = hex_chars[bytes[i] & 0x0f];
idx += 2;
}
return try allocator.dupe(u8, &buf);
}
/// Get current timestamp as ISO8601 string
pub fn currentTimestamp(allocator: std.mem.Allocator) ![]const u8 {
const now = std.time.timestamp();
const epoch_seconds = std.time.epoch.EpochSeconds{ .secs = @intCast(now) };
const epoch_day = epoch_seconds.getEpochDay();
const year_day = epoch_day.calculateYearDay();
const month_day = year_day.calculateMonthDay();
const day_seconds = epoch_seconds.getDaySeconds();
var buf: [20]u8 = undefined;
const len = try std.fmt.bufPrint(&buf, "{d:0>4}-{d:0>2}-{d:0>2} {d:0>2}:{d:0>2}:{d:0>2}", .{
year_day.year,
month_day.month.numeric(),
month_day.day_index + 1,
day_seconds.getHoursIntoDay(),
day_seconds.getMinutesIntoHour(),
day_seconds.getSecondsIntoMinute(),
});
return try allocator.dupe(u8, len);
}