moondb

Guide

Ratify the contract

Implementing Driver is how a backend joins the moondb family. You write five operations; the other two are given to you.

§1

Install

moon add moonbitstack/moondb

moondb is a library package. It defines the interface; it opens no connections itself. A driver package depends on it and implements Driver.

§2

Implement the trait

A driver is a struct that owns the connection handle, plus an impl Driver block. You must supply execute, query, begin, commit, rollback, and close.

pub struct MyConn {
  dsn : String
  mut closed : Bool
}

pub fn open(dsn : String) -> MyConn {
  { dsn, closed: false }
}

impl @moondb.Driver for MyConn with fn execute(
  self,
  sql,
  params,
) -> @moondb.ExecResult raise @moondb.DbError {
  ignore(params)
  if self.closed {
    raise @moondb.Closed
  }
  raise @moondb.QueryError("execute is not wired to a backend yet: " + sql)
}

impl @moondb.Driver for MyConn with fn query(
  self,
  sql,
  params,
) -> Array[@moondb.Row] raise @moondb.DbError {
  ignore(params)
  if self.closed {
    raise @moondb.Closed
  }
  raise @moondb.QueryError("query is not wired to a backend yet: " + sql)
}

impl @moondb.Driver for MyConn with fn begin(self) -> Unit raise @moondb.DbError {
  raise @moondb.QueryError("transactions are not wired yet: " + self.dsn)
}

impl @moondb.Driver for MyConn with fn commit(self) -> Unit raise @moondb.DbError {
  raise @moondb.QueryError("transactions are not wired yet: " + self.dsn)
}

impl @moondb.Driver for MyConn with fn rollback(self) -> Unit raise @moondb.DbError {
  raise @moondb.QueryError("transactions are not wired yet: " + self.dsn)
}

impl @moondb.Driver for MyConn with fn close(self) -> Unit {
  self.closed = true
}

extend MyConn with @moondb.Driver::{ping, close}

pub fn healthy(conn : MyConn) -> Bool {
  conn.ping()
}

pub fn shutdown(conn : MyConn) -> Unit {
  conn.close()
}

§3

Values & parameters

Parameters arrive as Array[Value] — one per positional placeholder, in order. Never interpolate them into the SQL string; bind them out-of-band. The placeholder token is the driver's choice (? for SQLite/MySQL, $1 for PostgreSQL).

pub(all) enum Value {
  Null | Bool(Bool) | Int(Int32) | Int64(Int64)
  | Double(Double) | Text(String) | Blob(Bytes)
}

Use Value::kind and Value::is_null when classifying an inbound value.

§4

Reading rows

A Row is read positionally or by name, with one typed accessor per Value case. Each raises DbError::TypeError if the column is not the requested shape.

pub fn read(row : @moondb.Row) -> Unit raise @moondb.DbError {
  let id = row.int64(0)            // column 0, as Int64
  let email = row.text_by("email") // by column name
  let unset = row.is_null(2)       // SQL NULL check
  let width = row.width()          // column count
  println("\{id} \{email} \{unset} \{width}")
}

§5

Defaults & overrides

Two operations ship with defaults you may keep or override:

i

ping → Bool

Default issues SELECT 1 and returns whether it round-trips, never raising. Override with a cheaper heartbeat if your backend has one.

ii

query_stream → &Cursor

Default materialises via query and returns an ArrayCursor. A driver with incremental fetch overrides this with a live cursor for bounded memory.

§6

Built on moondb

ORMs such as moonorm write their SQL generation against the Driver trait — not against any one backend. That is the whole point of the contract: one query layer, every ratified driver.