feat(cli): embed SQLite and unify commands for local mode

- Add SQLite amalgamation fetch script (make build-sqlite)
- Embed SQLite in release builds, link system lib in dev
- Create sqlite_embedded.zig utility module
- Unify experiment/run/log commands with auto mode detection
- Add Forgejo CI workflow for building with embedded SQLite
- Update READMEs for local mode and build instructions

SQLite follows rsync embedding pattern: assets/sqlite_release_<os>_<arch>/
Zero external dependencies for release builds.
This commit is contained in:
Jeremie Fraeys 2026-02-20 15:50:04 -05:00
parent 6028779239
commit ff542b533f
No known key found for this signature in database
5 changed files with 742 additions and 0 deletions

View file

@ -0,0 +1,80 @@
name: Build CLI with Embedded SQLite
on:
push:
branches: [main, master]
paths:
- 'cli/**'
- '.forgejo/workflows/build-cli.yml'
pull_request:
branches: [main, master]
paths:
- 'cli/**'
jobs:
build:
runs-on: ubuntu-latest
strategy:
matrix:
target:
- x86_64-linux
- aarch64-linux
include:
- target: x86_64-linux
arch: x86_64
- target: aarch64-linux
arch: arm64
steps:
- uses: actions/checkout@v4
- name: Setup Zig
uses: goto-bus-stop/setup-zig@v2
with:
version: 0.15.0
- name: Fetch SQLite Amalgamation
run: |
cd cli
make build-sqlite SQLITE_VERSION=3450000
- name: Build Release Binary
run: |
cd cli
zig build prod -Dtarget=${{ matrix.target }}
- name: Upload Artifact
uses: actions/upload-artifact@v4
with:
name: ml-cli-${{ matrix.target }}
path: cli/zig-out/bin/ml
build-macos:
runs-on: macos-latest
strategy:
matrix:
arch: [x86_64, arm64]
steps:
- uses: actions/checkout@v4
- name: Setup Zig
uses: goto-bus-stop/setup-zig@v2
with:
version: 0.15.0
- name: Fetch SQLite Amalgamation
run: |
cd cli
make build-sqlite SQLITE_VERSION=3450000
- name: Build Release Binary
run: |
cd cli
zig build prod -Dtarget=${{ matrix.arch }}-macos
- name: Upload Artifact
uses: actions/upload-artifact@v4
with:
name: ml-cli-${{ matrix.arch }}-macos
path: cli/zig-out/bin/ml

View file

