// The native SQLite backend for moondb: a real connection to a real SQLite database
// via the vendored amalgamation. `SqliteDriver` implements `@moondb.Driver`, so any
// moondb-based query layer — moonorm's `Session`, a hand-written statement, anything
// pinned to the interface — runs against SQLite unchanged. Bound parameters map to
// SQLite's storage classes; result columns decode back into `@moondb.Value` by their
// runtime type. This is the only C-touching package in the moondb/moonorm stack: the
// amalgamation lives here, isolated, so everything above it stays pure MoonBit.
///|
/// A live connection to a SQLite database. Open it with `SqliteDriver::open`; it
/// implements `@moondb.Driver`, so a moondb query layer can drive it directly. The
/// opaque `sqlite3*` is carried as its pointer bits in `handle`.
pub struct SqliteDriver {
handle : Int64
mut closed : Bool
}
///|
/// Open (or create) the database at `path`. Use `":memory:"` for a private in-memory
/// database. Raises `@moondb.ConnectError` if the file cannot be opened.
pub fn SqliteDriver::open(path : String) -> SqliteDriver raise @moondb.DbError {
let h = ffi_open(@utf8.encode(path))
if h == 0L {
raise @moondb.ConnectError("cannot open database: " + path)
}
{ handle: h, closed: false }
}
///|
/// Whether `close` has been called. Once closed, every statement raises
/// `@moondb.Closed` rather than touching the freed `sqlite3*`.
pub fn SqliteDriver::is_closed(self : SqliteDriver) -> Bool {
self.closed
}
///|
/// Raise `Closed` if the connection has been closed. Guarding here keeps a
/// use-after-close a clean `DbError` instead of a dangling-pointer call into C.
fn SqliteDriver::ensure_open(self : SqliteDriver) -> Unit raise @moondb.DbError {
if self.closed {
raise @moondb.Closed
}
}
///|
/// Run a semicolon-separated script (e.g. a schema DDL block) in one call. Unlike
/// `execute`, this uses SQLite's multi-statement `exec` path and binds no parameters.
/// Raises `@moondb.QueryError` with SQLite's message on failure.
pub fn SqliteDriver::exec_script(
self : SqliteDriver,
script : String,
) -> Unit raise @moondb.DbError {
self.ensure_open()
let rc = ffi_exec(self.handle, @utf8.encode(script))
if rc != 0 {
raise @moondb.QueryError(bytes_to_str(ffi_errmsg(self.handle)))
}
}
///|
fn bytes_to_str(b : Bytes) -> String {
@utf8.decode(b) catch {
_ => ""
}
}
///|
/// Bind each `@moondb.Value` to its 1-based SQLite parameter slot. Every variant maps
/// to a SQLite storage class out-of-band — nothing is ever spliced into the SQL text.
fn bind_params(st : Int64, params : Array[@moondb.Value]) -> Unit {
for i = 0; i < params.length(); i = i + 1 {
let idx = i + 1
match params[i] {
@moondb.Null => ffi_bind_null(st, idx) |> ignore
@moondb.Bool(v) => ffi_bind_int(st, idx, if v { 1 } else { 0 }) |> ignore
@moondb.Int(v) => ffi_bind_int(st, idx, v) |> ignore
@moondb.Int64(v) => ffi_bind_int64(st, idx, v) |> ignore
@moondb.Double(v) => ffi_bind_double(st, idx, v) |> ignore
@moondb.Text(v) => ffi_bind_text(st, idx, @utf8.encode(v)) |> ignore
@moondb.Blob(v) => ffi_bind_blob(st, idx, v) |> ignore
}
}
}
///|
/// Prepare + bind, returning the statement handle or raising `QueryError` with
/// SQLite's message if the prepare failed.
fn SqliteDriver::prepare_bound(
self : SqliteDriver,
sql : String,
params : Array[@moondb.Value],
) -> Int64 raise @moondb.DbError {
self.ensure_open()
let st = ffi_prepare(self.handle, @utf8.encode(sql))
if st == 0L {
raise @moondb.QueryError(bytes_to_str(ffi_errmsg(self.handle)))
}
bind_params(st, params)
st
}
///|
pub impl @moondb.Driver for SqliteDriver with fn execute(self, sql, params) {
let st = self.prepare_bound(sql, params)
let rc = ffi_step(st)
ffi_finalize(st) |> ignore
if rc != sqlite_done && rc != sqlite_row {
raise @moondb.QueryError(bytes_to_str(ffi_errmsg(self.handle)))
}
{
rows_affected: ffi_changes(self.handle).to_int64(),
last_insert_id: ffi_last_id(self.handle),
}
}
///|
pub impl @moondb.Driver for SqliteDriver with fn query(self, sql, params) {
let st = self.prepare_bound(sql, params)
let ncol = ffi_col_count(st)
let cols : Array[String] = []
for i = 0; i < ncol; i = i + 1 {
cols.push(bytes_to_str(ffi_col_name(st, i)))
}
let rows : Array[@moondb.Row] = []
for ;; {
let rc = ffi_step(st)
if rc != sqlite_row {
break
}
let vals : Array[@moondb.Value] = []
for i = 0; i < ncol; i = i + 1 {
// SQLite column types: 1=INTEGER 2=FLOAT 3=TEXT 4=BLOB 5=NULL.
let v : @moondb.Value = match ffi_col_type(st, i) {
1 => @moondb.Int64(ffi_col_int64(st, i))
2 => @moondb.Double(ffi_col_double(st, i))
3 => @moondb.Text(bytes_to_str(ffi_col_text(st, i)))
4 => @moondb.Blob(ffi_col_blob(st, i))
_ => @moondb.Null
}
vals.push(v)
}
rows.push({ columns: cols, values: vals })
}
ffi_finalize(st) |> ignore
rows
}
///|
pub impl @moondb.Driver for SqliteDriver with fn begin(self) {
self.run_txn("BEGIN")
}
///|
pub impl @moondb.Driver for SqliteDriver with fn commit(self) {
self.run_txn("COMMIT")
}
///|
pub impl @moondb.Driver for SqliteDriver with fn rollback(self) {
self.run_txn("ROLLBACK")
}
///|
/// Run a transaction-control keyword through SQLite's `exec` path, raising
/// `QueryError` with the backend message on failure.
fn SqliteDriver::run_txn(
self : SqliteDriver,
kw : String,
) -> Unit raise @moondb.DbError {
self.ensure_open()
let rc = ffi_exec(self.handle, @utf8.encode(kw))
if rc != 0 {
raise @moondb.QueryError(bytes_to_str(ffi_errmsg(self.handle)))
}
}
///|
pub impl @moondb.Driver for SqliteDriver with fn close(self) {
// Best-effort and idempotent (Go's io.Closer discipline): free the sqlite3*
// exactly once, then mark closed so later statements raise Closed.
if !self.closed {
ffi_close(self.handle) |> ignore
self.closed = true
}
}