- 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)
264 lines
8.4 KiB
Zig
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);
|
|
}
|