moondb

API · generated

The interface

330 declarations across 7 packages — the contract and the three drivers that ratify it — read out of pkg.generated.mbti and described from the /// docs in the sources.

moonbitstack/moondb

moondb

Types 28

pub struct ArrayCursor {
  rows : Array[Row]
  mut pos : Int32
}

A Cursor over an already-materialised Array[Row]. The fallback the default Driver::query_stream hands back for any driver that has not overridden it: it preserves the streaming interface (callers pull rows one at a time) even though the rows were fetched up front. The mock driver and the reference query layer test against it.

pub struct AsyncArrayCursor {
  rows : Array[Row]
  mut pos : Int32
}

An AsyncCursor over already-materialised rows — the async twin of ArrayCursor, and what the default AsyncDriver::query_stream hands back. It preserves the streaming interface (rows are pulled one at a time) even though they were all fetched up front.

It is a separate type from ArrayCursor rather than a second trait impl on it: one type implementing both Cursor and AsyncCursor makes every cursor.next() on the concrete type ambiguous between the two traits, which would break existing callers.

pub(open) trait AsyncCursor {
  async fn next(Self) -> Row? raise DbError
  async fn close(Self) -> Unit
}

The async counterpart of Cursor — a forward-only cursor whose next is awaited. A wire driver reads the next row off the socket when it is asked for, which is an I/O round trip and therefore async; the synchronous Cursor cannot express that, so a networked backend has no way to stream through it.

The resource discipline is Cursor's: MoonBit has no finalizer, so a driver-backed cursor MUST be drained (next until None) or [closed], or the statement leaks and the connection is left mid-result. AsyncArrayCursor is the materialised fallback, for a driver with no incremental fetch.

close does not raise, mirroring Cursor::close: abandoning a cursor is best-effort. A driver whose close has to do I/O — draining an unread result off the wire — swallows the failure, because a connection whose drain failed is already unusable and the next statement on it will say so.

pub(open) so out-of-tree drivers implement it.

pub(open) trait AsyncDriver {
  async fn execute(Self, String, Array[Value]) -> ExecResult raise DbError
  async fn query(Self, String, Array[Value]) -> Array[Row] raise DbError
  async fn query_stream(Self, String, Array[Value]) -> &AsyncCursor raise DbError = _
  async fn begin(Self) -> Unit raise DbError
  async fn commit(Self) -> Unit raise DbError
  async fn rollback(Self) -> Unit raise DbError
  async fn close(Self) -> Unit
  async fn ping(Self) -> Bool = _
  async fn prepare(Self, String) -> &AsyncStmt raise DbError = _
  fn dialect(Self) -> Dialect = _
  fn capabilities(Self) -> Capabilities = _
}

The contract a backend that talks over a network implements — Driver with every round trip awaited.

## Why there are two driver traits

Driver is synchronous, which fits a backend whose calls block in C: moonsqlite steps a prepared statement and returns. A backend reached over TCP cannot be written that way in MoonBit, because the only socket stack (moonbitlang/async) is async-only and an async fn cannot be called from a synchronous one. A wire driver forced to conform to Driver can therefore do nothing but raise — and, worse, it raises from ping too, so a Pool with pre_ping judges every one of its connections permanently unhealthy.

AsyncDriver is the seam for those backends. It is a peer of Driver, not a replacement: a synchronous backend keeps implementing Driver and does not become async to satisfy anything here. The two traits are deliberately identical method for method, so a query layer can offer a sync and an async path that read the same.

Everything Driver documents about the calling convention holds unchanged: sql carries positional placeholders in the backend's own spelling, params supplies one Value per placeholder in order, and values are bound out-of-band rather than interpolated.

This trait declares async methods but pulls in no async runtime — moondb stays dependency-free and still compiles on every backend. Only a driver that implements it, and a caller that runs it inside an event loop, need the runtime.

pub(open) so out-of-tree drivers (moonpostgres, moonmysql, …) implement it.

pub struct AsyncExecutor[T] {
  inner : T
}

The narrow view of an AsyncDriver. Obtain one with AsyncExecutor::new.

pub(open) trait AsyncQueryExecutor {
  async fn execute(Self, String, Array[Value]) -> ExecResult raise DbError
  async fn query(Self, String, Array[Value]) -> Array[Row] raise DbError
  async fn query_stream(Self, String, Array[Value]) -> &AsyncCursor raise DbError
  fn dialect(Self) -> Dialect
  fn capabilities(Self) -> Capabilities
}

The asynchronous counterpart of QueryExecutor, for a backend reached over a socket.

pub(open) trait AsyncStmt {
  async fn bind(Self, Array[Value]) -> Unit raise DbError
  async fn execute(Self) -> ExecResult raise DbError
  async fn query(Self) -> Array[Row] raise DbError
  async fn query_stream(Self) -> &AsyncCursor raise DbError = _
  async fn close(Self) -> Unit
  fn sql(Self) -> String
  fn params(Self) -> Array[String]
  async fn bind_named(Self, Map[String, Value]) -> Unit raise DbError = _
}

The asynchronous counterpart of Stmt, for a backend reached over a socket.

pub struct AsyncTxn[T] {
  driver : T
  mut depth : Int32
}

The asynchronous counterpart of Txn, for a backend reached over a socket.

let tx = @moondb.AsyncTxn::new(conn)
tx.run(async fn(tx) {
tx.driver().execute("INSERT INTO hero VALUES ($1, $2)", [Int(1), Text("Nova")])
|> ignore
})
pub struct BufferedAsyncStmt[T] {
  driver : T
  text : String
  mut params : Array[Value]
  mut closed : Bool
}

The statement handle AsyncDriver::prepare returns when the backend has nothing server-side to offer — the asynchronous twin of BufferedStmt.

pub struct BufferedStmt[T] {
  driver : T
  text : String
  mut params : Array[Value]
  mut closed : Bool
}

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(all) struct Capabilities {
  dialect : Dialect
  prepared_statements : Bool
  named_parameters : Bool
  server_cursors : Bool
  streaming_copy : Bool
  savepoints : Bool
  multiple_result_sets : Bool
  unsigned_integers : Bool
  returning : Bool
}
pub(open) trait Cursor {
  fn next(Self) -> Row? raise DbError
  fn close(Self) -> Unit
}

A forward-only cursor over a query's rows — the streaming counterpart to Driver::query. Where query materialises the whole result, a cursor yields one Row at a time, Python DB-API's fetchone).

next advances and returns the next row, or None once the result is exhausted; close releases the cursor early (a driver holding a server-side cursor or prepared statement frees it here). A driver with real incremental fetch — moonsqlite steps its prepared statement, an async driver reads rows off the wire on demand — returns a live cursor; the default Driver::query_stream falls back to an ArrayCursor over a materialised result, which honours the same interface without the memory bound.

A live cursor holds a resource (a prepared statement, a busy connection). MoonBit has no finalizer to reclaim it, so a driver-backed cursor MUST be fully drained (next until None) or closed — abandoning it leaks the statement or leaves the connection mid-result. The materialised ArrayCursor holds nothing and is safe to drop.

pub(open) so out-of-tree drivers implement it.

pub(all) suberror DbError {
  ConnectError(String)
  QueryError(String)
  TypeError(String)
  Closed
  Server(ServerDetail)
}

A database-layer failure. Every fallible operation on the interface raises this one error type, so a query layer catches a single kind. Kept deliberately few so drivers can classify without a taxonomy:.

* ConnectError — opening, attaching, or handshaking a connection failed, or the connection dropped mid-flight. * QueryError — the backend rejected a statement, or the driver hit a protocol error, without a structured server response to report. * TypeError — a Row typed accessor was asked to read a column as a type it does not hold (e.g. int on a Text), or a column index/name that does not exist in the row. * Closed — the connection or statement was used after close. * Server — the server rejected the statement and reported a SQLSTATE and/or a numeric error code; see ServerDetail and DbError::kind.

Declared pub(all) so out-of-tree drivers (moonpostgres, moonmysql, …) that live in their own packages can construct and raise these cases — a plain pub suberror would let them catch a DbError but not build one.

pub(all) enum Dialect {
  Sqlite
  Postgres
  Mysql
}
pub(open) trait Driver {
  fn execute(Self, String, Array[Value]) -> ExecResult raise DbError
  fn query(Self, String, Array[Value]) -> Array[Row] raise DbError
  fn query_stream(Self, String, Array[Value]) -> &Cursor raise DbError = _
  fn begin(Self) -> Unit raise DbError
  fn commit(Self) -> Unit raise DbError
  fn rollback(Self) -> Unit raise DbError
  fn close(Self) -> Unit
  fn ping(Self) -> Bool = _
  fn prepare(Self, String) -> &Stmt raise DbError = _
  fn dialect(Self) -> Dialect = _
  fn capabilities(Self) -> Capabilities = _
}

The contract every database backend implements and every query layer is written against — the seam moondb exists to define. Reduced to the smallest set of operations a relational backend must offer.

## The binding contract

execute and query both take (sql, params). sql carries **positional placeholders** and params supplies one Value per placeholder, in order. A driver is expected to bind values out-of-band rather than interpolate them into the SQL text. A driver that cannot — the MySQL text protocol has no out-of-band binding until prepared statements land — has to escape for the server's actual sql_mode, and must say so in its own documentation.

The placeholder token itself is dialect-specific and chosen by the driver (SQLite/MySQL use ?, PostgreSQL uses $1, $2, …); a query layer that emits SQL for a given backend uses that backend's token. moondb fixes the calling convention (ordered params array), not the spelling.

* execute runs a statement that returns no rows (INSERT/UPDATE/DELETE/DDL) and reports an ExecResult. * query runs a statement that returns rows and materialises every Row. * begin / commit / rollback bracket an explicit transaction. Nesting (savepoints), isolation levels, and read-only hints are driver concerns layered on top; the base contract is the three flat operations. * close releases the connection. It does not raise: closing is best-effort and idempotent.

