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); }