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
notifications : Bool
transactions : 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. An embedded engine reports through this case too, with the numeric code alone.
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
Clickhouse
Duckdb
Sqlserver
}
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 100
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 async fn AsyncArrayCursor::close(Self) -> Unit
AsyncCursor's close, callable on the value directly.
pub fn AsyncArrayCursor::new(Array[Row]) -> Self
A cursor positioned before the first of rows.
pub async fn AsyncArrayCursor::next(Self) -> Row? raise DbError
AsyncCursor's next, callable on the value directly.
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
pub async fn[T : AsyncDriver] AsyncExecutor::execute(Self[T], String, Array[Value]) -> ExecResult raise DbError
pub fn[T] AsyncExecutor::new(T) -> Self[T]
The narrow view over inner.
pub async fn[T : AsyncDriver] AsyncExecutor::query(Self[T], String, Array[Value]) -> Array[Row] raise DbError
Run a row-returning statement and materialise every row.
pub async fn[T : AsyncDriver] AsyncExecutor::query_stream(Self[T], String, Array[Value]) -> &AsyncCursor raise DbError
Stream a row-returning statement's rows.
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 async fn[T : AsyncDriver] AsyncTxn::run(Self[T], async (Self[T]) -> Unit raise DbError) -> Unit raise DbError
Run body inside a transaction, or inside a savepoint when one is already open. Same contract as Txn::run, with every round trip awaited.
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 async fn drain_async(&AsyncCursor) -> Array[Row] raise DbError
Drain an async cursor into an array — the drain of the async seam, for a caller that wants every row after all, 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/blocking
blocking
Types 3
pub struct Blocking[T] {
driver : T
}A @moondb.Driver seen as an @moondb.AsyncDriver, so a backend whose work happens inside a C call can be handed to code written against the asynchronous seam — the checkpoint store of an agent framework, a retrieval index, a server that runs one event loop for every dependency.
The two traits are method-for-method identical, so the bridge is a delegation and nothing more: each async method calls its blocking twin and returns its result. There is no thread, no queue and no wake-up, because there is nothing to wait for — SQLite steps a sqlite3_stmt and returns.
What the bridge does not do is as important. The call runs on the task's own thread, so it occupies the event loop for as long as the driver takes: that is the right trade for a backend answering in microseconds out of the page cache, and the wrong one for a backend answering in milliseconds over a socket — a networked backend implements @moondb.AsyncDriver itself, where the wait is real and other tasks keep running through it. Nor does the bridge serialise: a driver that is not safe to use from two tasks at once is still not safe here, so give each task its own connection, or park them behind @moondb/apool.AsyncPool, which hands one connection to one caller at a time.
What it does keep is everything beyond the base contract: prepare hands back the driver's own server-side handle rather than a buffered re-run, query_stream keeps the driver's live cursor rather than materialising the result, and dialect and capabilities come from the wrapped driver, so a query layer sees the real backend underneath.
test "a blocking driver behind the async seam" {
let db = @blocking.Blocking::new(@moondb.MockDriver::new())
assert_eq(db.dialect(), @moondb.Dialect::Sqlite)
assert_true(db.capabilities().savepoints)
}
pub struct BlockingCursor {
cursor : &@moondb.Cursor
}A blocking driver's @moondb.Cursor behind the asynchronous seam.
The default async query_stream materialises the whole result and hands back an array cursor, which is exactly the memory bound streaming exists to avoid. This adapter pulls from the wrapped cursor instead, so a backend that steps a prepared statement one row at a time keeps doing so — and the drain discipline is the wrapped cursor's: it must be read to None or closed.
pub struct BlockingStmt {
stmt : &@moondb.Stmt
}A blocking driver's @moondb.Stmt behind the asynchronous seam.
The default async prepare would keep the SQL text and re-run the driver's execute/query on every call, which throws away the thing a prepared handle exists for. This adapter keeps the wrapped handle instead, so the SQLite sqlite3_stmt — compiled, planned and bound — is what each run reaches.
Functions & methods 25
pub async fn[T : @moondb.Driver] Blocking::begin(Self[T]) -> Unit raise @moondb.DbError
@moondb.AsyncDriver's begin, callable on the value directly.
pub fn[T : @moondb.Driver] Blocking::capabilities(Self[T]) -> @moondb.Capabilities
@moondb.AsyncDriver's capabilities, callable on the value directly.
pub async fn[T : @moondb.Driver] Blocking::close(Self[T]) -> Unit
@moondb.AsyncDriver's close, callable on the value directly.
pub async fn[T : @moondb.Driver] Blocking::commit(Self[T]) -> Unit raise @moondb.DbError
@moondb.AsyncDriver's commit, callable on the value directly.
pub fn[T : @moondb.Driver] Blocking::dialect(Self[T]) -> @moondb.Dialect
@moondb.AsyncDriver's dialect, callable on the value directly.
pub async fn[T : @moondb.Driver] Blocking::execute(Self[T], String, Array[@moondb.Value]) -> @moondb.ExecResult raise @moondb.DbError
@moondb.AsyncDriver's execute, callable on the value directly.
pub fn[T] Blocking::inner(Self[T]) -> T
The driver this adapter wraps, for the methods the contract does not carry — a backend's own connection controls, an index rebuild, a file handle.
pub fn[T] Blocking::new(T) -> Self[T]
driver, presented as an @moondb.AsyncDriver.
pub async fn[T : @moondb.Driver] Blocking::ping(Self[T]) -> Bool
@moondb.AsyncDriver's ping, callable on the value directly.
pub async fn[T : @moondb.Driver] Blocking::prepare(Self[T], String) -> &@moondb.AsyncStmt raise @moondb.DbError
@moondb.AsyncDriver's prepare, callable on the value directly.
pub async fn[T : @moondb.Driver] Blocking::query(Self[T], String, Array[@moondb.Value]) -> Array[@moondb.Row] raise @moondb.DbError
@moondb.AsyncDriver's query, callable on the value directly.
pub async fn[T : @moondb.Driver] Blocking::query_stream(Self[T], String, Array[@moondb.Value]) -> &@moondb.AsyncCursor raise @moondb.DbError
@moondb.AsyncDriver's query_stream, callable on the value directly.
pub async fn[T : @moondb.Driver] Blocking::rollback(Self[T]) -> Unit raise @moondb.DbError
@moondb.AsyncDriver's rollback, callable on the value directly.
pub async fn BlockingCursor::close(Self) -> Unit
@moondb.AsyncCursor's close, callable on the value directly.
pub fn BlockingCursor::new(&@moondb.Cursor) -> Self
cursor, presented as an @moondb.AsyncCursor.
pub async fn BlockingCursor::next(Self) -> @moondb.Row? raise @moondb.DbError
@moondb.AsyncCursor's next, callable on the value directly.
pub async fn BlockingStmt::bind(Self, Array[@moondb.Value]) -> Unit raise @moondb.DbError
@moondb.AsyncStmt's bind, callable on the value directly.
pub async fn BlockingStmt::bind_named(Self, Map[String, @moondb.Value]) -> Unit raise @moondb.DbError
@moondb.AsyncStmt's bind_named, callable on the value directly.
pub async fn BlockingStmt::close(Self) -> Unit
@moondb.AsyncStmt's close, callable on the value directly.
pub async fn BlockingStmt::execute(Self) -> @moondb.ExecResult raise @moondb.DbError
@moondb.AsyncStmt's execute, callable on the value directly.
pub fn BlockingStmt::new(&@moondb.Stmt) -> Self
stmt, presented as an @moondb.AsyncStmt.
pub fn BlockingStmt::params(Self) -> Array[String]
@moondb.AsyncStmt's params, callable on the value directly.
pub async fn BlockingStmt::query(Self) -> Array[@moondb.Row] raise @moondb.DbError
@moondb.AsyncStmt's query, callable on the value directly.
pub async fn BlockingStmt::query_stream(Self) -> &@moondb.AsyncCursor raise @moondb.DbError
@moondb.AsyncStmt's query_stream, callable on the value directly.
pub fn BlockingStmt::sql(Self) -> String
@moondb.AsyncStmt's sql, 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]
described : Map[String, (Array[String], Array[Int32])]
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]
formats : 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
col_names : Array[String]
col_oids : Array[Int32]
formats : Array[Int32]
binary : 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 101
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], formats? : Array[Int32]) -> Bytes
Bind ('B') params to a named (or unnamed) prepared statement, producing a named (or unnamed) portal. Parameters always go as text; formats says, one entry per result column, whether that column comes back in binary (1) or text (0). An empty formats is the protocol's own spelling of "all text".
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_describe_statement(String) -> Bytes
Describe ('D') a prepared statement, which answers with its parameter types and a RowDescription. That description is what a binary result needs before the Bind: the format code of each result column is chosen from its type.
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_BPCHAR : Int32 = 1042
The pg_type OID of char(n) (blank-padded).
pub const OID_BYTEA : Int32 = 17
The pg_type OID of bytea, as a column description reports it.
pub const OID_DATE : Int32 = 1082
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
pub const OID_JSONB : Int32 = 3802
The pg_type OID of jsonb.
pub const OID_NAME : Int32 = 19
The pg_type OID of name (an identifier column).
pub const OID_NUMERIC : Int32 = 1700
The pg_type OID of numeric.
pub const OID_TIME : Int32 = 1083
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
pub const OID_VARCHAR : Int32 = 1043
The pg_type OID of varchar.
pub fn parse_double(String) -> Double?
Parse a floating-point literal from PostgreSQL text (123.45, -1e10, Infinity, NaN), or None if malformed.
The three special tokens PostgreSQL emits are handled here; everything else goes to the language runtime's decimal-to-binary conversion, which rounds once and correctly. A hand-rolled mantissa × 10^exponent loop would not: multiplying by ten three hundred times drifts by several ULP, and 1e300 would come back as the double next to it — a value that then differs from the same column read in binary.
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 async fn PgConn::begin(Self) -> Unit raise @moondb.DbError
Begin an explicit transaction (BEGIN).
pub async fn PgConn::cancel(Self) -> Unit raise @moondb.DbError
Cancel the query currently running on this connection. PostgreSQL requires the request on a separate connection: this opens one, sends CancelRequest with the backend key, and closes it.
pub fn PgConn::capabilities(Self) -> @moondb.Capabilities
@moondb.AsyncDriver's capabilities, callable on the value directly.
pub async fn PgConn::close(Self) -> Unit
Close the connection: best-effort Terminate, then close the socket. Idempotent — a second call is a no-op.
pub async fn PgConn::close_cursor(Self, String) -> Unit raise @moondb.DbError
pub async fn PgConn::commit(Self) -> Unit raise @moondb.DbError
Commit the current transaction (COMMIT).
pub async fn PgConn::connect(String, Int32, String, String, String, tls? : @tls.Client) -> Self raise @moondb.DbError
Open a connection and complete the startup handshake: send StartupMessage, satisfy the authentication request (trust/AuthenticationOk, cleartext, MD5, or SASL/SCRAM-SHA-256), and drain server parameters up to the first ReadyForQuery. host may be a hostname or a literal IP; port is typically 5432. With tls, the connection first sends PostgreSQL's SSLRequest and negotiates TLS (a server that answers N is an error, not a silent downgrade), then runs SCRAM with channel binding when the server offers -PLUS. Raises ConnectError on any transport, TLS, or handshake failure.
pub async fn PgConn::copy_in(Self, String, Bytes) -> @moondb.ExecResult raise @moondb.DbError
Run a COPY … FROM STDIN, streaming data to the server in bounded chunks, and report the command tag's affected-row count. The convenience wrapper over copy_in_stream(#copy_in_stream) for a payload that is already in memory.
pub async fn PgConn::copy_in_stream(Self, String) -> PgCopyIn raise @moondb.DbError
Start a COPY … FROM STDIN. Returns once the server has signalled it is ready for data.
pub async fn PgConn::copy_out(Self, String) -> Bytes raise @moondb.DbError
Run a COPY … TO STDOUT and return everything it produced. The convenience wrapper over copy_out_stream(#copy_out_stream) for a result that does fit in memory.
pub async fn PgConn::copy_out_stream(Self, String) -> PgCopyOut raise @moondb.DbError
Start a COPY … TO STDOUT and hand back the stream. Nothing is buffered: the first chunk is read by the first read_chunk.
pub async fn PgConn::cursor(Self, String, batch? : Int32) -> PgCursor raise @moondb.DbError
Declare a cursor over sql inside the current transaction. batch is how many rows one FETCH asks for, and is the knob that trades round trips for memory.
pub async fn PgConn::declare_cursor(Self, String, String) -> Unit raise @moondb.DbError
Declare a server-side cursor over sql inside the current transaction. The caller must be inside an explicit transaction (begin); the cursor is closed by close_cursor or at commit/rollback.
pub fn PgConn::dialect(Self) -> @moondb.Dialect
@moondb.AsyncDriver's dialect, callable on the value directly.
pub async fn PgConn::execute(Self, String, Array[@moondb.Value]) -> @moondb.ExecResult raise @moondb.DbError
Run a non-row statement (INSERT/UPDATE/DELETE/DDL) and report rows changed. last_insert_id is 0: the simple/extended protocols do not surface a generated key without a RETURNING clause (roadmap).
pub async fn PgConn::fetch_cursor(Self, String, Int32) -> Array[@moondb.Row] raise @moondb.DbError
Fetch up to count rows forward from a declared cursor.
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 async fn PgConn::listen(Self, String) -> Unit raise @moondb.DbError
Start listening on channel; next_notification then yields the payloads other sessions send with NOTIFY.
pub async fn PgConn::next_notification(Self) -> (String, String) raise @moondb.DbError
Block until a notification arrives on a channel this connection listens on, returning (channel, payload). Skips ParameterStatus and ReadyForQuery while waiting; an ErrorResponse raises.
pub async fn PgConn::notify(Self, String, payload? : String) -> Unit raise @moondb.DbError
Send a notification on channel, optionally carrying payload.
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 async fn PgConn::ping(Self) -> Bool
@moondb.AsyncDriver's ping, callable on the value directly.
pub async fn PgConn::prepare(Self, String) -> &@moondb.AsyncStmt raise @moondb.DbError
@moondb.AsyncDriver's prepare, callable on the value directly.
pub async fn PgConn::prepare_stmt(Self, String, binary? : Bool) -> PgStmt raise @moondb.DbError
Parse sql on the server (or reuse the cached statement with the same text) and return the handle.
binary fixes the result format for the handle's whole life: each column is described once at prepare time and then bound in whichever format reads back the same value — binary where that is exact, text otherwise. A handle therefore pays for the description once instead of on every call, unlike PgConn::query.
pub async fn PgConn::query(Self, String, Array[@moondb.Value], binary? : Bool) -> Array[@moondb.Row] raise @moondb.DbError
Run a row-returning statement and materialise every row. Uses the simple Query protocol when params is empty and the result is text, and the extended Parse/Bind/Execute protocol (with ?→$n translation and out-of-band text-format binding) otherwise.
With binary, each result column whose type has an exact binary decoder is fetched in the binary format and the rest stay text — see decode_binary. Text is the default, and a binary result costs one extra round trip per call: the column types have to be described before the Bind can name a format for each.
pub async fn PgConn::query_stream(Self, String, Array[@moondb.Value], binary? : Bool) -> PgRowStream raise @moondb.DbError
Send sql (with bound params) and return a streaming cursor over its rows. binary asks for the same per-type binary result query does.
pub async fn PgConn::rollback(Self) -> Unit raise @moondb.DbError
Roll back the current transaction (ROLLBACK).
pub async fn PgConn::unlisten(Self, String) -> Unit raise @moondb.DbError
Stop listening on channel (or every channel when channel is "*").
pub async fn PgCopyIn::fail(Self, String) -> Unit raise @moondb.DbError
Abort the load (CopyFail with reason) and read the server's error. The connection stays usable; the rows already sent are rolled back with the statement.
pub async fn PgCopyIn::finish(Self) -> @moondb.ExecResult raise @moondb.DbError
Close the stream (CopyDone) and read the server's verdict, reporting the command tag's affected-row count.
pub async fn PgCopyIn::write_chunk(Self, Bytes) -> Unit raise @moondb.DbError
Send one piece of COPY data. The chunk is framed here, so the caller only supplies the payload — rows in COPY's own format.
pub async fn PgCopyOut::cancel(Self) -> Unit raise @moondb.DbError
Abandon the stream: finish reading the server's response so the connection can run the next statement. Everything already read stays read.
pub async fn PgCopyOut::read_chunk(Self) -> Bytes? raise @moondb.DbError
The next chunk of copied data, or None once the server has finished. The command tag's row count is reported by rows(#rows) after the stream ends.
pub fn PgCopyOut::rows(Self) -> Int64
How many rows the finished COPY reported, or 0 while it is still running.
pub async fn PgCursor::close(Self) -> Unit raise @moondb.DbError
Close the cursor. Idempotent; a cursor also disappears with its transaction.
pub async fn PgCursor::next_batch(Self) -> Array[@moondb.Row] raise @moondb.DbError
The next batch of rows, in order. An empty array means the cursor is exhausted.
pub async fn PgDriver::connect(Self) -> PgConn raise @moondb.DbError
Open the connection this descriptor points at. Call it inside an event loop (async test / async fn main); the returned PgConn is an @moondb.AsyncDriver.
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 async fn PgRowStream::close(Self) -> Unit raise @moondb.DbError
Abandon the stream early, draining any unread response to ReadyForQuery so the connection can run the next statement. Idempotent once the stream is done.
pub async fn PgRowStream::next(Self) -> @moondb.Row? raise @moondb.DbError
The next row, or None once the result is exhausted. Reads messages until a DataRow (updating the column metadata from any RowDescription first), and on ReadyForQuery marks the stream done. An ErrorResponse is drained to ReadyForQuery before raising, so the connection stays usable.
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 async fn PgWire::read_exactly(Self, Int32) -> Bytes
Read exactly n bytes, waiting for them if the peer has not sent them yet.
pub fn PgWire::tcp(@tcp.Conn) -> Self
Wrap a socket — plain, or the TLS session @tcp.Conn::client returned.
pub async fn PgWire::write(Self, Bytes) -> Unit
Write every byte of bytes, then return. The peer sees them in one piece.
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 67
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], streamed? : Array[Int32]) -> 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.
streamed names the parameters whose value was already sent with COM_STMT_SEND_LONG_DATA: their type is still declared here — the server needs to know how to read the buffered bytes — but their value is not repeated.
pub fn build_stmt_prepare(String) -> Bytes
COM_STMT_PREPARE (0x16): the SQL text follows the command byte.
pub fn build_stmt_send_long_data(Int32, Int32, Bytes) -> Bytes
COM_STMT_SEND_LONG_DATA (0x18): statement id, parameter index, then one slice of that parameter's value. The server appends slices in the order they arrive, answers nothing at all, and forgets the accumulated value once the statement is executed or reset — so the pieces must be sent immediately before the execute that binds them.
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 long_data_chunk : Int32
How much of one parameter a COM_STMT_SEND_LONG_DATA packet carries, and the length at which a parameter stops travelling inside the execute packet and is streamed this way instead.
A packet is capped by the server's max_allowed_packet (64 MiB on MySQL 8 by default, 16 MiB on MariaDB), and the execute packet has to hold every parameter at once, so a value near that cap used to be un-sendable however it was encoded. One mebibyte is a sixteenth of the smaller default, which leaves room for the rest of the packet whatever the server is set to.
pub fn long_data_params(Array[@moondb.Value]) -> Array[Int32]
The parameter indexes, ascending, whose value is streamed rather than inlined. build_stmt_execute takes this list so it can leave their value bytes out.
pub fn long_data_slices(Array[@moondb.Value], Int32) -> Array[Bytes]
The slices of parameter index to send with COM_STMT_SEND_LONG_DATA, in order. Empty when the value fits in the execute packet, which is the case for everything shorter than long_data_chunk and for NULL.
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
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
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
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 29
pub async fn MysqlConn::begin(Self) -> Unit
Begin a server-side transaction.
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 async fn MysqlConn::commit(Self) -> Unit
Commit the current transaction.
pub async fn MysqlConn::connect(String, Int32, String, String, String, tls? : @tls.Client, charset? : Int32) -> Self
Open a connection to host:port and complete the auth handshake against user/password, selecting database (empty for none). Works against both MySQL and MariaDB (the reported dialect is on server_kind).
mysql_native_password and caching_sha2_password (MySQL 8's default, including the RSA full-auth path on a cache miss) are handled directly, whether the server drives them from its default or via an AuthSwitchRequest. With tls, the connection sends an SSLRequest after the greeting and runs the whole handshake over TLS; the caching_sha2 full-auth step then sends the password in cleartext under TLS instead of RSA-encrypting it. A switch to any other plugin (e.g. client_ed25519, on the README roadmap) raises @moonmysql.MysqlError::UnsupportedError.
pub fn MysqlConn::dialect(Self) -> @moondb.Dialect
@moondb.AsyncDriver's dialect, callable on the value directly.
pub async fn MysqlConn::execute(Self, String, Array[@moondb.Value]) -> @moondb.ExecResult
Run a non-row statement and report the rows affected and last insert id. With bound params it uses a prepared statement (binary protocol, out-of-band binding); without, the text protocol.
pub async fn MysqlConn::ping(Self) -> Bool
@moondb.AsyncDriver's ping, callable on the value directly.
pub async fn MysqlConn::prepare(Self, String) -> &@moondb.AsyncStmt raise @moondb.DbError
@moondb.AsyncDriver's prepare, callable on the value directly.
pub async fn MysqlConn::prepare_stmt(Self, String) -> MysqlStmt
Ask the server to prepare sql and return the handle.
pub async fn MysqlConn::query(Self, String, Array[@moondb.Value]) -> Array[@moondb.Row]
Run a row-returning statement and materialise every @moondb.Row. With bound params it uses a prepared statement (binary protocol); without, the text protocol.
pub async fn MysqlConn::query_multi(Self, String, Array[@moondb.Value]) -> Array[Array[@moondb.Row]]
Run a statement that answers with more than one result set and return every set, in the order the server sent them — a stored procedure's CALL, or a multi-statement packet. Values bind only on the prepared path, which has no multi-result protocol of its own, so params must be empty here.
pub async fn MysqlConn::query_stream(Self, String, Array[@moondb.Value]) -> MysqlRowStream
Send a row-returning statement and return a streaming cursor over its rows. The column definitions and their terminating EOF are consumed up front; the rows themselves are read one at a time by next(MysqlRowStream::next).
pub async fn MysqlConn::rollback(Self) -> Unit
Roll back the current transaction.
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.
pub async fn MysqlRowStream::close(Self) -> Unit
Abandon the stream early, draining any unread rows to the terminating EOF so the connection can run the next statement. Idempotent once the stream is done.
pub async fn MysqlRowStream::next(Self) -> @moondb.Row?
The next row, or None at the EOF that ends the result set. An ErrorResponse mid-stream is raised.
moonbitstack/moonsqls
moonsqls
Types 3
pub struct SqlServerConn {
wire : TdsWire
mut closed : Bool
database : String
mut txn : Bytes
}A live connection to a SQL Server instance, speaking TDS 7.4 directly. Open it with SqlServerConn::connect; it implements @moondb.AsyncDriver, so a query layer written for PostgreSQL or MySQL drives it unchanged.
Every statement is either a SQL batch or an RPC to sp_executesql, and the answer is read out of the token stream — COLMETADATA, ROW, NBCROW, DONE, ERROR — rather than through a client library.
pub struct SqlServerStmt {
conn : SqlServerConn
sql : String
mut params : Array[@moondb.Value]
mut handle : Int32
mut declaration : String
mut closed : Bool
}One SQL text compiled by the server, run with new values on every call.
The handle comes from sp_prepare and is released by sp_unprepare; between the two, a run is a sp_execute RPC that sends the handle and the values and nothing else — the statement text does not travel again.
A statement's declaration is part of what the server compiles: sp_prepare is told the type of every parameter, and those types come from the values. So a handle is compiled at the first bind that has values (or immediately, for a statement with no placeholders), and a later bind whose values declare different types drops the old handle and compiles a new one rather than sending values the plan did not expect.
pub struct TdsWire {
sock : @socket.Tcp
mut tls : @tls.Conn?
mut pending : Bytes
mut packet_id : Int32
}The transport under the protocol.
Functions & methods 25
pub fn permissive_tls() -> @tls.Client
A TLS configuration that encrypts without authenticating: SQL Server ships a self-signed certificate, and a driver that refused it would refuse every default install. Pass your own @tls.Client to connect to check the chain instead.
pub async fn SqlServerConn::begin(Self) -> Unit raise @moondb.DbError
@moondb.AsyncDriver's begin, callable on the value directly.
pub async fn SqlServerConn::bulk_copy(Self, String, Array[String], Array[Array[@moondb.Value]]) -> Int64 raise @moondb.DbError
Copy rows into table, one cell per name in columns, and report how many rows the server took.
The rows travel in the protocol's bulk-load format: INSERT BULK declares the shape, then one message carries the column metadata and every row. Values are bound into that stream, never spliced into the statement, so a string containing a quote is a string.
let copied = conn.bulk_copy("hero", ["id", "name"], [
[@moondb.Int(1), @moondb.Text("Nova")],
[@moondb.Int(2), @moondb.Text("Ivan")],
])
pub fn SqlServerConn::capabilities(Self) -> @moondb.Capabilities
@moondb.AsyncDriver's capabilities, callable on the value directly.
pub async fn SqlServerConn::close(Self) -> Unit
@moondb.AsyncDriver's close, callable on the value directly.
pub async fn SqlServerConn::commit(Self) -> Unit raise @moondb.DbError
@moondb.AsyncDriver's commit, callable on the value directly.
pub async fn SqlServerConn::connect(String, Int32, String, String, String, encrypt? : Bool, strict? : Bool, tls? : @tls.Client) -> Self raise @moondb.DbError
Open a connection. encrypt defaults to true: the login packet carries a password, and prelogin is where the client says so. A server that answers "encryption not supported" gets a clear session, because the alternative is refusing to connect to it.
strict selects TDS 8.0, where TLS is not negotiated at all: it has already started before the first TDS byte, so prelogin and the login inside it are encrypted whether the server would have offered encryption or not. A server older than SQL Server 2022 fails the handshake — which is the point of asking for it.
pub fn SqlServerConn::database(Self) -> String
The database the session is in.
pub fn SqlServerConn::dialect(Self) -> @moondb.Dialect
@moondb.AsyncDriver's dialect, callable on the value directly.
pub async fn SqlServerConn::execute(Self, String, Array[@moondb.Value]) -> @moondb.ExecResult raise @moondb.DbError
@moondb.AsyncDriver's execute, callable on the value directly.
pub async fn SqlServerConn::ping(Self) -> Bool
@moondb.AsyncDriver's ping, callable on the value directly.
pub async fn SqlServerConn::prepare(Self, String) -> &@moondb.AsyncStmt raise @moondb.DbError
@moondb.AsyncDriver's prepare, callable on the value directly.
pub async fn SqlServerConn::query(Self, String, Array[@moondb.Value]) -> Array[@moondb.Row] raise @moondb.DbError
@moondb.AsyncDriver's query, callable on the value directly.
pub async fn SqlServerConn::query_stream(Self, String, Array[@moondb.Value]) -> &@moondb.AsyncCursor raise @moondb.DbError
@moondb.AsyncDriver's query_stream, callable on the value directly.
pub async fn SqlServerConn::rollback(Self) -> Unit raise @moondb.DbError
@moondb.AsyncDriver's rollback, callable on the value directly.
pub async fn SqlServerStmt::bind(Self, Array[@moondb.Value]) -> Unit raise @moondb.DbError
@moondb.AsyncStmt's bind, callable on the value directly.
pub async fn SqlServerStmt::bind_named(Self, Map[String, @moondb.Value]) -> Unit raise @moondb.DbError
@moondb.AsyncStmt's bind_named, callable on the value directly.
pub async fn SqlServerStmt::close(Self) -> Unit
@moondb.AsyncStmt's close, callable on the value directly.
pub async fn SqlServerStmt::execute(Self) -> @moondb.ExecResult raise @moondb.DbError
@moondb.AsyncStmt's execute, callable on the value directly.
pub fn SqlServerStmt::new(SqlServerConn, String) -> Self
pub fn SqlServerStmt::params(Self) -> Array[String]
@moondb.AsyncStmt's params, callable on the value directly.
pub async fn SqlServerStmt::query(Self) -> Array[@moondb.Row] raise @moondb.DbError
@moondb.AsyncStmt's query, callable on the value directly.
pub async fn SqlServerStmt::query_stream(Self) -> &@moondb.AsyncCursor raise @moondb.DbError
@moondb.AsyncStmt's query_stream, callable on the value directly.
pub fn SqlServerStmt::sql(Self) -> String
@moondb.AsyncStmt's sql, callable on the value directly.
pub fn strict_tls() -> @tls.Client
The configuration a TDS 8.0 session needs: the same permissive trust as permissive_tls, plus the ALPN protocol tds/8.0. The server uses ALPN to tell which protocol version a TLS connection that started before any TDS byte is carrying; without it the handshake is refused, so a caller who supplies their own configuration for a strict connection has to name the protocol themselves.