Files
tasksoftwaretest/server/sqlite.jai
T
2026-10-29 23:40:41 +01:00

152 lines
5.8 KiB
Plaintext

// Minimal SQLite binding, loaded at runtime (sqlite3.dll on Windows, libsqlite3.so.0 on Linux).
// Loading dynamically means the server binary has no link-time dependency on SQLite, so it can be
// cross-compiled and then just needs the distro package (libsqlite3-0) on the droplet.
SQLITE_OK :: 0;
SQLITE_ROW :: 100;
SQLITE_DONE :: 101;
SQLITE_OPEN_READWRITE :: 0x00000002;
SQLITE_OPEN_CREATE :: 0x00000004;
SQLITE_OPEN_NOMUTEX :: 0x00008000;
SQLITE_TRANSIENT :: cast(*void) -1;
sqlite3 :: void;
sqlite3_stmt :: void;
Sqlite_Api :: struct {
sqlite3_open_v2: (filename: *u8, db: **sqlite3, flags: s32, vfs: *u8) -> s32 #c_call;
sqlite3_close_v2: (db: *sqlite3) -> s32 #c_call;
sqlite3_exec: (db: *sqlite3, sql: *u8, callback: *void, arg: *void, errmsg: **u8) -> s32 #c_call;
sqlite3_prepare_v2: (db: *sqlite3, sql: *u8, bytes: s32, stmt: **sqlite3_stmt, tail: **u8) -> s32 #c_call;
sqlite3_step: (stmt: *sqlite3_stmt) -> s32 #c_call;
sqlite3_reset: (stmt: *sqlite3_stmt) -> s32 #c_call;
sqlite3_finalize: (stmt: *sqlite3_stmt) -> s32 #c_call;
sqlite3_bind_int64: (stmt: *sqlite3_stmt, index: s32, value: s64) -> s32 #c_call;
sqlite3_bind_text: (stmt: *sqlite3_stmt, index: s32, text: *u8, bytes: s32, destructor: *void) -> s32 #c_call;
sqlite3_bind_blob: (stmt: *sqlite3_stmt, index: s32, data: *void, bytes: s32, destructor: *void) -> s32 #c_call;
sqlite3_column_int64: (stmt: *sqlite3_stmt, column: s32) -> s64 #c_call;
sqlite3_column_text: (stmt: *sqlite3_stmt, column: s32) -> *u8 #c_call;
sqlite3_column_blob: (stmt: *sqlite3_stmt, column: s32) -> *void #c_call;
sqlite3_column_bytes: (stmt: *sqlite3_stmt, column: s32) -> s32 #c_call;
sqlite3_errmsg: (db: *sqlite3) -> *u8 #c_call;
sqlite3_changes: (db: *sqlite3) -> s32 #c_call;
sqlite3_last_insert_rowid: (db: *sqlite3) -> s64 #c_call;
sqlite3_busy_timeout: (db: *sqlite3, ms: s32) -> s32 #c_call;
sqlite3_libversion: () -> *u8 #c_call;
}
sql: Sqlite_Api;
load_sqlite :: () -> bool {
#if OS == .WINDOWS {
tf_kernel32 :: #system_library "kernel32";
LoadLibraryA :: (name: *u8) -> *void #foreign tf_kernel32;
GetProcAddress :: (module: *void, name: *u8) -> *void #foreign tf_kernel32;
lib := LoadLibraryA("sqlite3.dll");
lookup :: GetProcAddress;
} else {
lib := dlopen("libsqlite3.so.0", RTLD_NOW);
if !lib lib = dlopen("libsqlite3.so", RTLD_NOW);
lookup :: dlsym;
}
if !lib {
log_error("Could not load SQLite. On Ubuntu/Debian: sudo apt install libsqlite3-0. On Windows: put sqlite3.dll next to the server.");
return false;
}
info := type_info(Sqlite_Api);
for info.members {
address := lookup(lib, temp_c_string(it.name));
if !address {
log_error("SQLite is missing %; it may be too old.", it.name);
return false;
}
(cast(**void) (cast(*u8) *sql + it.offset_in_bytes)).* = address;
}
return true;
}
//
// Small conveniences.
//
Db :: struct {
handle: *sqlite3;
}
db_open :: (path: string) -> Db, bool {
db: Db;
rc := sql.sqlite3_open_v2(temp_c_string(path), *db.handle, SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE | SQLITE_OPEN_NOMUTEX, null);
if rc != SQLITE_OK {
log_error("Could not open database %: %", path, db_error(db));
return db, false;
}
sql.sqlite3_busy_timeout(db.handle, 5000);
db_exec(db, "PRAGMA journal_mode = WAL;");
db_exec(db, "PRAGMA synchronous = NORMAL;");
db_exec(db, "PRAGMA foreign_keys = ON;");
return db, true;
}
db_error :: (db: Db) -> string {
return to_string(sql.sqlite3_errmsg(db.handle));
}
db_exec :: (db: Db, statement: string) -> bool {
err: *u8;
rc := sql.sqlite3_exec(db.handle, temp_c_string(statement), null, null, *err);
if rc != SQLITE_OK {
log_error("SQL error in \"%\": %", statement, to_string(err));
return false;
}
return true;
}
Stmt :: struct {
handle: *sqlite3_stmt;
db: Db;
}
prepare :: (db: Db, statement: string) -> Stmt {
s: Stmt;
s.db = db;
rc := sql.sqlite3_prepare_v2(db.handle, statement.data, cast(s32) statement.count, *s.handle, null);
if rc != SQLITE_OK log_error("SQL prepare failed for \"%\": %", statement, db_error(db));
return s;
}
bind :: (s: Stmt, index: s32, value: s64) { sql.sqlite3_bind_int64(s.handle, index, value); }
bind :: (s: Stmt, index: s32, value: string) { sql.sqlite3_bind_text(s.handle, index, value.data, cast(s32) value.count, SQLITE_TRANSIENT); }
bind_blob :: (s: Stmt, index: s32, value: string) { sql.sqlite3_bind_blob(s.handle, index, value.data, cast(s32) value.count, SQLITE_TRANSIENT); }
step :: (s: Stmt) -> bool {
rc := sql.sqlite3_step(s.handle);
if rc == SQLITE_ROW return true;
if rc != SQLITE_DONE log_error("SQL step failed: %", db_error(s.db));
return false;
}
// Runs a statement that returns no rows; returns success.
run :: (s: Stmt) -> bool {
rc := sql.sqlite3_step(s.handle);
if rc != SQLITE_DONE && rc != SQLITE_ROW {
log_error("SQL failed: %", db_error(s.db));
return false;
}
return true;
}
reset :: (s: Stmt) { sql.sqlite3_reset(s.handle); }
finalize :: (s: Stmt) { sql.sqlite3_finalize(s.handle); }
column_int :: (s: Stmt, column: s32) -> s64 { return sql.sqlite3_column_int64(s.handle, column); }
// Column text/blob, copied into temporary storage.
column_text :: (s: Stmt, column: s32) -> string {
data := sql.sqlite3_column_blob(s.handle, column);
count := sql.sqlite3_column_bytes(s.handle, column);
result := alloc_string(count,, temp);
if count memcpy(result.data, data, count);
return result;
}