This trait is pub(open) so out-of-tree drivers (moonsqlite, moonpostgres, moonmysql, …) can implement it.

pub(all) enum ErrorKind {
  Interface
  Database
  Data
  Operational
  Integrity
  Programming
  Internal
  NotSupported
}
pub(all) struct ExecResult {
  rows_affected : Int64
  last_insert_id : Int64
}

The outcome of a non-row statement, aliased from moondb.

pub struct Executor[T] {
  inner : T
}

The narrow view of a Driver: everything needed to run SQL, and nothing that owns the connection. Obtain one with Driver::executor.

pub(open) trait FromValue {
  fn from_value(Value) -> Self raise DbError
}

The conversions are the ones Row's typed accessors make. A refusal is a TypeError whose message reads after "column N is", such as "Text, not Int".

pub struct MockDriver {
  mut rows : Array[Row]
  mut next_id : Int64
  savepoints : Array[Int32]
  named_savepoints : Array[(String, Int32)]
  mut closed : Bool
}

A dependency-free, in-memory reference Driver. It exists for two reasons: to prove the interface is implementable end to end, and to give query layers built on moondb (moonorm and friends) a real test double they can run against with no database and no native backend — it compiles on every target.

It is a deliberately naive echo store, **not** a SQL engine: it does not parse sql. Each execute appends one Row built from the bound params (columns named c0, c1, …) and hands back an ExecResult with a monotonically increasing last_insert_id; each query returns every stored row. What it does model faithfully is the transaction bracket: begin snapshots the store, rollback restores it, commit keeps the changes — so a test can assert real rollback semantics against it.

pub(all) struct Named {
  sql : String
  slots : Array[String]
}

A statement whose :name parameters have been rewritten to the backend's own positional spelling, together with the name of the value each position takes.

The rewrite happens once, before the statement reaches the server: PostgreSQL's extended-query protocol wants $1, $2, …, SQLite and MySQL want one ? per parameter. bind(#bind) turns a name-keyed argument map into the ordered array the driver expects, so a query layer writes SELECT * FROM hero WHERE id = :id AND realm = :realm once and runs it on all three backends.

The rewrite is a lexer, not a search-and-replace: a :name inside a string literal, a quoted identifier, a -- / # / /* */ comment or a PostgreSQL $tag$ … $tag$ body is left alone, and neither :: (a cast) nor := (a named function argument) opens a parameter.

test "named parameters become the backend's own spelling" {
let q = @moondb.rewrite_named(
"SELECT * FROM hero WHERE id = :id AND realm = :realm",
@moondb.Dialect::Postgres,
)
assert_eq(q.sql, "SELECT * FROM hero WHERE id = $1 AND realm = $2")
assert_eq(q.slots, ["id", "realm"])

let m = @moondb.rewrite_named(
"SELECT * FROM hero WHERE id = :id",
@moondb.Dialect::Mysql,
)
assert_eq(m.sql, "SELECT * FROM hero WHERE id = ?")
}
pub(open) trait QueryExecutor {
  fn execute(Self, String, Array[Value]) -> ExecResult raise DbError
  fn query(Self, String, Array[Value]) -> Array[Row] raise DbError
  fn query_stream(Self, String, Array[Value]) -> &Cursor raise DbError
  fn dialect(Self) -> Dialect
  fn capabilities(Self) -> Capabilities
}

The part of a driver a query layer actually needs: run a statement, and ask what the backend is.

Driver bundles three unrelated responsibilities — running SQL, owning a transaction, and owning a connection. A repository method, a query builder, a migration step all want the first and must not be able to close the connection out from under the caller that lent it to them. That is what the narrow view is for: Driver::executor hands out an Executor, and a function that takes &QueryExecutor can run SQL and cannot end the session.

It also carries the two questions a dialect-sensitive layer asks before it emits SQL: dialect(#dialect) for the placeholder spelling and capabilities(#capabilities) for everything the backend can do beyond the base contract. A layer that must prepare statements takes the full Driver instead.

test "a repository only needs to run SQL" {
let db = @moondb.MockDriver::new()
fn count(db : &@moondb.QueryExecutor) -> Int32 raise @moondb.DbError {
db.query("SELECT 1", []).length()
}
assert_eq(count(@moondb.Executor::new(db)), 0)
assert_eq(@moondb.Executor::new(db).dialect(), @moondb.Dialect::Sqlite)
}
pub(all) struct Row {
  columns : Array[String]
  values : Array[Value]
}

One decoded result row (column names + values), aliased from moondb.

pub(all) struct ServerDetail {
  kind : ErrorKind
  sqlstate : String?
  code : Int32?
  message : String
  detail : String?
  hint : String?
  severity : String?
}

What a server-reported failure carries beyond its message: the five-character SQLSTATE, the vendor error number, and the server's severity, detail and hint. Drivers fill what their protocol supplies and leave the rest None. The coarse ErrorKind is derived from the SQLSTATE (and, for MySQL's generic HY000, from the errno).

pub(open) trait Stmt {
  fn bind(Self, Array[Value]) -> Unit raise DbError
  fn execute(Self) -> ExecResult raise DbError
  fn query(Self) -> Array[Row] raise DbError
  fn query_stream(Self) -> &Cursor raise DbError = _
  fn close(Self) -> Unit
  fn sql(Self) -> String
  fn params(Self) -> Array[String]
  fn bind_named(Self, Map[String, Value]) -> Unit raise DbError = _
}

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.

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 ToValue {
  fn to_value(Self) -> Value
}

Implemented for the type behind each Value case and for T?, where None binds NULL. A type of the program's own implements it to be stored as one of those cases.

pub struct Txn[T] {
  driver : T
  mut depth : Int32
}

