// Copyright 2026 Leo Cheng
// SPDX-License-Identifier: Apache-2.0
///|
/// A statement that is prepared once and run many times with new values.
///
/// `Driver::execute` and `Driver::query` prepare, bind, run and discard in one
/// call, which is right for a one-off and wasteful for a loop: a server that has
/// already parsed and planned a statement can execute it again with nothing but the
/// values. A `Stmt` is that handle. [`bind`](#bind) replaces the values,
/// [`execute`](#execute) and [`query`](#query) run the statement, and
/// [`close`](#close) releases the server-side resources.
///
/// A handle is not a connection: it belongs to the connection that prepared it, and
/// running it after that connection closed raises `Closed`. It is not safe to share
/// between tasks for the same reason — one wire, one statement at a time.
///
/// ```moonbit check
/// test "one statement, two bindings" {
/// let db = @moondb.MockDriver::new()
/// let stmt = db.prepare("INSERT INTO hero (id, name) VALUES (?, ?)")
/// stmt.bind([@moondb.Value::Int(1), @moondb.Value::Text("Nova")])
/// stmt.execute() |> ignore
/// stmt.bind([@moondb.Value::Int(2), @moondb.Value::Text("Iris")])
/// stmt.execute() |> ignore
/// stmt.close()
/// assert_eq(db.row_count(), 2)
/// }
/// ```
pub(open) trait Stmt {
/// Replace the values the next run binds. The count and the order must match the
/// statement's parameters; a mismatch raises `TypeError` from the server or from
/// the driver, not from this call.
fn bind(Self, Array[Value]) -> Unit raise DbError
/// Run the statement and report rows changed and the last insert id.
fn execute(Self) -> ExecResult raise DbError
/// Run the statement and materialise every row.
fn query(Self) -> Array[Row] raise DbError
/// Run the statement and stream its rows. The default materialises through
/// [`query`](#query); a driver that fetches lazily overrides it.
fn query_stream(Self) -> &Cursor raise DbError = _
/// Release the server-side handle. Idempotent; running a closed handle raises
/// `Closed`.
fn close(Self) -> Unit
/// The SQL text this handle was prepared from, as handed to `prepare`.
fn sql(Self) -> String
/// The parameter name each slot takes, in order. Empty when the statement binds
/// positionally, which is what a handle that never rewrote `:name` reports.
fn params(Self) -> Array[String]
/// Bind a name-keyed map. The default maps the names [`params`](#params) reports
/// onto the positional array and then calls [`bind`](#bind); a missing or unknown
/// name raises `TypeError`.
fn bind_named(Self, Map[String, Value]) -> Unit raise DbError = _
}
///|
/// The default `bind_named`: resolve each slot's name against the map and bind the
/// ordered array.
impl Stmt with fn bind_named(self : Self, args : Map[String, Value]) raise DbError {
let names = self.params()
if names.is_empty() {
raise TypeError("statement has no named parameters")
}
let out : Array[Value] = []
for name in names {
match args.get(name) {
Some(v) => out.push(v)
None => raise TypeError("missing parameter :" + name)
}
}
for name in args.keys() {
if !names.contains(name) {
raise TypeError("unexpected parameter :" + name)
}
}
self.bind(out)
}
///|
/// The default async `bind_named`: the same resolution, awaited on `bind`.
impl AsyncStmt with fn bind_named(self : Self, args : Map[String, Value]) raise DbError {
let names = self.params()
if names.is_empty() {
raise TypeError("statement has no named parameters")
}
let out : Array[Value] = []
for name in names {
match args.get(name) {
Some(v) => out.push(v)
None => raise TypeError("missing parameter :" + name)
}
}
for name in args.keys() {
if !names.contains(name) {
raise TypeError("unexpected parameter :" + name)
}
}
self.bind(out)
}
///|
/// The default `query_stream` for a statement: materialise through `query` and hand
/// back an [`ArrayCursor`].
impl Stmt with fn query_stream(self : Self) -> &Cursor raise DbError {
ArrayCursor::new(self.query())
}
///|
/// The asynchronous counterpart of [`Stmt`], for a backend reached over a socket.
pub(open) trait AsyncStmt {
/// Replace the values the next run binds.
async fn bind(Self, Array[Value]) -> Unit raise DbError
/// Run the statement and report rows changed and the last insert id.
async fn execute(Self) -> ExecResult raise DbError
/// Run the statement and materialise every row.
async fn query(Self) -> Array[Row] raise DbError
/// Run the statement and stream its rows.
async fn query_stream(Self) -> &AsyncCursor raise DbError = _
/// Release the server-side handle — a round trip on a wire driver. Idempotent.
async fn close(Self) -> Unit
/// The SQL text this handle was prepared from.
fn sql(Self) -> String
/// The parameter name each slot takes, in order.
fn params(Self) -> Array[String]
/// Bind a name-keyed map.
async fn bind_named(Self, Map[String, Value]) -> Unit raise DbError = _
}
///|
/// The default async `query_stream` for a statement: materialise through `query`.
impl AsyncStmt with fn query_stream(self : Self) -> &AsyncCursor raise DbError {
AsyncArrayCursor::new(self.query())
}
///|
/// The statement handle `Driver::prepare` returns when a driver has nothing
/// server-side to offer: it keeps the text and the bound values and re-runs the
/// driver's ordinary `execute`/`query` on every call. It makes the [`Stmt`]
/// contract available to every existing driver without changing it.
pub struct BufferedStmt[T] {
driver : T
text : String
mut params : Array[Value]
mut closed : Bool
}
///|
/// A buffered handle over `driver` for `sql`, with no values bound yet.
pub fn[T] BufferedStmt::new(driver : T, sql : String) -> BufferedStmt[T] {
{ driver, text: sql, params: [], closed: false, }
}
///|
/// Raise `Closed` if the handle has already been closed.
fn[T] BufferedStmt::ensure_open(self : BufferedStmt[T]) -> Unit raise DbError {
if self.closed {
raise Closed
}
}
///|
impl[T : Driver] Stmt for BufferedStmt[T] with fn bind(self, params) raise DbError {
self.ensure_open()
self.params = params
}
///|
impl[T : Driver] Stmt for BufferedStmt[T] with fn execute(self) raise DbError {
self.ensure_open()
self.driver.execute(self.text, self.params)
}
///|
impl[T : Driver] Stmt for BufferedStmt[T] with fn query(self) raise DbError {
self.ensure_open()
self.driver.query(self.text, self.params)
}
///|
impl[T : Driver] Stmt for BufferedStmt[T] with fn query_stream(self) raise DbError {
self.ensure_open()
self.driver.query_stream(self.text, self.params)
}
///|
impl[T : Driver] Stmt for BufferedStmt[T] with fn close(self) {
self.closed = true
}
///|
impl[T : Driver] Stmt for BufferedStmt[T] with fn sql(self) {
self.text
}
///|
impl[T : Driver] Stmt for BufferedStmt[T] with fn params(_self) {
[]
}
///|
/// The statement handle `AsyncDriver::prepare` returns when the backend has
/// nothing server-side to offer — the asynchronous twin of [`BufferedStmt`].
pub struct BufferedAsyncStmt[T] {
driver : T
text : String
mut params : Array[Value]
mut closed : Bool
}
///|
/// A buffered async handle over `driver` for `sql`, with no values bound yet.
pub fn[T] BufferedAsyncStmt::new(
driver : T,
sql : String,
) -> BufferedAsyncStmt[T] {
{ driver, text: sql, params: [], closed: false, }
}
///|
/// Raise `Closed` if the handle has already been closed.
fn[T] BufferedAsyncStmt::ensure_open(
self : BufferedAsyncStmt[T],
) -> Unit raise DbError {
if self.closed {
raise Closed
}
}
///|
impl[T : AsyncDriver] AsyncStmt for BufferedAsyncStmt[T] with fn bind(
self,
params,
) raise DbError {
self.ensure_open()
self.params = params
}
///|
impl[T : AsyncDriver] AsyncStmt for BufferedAsyncStmt[T] with fn execute(self) raise DbError {
self.ensure_open()
self.driver.execute(self.text, self.params)
}
///|
impl[T : AsyncDriver] AsyncStmt for BufferedAsyncStmt[T] with fn query(self) raise DbError {
self.ensure_open()
self.driver.query(self.text, self.params)
}
///|
impl[T : AsyncDriver] AsyncStmt for BufferedAsyncStmt[T] with fn query_stream(
self,
) raise DbError {
self.ensure_open()
self.driver.query_stream(self.text, self.params)
}
///|
impl[T : AsyncDriver] AsyncStmt for BufferedAsyncStmt[T] with fn close(self) {
self.closed = true
}
///|
impl[T : AsyncDriver] AsyncStmt for BufferedAsyncStmt[T] with fn sql(self) {
self.text
}
///|
impl[T : AsyncDriver] AsyncStmt for BufferedAsyncStmt[T] with fn params(_self) {
[]
}