// Copyright 2026 Leo Cheng
// SPDX-License-Identifier: Apache-2.0
///|
/// A statement SQLite prepares once and runs many times.
///
/// `prepare` compiles the SQL into a `sqlite3_stmt` and keeps it alive; `bind`
/// resets the compiled program and binds a fresh set of values, so a loop over a
/// thousand rows pays for parsing and planning once. `close` finalises the
/// statement. A `:name` statement is rewritten to `?` at prepare time and remembers
/// the slot names, so `bind_named` works without the caller numbering anything.
pub struct SqliteStmt {
driver : SqliteDriver
// The compiled `sqlite3_stmt*`, as pointer bits.
st : Int64
text : String
slots : Array[String]
cols : Array[String]
mut closed : Bool
}
///|
/// Compile `sql` into a reusable statement. A statement using `:name` parameters is
/// rewritten to SQLite's `?` spelling first.
pub fn SqliteDriver::prepare_stmt(
self : SqliteDriver,
sql : String,
) -> SqliteStmt raise @moondb.DbError {
self.ensure_open()
let named = @moondb.rewrite_named(sql, @moondb.Dialect::Sqlite)
let st = ffi_prepare(self.handle, @utf8.encode(named.sql))
if st == 0L {
raise @moondb.QueryError(bytes_to_str(ffi_errmsg(self.handle)))
}
{ driver: self, st, text: sql, slots: named.slots, cols: [], closed: false, }
}
///|
fn SqliteStmt::ensure_open(self : SqliteStmt) -> Unit raise @moondb.DbError {
if self.closed {
raise @moondb.Closed
}
}
///|
fn SqliteStmt::reset_bound(
self : SqliteStmt,
params : Array[@moondb.Value],
) -> Unit raise @moondb.DbError {
self.ensure_open()
ffi_reset(self.st) |> ignore
ffi_clear_bindings(self.st) |> ignore
bind_params(self.st, params)
}
///|
impl @moondb.Stmt for SqliteStmt with fn bind(self, params) raise @moondb.DbError {
self.reset_bound(params)
}
///|
impl @moondb.Stmt for SqliteStmt with fn execute(self) raise @moondb.DbError {
self.ensure_open()
let rc = ffi_step(self.st)
// The program has run to completion; reset so the next `execute` starts over.
ffi_reset(self.st) |> ignore
if rc != sqlite_done && rc != sqlite_row {
raise @moondb.QueryError(bytes_to_str(ffi_errmsg(self.driver.handle)))
}
{
rows_affected: ffi_changes(self.driver.handle).to_int64(),
last_insert_id: ffi_last_id(self.driver.handle),
}
}
///|
impl @moondb.Stmt for SqliteStmt with fn query(self) raise @moondb.DbError {
self.ensure_open()
ffi_reset(self.st) |> ignore
let ncol = ffi_col_count(self.st)
let cols : Array[String] = []
for i = 0; i < ncol; i = i + 1 {
cols.push(bytes_to_str(ffi_col_name(self.st, i)))
}
let rows : Array[@moondb.Row] = []
let mut term = sqlite_done
for ;; {
let rc = ffi_step(self.st)
if rc != sqlite_row {
term = rc
break
}
rows.push({ columns: cols, values: read_row(self.st, ncol), })
}
ffi_reset(self.st) |> ignore
if term != sqlite_done {
raise @moondb.QueryError(bytes_to_str(ffi_errmsg(self.driver.handle)))
}
rows
}
///|
impl @moondb.Stmt for SqliteStmt with fn query_stream(self) raise @moondb.DbError {
self.ensure_open()
ffi_reset(self.st) |> ignore
let ncol = ffi_col_count(self.st)
let cols : Array[String] = []
for i = 0; i < ncol; i = i + 1 {
cols.push(bytes_to_str(ffi_col_name(self.st, i)))
}
SqliteStmtCursor::{ stmt: self, cols, ncol, done: false, }
}
///|
impl @moondb.Stmt for SqliteStmt with fn close(self) {
if !self.closed {
ffi_finalize(self.st) |> ignore
self.closed = true
}
}
///|
impl @moondb.Stmt for SqliteStmt with fn sql(self) {
self.text
}
///|
impl @moondb.Stmt for SqliteStmt with fn params(self) {
self.slots
}
///|
/// Read one row whose statement is positioned on it. SQLite column types:
/// 1=INTEGER 2=FLOAT 3=TEXT 4=BLOB 5=NULL.
fn read_row(st : Int64, ncol : Int32) -> Array[@moondb.Value] {
let vals : Array[@moondb.Value] = []
for i = 0; i < ncol; i = i + 1 {
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)
}
vals
}
///|
/// A cursor over a [`SqliteStmt`]'s result. Unlike the one-shot
/// [`SqliteCursor`](sqlite.mbt), exhausting or closing it *resets* the statement
/// instead of finalising it, so the handle stays reusable.
struct SqliteStmtCursor {
stmt : SqliteStmt
cols : Array[String]
ncol : Int32
mut done : Bool
}
///|
impl @moondb.Cursor for SqliteStmtCursor with fn next(self) raise @moondb.DbError {
if self.done {
return None
}
let rc = ffi_step(self.stmt.st)
if rc != sqlite_row {
self.done = true
ffi_reset(self.stmt.st) |> ignore
if rc != sqlite_done {
raise @moondb.QueryError(
bytes_to_str(ffi_errmsg(self.stmt.driver.handle)),
)
}
return None
}
Some({ columns: self.cols, values: read_row(self.stmt.st, self.ncol), })
}
///|
impl @moondb.Cursor for SqliteStmtCursor with fn close(self) {
if !self.done {
self.done = true
ffi_reset(self.stmt.st) |> ignore
}
}
///|
pub extend SqliteStmtCursor with @moondb.Cursor::{close, next}