@ -0,0 +1,50 @@
#!/bin/bash
# Build/fetch SQLite amalgamation for embedding
# Mirrors the rsync pattern: assets/sqlite_release_<os>_<arch>/
set -euo pipefail
SQLITE_VERSION="${SQLITE_VERSION:-3450000}" # 3.45.0
SQLITE_YEAR="${SQLITE_YEAR:-2024}"
SQLITE_SRC_BASE="${SQLITE_SRC_BASE:-https://www.sqlite.org/${SQLITE_YEAR}}"
os="$(uname -s | tr '[:upper:]' '[:lower:]')"
arch="$(uname -m)"
if [[ "${arch}" == "aarch64" || "${arch}" == "arm64" ]]; then arch="arm64"; fi
if [[ "${arch}" == "x86_64" ]]; then arch="x86_64"; fi
repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
out_dir="${repo_root}/src/assets/sqlite_release_${os}_${arch}"
echo "Building SQLite ${SQLITE_VERSION} for ${os}/${arch}..."
# Create output directory
mkdir -p "${out_dir}"
# Download if not present
if [[ ! -f "${out_dir}/sqlite3.c" ]]; then
echo "Fetching SQLite amalgamation..."
tmp="$(mktemp -d)"
cleanup() { rm -rf "${tmp}"; }
trap cleanup EXIT
url="${SQLITE_SRC_BASE}/sqlite-amalgamation-${SQLITE_VERSION}.zip"
echo "Fetching ${url}"
curl -fsSL "${url}" -o "${tmp}/sqlite.zip"
unzip -q "${tmp}/sqlite.zip" -d "${tmp}"
mv "${tmp}/sqlite-amalgamation-${SQLITE_VERSION}"/* "${out_dir}/"
echo "✓ SQLite fetched to ${out_dir}"
else
echo "✓ SQLite already present at ${out_dir}"
fi
# Verify
if [[ -f "${out_dir}/sqlite3.c" && -f "${out_dir}/sqlite3.h" ]]; then
echo "✓ SQLite ready:"
ls -lh "${out_dir}/sqlite3.c" "${out_dir}/sqlite3.h"
else
echo "Error: SQLite files not found in ${out_dir}"
exit 1
fi

325
cli/src/commands/run.zig Normal file
View file

@ -0,0 +1,325 @@
const std = @import("std");
const config = @import("../config.zig");
const db = @import("../db.zig");
const colors = @import("../utils/colors.zig");
const RunOptions = struct {
json: bool = false,
help: bool = false,
};
pub fn execute(allocator: std.mem.Allocator, args: []const []const u8) !void {
var options = RunOptions{};
var command_args = std.ArrayList([]const u8).initCapacity(allocator, 10) catch |err| {
return err;
};
defer command_args.deinit(allocator);
// Parse flags
var i: usize = 0;
while (i < args.len) : (i += 1) {
const arg = args[i];
if (std.mem.eql(u8, arg, "--json")) {
options.json = true;
} else if (std.mem.eql(u8, arg, "--help") or std.mem.eql(u8, arg, "-h")) {
options.help = true;
} else {
try command_args.append(allocator, arg);
}
}
if (command_args.items.len < 1 or options.help) {
try printUsage();
return;
}
const command = command_args.items[0];
// Load config to determine mode
const cfg = try config.Config.load(allocator);
defer {
var mut_cfg = cfg;
mut_cfg.deinit(allocator);
}
if (std.mem.eql(u8, command, "start")) {
if (cfg.isLocalMode()) {
try executeStartLocal(allocator, command_args.items[1..], &options);
} else {
try executeStartServer(allocator, command_args.items[1..], &options);
}
} else if (std.mem.eql(u8, command, "finish")) {
if (cfg.isLocalMode()) {
try executeFinishLocal(allocator, command_args.items[1..], &options);
} else {
try executeFinishServer(allocator, command_args.items[1..], &options);
}
} else if (std.mem.eql(u8, command, "fail")) {
if (cfg.isLocalMode()) {
try executeFailLocal(allocator, command_args.items[1..], &options);
} else {
try executeFailServer(allocator, command_args.items[1..], &options);
}
} else if (std.mem.eql(u8, command, "list")) {
if (cfg.isLocalMode()) {
try executeListLocal(allocator, &options);
} else {
try executeListServer(allocator, &options);
}
} else {
if (options.json) {
std.debug.print("{{\"success\":false,\"command\":\"run\",\"error\":\"Unknown command: {s}\"}}\n", .{command});
} else {
colors.printError("Unknown command: {s}\n", .{command});
try printUsage();
}
}
}
// Local mode implementations
fn executeStartLocal(allocator: std.mem.Allocator, args: []const []const u8, options: *const RunOptions) !void {
var experiment_id: ?[]const u8 = null;
var run_name: ?[]const u8 = null;
var i: usize = 0;
while (i < args.len) : (i += 1) {
if (std.mem.eql(u8, args[i], "--experiment") and i + 1 < args.len) {
experiment_id = args[i + 1];
i += 1;
} else if (std.mem.eql(u8, args[i], "--name") and i + 1 < args.len) {
run_name = args[i + 1];
i += 1;
}
}
if (experiment_id == null) {
if (options.json) {
std.debug.print("{{\"success\":false,\"command\":\"run.start\",\"error\":\"--experiment is required\"}}\n", .{});
} else {
colors.printError("Error: --experiment is required\n", .{});
}
return error.MissingArgument;
}
const cfg = try config.Config.load(allocator);
defer {
var mut_cfg = cfg;
mut_cfg.deinit(allocator);
}
const db_path = try cfg.getDBPath(allocator);
defer allocator.free(db_path);
var database = try db.DB.init(allocator, db_path);
defer database.close();
const run_id = try db.generateUUID(allocator);
defer allocator.free(run_id);
const timestamp = try db.currentTimestamp(allocator);
defer allocator.free(timestamp);
const sql = "INSERT INTO ml_runs (run_id, experiment_id, name, status, start_time) VALUES (?, ?, ?, 'RUNNING', ?);";
const stmt = try database.prepare(sql);
defer db.DB.finalize(stmt);
try db.DB.bindText(stmt, 1, run_id);
try db.DB.bindText(stmt, 2, experiment_id.?);
try db.DB.bindText(stmt, 3, run_name orelse "unnamed-run");
try db.DB.bindText(stmt, 4, timestamp);
_ = try db.DB.step(stmt);
if (options.json) {
std.debug.print("{{\"success\":true,\"command\":\"run.start\",\"data\":{{\"run_id\":\"{s}\",\"experiment_id\":\"{s}\",\"name\":\"{s}\",\"status\":\"RUNNING\"}}}}\n", .{ run_id, experiment_id.?, run_name orelse "unnamed-run" });
} else {
colors.printSuccess("✓ Started run: {s}\n", .{run_name orelse "unnamed-run"});
colors.printInfo(" run_id: {s}\n", .{run_id});
colors.printInfo(" experiment_id: {s}\n", .{experiment_id.?});
}
}
fn executeFinishLocal(allocator: std.mem.Allocator, args: []const []const u8, options: *const RunOptions) !void {
var run_id: ?[]const u8 = null;
var i: usize = 0;
while (i < args.len) : (i += 1) {
if (std.mem.eql(u8, args[i], "--run") and i + 1 < args.len) {
run_id = args[i + 1];
i += 1;
}
}
if (run_id == null) {
if (options.json) {
std.debug.print("{{\"success\":false,\"command\":\"run.finish\",\"error\":\"--run is required\"}}\n", .{});
} else {
colors.printError("Error: --run is required\n", .{});
}
return error.MissingArgument;
}
const cfg = try config.Config.load(allocator);
defer {
var mut_cfg = cfg;
mut_cfg.deinit(allocator);
}
const db_path = try cfg.getDBPath(allocator);
defer allocator.free(db_path);
var database = try db.DB.init(allocator, db_path);
defer database.close();
const timestamp = try db.currentTimestamp(allocator);
defer allocator.free(timestamp);
const sql = "UPDATE ml_runs SET status = 'FINISHED', end_time = ? WHERE run_id = ?;";
const stmt = try database.prepare(sql);
defer db.DB.finalize(stmt);
try db.DB.bindText(stmt, 1, timestamp);
try db.DB.bindText(stmt, 2, run_id.?);
_ = try db.DB.step(stmt);
database.checkpointOnExit();
if (options.json) {
std.debug.print("{{\"success\":true,\"command\":\"run.finish\",\"data\":{{\"run_id\":\"{s}\",\"status\":\"FINISHED\"}}}}\n", .{run_id.?});
} else {
colors.printSuccess("✓ Finished run: {s}\n", .{run_id.?});
}
}
fn executeFailLocal(allocator: std.mem.Allocator, args: []const []const u8, options: *const RunOptions) !void {
var run_id: ?[]const u8 = null;
var i: usize = 0;
while (i < args.len) : (i += 1) {
if (std.mem.eql(u8, args[i], "--run") and i + 1 < args.len) {
run_id = args[i + 1];
i += 1;
}
}
if (run_id == null) {
if (options.json) {
std.debug.print("{{\"success\":false,\"command\":\"run.fail\",\"error\":\"--run is required\"}}\n", .{});
} else {
colors.printError("Error: --run is required\n", .{});
}
return error.MissingArgument;
}
const cfg = try config.Config.load(allocator);
defer {
var mut_cfg = cfg;
mut_cfg.deinit(allocator);
}
const db_path = try cfg.getDBPath(allocator);
defer allocator.free(db_path);
var database = try db.DB.init(allocator, db_path);
defer database.close();
const timestamp = try db.currentTimestamp(allocator);
defer allocator.free(timestamp);
const sql = "UPDATE ml_runs SET status = 'FAILED', end_time = ? WHERE run_id = ?;";
const stmt = try database.prepare(sql);
defer db.DB.finalize(stmt);
try db.DB.bindText(stmt, 1, timestamp);
try db.DB.bindText(stmt, 2, run_id.?);
_ = try db.DB.step(stmt);
database.checkpointOnExit();
if (options.json) {
std.debug.print("{{\"success\":true,\"command\":\"run.fail\",\"data\":{{\"run_id\":\"{s}\",\"status\":\"FAILED\"}}}}\n", .{run_id.?});
} else {
colors.printSuccess("✓ Marked run as failed: {s}\n", .{run_id.?});
}
}
fn executeListLocal(allocator: std.mem.Allocator, options: *const RunOptions) !void {
const cfg = try config.Config.load(allocator);
defer {
var mut_cfg = cfg;
mut_cfg.deinit(allocator);
}
const db_path = try cfg.getDBPath(allocator);
defer allocator.free(db_path);
var database = try db.DB.init(allocator, db_path);
defer database.close();
const sql = "SELECT run_id, experiment_id, name, status, start_time FROM ml_runs ORDER BY start_time DESC;";
const stmt = try database.prepare(sql);
defer db.DB.finalize(stmt);
if (options.json) {
std.debug.print("{{\"success\":true,\"command\":\"run.list\",\"data\":{{\"runs\":[", .{});
var first = true;
while (try db.DB.step(stmt)) {
if (!first) std.debug.print(",", .{});
first = false;
const run_id = db.DB.columnText(stmt, 0);
const exp_id = db.DB.columnText(stmt, 1);
const name = db.DB.columnText(stmt, 2);
const status = db.DB.columnText(stmt, 3);
const start = db.DB.columnText(stmt, 4);
std.debug.print("{{\"run_id\":\"{s}\",\"experiment_id\":\"{s}\",\"name\":\"{s}\",\"status\":\"{s}\",\"start_time\":\"{s}\"}}", .{ run_id, exp_id, name, status, start });
}
std.debug.print("]}}}\n", .{});
} else {
colors.printInfo("\nRuns:\n", .{});
colors.printInfo("{s:-<80}\n", .{""});
var count: usize = 0;
while (try db.DB.step(stmt)) {
const run_id = db.DB.columnText(stmt, 0);
const exp_id = db.DB.columnText(stmt, 1);
const name = db.DB.columnText(stmt, 2);
const status = db.DB.columnText(stmt, 3);
std.debug.print("{s} | {s} | {s} | {s}\n", .{ run_id, exp_id, name, status });
count += 1;
}
if (count == 0) {
colors.printWarning("No runs found. Start one with: ml run start --experiment <id>\n", .{});
}
}
}
// Server mode stubs (to be implemented)
fn executeStartServer(_: std.mem.Allocator, _: []const []const u8, _: *const RunOptions) !void {
std.debug.print("Server mode run start not yet implemented\n", .{});
}
fn executeFinishServer(_: std.mem.Allocator, _: []const []const u8, _: *const RunOptions) !void {
std.debug.print("Server mode run finish not yet implemented\n", .{});
}
fn executeFailServer(_: std.mem.Allocator, _: []const []const u8, _: *const RunOptions) !void {
std.debug.print("Server mode run fail not yet implemented\n", .{});
}
fn executeListServer(_: std.mem.Allocator, _: *const RunOptions) !void {
std.debug.print("Server mode run list not yet implemented\n", .{});
}
fn printUsage() !void {
colors.printInfo("Usage: ml run [options] <command> [args]\n", .{});
colors.printInfo("\nOptions:\n", .{});
colors.printInfo(" --json Output structured JSON\n", .{});
colors.printInfo(" --help, -h Show this help message\n", .{});
colors.printInfo("\nCommands:\n", .{});
colors.printInfo(" start --experiment <id> [--name <name>] Start a new run\n", .{});
colors.printInfo(" finish --run <id> Mark run as finished\n", .{});
colors.printInfo(" fail --run <id> Mark run as failed\n", .{});
colors.printInfo(" list List all runs\n", .{});
colors.printInfo("\nExamples:\n", .{});
colors.printInfo(" ml run start --experiment <exp_id> --name training\n", .{});
colors.printInfo(" ml run finish --run <run_id>\n", .{});
}

251
cli/src/db.zig Normal file
View file

@ -0,0 +1,251 @@
const std = @import("std");
// SQLite C bindings
const c = @cImport({
@cInclude("sqlite3.h");
});
// 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 NOT NULL,
\\ name TEXT,
\\ status TEXT,
\\ start_time DATETIME,
\\ end_time DATETIME,
\\ artifact_uri TEXT,
\\ 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, &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), c.SQLITE_TRANSIENT);
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 tm = std.time.epoch.EpochSeconds{ .secs = @intCast(now) };
const dt = tm.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}", .{
tm.getEpochDay().year,
tm.getEpochDay().month,
tm.getEpochDay().day,
dt.getHoursIntoDay(),
dt.getMinutesIntoHour(),
dt.getSecondsIntoMinute(),
});
return try allocator.dupe(u8, len);
}

View file

@ -0,0 +1,36 @@
const std = @import("std");
const build_options = @import("build_options");
/// SQLite embedding strategy (mirrors rsync pattern)
///
/// For dev builds: link against system SQLite library
/// For release builds: compile SQLite from downloaded amalgamation
///
/// To prepare for release:
/// 1. Run: make build-sqlite
/// 2. Build with: zig build prod
pub const USE_EMBEDDED_SQLITE = build_options.has_sqlite_release;
/// Compile flags for embedded SQLite
pub const SQLITE_FLAGS = &[_][]const u8{
"-DSQLITE_ENABLE_FTS5",
"-DSQLITE_ENABLE_JSON1",
"-DSQLITE_THREADSAFE=1",
"-DSQLITE_USE_URI",
"-DSQLITE_ENABLE_COLUMN_METADATA",
"-DSQLITE_ENABLE_STAT4",
};
/// Get SQLite include path for embedded builds
pub fn getSqliteIncludePath(b: *std.Build) ?std.Build.LazyPath {
if (!USE_EMBEDDED_SQLITE) return null;
return b.path(build_options.sqlite_release_path);
}
/// Get SQLite source file path for embedded builds
pub fn getSqliteSourcePath(b: *std.Build) ?std.Build.LazyPath {
if (!USE_EMBEDDED_SQLITE) return null;
const path = std.fs.path.join(b.allocator, &.{ build_options.sqlite_release_path, "sqlite3.c" }) catch return null;
return b.path(path);
}