A transaction that nests: the outermost run(#run) opens a real transaction, every nested one opens a savepoint, and an error undoes only the work of the level that raised.

Driver::begin / commit / rollback are flat, which is all a backend has to offer, but application code nests: a service method opens a transaction, calls a repository method that opens one too, and the inner failure must not discard the outer work. Txn closes the bracket around a body so the pairing cannot be forgotten, and derives the savepoint name from the nesting depth so two levels never collide.

test "a nested body that fails keeps the outer work" {
let db = @moondb.MockDriver::new()
let tx = @moondb.Txn::new(db)
tx.run(fn(tx) raise @moondb.DbError {
tx
.driver()
.execute("INSERT INTO hero VALUES (?, ?)", [
@moondb.Value::Int(1),
@moondb.Value::Text("Nova"),
])
|> ignore
// The inner level fails; only its own work is undone.
(tx.run(fn(_tx) raise @moondb.DbError {
raise @moondb.QueryError("inner failed")
}) catch {
_ => ()
})
|> ignore
})
assert_eq(db.row_count(), 1)
}
pub(all) enum Value {
  Null
  Bool(Bool)
  Int(Int32)
  Int64(Int64)
  Double(Double)
  Text(String)
  Blob(Bytes)
  Time(String)
  Numeric(String)
  Uuid(Bytes)
  Json(String)
  Array(Array[Value])
}

The dialect-neutral value type this driver binds and decodes. Aliased so the wire code reads Value rather than @moondb.Value throughout; the enum constructors (Int, Text, …) are still spelled @moondb.Int where a value is built.

Functions & methods 93

pub fn ArrayCursor::close(Self) -> Unit

Cursor's close, callable on the value directly.

pub fn ArrayCursor::new(Array[Row]) -> Self

A cursor positioned before the first of rows.

pub fn ArrayCursor::next(Self) -> Row? raise DbError

Cursor's next, callable on the value directly.

pub fn AsyncArrayCursor::new(Array[Row]) -> Self

A cursor positioned before the first of rows.

pub fn[T : AsyncDriver] AsyncExecutor::capabilities(Self[T]) -> Capabilities

What the driver underneath can do beyond the base contract.

pub fn[T : AsyncDriver] AsyncExecutor::dialect(Self[T]) -> Dialect

The SQL dialect of the driver underneath.

pub fn[T] AsyncExecutor::driver(Self[T]) -> T

The driver underneath.

pub fn[T] AsyncExecutor::new(T) -> Self[T]

The narrow view over inner.

pub fn[T] AsyncTxn::depth(Self[T]) -> Int32

How many bodies are currently open, zero when none is.

pub fn[T] AsyncTxn::driver(Self[T]) -> T

The connection this handle opens transactions on.

pub fn[T] AsyncTxn::new(T) -> Self[T]

An async transaction handle over driver, at depth zero and not yet open.

pub fn[T] BufferedAsyncStmt::new(T, String) -> Self[T]

A buffered async handle over driver for sql, with no values bound yet.

pub fn[T] BufferedStmt::new(T, String) -> Self[T]

A buffered handle over driver for sql, with no values bound yet.

pub fn Capabilities::count(Self) -> Int32

The number of capabilities the backend has, which is the useful shape for a feature matrix in a test: a preset that loses one silently stops adding up.

pub fn Capabilities::for_dialect(Dialect) -> Self

The capabilities of the driver moondb ships for dialect.

pub fn Capabilities::is_for(Self, Dialect) -> Bool

Whether this report describes dialect.

pub fn classify_sqlstate(String) -> ErrorKind

Classify a five-character SQLSTATE into an ErrorKind using the class (its first two characters), per the SQL:2016 / PostgreSQL table of class codes. Unknown, empty, or short states fall back to Database.

pub fn DbError::code(Self) -> Int32?

The vendor error number the server reported (MySQL's errno), or None.

pub fn DbError::kind(Self) -> ErrorKind

The failure class of any DbError, including the ones that carry no server response. ConnectError is operational (a retry may succeed); a plain QueryError is a generic database failure; a TypeError or Closed is interface misuse; a structured Server reports the class its SQLSTATE implies.

pub fn DbError::sqlstate(Self) -> String?

The five-character SQLSTATE the server reported, or None when the failure did not come from a structured server response.

pub fn DbError::to_string(Self) -> String

A one-line rendering of the error, e.g. QueryError: no such table: hero or ServerError: relation "hero" does not exist [42P01].

pub fn Dialect::name(Self) -> String

The dialect's short name in lower case — the spelling used in configuration and in a driver's Capabilities.

pub fn Dialect::parse(String) -> Self?

Look a dialect up by its short name, ignoring case. Accepts the usual aliases (postgresql, pg, mariadb) because a configuration file is written by a person, not by this module.

pub fn Dialect::placeholder(Self, Int32) -> String

The placeholder for the n-th bound parameter (1-based).

PostgreSQL's extended-query protocol numbers parameters, so each one is spelled $n; SQLite and MySQL bind positionally and spell every one ?. The index is ignored by the two positional dialects, which is why a builder can call this unconditionally.

pub fn drain(&Cursor) -> Array[Row] raise DbError

Drain a cursor into an array — the inverse of streaming, for callers that do want every row (and for asserting a cursor yields exactly what query would).

pub fn ErrorKind::to_string(Self) -> String

The lowercase name of the class, for logs and test output.

pub fn[T : Driver] Executor::capabilities(Self[T]) -> Capabilities

What the driver underneath can do beyond the base contract.

pub fn[T : Driver] Executor::dialect(Self[T]) -> Dialect

The SQL dialect of the driver underneath.

pub fn[T] Executor::driver(Self[T]) -> T

The driver underneath, for the caller that does own the connection.

pub fn[T : Driver] Executor::execute(Self[T], String, Array[Value]) -> ExecResult raise DbError

Run a non-row statement. Present as a plain method as well as a trait method so the view reads like the driver it wraps.

pub fn[T] Executor::new(T) -> Self[T]

The narrow view over inner.

pub fn[T : Driver] Executor::query(Self[T], String, Array[Value]) -> Array[Row] raise DbError

Run a row-returning statement and materialise every row.

pub fn[T : Driver] Executor::query_stream(Self[T], String, Array[Value]) -> &Cursor raise DbError

Stream a row-returning statement's rows.

pub fn MockDriver::begin(Self) -> Unit raise DbError

Driver's begin, callable on the value directly.

pub fn MockDriver::capabilities(Self) -> Capabilities

Driver's capabilities, callable on the value directly.

pub fn MockDriver::close(Self) -> Unit

Driver's close, callable on the value directly.

pub fn MockDriver::commit(Self) -> Unit raise DbError

Driver's commit, callable on the value directly.

pub fn MockDriver::dialect(Self) -> Dialect

Driver's dialect, callable on the value directly.

pub fn MockDriver::execute(Self, String, Array[Value]) -> ExecResult raise DbError

Driver's execute, callable on the value directly.

pub fn MockDriver::in_transaction(Self) -> Bool

Whether a transaction is currently open (at least one un-committed begin).

pub fn MockDriver::is_closed(Self) -> Bool

Whether the connection has been closed.

pub fn MockDriver::new() -> Self

A fresh, empty mock connection.

pub fn MockDriver::ping(Self) -> Bool

Driver's ping, callable on the value directly.

pub fn MockDriver::prepare(Self, String) -> &Stmt raise DbError

Driver's prepare, callable on the value directly.

pub fn MockDriver::query(Self, String, Array[Value]) -> Array[Row] raise DbError

Driver's query, callable on the value directly.

pub fn MockDriver::query_stream(Self, String, Array[Value]) -> &Cursor raise DbError

Driver's query_stream, callable on the value directly.

pub fn MockDriver::rollback(Self) -> Unit raise DbError

Driver's rollback, callable on the value directly.

pub fn MockDriver::row_count(Self) -> Int32

How many rows the store currently holds. A test-facing helper, not part of the Driver contract.

pub fn Named::arity(Self) -> Int32

How many values the statement binds.

pub fn Named::bind(Self, Map[String, Value]) -> Array[Value] raise DbError

Turn a name-keyed argument map into the positional array the statement binds. A missing name raises a TypeError naming the parameter; a key the statement never mentions raises one too, so a typo in a repository method is caught at the call rather than silently ignored.

pub fn Named::is_named(Self) -> Bool

Whether the statement takes named parameters at all.

pub fn Named::names(Self) -> Array[String]

The distinct parameter names, in first-use order.

pub fn rewrite_named(String, Dialect) -> Named raise DbError

Rewrite :name parameters in sql into dialect's positional spelling.

A statement may use either :name or positional ?, never both; mixing them raises a QueryError naming the offset, because the two conventions disagree about what the caller's array means.

pub fn Row::array(Self, Int32) -> Array[Value] raise DbError

Read column idx as an SQL array. Raises TypeError unless the cell is Array.

pub fn Row::array_by(Self, String) -> Array[Value] raise DbError

Read the column named name as an SQL array.

pub fn Row::blob(Self, Int32) -> Bytes raise DbError

Read column idx as an opaque blob. Raises TypeError unless the cell is Blob.

pub fn Row::blob_by(Self, String) -> Bytes raise DbError

Read the column named name as an opaque blob.

pub fn Row::bool(Self, Int32) -> Bool raise DbError

Read column idx as a Bool. Raises TypeError unless the cell is Bool.

pub fn Row::bool_by(Self, String) -> Bool raise DbError

Read the column named name as a Bool.

pub fn Row::by_name(Self, String) -> Value?

The Value of the column named name, or None if the row has no such column. Names match exactly as the driver reported them (case-sensitive).

pub fn Row::double(Self, Int32) -> Double raise DbError

Read column idx as a Double, widening an integer. Raises TypeError unless the cell is Double, Int, or Int64.

pub fn Row::double_by(Self, String) -> Double raise DbError

Read the column named name as a Double.

pub fn Row::get(Self, Int32) -> Value raise DbError

The raw Value at column idx (0-based, projection order). Raises QueryError if idx is out of range.

pub fn Row::index_of(Self, String) -> Int32 raise DbError

Resolve a column name to its index, raising QueryError if absent. The positional accessors are the primitives; the _by-name accessors resolve through this.

pub fn Row::int(Self, Int32) -> Int32 raise DbError

Read column idx as an Int, narrowing an Int64 to its low 32 bits. Raises TypeError unless the cell is Int or Int64.

pub fn Row::int64(Self, Int32) -> Int64 raise DbError

Read column idx as an Int64, widening an Int. Raises TypeError unless the cell is Int or Int64.

pub fn Row::int64_by(Self, String) -> Int64 raise DbError

Read the column named name as an Int64.

pub fn Row::int_by(Self, String) -> Int32 raise DbError

Read the column named name as an Int.

pub fn Row::is_null(Self, Int32) -> Bool raise DbError

Whether column idx is SQL NULL. Raises QueryError if idx is out of range. Call this before a typed accessor when a column is nullable, since the typed accessors treat NULL as a type mismatch.

pub fn Row::is_null_by(Self, String) -> Bool raise DbError

Whether the column named name is SQL NULL. Raises QueryError if the column is absent.

pub fn Row::json(Self, Int32) -> String raise DbError

Read column idx as JSON text. Raises TypeError unless the cell is Json.

pub fn Row::json_by(Self, String) -> String raise DbError

Read the column named name as JSON text.

pub fn Row::numeric(Self, Int32) -> String raise DbError

Read column idx as an exact decimal string. Raises TypeError unless the cell is Numeric.

pub fn Row::numeric_by(Self, String) -> String raise DbError

Read the column named name as an exact decimal string.

pub fn[T : FromValue] Row::read(Self, Int32) -> T raise DbError

Read column idx as any FromValue type, the typed accessors' common form: let age : Int? = row.read(3). A refusal names the column.

pub fn[T : FromValue] Row::read_by(Self, String) -> T raise DbError

Read the column named name as any FromValue type.

pub fn Row::text(Self, Int32) -> String raise DbError

Read column idx as text. Raises TypeError unless the cell is Text.

pub fn Row::text_by(Self, String) -> String raise DbError

Read the column named name as text.

pub fn Row::time(Self, Int32) -> String raise DbError

Read column idx as a temporal value's ISO-8601 text. Raises TypeError unless the cell is Time.

pub fn Row::time_by(Self, String) -> String raise DbError

Read the column named name as temporal ISO-8601 text.

pub fn Row::uuid(Self, Int32) -> Bytes raise DbError

Read column idx as the raw bytes of a UUID. Raises TypeError unless the cell is Uuid.

pub fn Row::uuid_by(Self, String) -> Bytes raise DbError

Read the column named name as UUID bytes.

pub fn Row::width(Self) -> Int32

The number of columns in the row.

pub fn[T] Txn::depth(Self[T]) -> Int32

How many bodies are currently open, zero when none is.

pub fn[T] Txn::driver(Self[T]) -> T

The connection this handle opens transactions on.

pub fn[T] Txn::is_open(Self[T]) -> Bool

Whether a body is currently open.

pub fn[T] Txn::new(T) -> Self[T]

A transaction handle over driver, at depth zero and not yet open.

pub fn[T : Driver] Txn::run(Self[T], (Self[T]) -> Unit raise DbError) -> Unit raise DbError

Run body inside a transaction, or inside a savepoint when a transaction is already open. Returning normally commits (or releases the savepoint); raising rolls back (or rolls back to the savepoint) and re-raises the original error.

A body that catches an inner failure and carries on keeps the outer work, which is the point of the savepoint: only the level that failed is undone.

pub fn Value::from_value(Self) -> Self raise DbError

FromValue's from_value, callable on the value directly.

pub fn Value::is_null(Self) -> Bool

Whether this value is SQL NULL.

pub fn Value::json_text(Self) -> String

Render this value as JSON text. Scalars map to their JSON counterparts, text is quoted and escaped, Time/Numeric become strings, Json is embedded as-is, and Array becomes a JSON array. Drivers use this to bind array and JSON values on protocols that carry parameters as text.

pub fn Value::kind(Self) -> String

The name of a value's variant ("Null", "Int", "Blob", …). Used to build legible DbError::TypeError messages when a typed accessor is asked for the wrong shape.

pub fn Value::to_value(Self) -> Self

ToValue's to_value, callable on the value directly.

moonbitstack/moondb/pool

pool

Types 1

pub struct Pool[D] {
  // private fields
}

A fixed-ceiling connection pool over any Driver. Opening a database connection is expensive — a TCP handshake and auth round trip for a networked backend — so a server hands out a small set of connections and reuses them instead of opening one per request.

The pool is generic in the concrete driver D rather than holding &Driver trait objects, so acquire gives back the real driver type and a caller keeps access to backend-specific methods (SQLite's exec_script, a driver's prepared-statement handle). new takes a make factory that opens one fresh connection; the pool calls it only when no idle connection is available and the open count is still below max_size.

Beyond plain reuse, the pool keeps a connection healthy over its lifetime.

* **pre-ping** (pre_ping) — before an idle connection is handed back it is probed with Driver::ping; a connection that fails the probe is closed and skipped, so a caller never receives a connection the backend has already dropped. * **max lifetime** (max_lifetime_ms) — a connection older than this is retired on acquire and replaced with a fresh one, so long-lived pools recycle connections a load balancer or the server may have aged out. Age is measured with an injected clock, exactly as database/sql swaps nowFunc in tests; the default clock disables the check. * **acquire timeout** (acquire_timeout_ms) — the wait budget an exhausted acquire is allowed. See the note on synchrony below.

It is synchronous and single-threaded by design — moondb's base contract is sync, and MoonBit's pure backends have no shared-memory threads — so acquire never truly blocks: nothing else can return a connection while one call waits. A request that finds the pool exhausted therefore fails immediately rather than sleeping; acquire_timeout_ms is carried into that failure's message and is the budget a driver running on an async runtime honours when it layers real waiting on top. Use try_acquire for the non-raising "give me one only if free" path. The reuse, ceiling, health, and lifecycle bookkeeping all live here.

The counting is moonpool's Pool: live, idle and lent, each lent connection's birth recognised on its way back so max_lifetime counts from creation. So is the optional ward in front of the database: backoff after failed connects, a token bucket spacing reconnections, a breaker, a shedder. This type adds only what needs a driver — opening, closing, probing.

Functions & methods 15

pub fn[D : @moondb.Driver] Pool::acquire(Self[D]) -> D raise @moondb.DbError

Take a connection: reuse an idle one if the pool has a healthy, unexpired one (the common path, avoiding a fresh handshake), otherwise open a new one through make as long as the open count is below max_size.

An idle connection past max_lifetime_ms, or one that fails the Driver::ping probe when pre_ping is on, is closed and dropped rather than handed back; the pool then tries the next idle connection or opens a fresh one. Raises QueryError if the pool is exhausted (all max_size connections are checked out), ConnectError if the ward holds back or refuses a new connection, or Closed if it has been closed. The caller must return the connection with release — or use with_conn, which does so even on error.

pub fn[D] Pool::acquire_timeout_ms(Self[D]) -> Int64

The configured acquire wait budget in milliseconds (0 = fail immediately).

pub fn[D : @moondb.Driver] Pool::close_all(Self[D]) -> Unit

Close every idle connection and mark the pool closed. Connections still checked out are not touched — each is closed as it is released back. After this, acquire raises Closed. Idempotent.

This is the graceful half of a shutdown: nothing in flight is cut off. To wait for the rest, poll in_use_count until it reaches zero — this pool is synchronous, so the waiting belongs to whatever async layer owns it.

pub fn[D] Pool::idle_count(Self[D]) -> Int32

How many connections are currently idle (checked in and reusable).

pub fn[D] Pool::in_use_count(Self[D]) -> Int32

How many connections are checked out right now. A shutdown that wants to wait for borrowed connections to come home polls this after close_all: the close empties the idle set, so what is left is exactly what is still in use.

pub fn[D] Pool::is_closed(Self[D]) -> Bool

Whether close_all has been called.

pub fn[D] Pool::max_lifetime_ms(Self[D]) -> Int64

The configured maximum connection lifetime in milliseconds (0 = unlimited).

pub fn[D] Pool::max_size(Self[D]) -> Int32

The pool's ceiling on simultaneously open connections.

pub fn[D] Pool::new(() -> D raise @moondb.DbError, max_size? : Int32, overflow? : Int32, max_lifetime_ms? : Int64, pre_ping? : Bool, acquire_timeout_ms? : Int64, clock? : () -> Int64, ward? : @moonpool.Ward, reset? : (D) -> Unit raise @moondb.DbError, rand? : () -> Double, cpu? : () -> Int32) -> Self[D] raise @moondb.DbError

Build a pool whose connections come from make.

* max_size caps how many connections may be open at once (in use plus idle); it defaults to 10, and must be positive. * max_lifetime_ms retires a connection older than this many milliseconds on acquire (0, the default, means no age limit). Enforcing it requires a real clock; with the default clock every connection reads as age 0. * pre_ping turns on the Driver::ping health probe before an idle connection is reused (off by default). * acquire_timeout_ms records the wait budget for an exhausted acquire; 0 (the default) means "fail immediately". In this synchronous pool the wait is always degenerate (see the type doc), so the value serves the error message and an async layer above. * clock returns a monotonically non-decreasing millisecond reading and exists so max_lifetime is testable and portable; it defaults to a constant 0, which disables age-based retirement. * ward is moonpool's, none by default. A ward that times anything needs a real clock. * rand draws from [0, 1) for the ward's breaker and backoff jitter; without it the pool draws from a system-seeded generator. * cpu reads the processor load in thousandths for the ward's shedder; without it the load reads as zero and the shedder never sheds.

pub fn[D] Pool::open_count(Self[D]) -> Int32

How many connections the pool has open in total: those checked out plus those sitting idle. Never exceeds max_size.

pub fn[D] Pool::overflow(Self[D]) -> Int32

How many connections may be opened above max_size when the pool is busy.

pub fn[D] Pool::pre_ping(Self[D]) -> Bool

Whether the pre-ping health probe is enabled.

pub fn[D : @moondb.Driver] Pool::release(Self[D], D) -> Unit

Return a connection to the idle set so a later acquire can reuse it, preserving the birth time it was opened with so max_lifetime keeps counting from creation rather than resetting on every checkout. A connection released back into a closed pool is closed immediately rather than pooled, so no handle outlives close_all.

pub fn[D : @moondb.Driver] Pool::try_acquire(Self[D]) -> D? raise @moondb.DbError

Take a connection only if one is immediately available — an idle one or fresh headroom under max_size — returning None instead of raising when the pool is exhausted or its ward says no. This is the non-blocking counterpart of acquire, so a caller can choose to shed load or retry. Health and lifetime eviction apply exactly as in acquire; a genuine failure while opening a fresh connection (the make factory raising) still propagates.

pub fn[D : @moondb.Driver, R] Pool::with_conn(Self[D], (D) -> R raise @moondb.DbError) -> R raise @moondb.DbError

Run f with a pooled connection, returning it afterwards whether f returns or raises. This is the leak-proof way to use the pool: the release happens on every path, so a raising query never strands a connection checked out.

moonbitstack/moondb/apool

apool

Types 1

pub struct AsyncPool[D] {
  // private fields
}

A connection pool for an @moondb.AsyncDriver — the counterpart of @moondb/pool.Pool for a backend reached over the network.

It exists because a synchronous pool cannot do the one thing an exhausted pool is supposed to do: wait. In @moondb/pool.Pool nothing can return a connection while a caller is inside acquire, so a full pool fails immediately and acquire_timeout_ms only reaches the error message. Here the caller is a task in an event loop: other tasks keep running while it waits, so a full pool parks the caller on a permit and wakes it when a connection comes home. And it is what makes overflow and a real timeout meaningful.

The division of labour mirrors the sync pool: @moonpool keeps the accounting — live, idle, lent, each connection's birth time — and this type adds what only a driver can do (opening, probing, resetting, closing) plus the permit that makes an exhausted acquire wait.

let pool : @apool.AsyncPool[@postgres.PgConn] = @apool.AsyncPool::new(
() => @postgres.PgConn::connect("db", 5432, "app", "secret", "app"),
max_size=8,
overflow=2,
pre_ping=true,
acquire_timeout_ms=3000L,
reset=conn => conn.rollback() catch { _ => () },
)

let rows = pool.with_conn(conn => conn.query("SELECT 1", []))

Functions & methods 11

pub fn[D] AsyncPool::acquire_timeout_ms(Self[D]) -> Int64

How long an acquire may wait for a permit in milliseconds (0 = no limit).

pub fn[D] AsyncPool::capacity(Self[D]) -> Int32

The hard ceiling on concurrent checkouts: max_size + overflow.

pub fn[D] AsyncPool::idle_count(Self[D]) -> Int32

How many connections are idle and reusable right now.

pub fn[D] AsyncPool::in_use_count(Self[D]) -> Int32

How many connections are lent out right now.

pub fn[D] AsyncPool::is_closed(Self[D]) -> Bool

Whether close_all has been called.

pub fn[D] AsyncPool::max_lifetime_ms(Self[D]) -> Int64

The configured connection lifetime in milliseconds (0 = unlimited).

pub fn[D] AsyncPool::max_size(Self[D]) -> Int32

The steady-state ceiling on open connections.

pub fn[D] AsyncPool::new(async () -> D raise @moondb.DbError, max_size? : Int32, overflow? : Int32, max_lifetime_ms? : Int64, pre_ping? : Bool, acquire_timeout_ms? : Int64, clock? : () -> Int64, ward? : @moonpool.Ward, reset? : async (D) -> Unit raise @moondb.DbError, rand? : () -> Double, cpu? : () -> Int32) -> Self[D] raise @moondb.DbError

Build a pool whose connections come from make, which runs inside the event loop and may await the handshake.

* max_size caps the steady-state number of open connections (default 10) and must be positive. * overflow is how many connections may be opened above max_size while every steady-state connection is lent; a connection that came from that allowance is closed on return rather than idled, so the pool settles back to max_size. * max_lifetime_ms retires a connection older than this on acquire (0, the default, means no age limit). Enforcing it requires a real clock. * pre_ping probes an idle connection with AsyncDriver::ping before handing it out, closing it and trying the next if the backend has dropped it. * acquire_timeout_ms is how long a caller may wait for a permit before failing (0, the default, means wait as long as it takes — the pool never gives up on its own). * reset runs on return before the connection becomes reusable: roll back a leftover transaction, clear session state. A reset that raises closes the connection instead of pooling it. * clock returns monotonic milliseconds and defaults to a constant 0, which disables age-based retirement; rand/cpu feed the ward's jitter and shedder.

pub fn[D] AsyncPool::open_count(Self[D]) -> Int32

How many connections the pool has open: lent plus idle.

pub fn[D] AsyncPool::overflow(Self[D]) -> Int32

How many connections may be opened above max_size while the pool is busy.

pub fn[D] AsyncPool::pre_ping(Self[D]) -> Bool

Whether the pre-ping probe is enabled.

moonbitstack/moonsqlite

moonsqlite

Types 2

pub struct SqliteDriver {
  handle : Int64
  mut closed : Bool
}

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 SqliteStmt {
  driver : SqliteDriver
  st : Int64
  text : String
  slots : Array[String]
  cols : Array[String]
  mut closed : Bool
}

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.

Functions & methods 19

pub fn SqliteCursor::close(Self) -> Unit

@moondb.Cursor's close, callable on the value directly.

pub fn SqliteCursor::next(Self) -> @moondb.Row? raise @moondb.DbError

@moondb.Cursor's next, callable on the value directly.

pub fn SqliteDriver::begin(Self) -> Unit raise @moondb.DbError

@moondb.Driver's begin, callable on the value directly.

pub fn SqliteDriver::capabilities(Self) -> @moondb.Capabilities

@moondb.Driver's capabilities, callable on the value directly.

pub fn SqliteDriver::close(Self) -> Unit

@moondb.Driver's close, callable on the value directly.

pub fn SqliteDriver::commit(Self) -> Unit raise @moondb.DbError

@moondb.Driver's commit, callable on the value directly.

pub fn SqliteDriver::dialect(Self) -> @moondb.Dialect

@moondb.Driver's dialect, callable on the value directly.

pub fn SqliteDriver::exec_script(Self, String) -> Unit raise @moondb.DbError

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::execute(Self, String, Array[@moondb.Value]) -> @moondb.ExecResult raise @moondb.DbError

@moondb.Driver's execute, callable on the value directly.

pub fn SqliteDriver::is_closed(Self) -> Bool

Whether close has been called. Once closed, every statement raises @moondb.Closed rather than touching the freed sqlite3*.

pub fn SqliteDriver::open(String) -> Self raise @moondb.DbError

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::ping(Self) -> Bool

@moondb.Driver's ping, callable on the value directly.

pub fn SqliteDriver::prepare(Self, String) -> &@moondb.Stmt raise @moondb.DbError

@moondb.Driver's prepare, callable on the value directly.

pub fn SqliteDriver::prepare_stmt(Self, String) -> SqliteStmt raise @moondb.DbError

Compile sql into a reusable statement. A statement using :name parameters is rewritten to SQLite's ? spelling first.

pub fn SqliteDriver::query(Self, String, Array[@moondb.Value]) -> Array[@moondb.Row] raise @moondb.DbError

@moondb.Driver's query, callable on the value directly.

pub fn SqliteDriver::query_stream(Self, String, Array[@moondb.Value]) -> &@moondb.Cursor raise @moondb.DbError

@moondb.Driver's query_stream, callable on the value directly.

pub fn SqliteDriver::rollback(Self) -> Unit raise @moondb.DbError

@moondb.Driver's rollback, callable on the value directly.

pub fn SqliteStmtCursor::close(Self) -> Unit

@moondb.Cursor's close, callable on the value directly.

pub fn SqliteStmtCursor::next(Self) -> @moondb.Row? raise @moondb.DbError

@moondb.Cursor's next, callable on the value directly.

moonbitstack/moonpostgres

moonpostgres

Types 8

pub struct PgConn {
  stream : PgWire
  host : String
  port : Int32
  mut closed : Bool
  mut streaming : Bool
  params : Map[String, String]
  mut pid : Int32
  mut secret : Int32
  statements : Map[String, String]
  mut next_stmt : Int32
}

A live connection to a PostgreSQL backend, already past the startup handshake and sitting at ReadyForQuery. Runs the v3 frontend/backend protocol over a single @socket.Tcp. Not safe for concurrent use by multiple tasks: one statement must complete (drain to ReadyForQuery) before the next begins, as the wire protocol is a strict request/response pipeline per connection.

pub struct PgCopyIn {
  conn : PgConn
  mut done : Bool
  mut failed : Bool
}

A COPY … FROM STDIN in progress: write_chunk(#write_chunk) pushes pieces as they are produced and finish(#finish) closes the stream and reports what the server did with it.

The stream owns the connection until finish (or fail(#fail)); writing after either is a QueryError. Chunks are passed through as COPY data messages, so a caller can stream a file of any size without materialising it.

pub struct PgCopyOut {
  conn : PgConn
  mut done : Bool
  mut rows : Int64
}

A COPY … TO STDOUT in progress: the server sends the data in pieces and read_chunk(#read_chunk) hands them over one at a time, so a table or a query result larger than memory can be written straight to a file or a socket.

The stream owns the connection's single-response pipeline until it ends: read it to None (or cancel(#cancel) it) before the next statement. The chunks are the server's own framing — COPY text rows are newline-terminated, CSV rows likewise, binary rows are length-prefixed — so a caller writes them out verbatim.

pub struct PgCursor {
  conn : PgConn
  name : String
  batch : Int32
  mut exhausted : Bool
  mut closed : Bool
}

A server-side cursor: DECLARE … CURSOR opens it inside the current transaction and next_batch(#next_batch) pulls at most batch rows per round trip.

This is the cursor for a result too large to hold, and it is different from PgRowStream, which streams one statement's response: the server owns the position, so the connection is free between fetches, and the rows already read are never resent. It requires an open transaction — a cursor declared outside one is closed by the implicit commit at the end of the statement.

pub struct PgDriver {
  host : String
  port : Int32
  user : String
  password : String
  database : String
}

The parameters needed to reach a PostgreSQL backend — a connection descriptor, not a connection. connect(PgDriver::connect) opens the live PgConn, which is the thing that implements the driver seam.

It exists so a caller can carry connection settings around (a config value, a pool factory) and open a connection from them on demand, the way a DSN string works elsewhere.

pub struct PgRowStream {
  conn : PgConn
  mut col_names : Array[String]
  mut col_oids : Array[Int32]
  mut done : Bool
}

A forward-only cursor that reads a query's rows off the wire on demand instead of buffering them, the streaming counterpart to query, for results too large to materialise (asyncpg's cursor. It is bound to its PgConn, which is a single-statement pipeline: the stream must be drained (next returns None) or [closed](PgRowStream::close) before another statement runs on that connection.

pub struct PgStmt {
  conn : PgConn
  name : String
  key : String
  translated : String
  text : String
  slots : Array[String]
  mut params : Array[@moondb.Value]
  mut closed : Bool
}

A statement PostgreSQL prepares once, on the server, and runs many times.

prepare sends Parse for the statement's text under a generated name, so the parse and the plan survive between calls; each execute/query sends only Bind/Describe/Execute with new values. The name comes from the connection's statement cache keyed by the SQL text, so preparing the same text twice reuses one server-side statement rather than allocating a second.

A :name statement is preprocessed here — rewritten to $1, $2, … and the slot names remembered — which is what makes bind_named work without the caller numbering the parameters. A statement written with ? is translated the same way execute translates it, and a bare ? that is the JSONB operator is left alone.

pub struct PgWire {
  // private fields
}

The byte channel a connection speaks the PostgreSQL v3 protocol over.

A real connection uses a socket — plain, or a TLS session layered on one. The protocol code above it, though, only ever needs three things: read exactly n bytes, write these bytes, close. Pulling that out of PgConn is what lets the state machine (statement sequencing, the streaming guard, error draining, COPY, notifications) be tested against a scripted peer with no machine and no server behind it: pipe(PgWire::pipe) hands it one end of an in-memory pipe pair and the test plays the server on the other.

It is an enum rather than a trait object because the two transports have different concrete types already (@tcp.Conn, @io.PipeRead + @io.PipeWrite) and a closed set keeps the dispatch a match with no allocation.

Functions & methods 61

pub fn build_bind(Array[@moondb.Value]) -> Bytes

Bind ('B'): bind params (all text format) to the unnamed statement, producing the unnamed portal, and request all result columns in text format.

pub fn build_bind_named(String, String, Array[@moondb.Value]) -> Bytes

Bind ('B') params to a named (or unnamed) prepared statement, producing a named (or unnamed) portal. All parameters and result columns use text format.

pub fn build_cancel_request(Int32, Int32) -> Bytes

CancelRequest: Int32 length 16, Int32 request code 80877102, then the backend process id and secret key from BackendKeyData. Sent on a new connection to ask the server to cancel the query running on the old one.

pub fn build_close_statement(String) -> Bytes

Close ('C') for a named prepared statement: the server frees it and answers CloseComplete. A Sync follows so the message reaches a ready state on its own.

pub fn build_copy_data(Bytes) -> Bytes

CopyData ('d'): one chunk of COPY data.

pub fn build_copy_done() -> Bytes

CopyDone ('c'): the client has sent every COPY chunk.

pub fn build_copy_fail(String) -> Bytes

CopyFail ('f'): abort a COPY FROM STDIN with a message.

pub fn build_describe_portal() -> Bytes

Describe ('D') the unnamed portal, so the server sends a RowDescription before the DataRows (giving column names + type OIDs for decoding).

pub fn build_execute() -> Bytes

Execute ('E') the unnamed portal with no row limit (0 = all rows).

pub fn build_parse(String) -> Bytes

Parse ('P'): prepare the unnamed statement from sql (with $n placeholders). Zero declared parameter types — the server infers them.

pub fn build_parse_named(String, String) -> Bytes

Parse ('P') into a named prepared statement, so a later Bind can reuse it without re-parsing. An empty name is the unnamed statement.

pub fn build_password(String) -> Bytes

PasswordMessage ('p'): the auth response token (cleartext, or the md5… digest), as a C string.

pub fn build_query(String) -> Bytes

Simple Query ('Q'): one SQL string, no bound parameters.

pub fn build_sasl_initial(String, String) -> Bytes

SASLInitialResponse ('p'): the chosen mechanism name, then the length-prefixed client-first SCRAM message (PG protocol §55.2.1).

pub fn build_sasl_response(String) -> Bytes

SASLResponse ('p'): the raw client-final SCRAM message, no length prefix.

pub fn build_ssl_request() -> Bytes

SSLRequest: Int32 length 8, Int32 request code 80877103. Sent before the startup message; the server answers a single byte — S to begin a TLS handshake, N to continue in cleartext.

pub fn build_startup(String, String) -> Bytes

StartupMessage: protocol version 3.0 (196608) plus the user / database parameters and a client_encoding=UTF8 request, terminated by an empty key.

pub fn build_sync() -> Bytes

Sync ('S'): close the extended-query batch; the server replies ReadyForQuery.

pub fn build_terminate() -> Bytes

Terminate ('X'): ask the backend to close the connection.

pub fn concat_bytes(Bytes, Bytes) -> Bytes

Concatenate two byte strings. A small helper the MD5 auth path and message framing lean on; MoonBit's Bytes is immutable so this allocates once.

pub fn count_placeholders(String) -> Int32

The number of ? placeholders translate_placeholders would consume — the count of parameters a statement expects. Shares the same scanner discipline so ?? escapes and quoted/comment spans do not count.

pub fn decode_bytea(String) -> Bytes

Decode PostgreSQL bytea hex text (\xDEADBEEF) back to raw bytes. A value not in hex form (legacy escape format) rides back as its UTF-8 bytes.

pub fn decode_value(Int32, Bool, String) -> @moondb.Value

Decode a text-format result cell (raw, already UTF-8) tagged with its column oid into a Value. is_null marks a wire NULL (length -1), which decodes to Null regardless of type. Boolean, integer, floating-point, numeric, uuid, json/jsonb and the date/time OIDs decode to their typed cases; an OID the table does not know — an enum or a domain — rides back as Text, exactly the dialect-neutral contract moondb documents.

pub fn encode_param(@moondb.Value) -> Bytes?

Encode a bound parameter to its text-format bytes, or None for SQL NULL (which the Bind message sends as a length of -1). Binding is out-of-band — the value never touches the SQL string — so this is the injection-safe path.

* integers / doubles / bools render to their canonical PostgreSQL text literals (t/f for booleans); * Text passes through UTF-8; * Blob uses the bytea hex format (\x + lowercase hex), which the server accepts for a text-format bytea parameter.

pub const OID_BOOL : Int32 = 16

The pg_type OID of bool, as a column description reports it.

pub const OID_BYTEA : Int32 = 17

The pg_type OID of bytea, as a column description reports it.

pub const OID_DATE : Int32 = 1082

The pg_type OID of date.

pub const OID_FLOAT4 : Int32 = 700

The pg_type OID of float4, as a column description reports it.

pub const OID_FLOAT8 : Int32 = 701

The pg_type OID of float8, as a column description reports it.

pub const OID_INT2 : Int32 = 21

The pg_type OID of int2, as a column description reports it.

pub const OID_INT4 : Int32 = 23

The pg_type OID of int4, as a column description reports it.

pub const OID_INT8 : Int32 = 20

The pg_type OID of int8, as a column description reports it.

pub const OID_JSON : Int32 = 114

The pg_type OID of json.

pub const OID_JSONB : Int32 = 3802

The pg_type OID of jsonb.

pub const OID_NUMERIC : Int32 = 1700

The pg_type OID of numeric.

pub const OID_TIME : Int32 = 1083

The pg_type OID of time.

pub const OID_TIMESTAMP : Int32 = 1114

The pg_type OID of timestamp.

pub const OID_TIMESTAMPTZ : Int32 = 1184

The pg_type OID of timestamptz.

pub const OID_TIMETZ : Int32 = 1266

The pg_type OID of timetz.

pub const OID_UUID : Int32 = 2950

The pg_type OID of uuid.

pub fn parse_double(String) -> Double?

Parse a floating-point literal from PostgreSQL text (123.45, -1e10, Infinity, NaN), or None if malformed. Handles sign, fraction, and a base-10 exponent; the special IEEE tokens PostgreSQL emits are recognised.

pub fn parse_int(String) -> Int32?

Parse a base-10 Int, or None if s is not a well-formed integer. Used to decode int2/int4 result text; a parse failure falls back to Text so a surprising server rendering never silently becomes a wrong number.

pub fn parse_int64(String) -> Int64?

Parse a base-10 Int64, or None on any non-digit (after an optional sign).

pub fn pg_md5_password(String, String, Bytes) -> String

"md5" + hex(md5(hex(md5(password + user)) + salt)).

It is what the server asks for and nothing else uses it, which is why the shape is stated rather than assembled from parts a caller might reorder.

pub fn PgConn::backend_pid(Self) -> Int32

The backend process id, from BackendKeyData.

pub fn PgConn::capabilities(Self) -> @moondb.Capabilities

@moondb.AsyncDriver's capabilities, callable on the value directly.

pub fn PgConn::dialect(Self) -> @moondb.Dialect

@moondb.AsyncDriver's dialect, callable on the value directly.

pub fn PgConn::is_closed(Self) -> Bool

Whether close(PgConn::close) has been called. A closed connection refuses further statements with @moondb.Closed.

pub fn PgConn::is_streaming(Self) -> Bool

Whether a PgRowStream is still outstanding. While one is, this connection refuses new statements — the wire is a single-statement pipeline — until the stream is drained or [closed](PgRowStream::close).

pub fn PgConn::parameter(Self, String) -> String?

A session parameter the server reported (server_version, client_encoding, TimeZone, …), or None if it has not been reported.

pub fn PgCopyOut::rows(Self) -> Int64

How many rows the finished COPY reported, or 0 while it is still running.

pub fn PgDriver::new(host~ : String, port? : Int32, user~ : String, password? : String, database~ : String) -> Self

Build a connection descriptor. Does not connect: PostgreSQL I/O is async, so the connection is opened by connect(PgDriver::connect) inside an event loop.

pub fn PgWire::close(Self) -> Unit

Close the channel. Idempotent at the transport level, so a double close after a failed handshake is harmless.

pub fn PgWire::pipe(@io.PipeRead, @io.PipeWrite) -> Self

Wrap the client end of an in-memory pipe pair: from_server is where the peer's writes arrive, to_server is where this connection's writes go. Used by the offline protocol tests; a live connection never takes this path.

pub fn PgWire::tcp(@tcp.Conn) -> Self

Wrap a socket — plain, or the TLS session @tcp.Conn::client returned.

pub fn saslprep(String) -> String

SASLprep (RFC 4013) on a password before SCRAM: map the non-ASCII space characters to U+0020, drop the "commonly mapped to nothing" code points, then NFKC-normalize. An ASCII password is returned unchanged, which is the common case; a non-ASCII one would otherwise derive a different key from the server's stored verifier.

pub fn scram_client_final(Bytes, String, String, gs2_header? : String, cbind? : Bytes) -> (String, Bytes) raise @moondb.DbError

Build the SCRAM client-final message and the expected server signature from the server's first message (RFC 5802). client_first_bare is n=…,r=clientnonce; server_first is r=nonce,s=salt,i=iters. Returns (client_final_message, server_signature) — the client sends the first and verifies the server's v= against the base64 of the second.

pub fn scram_client_proof(Bytes, Bytes) -> Bytes

The SCRAM ClientProof for the AuthMessage: ClientKey XOR HMAC(StoredKey, AuthMessage), where ClientKey = HMAC(SaltedPassword, "Client Key") and StoredKey = SHA256(ClientKey) (RFC 5802 §3).

pub fn scram_server_signature(Bytes, Bytes) -> Bytes

The SCRAM ServerSignature: HMAC(ServerKey, AuthMessage), ServerKey = HMAC(SaltedPassword, "Server Key"). The client verifies the server's v= against this to authenticate the server (RFC 5802 §3).

pub fn translate_placeholders(String) -> String

Rewrite moondb's dialect-neutral ? positional placeholders into PostgreSQL's numbered $1, $2, … form. moondb fixes the calling convention (an ordered params array) but leaves the placeholder spelling to the driver; PostgreSQL's extended-query protocol requires $n, so a query layer that emits ? for portability is translated here before Parse.

A ? is only a placeholder in SQL text — never inside a single-quoted string literal, a dollar-quoted string, a "-quoted identifier, or a -- / /* */ comment. Those spans are scanned through verbatim so a literal ? in data or a comment is left untouched and does not shift the parameter numbering. A literal ? an application genuinely needs in output can be written ??, which collapses to a single ? (mirroring JDBC-style escaping).

pub fn utf8_decode(Bytes) -> String

Decode UTF-8 bytes to a String. PostgreSQL text values, column names, and error fields all arrive UTF-8 (the startup message negotiates client_encoding). Decodes the ASCII fast path directly and multi-byte sequences by code point; malformed input yields U+FFFD rather than raising, matching a lenient text codec.

moonbitstack/moonmysql

moonmysql

Types 8

pub(all) struct ColumnDef {
  name : String
  column_type : Int32
  charset : Int32
  flags : Int32
}
pub struct Handshake {
  server_version : String
  server_kind : ServerKind
  connection_id : Int32
  salt : Bytes
  capability : Int32
  mariadb_capability : Int32
  charset : Int32
  status : Int32
  auth_plugin : String
}

The server's initial Handshake packet (protocol version 10), decoded down to the fields the client needs: the 20-byte auth salt, the negotiated capabilities, and the auth-plugin name that selects the scramble.

pub(all) suberror MysqlError {
  ProtocolError(String)
  ServerError(Int32, String, String)
  UnsupportedError(String)
}
pub(all) struct OkPacket {
  affected_rows : Int64
  last_insert_id : Int64
  status : Int32
  warnings : Int32
}
pub struct PacketReader {
  data : Bytes
  mut pos : Int32
}

A cursor over one decoded MySQL packet payload. Every field type the protocol uses — fixed-width little-endian integers, length-encoded integers and strings, NUL-terminated strings, and raw byte runs — is read through this, advancing a position and raising MysqlError::ProtocolError on any short read. It holds no socket: the transport reads a full payload into Bytes first, then parses it here, which is what lets the whole codec be tested off any backend.

pub(all) enum QuoteMode {
  Backslash
  DoubleQuote
}
pub(all) enum ServerKind {
  MySQL
  MariaDB
}
pub(all) struct StmtOk {
  id : Int32
  num_columns : Int32
  num_params : Int32
}

The server's reply to COM_STMT_PREPARE: the statement handle and how many parameters and result columns the statement has.

Functions & methods 63

pub fn bind_params(String, Array[@moondb.Value], mode? : QuoteMode) -> Bytes raise MysqlError

Substitute the ordered params for the ? placeholders in sql, producing the UTF-8 query bytes a COM_QUERY carries.

The text protocol has no out-of-band parameter binding — that arrives with prepared statements (COM_STMT_PREPARE, binary protocol) in a later round — so values are rendered as escaped literals here. The scanner tracks single-, double-, and backtick-quoted spans (honouring backslash escapes) so a literal ? inside a string or identifier is never mistaken for a placeholder, and it raises when the placeholder and parameter counts disagree.

pub fn build_handshake_response(Handshake, String, Bytes, String, ssl? : Bool, charset? : Int32) -> Bytes raise MysqlError

Build the client's HandshakeResponse41 payload for user/password/database.

mysql_native_password and caching_sha2_password (MySQL 8's default, fast-path scramble here and full auth driven in MysqlConn::connect) are both handled directly. For any other plugin the client advertises mysql_native_password and sends a native token, so a native-capable account authenticates and otherwise the server drives an AuthSwitchRequest the connection layer answers (native or caching_sha2) or rejects. Only a server that requires a non-native plugin and does not offer pluggable auth is rejected here. client_ed25519 is answered by the connection layer when MariaDB switches to it.

pub fn build_ssl_request(String, charset? : Int32) -> Bytes

SSLRequest: Int32 length 8, Int32 request code 80877103. Sent before the startup message; the server answers a single byte — S to begin a TLS handshake, N to continue in cleartext.

pub fn build_stmt_close(Int32) -> Bytes

COM_STMT_CLOSE (0x19): the statement id; the server answers nothing.

pub fn build_stmt_execute(Int32, Array[@moondb.Value]) -> Bytes

COM_STMT_EXECUTE (0x17) for statement id, binding params out-of-band. Every non-NULL parameter is declared as VAR_STRING (or BLOB for bytes) and sent length-encoded; the server coerces it to the column type. The NULL bitmap uses bit i for parameter i, LSB first.

pub fn build_stmt_prepare(String) -> Bytes

COM_STMT_PREPARE (0x16): the SQL text follows the command byte.

pub fn build_text_rows(Array[ColumnDef], Array[Bytes]) -> Array[@moondb.Row] raise MysqlError

Build the materialised rows of a text-protocol result set from its column definitions and the raw row packets. This is the pure heart of query: the socket transport collects columns and row_payloads, and every cell decode happens here, off any backend.

pub fn bytes_to_string(Bytes) -> String

Bytes → String, lossily (server version, error text, text-protocol cells).

pub fn caching_sha2_full_auth_token(Bytes, Bytes, String, Bytes) -> Bytes raise MysqlError

The caching_sha2_password full-auth token: the NUL-terminated password XORed with the nonce (cycled), then RSA-OAEP encrypted under the server's public key.

seed is the 20 random octets OAEP masks with. MySQL's own client draws them from OpenSSL; here they are a parameter, because this package has no entropy source and a caller that reuses a seed should have to write that down.

pub fn caching_sha2_scramble(Bytes, Bytes) -> Bytes

The caching_sha2_password fast-auth scramble: SHA256(pw) XOR SHA256( SHA256(SHA256(pw)) ‖ nonce ), 32 bytes. An empty password sends an empty token.

pub let client_connect_with_db : Int32

The CLIENT_CONNECT_WITH_DB capability flag of the connection handshake.

pub let client_long_flag : Int32

The CLIENT_LONG_FLAG capability flag of the connection handshake.

pub let client_long_password : Int32

The CLIENT_LONG_PASSWORD capability flag of the connection handshake.

pub let client_multi_results : Int32

CLIENT_MULTI_RESULTS: one statement may answer with more than one result set — what a stored procedure's CALL needs. Without it the server rejects a procedure that returns rows (ER_SP_BADSELECT).

pub let client_multi_statements : Int32

CLIENT_MULTI_STATEMENTS: one COM_QUERY may carry several ;-separated statements.

pub let client_plugin_auth : Int32

The CLIENT_PLUGIN_AUTH capability flag of the connection handshake.

pub let client_protocol_41 : Int32

The CLIENT_PROTOCOL_41 capability flag of the connection handshake.

pub let client_secure_connection : Int32

The CLIENT_SECURE_CONNECTION capability flag of the connection handshake.

pub let client_ssl : Int32

The CLIENT_SSL capability flag of the connection handshake: the client will begin a TLS handshake after an SSLRequest packet.

pub let client_transactions : Int32

The CLIENT_TRANSACTIONS capability flag of the connection handshake.

pub fn concat_bytes(Bytes, Bytes) -> Bytes

Concatenate two byte strings. A small helper the MD5 auth path and message framing lean on; MoonBit's Bytes is immutable so this allocates once.

pub fn decode_binary_row(Bytes, Array[ColumnDef]) -> @moondb.Row raise MysqlError

Decode one binary-protocol result row against columns into a @moondb.Row. The row header is 0x00, then a NULL bitmap offset by two bits, then each non-NULL value in the column's binary type.

pub fn decode_text_value(Bytes?, ColumnDef) -> @moondb.Value raise MysqlError

Map one text-protocol cell (None = the 0xFB NULL sentinel) onto a @moondb.Value using the column's declared type. Integers narrow to Int except BIGINT, which keeps 64 bits; FLOAT/DOUBLE become Double; binary-collation strings become Blob; everything else (VARCHAR, TEXT, DECIMAL, temporal types as ISO text, JSON) becomes Text.

pub fn eof_status(Bytes) -> Int32

The status-flags word of an EOF packet (0xFE, warnings LE16, status LE16), or 0 for a packet that is not a protocol-41 EOF.

pub fn has_more_results(Bytes) -> Bool

Whether an EOF packet's status word says another result set follows.

pub fn is_auth_switch_request(Bytes) -> Bool

Whether payload is an AuthSwitchRequest (0xFE lead byte with a body — the length is what tells it apart from a 5-byte EOF, exactly as is_eof_packet keys off < 9).

pub fn is_eof_packet(Bytes) -> Bool

Whether payload is an EOF packet (first byte 0xFE, fewer than 9 bytes — which is what tells it apart from a row whose first cell is an 8-byte length-encoded value).

pub fn is_err_packet(Bytes) -> Bool

Whether payload is an ERR packet (first byte 0xFF).

pub fn is_ok_packet(Bytes) -> Bool

Whether payload is an OK packet (first byte 0x00, at least 7 bytes).

pub let mariadb_client_cache_metadata : Int32

MariaDB's MARIADB_CLIENT_CACHE_METADATA capability flag of the connection handshake.

pub let mariadb_client_com_multi : Int32

MariaDB's MARIADB_CLIENT_COM_MULTI capability flag of the connection handshake.

pub let mariadb_client_extended_metadata : Int32

MariaDB's MARIADB_CLIENT_EXTENDED_METADATA capability flag of the connection handshake.

pub let mariadb_client_progress : Int32

MariaDB's MARIADB_CLIENT_PROGRESS capability flag of the connection handshake.

pub let mariadb_client_stmt_bulk_operations : Int32

MariaDB's MARIADB_CLIENT_STMT_BULK_OPERATIONS capability flag of the connection handshake.

pub fn mariadb_ed25519_response(Bytes, Bytes) -> Bytes

The MariaDB client_ed25519 response: the 64-byte Ed25519 signature of the server's 32-byte challenge, keyed by the password.

MariaDB keys the signature with the password itself, where RFC 8032 would expand a 32-byte seed — the derivation PrivateKey::of_secret exists for. The plugin signs unconditionally, so an empty password expands to SHA-512("") and still yields a valid signature.

pub fn MysqlError::to_string(Self) -> String

A one-line rendering, e.g. ServerError(1146, 42S02): Table 'test.t' doesn't exist.

pub fn native_password_scramble(Bytes, Bytes) -> Bytes

The mysql_native_password challenge response: SHA1(password) XOR SHA1(salt ++ SHA1(SHA1(password))), 20 bytes. An empty password produces an empty response (the server accepts a zero-length token).

pub fn PacketReader::at_end(Self) -> Bool

Whether the cursor has consumed the whole payload.

pub fn PacketReader::bytes(Self, Int32) -> Bytes raise MysqlError

Read n raw bytes.

pub fn PacketReader::lenenc_bytes(Self) -> Bytes? raise MysqlError

Read a length-encoded string (the string<lenenc> type), returning None for the 0xFB NULL sentinel that appears in text-protocol rows.

pub fn PacketReader::lenenc_uint(Self) -> Int64 raise MysqlError

Read a length-encoded unsigned integer (the int<lenenc> type). The 0xFB NULL sentinel and 0xFF are rejected here — NULL only has meaning inside a row, which lenenc_bytes handles.

pub fn PacketReader::new(Bytes) -> Self

Wrap a decoded packet payload for reading from the start.

pub fn PacketReader::peek(Self) -> Int32

Peek the next byte without advancing; -1 at end of payload.

pub fn PacketReader::remaining(Self) -> Int32

Bytes not yet consumed.

pub fn PacketReader::rest(Self) -> Bytes

Read everything left in the payload (an EOF-terminated string field).

pub fn PacketReader::skip(Self, Int32) -> Unit raise MysqlError

Skip n bytes.

pub fn PacketReader::string_nul(Self) -> Bytes raise MysqlError

Read a NUL-terminated string, consuming the terminator.

pub fn PacketReader::u8(Self) -> Int32 raise MysqlError

Read one byte as an Int in 0..=255.

pub fn PacketReader::uint_le(Self, Int32) -> Int64 raise MysqlError

Read an n-byte little-endian unsigned integer.

pub fn parse_auth_switch_request(Bytes) -> (String, Bytes) raise MysqlError

Decode an AuthSwitchRequest into the plugin name the server wants and the fresh 20-byte auth salt for it. The trailing NUL that follows the scramble is dropped so the salt feeds native_password_scramble directly.

pub fn parse_column_def(Bytes) -> ColumnDef raise MysqlError

Decode a column-definition packet (protocol 41). Only the fields that steer text decoding are kept; catalog/schema/table names and the length/decimals fields are read past.

pub fn parse_ed25519_challenge(Bytes) -> Bytes raise MysqlError

The 32-byte nonce from a client_ed25519 AuthSwitchRequest. MariaDB sends exactly NONCE_BYTES (= 32) with no NUL terminator and the client signs all of them, so — unlike the native scramble — the full 32 bytes are read raw rather than through the NUL-stripping parse_auth_switch_request.

pub fn parse_err(Bytes) -> MysqlError raise MysqlError

Decode a full ERR packet (including its 0xFF marker) into the MysqlError::ServerError it represents.

pub fn parse_handshake(Bytes) -> Handshake raise MysqlError

Parse a protocol-10 initial Handshake payload. An ERR packet in its place (0xFF, e.g. "Host is blocked" / "Too many connections") is surfaced as a MysqlError::ServerError.

pub fn parse_ok(Bytes) -> OkPacket raise MysqlError

Decode an OK packet (including its 0x00 marker).

pub fn parse_rsa_public_key(String) -> @rsa.PublicKey raise MysqlError

The RSA public key inside the PEM SubjectPublicKeyInfo the server sends.

pub fn parse_server_version(String) -> (ServerKind, String)

Classify a raw handshake version string and recover the real version.

MariaDB 10+ prefixes its version with the fake 5.5.5- sentinel (MySQL 5.5.5 was never released), so pre-10 clients that gate on the leading 5. still connect. Strip it to get the true version — e.g. 5.5.5-11.4.2-MariaDB-ubu2404 → 11.4.2-MariaDB-ubu2404 as ServerKind::MariaDB; a plain 8.0.35 is returned unchanged as ServerKind::MySQL.

pub fn parse_stmt_prepare_ok(Bytes) -> StmtOk raise MysqlError

Decode the COM_STMT_PREPARE_OK packet: 0x00, statement id (LE32), column count (LE16), parameter count (LE16), one reserved byte, warning count (LE16).

pub fn put_lenenc_bytes(@buffer.Buffer, Bytes) -> Unit

Append a length-encoded string (its lenenc length prefix, then the bytes).

pub fn put_lenenc_uint(@buffer.Buffer, Int64) -> Unit

Append a length-encoded unsigned integer.

pub fn put_string_nul(@buffer.Buffer, Bytes) -> Unit

Append raw bytes followed by a NUL terminator.

pub fn put_uint_le(@buffer.Buffer, Int64, Int32) -> Unit

Append an n-byte little-endian unsigned integer.

pub let server_more_results_exists : Int32

SERVER_MORE_RESULTS_EXISTS: the status flag that says another result set follows the one just terminated.

moonbitstack/moonmysql/client

client

Types 4

pub struct MysqlConn {
  stream : @tcp.Conn
  mut seq : Int32
  mut closed : Bool
  server_kind : @moonmysql.ServerKind
  mut streaming : Bool
  mut quote_mode : @moonmysql.QuoteMode
}

A live MySQL connection over a raw TCP socket, speaking the wire protocol in pure MoonBit. This is the asynchronous, full-fidelity driver: it owns the socket for its whole lifetime, so begin/commit/rollback bracket a real server-side transaction. The synchronous @moondb.Driver adapter (MysqlDriver) is layered on top for the moondb contract.

Framing is the classic 3-byte little-endian length + 1-byte sequence id header; the transport transparently reassembles the 0xFFFFFF continuation frames a large payload is split into.

pub struct MysqlDriver {
  host : String
  port : Int32
  user : String
  password : String
  database : String
  tls : @tls.Client?
}

A synchronous @moondb.Driver over MySQL — the adapter that lets a moondb query layer (moonorm) talk to MySQL through the same contract as the SQLite and Postgres drivers.

## The async wall, and what it means here

moonbitlang/async sockets are asynchronous, but @moondb.Driver's methods are synchronous, and MoonBit forbids calling an async function from a non-async one. The bridge is @async.run_async_main, which spins an event loop, runs an async body to completion, and returns — the same hook the toolchain uses to enter async fn main.

A socket does **not** survive across two such event loops (each tears its file descriptors down; verified empirically), so every execute/query opens a fresh connection, authenticates, runs its one statement, and closes. That is correct and efficient enough for autocommit work — which is exactly what the moondb roundtrip is — but it means a transaction cannot be held open across separate Driver calls. begin/commit/rollback therefore raise a clear error directing callers to the asynchronous MysqlConn API, which owns its socket for its whole lifetime and brackets real transactions. This is a language constraint, not a shortcut, and it is documented in the README.

pub struct MysqlRowStream {
  conn : MysqlConn
  columns : Array[@moonmysql.ColumnDef]
  mut pending : Bytes?
  mut done : Bool
}

A forward-only cursor that reads a query's rows off the wire on demand instead of buffering them, the streaming counterpart to query, for results too large to materialise at once (mysql_use_result. Bound to its MysqlConn, a single-command pipeline: the stream must be drained (next returns None) or [closed](MysqlRowStream::close) before another statement runs on that connection.

pub struct MysqlStmt {
  conn : MysqlConn
  id : Int32
  text : String
  slots : Array[String]
  mut params : Array[@moondb.Value]
  mut closed : Bool
}

A statement MySQL prepares once and runs many times.

prepare sends COM_STMT_PREPARE and keeps the server-side handle; every execute/query sends only the statement id and the values, which is both cheaper than re-parsing and the path that binds binary payloads without hex expansion. close sends COM_STMT_CLOSE, after which the handle raises Closed.

A :name statement is rewritten to MySQL's ? spelling at prepare time and the slot names are kept, so bind_named works without the caller counting anything. A handle belongs to the connection that prepared it: one wire, one statement at a time.

Functions & methods 16

pub fn MysqlConn::capabilities(Self) -> @moondb.Capabilities

@moondb.AsyncDriver's capabilities, callable on the value directly.

pub fn MysqlConn::close(Self) -> Unit

Close the socket (best-effort, idempotent).

pub fn MysqlConn::dialect(Self) -> @moondb.Dialect

@moondb.AsyncDriver's dialect, callable on the value directly.

pub fn MysqlConn::server_kind(Self) -> @moonmysql.ServerKind

The dialect (MySQL or MariaDB) reported by the server's handshake.

pub fn MysqlDriver::begin(Self) -> Unit raise @moondb.DbError

@moondb.Driver's begin, callable on the value directly.

pub fn MysqlDriver::capabilities(Self) -> @moondb.Capabilities

@moondb.Driver's capabilities, callable on the value directly.

pub fn MysqlDriver::close(Self) -> Unit

@moondb.Driver's close, callable on the value directly.

pub fn MysqlDriver::commit(Self) -> Unit raise @moondb.DbError

@moondb.Driver's commit, callable on the value directly.

pub fn MysqlDriver::dialect(Self) -> @moondb.Dialect

@moondb.Driver's dialect, callable on the value directly.

pub fn MysqlDriver::execute(Self, String, Array[@moondb.Value]) -> @moondb.ExecResult raise @moondb.DbError

@moondb.Driver's execute, callable on the value directly.

pub fn MysqlDriver::new(user~ : String, password? : String, database? : String, host? : String, port? : Int32, tls? : @tls.Client) -> Self

Configure a driver. host defaults to loopback, port to 3306; password and database may be empty. tls turns on TLS for the connection.

pub fn MysqlDriver::ping(Self) -> Bool

@moondb.Driver's ping, callable on the value directly.

pub fn MysqlDriver::prepare(Self, String) -> &@moondb.Stmt raise @moondb.DbError

@moondb.Driver's prepare, callable on the value directly.

pub fn MysqlDriver::query(Self, String, Array[@moondb.Value]) -> Array[@moondb.Row] raise @moondb.DbError

@moondb.Driver's query, callable on the value directly.

pub fn MysqlDriver::query_stream(Self, String, Array[@moondb.Value]) -> &@moondb.Cursor raise @moondb.DbError

@moondb.Driver's query_stream, callable on the value directly.

pub fn MysqlDriver::rollback(Self) -> Unit raise @moondb.DbError

@moondb.Driver's rollback, callable on the value directly.