moondb

    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.

    database
    db
    sql
    driver
    interface
    db-api
    dbapi
    pep249
    database-sql
    connection-pool
    pool
    transaction
    prepared-statement
    async
    sqlite
    postgres
    postgresql
    mysql
    mariadb
    moonbit
    Download zip
    Version
    0.2.7
    License
    Apache-2.0
    Last updated
    13 hours ago
    Downloads
    2K

    #moondb

    The standard database-access interface for MoonBit.

    The small, pure contract that sits between database drivers and query layers — MoonBit's answer to Go's database/sql/driver and Python's DB-API 2.0 (PEP 249).

    Check and Test tests API docs mooncakes license

    Moved on mooncakes from Lfan-ke/moondb to moonbitstack/moondb.

    moondb defines one thing: the boundary every SQL database backend implements and every ORM / query builder is written against. The interface package has zero dependencies, is pure (compiles on wasm, wasm-gc, js, and native alike), and ships a dependency-free reference driver so the whole stack above it can be tested without a database. The connection pools live in their own packages — moonbitstack/moondb/pool for the synchronous Driver, moonbitstack/moondb/apool for the async one — so a driver that only implements the interface carries none of the pool's dependencies, and a synchronous driver never pulls in an async runtime.

    It deliberately does not connect to a database, speak a wire protocol, or build SQL. Those belong on either side of the seam:

    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 --> MOCK

    The value of a contract package is that the two sides are written by different people at different times and still fit: a driver author implements Driver once, and every query layer works against their backend; an ORM author targets Driver once, and every driver works under their ORM.

    #Install

    moon add moonbitstack/moondb

    #The interface at a glance

    TypeRoleTransliterated from
    Valueone dialect-neutral cell — the unit crossing the boundary both waysGo driver.Value, DB-API type objects
    Rowone result row: aligned column names + Values, with typed accessorssql.Rows / DB-API row tuple
    ToValue, FromValuehow a MoonBit value binds as a parameter and reads back from a cell; Row::read reads any FromValueGo driver.Valuer / sql.Scanner, Diesel ToSql / FromSql
    ExecResultoutcome of a non-query: rows affected + last insert idGo sql.Result, DB-API rowcount/lastrowid
    DbErrorthe one error every operation raisesDB-API exception hierarchy (flattened)
    Driverthe trait a backend implements / a query layer targets (with a default ping health probe)Go driver.Conn+Execer+Queryer+Pinger
    @pool.Poola fixed-ceiling connection pool over any Driver, in moonbitstack/moondb/poolGo sql.DB pool, SQLAlchemy QueuePool
    @apool.AsyncPoolthe same pool for an AsyncDriver, where an exhausted acquire really waits, in moonbitstack/moondb/apooldatabase/sql waiting, SQLAlchemy QueuePool(timeout=…)
    AsyncDriverthe same contract for a backend reached over a socket, every method awaitedasyncpg / asyncio DB-API
    AsyncCursorforward-only cursor whose next is awaited, for a wire driver's live rows—
    MockDriverdependency-free in-memory reference driver, for tests—
    DialectSqlite / Postgres / Mysql, and the placeholder each spells—
    Capabilitieswhat a backend can do beyond the base contract—
    QueryExecutorthe part of a driver that runs SQL, without the part that owns the connectionGo Queryer context, SQLAlchemy Connection.exec_driver_sql
    Stmta statement prepared once and run with new values, plus bind_namedGo *sql.Stmt, JDBC PreparedStatement
    Txna closure-scoped transaction that nests through savepointsSQLAlchemy Session.begin_nested

    #The binding contract

    execute and query both take (sql, params). The SQL string carries positional placeholders and params supplies one Value per placeholder, in order. Values are bound out-of-band by the driver and are never spliced into the SQL text — that is what makes a moondb-based stack injection-safe all the way to the wire. All three drivers bind out of band: SQLite through sqlite3_bind_*, PostgreSQL through the extended-query protocol, MySQL through COM_STMT_EXECUTE.

    The placeholder token is the driver's dialect (? for SQLite/MySQL, $1, $2, … for PostgreSQL); moondb fixes the calling convention — an ordered Array[Value] — not the spelling. Dialect::placeholder spells one, and rewrite_named accepts :name and produces the right form for whichever backend is underneath.

    #Value cases

    Value is a closed sum every backend maps onto: Null, Bool, Int, Int64, Double, Text and Blob, plus Time (ISO-8601 text), Numeric (exact decimal text, no Double rounding), Uuid (16 raw bytes), Json (JSON text) and Array (elementwise, including nested arrays). Temporal values cross the boundary as normalized ISO-8601 text rather than a struct, so this interface package stays dependency-free; a caller parses them with whichever date-time library it likes. The dedicated accessors are row.time, row.numeric, row.uuid, row.json and row.array (each with a _by name variant).

    #Quickstart

    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")
    }

    Swap MockDriver for moonsqlite / moonpostgres / moonmysql and the same code runs against a real database — that substitutability is the point of the package.

    #Your own types

    ToValue and FromValue convert between a MoonBit value and a Value. They are implemented for the type behind each case (Bool, Int, Int64, Double, String, Bytes), for Value itself, and for T?, where None is NULL. Row::read and Row::read_by read a cell as any FromValue type, so a nullable column reads straight into an option:

    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))
    }

    A type of the program's own implements both traits to be stored as one of the cases. A refusal is a TypeError whose message reads after "column N is":

    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")
    }
    }

    #One statement, many runs

    Driver::execute prepares, binds, runs and discards in one call. A Stmt is the handle that survives instead: the server parses and plans once, and each run sends only the values. bind replaces them, execute and query run the statement, close releases the server-side resources.

    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)
    }

    All three drivers prepare on the server: SQLite compiles a sqlite3_stmt, PostgreSQL sends Parse under a cached name, MySQL sends COM_STMT_PREPARE. A driver that has nothing server-side to offer still hands out a working handle — the default keeps the text and the values and re-runs the ordinary path.

    #Named parameters

    :name reads better than a column of ?, and it survives a statement being reordered. rewrite_named turns one into the backend's own spelling and records which value each position takes:

    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)
    }

    The rewrite is a lexer rather than a search-and-replace: a :name inside a string literal, a quoted identifier, a -- / # / /* */ comment or a PostgreSQL $tag$ … $tag$ body is left alone, a :: cast and a := argument are not parameters, an array slice arr[1:3] is not one either, and mixing ? with :name is refused with the offset that broke it. A repeated name reuses one $n on PostgreSQL and repeats its value on the positional dialects, which is what each wire actually allows.

    #Nested transactions

    begin / commit / rollback are flat, but application code nests: a service opens a transaction, calls a repository method that opens one too, and the inner failure must not discard the outer work. Txn brackets a body and derives the savepoint name from the nesting depth, so the pairing cannot be forgotten and 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
    (try {
    tx.run(fn(_tx) raise @moondb.DbError {
    raise @moondb.QueryError("inner failed")
    })
    } catch {
    _ => ()
    })
    |> ignore
    })
    assert_eq(db.row_count(), 1)
    }

    AsyncTxn is the same for an AsyncDriver, with every round trip awaited.

    #Dialects and capabilities

    A query layer that has to emit SQL needs two things from the backend: the placeholder spelling, and what the backend can do beyond the base contract. Both come from the driver, so the layer is written once and runs everywhere instead of the caller naming the backend a second time at every call site.

    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)
    }

    Capabilities reports prepared statements, named parameters, server-side cursors, streaming COPY, savepoints, multiple result sets, unsigned 64-bit integers and RETURNING — what the driver in this repository implements, not what the server software could do in principle.

    #The narrow view

    A repository method, a query builder, a migration step all want to run SQL; none of them should be able to close the connection out from under the caller that lent it to them. Executor::new(db) is that narrower handle, and a function that takes &QueryExecutor accepts it:

    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)
    }

    #Implementing a driver

    A backend implements the Driver trait: the four data operations, the three transaction brackets, close, and the two introspection methods dialect and capabilities. query_stream, ping and prepare have defaults, so a minimal driver is short. DbError is pub(all), so a driver in its own package can construct and raise every case:

    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.

    execute/query/begin/commit/rollback raise DbError on failure; close is best-effort and idempotent.

    #Design notes

    • Why raise, not Result. Typed accessors and driver calls raise DbError rather than returning Result[_, DbError], so a decode bug or a dropped connection surfaces at the call site instead of being silently swallowed. A caller opts into recovery with try/catch.
    • Typed accessors are strict. row.int(i) raises TypeError if the cell is not an integer — including when it is NULL. Read a nullable column as T? with read, or guard it with is_null first. Integer→integer and integer→double conversions are allowed and lossless; int narrows an Int64 and says so.
    • DbError is pub(all). A plain pub suberror can be caught from another package but not constructed — which would stop out-of-tree drivers from raising it. pub(all) opens the constructors.
    • DbError carries the server's own classification. Besides ConnectError / QueryError / TypeError / Closed, a Server case carries a ServerDetail: the five-character SQLSTATE, the vendor error number, and the server's message, detail and hint. DbError::kind() maps any failure onto a coarse ErrorKind (Integrity, Programming, Operational, Data, Internal, NotSupported, …) and sqlstate() / code() expose the raw fields, so a caller can branch on the class instead of parsing a message.
    • There are two driver traits, on purpose. Driver is synchronous, which is right for a backend whose calls block in C (moonsqlite steps a prepared statement and returns). A backend reached over TCP cannot be written that way: MoonBit's only socket stack is async-only, and an async fn cannot be called from a synchronous one. A wire driver forced to conform to Driver can do nothing but raise from every method — and because ping is a defaulted method built on query, it swallows that raise into false, so a Pool with pre_ping judges every one of its connections permanently unhealthy. AsyncDriver is the seam for those backends: the same eight operations, each awaited. It is a peer of Driver, not a replacement — a synchronous backend keeps implementing Driver and never becomes async. Declaring async methods pulls in no async runtime, so moondb stays dependency-free and still compiles on every backend; only a driver that implements the trait, and a caller that runs it in an event loop, need one.

    • The pool is synchronous, and its counting is moonpool's. @pool.Pool[D] reuses idle connections under a size ceiling, evicts a connection that fails its pre_ping probe or outlives max_lifetime (age measured by an injected clock, the way database/sql swaps nowFunc in tests), and offers a non-blocking try_acquire. Because moondb's base contract is sync and the pure backends have no threads, an exhausted acquire fails immediately rather than blocking — the acquire_timeout is the budget an async driver layers real waiting on top of. ping is a default Driver method (SELECT 1), so every driver gets a health probe for free. The bookkeeping underneath — how many are live, which are idle, when each was born — is moonpool's Pool, and so is the optional ward in front of the database: a backoff after failed connects (acquire then raises ConnectError until the delay has passed), a token bucket spacing reconnections, a breaker that stops connecting to a failing backend, a shedder. This package adds only opening, closing and probing. It is a separate package because that dependency is the pool's alone: import { "moonbitstack/moondb/pool" } where a pool is wanted, and the interface stays dependency-free where it is not.

    • The async pool is where an exhausted acquire actually waits. @apool.AsyncPool[D] is the counterpart for an AsyncDriver, and it exists because the synchronous pool cannot do the one thing a full pool is supposed to do. In @pool.Pool nothing can return a connection while a caller sits inside acquire, so a full pool fails immediately and acquire_timeout is only a number in the message. In an event loop, other tasks keep running while one waits — so AsyncPool puts the wait on a Semaphore with one permit per allowed checkout (max_size + overflow) and parks the caller there. release returns the permit and wakes exactly one waiter; acquire_timeout becomes a real deadline. The bookkeeping is still moonpool's, and overflow, pre_ping, max_lifetime and the reset hook mean what they mean in the sync pool — a returned connection is reset before it is reusable, and a connection that came from the overflow allowance is closed rather than idled so the pool settles back to max_size. It lives in its own package because it is the one part of moondb that imports moonbitlang/async: no synchronous driver reaches it, and the interface package itself still imports nothing. (The module declares the runtime so that package can resolve; a consumer that only wants the interface pays for a download, not for an async dependency in its own build.)

    • The PostgreSQL protocol state machine is testable without a machine. PgConn speaks over a PgWire, which is either a socket (plain or TLS-wrapped) or one end of an in-memory pipe pair. That seam is what lets the parts that used to be unreachable — statement sequencing, the streaming guard that refuses a second statement while a PgRowStream is open, draining the rest of a result after an ErrorResponse, the COPY sub-protocol, notifications, the statement cache — run against a scripted peer in a unit test, byte for byte, with no server and no network. The live integration suites still exist and still run against real TLS-enabled servers; the offline tests are what catch a regression on a laptop with no database.

    #What each driver supports

    SQLitePostgreSQLMySQL
    Prepared Stmtyesyes, with a statement cacheyes
    Named parametersyesyesyes
    Server-side cursornoyes, DECLARE/FETCH in batchesno
    Streaming COPYnoyes, both directionsno
    Savepointsyesyesyes
    Multiple result setsnoyesyes
    Unsigned 64-bitnonoyes
    RETURNINGyesyesno

    A backend outside this repository reports its own through Driver::dialect and Driver::capabilities; a capability reported false is a plan the query layer lowers, not an error it discovers mid-statement.

    #Tests

    The suite covers the value model, every typed accessor and its error path, the error type, the reference driver — including real transaction rollback/commit semantics and savepoint scope — both connection pools, the :name lexer (literals, identifiers, comments, dollar quotes, casts, JSONB operators, mixed conventions), the statement handles, and the PostgreSQL protocol state machine driven off-machine over an in-memory wire: statement sequencing, the streaming guard, the post-error drain, the prepared handle, the server-side cursor and the COPY sub-protocol. Key behaviours are mutation-verified: breaking rollback turns the transaction tests red; breaking the savepoint rollback scope turns the nested-transaction tests red; breaking the dollar-quote lexing turns the named-parameter tests red; disabling the pool's pre_ping eviction or its max_lifetime retirement turns the corresponding pool tests red; and dropping the PgRowStream guard, the post-error drain or the statement cache turns the offline protocol tests red. They run on all four backends:

    moon test --target all

    #License

    Apache-2.0 © Leo Cheng

    AsyncCursor

    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.

    AsyncDriver

    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.

    AsyncQueryExecutor

    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.

    AsyncStmt

    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.

    Cursor

    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, so a large result set is consumed in bounded memory (SQLAlchemy's yield_per / server-side cursors, Go's sql.Rows, 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.

    Driver

    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. It is the MoonBit transliteration of Go's database/sql/driver.Conn/Execer/Queryer and Python's DB-API 2.0 Connection/Cursor, 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, matching Go's io.Closer discipline for connections.

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

    FromValue

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

    A MoonBit value read back from a cell: Go's sql.Scanner, Diesel's FromSql. 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".
    impl FromValue for Bool
    impl FromValue for Int
    impl FromValue for Int64
    impl FromValue for Double
    impl FromValue for String
    impl FromValue for Option[T]
    impl FromValue for Bytes

    QueryExecutor

    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 for the placeholder spelling and 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)
    }

    Stmt

    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 replaces the values, execute and query run the statement, and 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)
    }

    ToValue

    pub(open) trait ToValue {
    fn to_value(Self) -> Value
    }

    A MoonBit value that binds as a parameter: Go's driver.Valuer, Diesel's ToSql. 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.
    impl ToValue for Bool
    impl ToValue for Int
    impl ToValue for Int64
    impl ToValue for Double
    impl ToValue for String
    impl ToValue for Option[T]
    impl ToValue for Bytes
    impl ToValue for Array[Value]

    DbError

    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. The cases mirror the coarse failure classes both Go's database/sql and Python's DB-API 2.0 draw a line around, kept deliberately few so drivers can classify without a taxonomy:

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

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

    DbError::code

    fn DbError::code(self : DbError) -> Int?

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

    DbError::kind

    fn DbError::kind(self : DbError) -> 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.

    DbError::output

    fn DbError::output(self : DbError, logger : &Logger) -> Unit

    DbError::sqlstate

    fn DbError::sqlstate(self : DbError) -> String?

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

    DbError::to_string

    fn DbError::to_string(self : DbError) -> String

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

    ArrayCursor

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

    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.

    ArrayCursor::close

    fn ArrayCursor::close(self : ArrayCursor) -> Unit

    ArrayCursor::new

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

    A cursor positioned before the first of rows.

    ArrayCursor::next

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

    AsyncArrayCursor

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

    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.

    AsyncArrayCursor::close

    async fn AsyncArrayCursor::close(self : AsyncArrayCursor) -> Unit

    AsyncArrayCursor::new

    A cursor positioned before the first of rows.

    AsyncArrayCursor::next

    async fn AsyncArrayCursor::next(self : AsyncArrayCursor) -> Row? raise DbError

    AsyncExecutor

    pub struct AsyncExecutor[T] {
    inner : T
    }

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

    AsyncExecutor::capabilities

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

    What the driver underneath can do beyond the base contract.

    AsyncExecutor::dialect

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

    The SQL dialect of the driver underneath.

    AsyncExecutor::driver

    fn[T] AsyncExecutor::driver(self : AsyncExecutor[T]) -> T

    The driver underneath.

    AsyncExecutor::execute

    async fn[T : AsyncDriver] AsyncExecutor::execute(self : AsyncExecutor[T], sql : String, params : Array[Value]) -> ExecResult raise DbError

    Run a non-row statement.

    AsyncExecutor::new

    fn[T] AsyncExecutor::new(inner : T) -> AsyncExecutor[T]

    The narrow view over inner.

    AsyncExecutor::query

    async fn[T : AsyncDriver] AsyncExecutor::query(self : AsyncExecutor[T], sql : String, params : Array[Value]) -> Array[Row] raise DbError

    Run a row-returning statement and materialise every row.

    AsyncExecutor::query_stream

    async fn[T : AsyncDriver] AsyncExecutor::query_stream(self : AsyncExecutor[T], sql : String, params : Array[Value]) -> &AsyncCursor raise DbError

    Stream a row-returning statement's rows.

    AsyncTxn

    pub struct AsyncTxn[T] {
    driver : T
    depth : Int
    }

    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
    })

    AsyncTxn::depth

    fn[T] AsyncTxn::depth(self : AsyncTxn[T]) -> Int

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

    AsyncTxn::driver

    fn[T] AsyncTxn::driver(self : AsyncTxn[T]) -> T

    The connection this handle opens transactions on.

    AsyncTxn::new

    fn[T] AsyncTxn::new(driver : T) -> AsyncTxn[T]

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

    AsyncTxn::run

    async fn[T : AsyncDriver] AsyncTxn::run(self : AsyncTxn[T], body : async (AsyncTxn[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.

    BufferedAsyncStmt

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

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

    BufferedAsyncStmt::new

    fn[T] BufferedAsyncStmt::new(driver : T, sql : String) -> BufferedAsyncStmt[T]

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

    BufferedStmt

    pub struct BufferedStmt[T] {
    driver : T
    text : String
    params : Array[Value]
    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.

    BufferedStmt::new

    fn[T] BufferedStmt::new(driver : T, sql : String) -> BufferedStmt[T]

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

    Capabilities

    pub(all) struct Capabilities {
    dialect : Dialect
    prepared_statements : Bool
    named_parameters : Bool
    server_cursors : Bool
    streaming_copy : Bool
    savepoints : Bool
    multiple_result_sets : Bool
    unsigned_integers : Bool
    returning : Bool
    } derive(Eq)

    What a backend can do beyond the four operations of the Driver contract.

    The base contract is deliberately the intersection every relational backend offers. Everything else — a prepared handle, a server-side cursor, COPY, more than one result set from one statement, an unsigned 64-bit column — is a capability some backends have and others do not. A query layer asks once, through QueryExecutor::capabilities, and lowers its plan instead of probing by running a statement and interpreting the failure.

    The presets below describe what the drivers in this repository implement, not what the server software could theoretically do: streaming_copy is true only where a driver actually exposes a streaming source and sink.

    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)
    }

    Capabilities::count

    fn Capabilities::count(self : Capabilities) -> Int

    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.

    Capabilities::equal

    Capabilities::for_dialect

    fn Capabilities::for_dialect(dialect : Dialect) -> Capabilities

    The capabilities of the driver moondb ships for dialect.

    Capabilities::is_for

    fn Capabilities::is_for(self : Capabilities, dialect : Dialect) -> Bool

    Whether this report describes dialect.

    Capabilities::not_equal

    fn Capabilities::not_equal(x : Capabilities, y : Capabilities) -> Bool

    Dialect

    pub(all) enum Dialect {
    Sqlite
    Postgres
    Mysql
    } derive(Eq,
    Debug
    )

    The three SQL dialects moondb's ratified drivers speak.

    A query layer that has to emit SQL — a builder, a migration runner, an ORM that paginates or upserts — needs two things from the backend it is talking to: the spelling of a bound parameter, and what the backend can do beyond the plain Driver contract. Reporting the dialect through the driver itself means the layer is written once and runs on all three, instead of the caller naming the backend a second time at every call site.

    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), "?")
    }

    Dialect::equal

    fn Dialect::equal(Dialect, Dialect) -> Bool

    Dialect::name

    fn Dialect::name(self : Dialect) -> String

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

    Dialect::not_equal

    fn Dialect::not_equal(x : Dialect, y : Dialect) -> Bool

    Dialect::parse

    fn Dialect::parse(name : String) -> Dialect?

    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.

    Dialect::placeholder

    fn Dialect::placeholder(self : Dialect, n : Int) -> 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.

    Dialect::to_repr

    ErrorKind

    pub(all) enum ErrorKind {
    Interface
    Database
    Data
    Operational
    Integrity
    Programming
    Internal
    NotSupported
    } derive(Eq,
    Debug
    )

    A coarse failure class, so a caller can branch on what kind of failure happened without parsing a message or knowing which backend produced it. The classes follow Python DB-API 2.0's exception tree (PEP 249), collapsed to the ones a driver can derive from a SQLSTATE or a vendor error code.

    • Interface — the client API was used wrongly: a closed handle, a row index out of range, a value read as the wrong type.
    • Database — a generic server-side failure the finer classes do not cover.
    • Data — a value is out of range, badly formed, or cannot be cast.
    • Operational — the connection, a lock, a transaction's state, or a resource limit: the class a retry or a reconnect may clear.
    • Integrity — a constraint (unique, foreign key, not-null, check) was violated.
    • Programming — the SQL or its references are wrong: syntax, unknown table or column, insufficient privilege.
    • Internal — the backend reports its own internal inconsistency.
    • NotSupported — the backend or driver does not implement the feature.

    ErrorKind::equal

    fn ErrorKind::equal(ErrorKind, ErrorKind) -> Bool

    ErrorKind::not_equal

    fn ErrorKind::not_equal(x : ErrorKind, y : ErrorKind) -> Bool

    ErrorKind::to_string

    fn ErrorKind::to_string(self : ErrorKind) -> String

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

    ExecResult

    pub(all) struct ExecResult {
    rows_affected : Int64
    last_insert_id : Int64
    } derive(Eq)

    The outcome of a non-query statement (INSERT/UPDATE/DELETE/DDL): how many rows it changed and the id of the last inserted row.

    rows_affected is 64-bit to hold bulk DML counts; last_insert_id is the backend's auto-increment / rowid value and is backend-defined (0 when the statement produced none). This mirrors Go's sql.Result (RowsAffected / LastInsertId) and DB-API's cursor.rowcount / lastrowid.

    ExecResult::equal

    fn ExecResult::equal(ExecResult, ExecResult) -> Bool

    ExecResult::not_equal

    fn ExecResult::not_equal(x : ExecResult, y : ExecResult) -> Bool

    Executor

    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.

    Executor::capabilities

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

    What the driver underneath can do beyond the base contract.

    Executor::dialect

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

    The SQL dialect of the driver underneath.

    Executor::driver

    fn[T] Executor::driver(self : Executor[T]) -> T

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

    Executor::execute

    fn[T : Driver] Executor::execute(self : Executor[T], sql : String, params : 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.

    Executor::new

    fn[T] Executor::new(inner : T) -> Executor[T]

    The narrow view over inner.

    Executor::query

    fn[T : Driver] Executor::query(self : Executor[T], sql : String, params : Array[Value]) -> Array[Row] raise DbError

    Run a row-returning statement and materialise every row.

    Executor::query_stream

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

    Stream a row-returning statement's rows.

    MockDriver

    pub struct MockDriver {
    rows : Array[Row]
    next_id : Int64
    savepoints : Array[Int]
    named_savepoints : Array[(String, Int)]
    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.

    MockDriver::begin

    fn MockDriver::begin(self : MockDriver) -> Unit raise DbError

    MockDriver::capabilities

    fn MockDriver::capabilities(self : MockDriver) -> Capabilities

    MockDriver::close

    fn MockDriver::close(self : MockDriver) -> Unit

    MockDriver::commit

    fn MockDriver::commit(self : MockDriver) -> Unit raise DbError

    MockDriver::dialect

    fn MockDriver::dialect(_self : MockDriver) -> Dialect

    MockDriver::execute

    fn MockDriver::execute(self : MockDriver, sql : String, params : Array[Value]) -> ExecResult raise DbError

    MockDriver::in_transaction

    fn MockDriver::in_transaction(self : MockDriver) -> Bool

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

    MockDriver::is_closed

    fn MockDriver::is_closed(self : MockDriver) -> Bool

    Whether the connection has been closed.

    MockDriver::new

    fn MockDriver::new() -> MockDriver

    A fresh, empty mock connection.

    MockDriver::ping

    fn MockDriver::ping(self : MockDriver) -> Bool

    MockDriver::prepare

    fn MockDriver::prepare(self : MockDriver, sql : String) -> &Stmt raise DbError

    MockDriver::query

    fn MockDriver::query(self : MockDriver, _sql : String, _params : Array[Value]) -> Array[Row] raise DbError

    MockDriver::query_stream

    fn MockDriver::query_stream(self : MockDriver, sql : String, params : Array[Value]) -> &Cursor raise DbError

    MockDriver::rollback

    fn MockDriver::rollback(self : MockDriver) -> Unit raise DbError

    MockDriver::row_count

    fn MockDriver::row_count(self : MockDriver) -> Int

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

    Named

    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 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 = ?")
    }

    Named::arity

    fn Named::arity(self : Named) -> Int

    How many values the statement binds.

    Named::bind

    fn Named::bind(self : Named, args : 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.

    Named::is_named

    fn Named::is_named(self : Named) -> Bool

    Whether the statement takes named parameters at all.

    Named::names

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

    The distinct parameter names, in first-use order.

    Row

    pub(all) struct Row {
    columns : Array[String]
    values : Array[Value]
    } derive(Eq,
    Debug
    )

    One row of a result set: the column names in projection order alongside the decoded [Value] for each. columns and values are the same length and are index-aligned — columns[i] names values[i].

    This is the shape drivers hand back from [Driver::query] and the shape a query layer decodes. Read cells either positionally ([get]/[int]/…) or by column name ([by_name]/[int_by]/…). The typed accessors raise on a type or lookup mismatch rather than returning a sentinel, so a decode bug surfaces as a DbError at the call site instead of silently reading a zero value.

    Row::array

    fn Row::array(self : Row, idx : Int) -> Array[Value] raise DbError

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

    Row::array_by

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

    Read the column named name as an SQL array.

    Row::blob

    fn Row::blob(self : Row, idx : Int) -> Bytes raise DbError

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

    Row::blob_by

    fn Row::blob_by(self : Row, name : String) -> Bytes raise DbError

    Read the column named name as an opaque blob.

    Row::bool

    fn Row::bool(self : Row, idx : Int) -> Bool raise DbError

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

    Row::bool_by

    fn Row::bool_by(self : Row, name : String) -> Bool raise DbError

    Read the column named name as a Bool.

    Row::by_name

    fn Row::by_name(self : Row, name : 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).

    Row::double

    fn Row::double(self : Row, idx : Int) -> Double raise DbError

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

    Row::double_by

    fn Row::double_by(self : Row, name : String) -> Double raise DbError

    Read the column named name as a Double.

    Row::equal

    fn Row::equal(Row, Row) -> Bool

    Row::get

    fn Row::get(self : Row, idx : Int) -> Value raise DbError

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

    Row::index_of

    fn Row::index_of(self : Row, name : String) -> Int 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.

    Row::int

    fn Row::int(self : Row, idx : Int) -> Int 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.

    Row::int64

    fn Row::int64(self : Row, idx : Int) -> Int64 raise DbError

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

    Row::int64_by

    fn Row::int64_by(self : Row, name : String) -> Int64 raise DbError

    Read the column named name as an Int64.

    Row::int_by

    fn Row::int_by(self : Row, name : String) -> Int raise DbError

    Read the column named name as an Int.

    Row::is_null

    fn Row::is_null(self : Row, idx : Int) -> 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.

    Row::is_null_by

    fn Row::is_null_by(self : Row, name : String) -> Bool raise DbError

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

    Row::json

    fn Row::json(self : Row, idx : Int) -> String raise DbError

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

    Row::json_by

    fn Row::json_by(self : Row, name : String) -> String raise DbError

    Read the column named name as JSON text.

    Row::not_equal

    fn Row::not_equal(x : Row, y : Row) -> Bool

    Row::numeric

    fn Row::numeric(self : Row, idx : Int) -> String raise DbError

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

    Row::numeric_by

    fn Row::numeric_by(self : Row, name : String) -> String raise DbError

    Read the column named name as an exact decimal string.

    Row::read

    fn[T : FromValue] Row::read(self : Row, idx : Int) -> 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.

    Row::read_by

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

    Read the column named name as any [FromValue] type.

    Row::text

    fn Row::text(self : Row, idx : Int) -> String raise DbError

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

    Row::text_by

    fn Row::text_by(self : Row, name : String) -> String raise DbError

    Read the column named name as text.

    Row::time

    fn Row::time(self : Row, idx : Int) -> String raise DbError

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

    Row::time_by

    fn Row::time_by(self : Row, name : String) -> String raise DbError

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

    Row::to_repr

    Row::uuid

    fn Row::uuid(self : Row, idx : Int) -> Bytes raise DbError

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

    Row::uuid_by

    fn Row::uuid_by(self : Row, name : String) -> Bytes raise DbError

    Read the column named name as UUID bytes.

    Row::width

    fn Row::width(self : Row) -> Int

    The number of columns in the row.

    ServerDetail

    pub(all) struct ServerDetail {
    kind : ErrorKind
    sqlstate : String?
    code : Int?
    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).

    Txn

    pub struct Txn[T] {
    driver : T
    depth : Int
    }

    A transaction that nests: the outermost 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)
    }

    Txn::depth

    fn[T] Txn::depth(self : Txn[T]) -> Int

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

    Txn::driver

    fn[T] Txn::driver(self : Txn[T]) -> T

    The connection this handle opens transactions on.

    Txn::is_open

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

    Whether a body is currently open.

    Txn::new

    fn[T] Txn::new(driver : T) -> Txn[T]

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

    Txn::run

    fn[T : Driver] Txn::run(self : Txn[T], body : (Txn[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.

    Value

    pub(all) enum Value {
    Null
    Bool(Bool)
    Int(Int)
    Int64(Int64)
    Double(Double)
    Text(String)
    Blob(Bytes)
    Time(String)
    Numeric(String)
    Uuid(Bytes)
    Json(String)
    Array(Array[Value])
    } derive(Eq,
    Debug
    )

    A single dialect-neutral database value — the unit of everything that crosses the driver boundary in either direction. Bound parameters travel in as Values (never spliced into the SQL string, so binding is injection-safe by construction) and result columns come back as Values.

    This is the moondb analogue of Go's driver.Value and Python DB-API's input type objects: a small, closed sum every backend can map onto its own wire types. Drivers normalise their column types down to these cases; a query layer built on moondb reads them back through [Row]'s typed accessors.

    • Null — SQL NULL / a missing value.
    • Bool — a boolean; backends without a native boolean map 0/1.
    • Int — a 32-bit signed integer.
    • Int64 — a 64-bit signed integer (rowids, BIGINT, counters).
    • Double — an IEEE-754 double (REAL / FLOAT8).
    • Text — a UTF-8 string (TEXT / VARCHAR).
    • Blob — an opaque byte string (BLOB / BYTEA).
    • Time — a temporal value as normalized ISO-8601 text (DATE, TIME, TIMESTAMP, TIMESTAMPTZ). It stays text so this interface package keeps zero dependencies; a caller parses it with a date-time library of its choice.
    • Numeric — an exact decimal (NUMERIC / DECIMAL) as its canonical digit text, so no precision is lost the way it would through Double.
    • Uuid — the 16 raw bytes of a UUID (UUID), kept binary rather than text.
    • Json — the JSON text of a JSON / JSONB column, unparsed.
    • Array — a one- or multi-dimensional SQL array (INTEGER[], TEXT[], …), elementwise.
    impl Show for Value
    impl FromValue for Value
    impl ToValue for Value

    Value::equal

    fn Value::equal(Value, Value) -> Bool

    Value::from_value

    fn Value::from_value(v : Value) -> Value raise DbError

    Value::is_null

    fn Value::is_null(self : Value) -> Bool

    Whether this value is SQL NULL.

    Value::json_text

    fn Value::json_text(self : Value) -> 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.

    Value::kind

    fn Value::kind(self : Value) -> 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.

    Value::not_equal

    fn Value::not_equal(x : Value, y : Value) -> Bool

    Value::output

    fn Value::output(self : Value, logger : &Logger) -> Unit

    Value::to_repr

    Value::to_string

    fn Value::to_string(self : Value) -> String

    Value::to_value

    fn Value::to_value(self : Value) -> Value

    classify_sqlstate

    fn classify_sqlstate(state : 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.

    drain

    fn drain(cursor : &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).

    drain_async

    async fn drain_async(cursor : &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.

    rewrite_named

    fn rewrite_named(sql : String, dialect : 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.