The standard database-access interface for MoonBit — the driver↔query-layer contract, transliterated from Go's database/sql/driver and Python's DB-API 2.0. ORMs (moonorm) build on it; drivers (moonsqlite / moonpostgres / moonmysql) implement it.
Moved on mooncakes from Lfan-ke/moondb to moonbitstack/moondb.
flowchart TD
subgraph layers["query layers — build on moondb"]
ORM["moonorm<br/><small>SQLAlchemy-style ORM / query builder</small>"]
APP["your app / repository code"]
end
IFACE(["<b>@moondb</b><br/><small>Value · Row · ExecResult · DbError · Driver</small>"])
subgraph drivers["drivers — implement moondb"]
SQLITE["moonsqlite<br/><small>C-FFI, native</small>"]
PG["moonpostgres<br/><small>pure-MoonBit wire</small>"]
MYSQL["moonmysql<br/><small>pure-MoonBit wire</small>"]
MOCK["MockDriver<br/><small>in-memory, ships here</small>"]
end
APP --> ORM --> IFACE
IFACE --> SQLITE
IFACE --> PG
IFACE --> MYSQL
IFACE --> MOCKmoon add moonbitstack/moondb| Type | Role | Transliterated from |
|---|---|---|
| Value | one dialect-neutral cell — the unit crossing the boundary both ways | Go driver.Value, DB-API type objects |
| Row | one result row: aligned column names + Values, with typed accessors | sql.Rows / DB-API row tuple |
| ToValue, FromValue | how a MoonBit value binds as a parameter and reads back from a cell; Row::read reads any FromValue | Go driver.Valuer / sql.Scanner, Diesel ToSql / FromSql |
| ExecResult | outcome of a non-query: rows affected + last insert id | Go sql.Result, DB-API rowcount/lastrowid |
| DbError | the one error every operation raises | DB-API exception hierarchy (flattened) |
| Driver | the trait a backend implements / a query layer targets (with a default ping health probe) | Go driver.Conn+Execer+Queryer+Pinger |
| @pool.Pool | a fixed-ceiling connection pool over any Driver, in moonbitstack/moondb/pool | Go sql.DB pool, SQLAlchemy QueuePool |
| @apool.AsyncPool | the same pool for an AsyncDriver, where an exhausted acquire really waits, in moonbitstack/moondb/apool | database/sql waiting, SQLAlchemy QueuePool(timeout=…) |
| AsyncDriver | the same contract for a backend reached over a socket, every method awaited | asyncpg / asyncio DB-API |
| AsyncCursor | forward-only cursor whose next is awaited, for a wire driver's live rows | — |
| MockDriver | dependency-free in-memory reference driver, for tests | — |
| Dialect | Sqlite / Postgres / Mysql, and the placeholder each spells | — |
| Capabilities | what a backend can do beyond the base contract | — |
| QueryExecutor | the part of a driver that runs SQL, without the part that owns the connection | Go Queryer context, SQLAlchemy Connection.exec_driver_sql |
| Stmt | a statement prepared once and run with new values, plus bind_named | Go *sql.Stmt, JDBC PreparedStatement |
| Txn | a closure-scoped transaction that nests through savepoints | SQLAlchemy Session.begin_nested |
test "round-trip through the interface" {
// A query layer is written against the `Driver` trait, not a concrete backend.
let db = @moondb.MockDriver::new()
// Bind values as parameters — never string-interpolated into the SQL.
db.execute("INSERT INTO hero (id, name) VALUES (?, ?)", [Int(1), Text("Nova")])
|> ignore
db.begin()
db.execute("INSERT INTO hero (id, name) VALUES (?, ?)", [Int(2), Text("Iris")])
|> ignore
db.rollback() // Iris is undone; Nova remains.
let rows = db.query("SELECT id, name FROM hero", [])
assert_eq(rows.length(), 1)
assert_eq(rows[0].int_by("c0"), 1)
assert_eq(rows[0].text_by("c1"), "Nova")
}test "read a nullable column as an option" {
let row : @moondb.Row = { columns: ["id", "age"], values: [Int(1), Null] }
let id : Int64 = row.read(0)
let age : Int? = row.read_by("age")
assert_eq((id, age), (1L, None))
}struct Email(String)
impl @moondb.ToValue for Email with fn to_value(self) {
let Email(s) = self
Text(s)
}
impl @moondb.FromValue for Email with fn from_value(v) {
match v {
Text(s) if s.contains("@") => Email(s)
other => raise TypeError(other.kind() + ", not Email")
}
}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)
}test "the same named statement for every backend" {
let sql = "SELECT * FROM hero WHERE id = :id AND realm = :realm"
let pg = @moondb.rewrite_named(sql, @moondb.Dialect::Postgres)
assert_eq(pg.sql, "SELECT * FROM hero WHERE id = $1 AND realm = $2")
let lite = @moondb.rewrite_named(sql, @moondb.Dialect::Sqlite)
assert_eq(lite.sql, "SELECT * FROM hero WHERE id = ? AND realm = ?")
let args : Map[String, @moondb.Value] = Map([])
args.set("id", @moondb.Value::Int(7))
args.set("realm", @moondb.Value::Text("north"))
assert_eq(lite.bind(args).length(), 2)
}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
(try {
tx.run(fn(_tx) raise @moondb.DbError {
raise @moondb.QueryError("inner failed")
})
} catch {
_ => ()
})
|> ignore
})
assert_eq(db.row_count(), 1)
}test "ask the driver, not the caller" {
let db = @moondb.MockDriver::new()
assert_eq(db.dialect(), @moondb.Dialect::Sqlite)
assert_eq(db.dialect().placeholder(1), "?")
let caps = db.capabilities()
assert_true(caps.savepoints)
assert_false(caps.streaming_copy)
}test "a repository only needs to run SQL" {
let db = @moondb.MockDriver::new()
fn count(db : &@moondb.QueryExecutor) -> Int raise @moondb.DbError {
db.query("SELECT 1", []).length()
}
assert_eq(count(@moondb.Executor::new(db)), 0)
}pub impl @moondb.Driver for MyConn with fn dialect(_self) { @moondb.Dialect::Sqlite }
pub impl @moondb.Driver for MyConn with fn execute(self, sql, params) {
// ... bind `params` positionally, run `sql`, then:
{ rows_affected: n, last_insert_id: id }
}
// query / begin / commit / rollback / close likewise.| SQLite | PostgreSQL | MySQL | |
|---|---|---|---|
| Prepared Stmt | yes | yes, with a statement cache | yes |
| Named parameters | yes | yes | yes |
| Server-side cursor | no | yes, DECLARE/FETCH in batches | no |
| Streaming COPY | no | yes, both directions | no |
| Savepoints | yes | yes | yes |
| Multiple result sets | no | yes | yes |
| Unsigned 64-bit | no | no | yes |
| RETURNING | yes | yes | no |
moon test --target allpub(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 = _
}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
}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 = _
}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 = _
}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(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 = _
}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(all) suberror DbError {
ConnectError(String)
QueryError(String)
TypeError(String)
Closed
Server(ServerDetail)
}impl Cursor for ArrayCursorimpl AsyncCursor for AsyncArrayCursorpub struct AsyncExecutor[T] {
inner : T
}impl AsyncQueryExecutor for AsyncExecutor[T]async fn[T : AsyncDriver] execute(self : AsyncExecutor[T], sql : String, params : Array[Value]) -> ExecResult raise DbErrorasync fn[T : AsyncDriver] query(self : AsyncExecutor[T], sql : String, params : Array[Value]) -> Array[Row] raise DbErrorasync fn[T : AsyncDriver] query_stream(self : AsyncExecutor[T], sql : String, params : Array[Value]) -> &AsyncCursor raise DbErrorasync fn[T : AsyncDriver] AsyncExecutor::execute(self : AsyncExecutor[T], sql : String, params : Array[Value]) -> ExecResult raise DbErrorasync fn[T : AsyncDriver] AsyncExecutor::query(self : AsyncExecutor[T], sql : String, params : Array[Value]) -> Array[Row] raise DbErrorasync fn[T : AsyncDriver] AsyncExecutor::query_stream(self : AsyncExecutor[T], sql : String, params : Array[Value]) -> &AsyncCursor raise DbErrorpub struct AsyncTxn[T] {
driver : T
depth : Int
}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
})test "a preset names its own dialect" {
let pg = @moondb.Capabilities::for_dialect(@moondb.Dialect::Postgres)
assert_eq(pg.dialect.name(), "postgres")
assert_true(pg.streaming_copy)
assert_false(pg.unsigned_integers)
}test "a dialect spells its own placeholder" {
assert_eq(@moondb.Dialect::Postgres.placeholder(1), "$1")
assert_eq(@moondb.Dialect::Postgres.placeholder(7), "$7")
assert_eq(@moondb.Dialect::Sqlite.placeholder(1), "?")
assert_eq(@moondb.Dialect::Mysql.placeholder(3), "?")
}pub struct Executor[T] {
inner : T
}impl QueryExecutor for Executor[T]impl Driver for MockDriverfn MockDriver::execute(self : MockDriver, sql : String, params : Array[Value]) -> ExecResult raise DbErrorfn MockDriver::query(self : MockDriver, _sql : String, _params : Array[Value]) -> Array[Row] raise DbErrorfn MockDriver::query_stream(self : MockDriver, sql : String, params : Array[Value]) -> &Cursor raise DbErrortest "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(all) struct ServerDetail {
kind : ErrorKind
sqlstate : String?
code : Int?
message : String
detail : String?
hint : String?
severity : String?
}pub struct Txn[T] {
driver : T
depth : Int
}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)
}Install
Download zipThe standard database-access interface for MoonBit — the driver↔query-layer contract, transliterated from Go's database/sql/driver and Python's DB-API 2.0. ORMs (moonorm) build on it; drivers (moonsqlite / moonpostgres / moonmysql) implement it.