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