//! SQLite bindings over the vendored amalgamation in `vendor/sqlite`. //! //! V's own `db.sqlite` module links whatever SQLite the host provides. Koh //! links its own copy instead, so macOS and Linux run the same SQLite and the //! build depends on nothing outside the repo. Only the calls Koh needs are //! declared here — no ORM, no pooling, no reflection. module storage #flag -I@VMODROOT/vendor/sqlite #flag @VMODROOT/vendor/sqlite/sqlite3.c #include "sqlite3.h" // Opaque handles. V wants every C type named in a declaration to exist. pub struct C.sqlite3 {} pub struct C.sqlite3_stmt {} fn C.sqlite3_libversion() &char fn C.sqlite3_open_v2(filename &char, pp_db &&C.sqlite3, flags i32, vfs &char) i32 fn C.sqlite3_close_v2(db &C.sqlite3) i32 fn C.sqlite3_errmsg(db &C.sqlite3) &char fn C.sqlite3_busy_timeout(db &C.sqlite3, ms i32) i32 fn C.sqlite3_exec(db &C.sqlite3, query &char, callback voidptr, arg voidptr, errmsg &&char) i32 fn C.sqlite3_free(ptr voidptr) fn C.sqlite3_prepare_v2(db &C.sqlite3, query &char, n i32, stmt &&C.sqlite3_stmt, tail &&char) i32 fn C.sqlite3_step(stmt &C.sqlite3_stmt) i32 fn C.sqlite3_reset(stmt &C.sqlite3_stmt) i32 fn C.sqlite3_finalize(stmt &C.sqlite3_stmt) i32 fn C.sqlite3_bind_blob(stmt &C.sqlite3_stmt, idx i32, val voidptr, n i32, done voidptr) i32 fn C.sqlite3_bind_text(stmt &C.sqlite3_stmt, idx i32, val &char, n i32, done voidptr) i32 fn C.sqlite3_bind_int64(stmt &C.sqlite3_stmt, idx i32, val i64) i32 fn C.sqlite3_bind_null(stmt &C.sqlite3_stmt, idx i32) i32 fn C.sqlite3_column_blob(stmt &C.sqlite3_stmt, idx i32) voidptr fn C.sqlite3_column_bytes(stmt &C.sqlite3_stmt, idx i32) i32 fn C.sqlite3_column_text(stmt &C.sqlite3_stmt, idx i32) &u8 fn C.sqlite3_column_int64(stmt &C.sqlite3_stmt, idx i32) i64 fn C.sqlite3_column_type(stmt &C.sqlite3_stmt, idx i32) i32 fn C.sqlite3_changes(db &C.sqlite3) i32 const sqlite_ok = 0 const sqlite_busy = 5 const sqlite_row = 100 const sqlite_done = 101 const sqlite_open_readwrite = 0x00000002 const sqlite_open_create = 0x00000004 const sqlite_null = 5 // SQLITE_TRANSIENT: tells SQLite to copy the buffer instead of borrowing it. const transient = unsafe { voidptr(isize(-1)) } fn cstring(s string) &char { buf := unsafe { malloc_noscan(s.len + 1) } unsafe { vmemcpy(buf, s.str, s.len) buf[s.len] = 0 } return &char(buf) } pub fn libversion() string { return unsafe { cstring_to_vstring(C.sqlite3_libversion()) } } @[heap] pub struct Db { mut: conn &C.sqlite3 = unsafe { nil } } // connect opens a database file, creating it when missing. `busy_timeout_ms` // is how long a second writer waits before giving up. pub fn connect(path string, busy_timeout_ms int) !Db { path_c := cstring(path) defer { unsafe { free(path_c) } } mut conn := &C.sqlite3(unsafe { nil }) flags := sqlite_open_readwrite | sqlite_open_create code := C.sqlite3_open_v2(path_c, &conn, flags, unsafe { nil }) if code != sqlite_ok { msg := unsafe { cstring_to_vstring(C.sqlite3_errmsg(conn)) } C.sqlite3_close_v2(conn) return error('cannot open ${path}: ${msg}') } C.sqlite3_busy_timeout(conn, busy_timeout_ms) return Db{ conn: conn } } pub fn (db &Db) close() { if db.conn != unsafe { nil } { C.sqlite3_close_v2(db.conn) } } pub fn (db &Db) last_error() string { return unsafe { cstring_to_vstring(C.sqlite3_errmsg(db.conn)) } } pub fn (db &Db) changes() int { return C.sqlite3_changes(db.conn) } // exec runs one or more statements that return no rows. pub fn (db &Db) exec(query string) ! { query_c := cstring(query) defer { unsafe { free(query_c) } } mut errmsg := &char(unsafe { nil }) code := C.sqlite3_exec(db.conn, query_c, unsafe { nil }, unsafe { nil }, &errmsg) if code == sqlite_busy { if errmsg != unsafe { nil } { C.sqlite3_free(errmsg) } return error(busy) } if code != sqlite_ok { msg := if errmsg != unsafe { nil } { unsafe { cstring_to_vstring(errmsg) } } else { db.last_error() } if errmsg != unsafe { nil } { C.sqlite3_free(errmsg) } return error(msg) } } // Stmt is a prepared statement. Call finalize() when done; `defer stmt.finalize()` // is the normal shape. @[heap] pub struct Stmt { mut: stmt &C.sqlite3_stmt = unsafe { nil } db &Db = unsafe { nil } } // prepare compiles one statement. The error names the query, since SQLite's message alone does not. pub fn (db &Db) prepare(query string) !Stmt { query_c := cstring(query) defer { unsafe { free(query_c) } } mut stmt := &C.sqlite3_stmt(unsafe { nil }) code := C.sqlite3_prepare_v2(db.conn, query_c, query.len, &stmt, unsafe { nil }) if code != sqlite_ok { return error('${db.last_error()} in: ${query}') } return Stmt{ stmt: stmt db: db } } pub fn (stmt &Stmt) bind_text(idx int, val string) ! { if C.sqlite3_bind_text(stmt.stmt, idx, val.str, val.len, transient) != sqlite_ok { return error(stmt.db.last_error()) } } pub fn (stmt &Stmt) bind_blob(idx int, val []u8) ! { mut ptr := unsafe { nil } if val.len > 0 { ptr = val.data } if C.sqlite3_bind_blob(stmt.stmt, idx, ptr, val.len, transient) != sqlite_ok { return error(stmt.db.last_error()) } } pub fn (stmt &Stmt) bind_int(idx int, val i64) ! { if C.sqlite3_bind_int64(stmt.stmt, idx, val) != sqlite_ok { return error(stmt.db.last_error()) } } pub fn (stmt &Stmt) bind_null(idx int) ! { if C.sqlite3_bind_null(stmt.stmt, idx) != sqlite_ok { return error(stmt.db.last_error()) } } // step advances the statement. Returns true while rows are available. Rows of // a result are read with text/blob/int between steps; a statement that has // returned false must be reset() before it can be bound and run again. pub fn (stmt &Stmt) step() !bool { code := C.sqlite3_step(stmt.stmt) match code { sqlite_row { return true } sqlite_done { return false } sqlite_busy { return error(busy) } else { return error(stmt.db.last_error()) } } } pub fn (stmt &Stmt) reset() { C.sqlite3_reset(stmt.stmt) } pub fn (stmt &Stmt) finalize() { if stmt.stmt != unsafe { nil } { C.sqlite3_finalize(stmt.stmt) } } fn (stmt &Stmt) is_null(idx int) bool { return C.sqlite3_column_type(stmt.stmt, idx) == sqlite_null } // text copies the column out of SQLite's row buffer. It must copy: SQLite owns // that memory only until the statement is stepped again or finalized, and a V // string built by aliasing it would point at freed bytes. pub fn (stmt &Stmt) text(idx int) string { ptr := C.sqlite3_column_text(stmt.stmt, idx) if ptr == unsafe { nil } { return '' } n := C.sqlite3_column_bytes(stmt.stmt, idx) if n == 0 { return '' } mut out := []u8{len: n} unsafe { vmemcpy(out.data, ptr, n) } return out.bytestr() } // blob copies for the same reason as text. pub fn (stmt &Stmt) blob(idx int) []u8 { ptr := C.sqlite3_column_blob(stmt.stmt, idx) n := C.sqlite3_column_bytes(stmt.stmt, idx) if ptr == unsafe { nil } || n == 0 { return [] } mut out := []u8{len: n} unsafe { vmemcpy(out.data, ptr, n) } return out } pub fn (stmt &Stmt) int(idx int) i64 { return C.sqlite3_column_int64(stmt.stmt, idx) } pub fn (stmt &Stmt) text_or_none(idx int) ?string { if stmt.is_null(idx) { return none } return stmt.text(idx) } pub fn (stmt &Stmt) int_or_none(idx int) ?i64 { if stmt.is_null(idx) { return none } return stmt.int(idx) }