moonorm

MoonBit · ORM & SQL toolkit

The seam between your type and its table.

Types are contracts. The seam that stitches a MoonBit struct into SQL is the query — and moonorm sews it at compile time. A field that cannot map is not a runtime surprise; it is a compile error, caught on this very line.

struct User → users
MoonBit — type side relation — SQL side
id: Int64 idBIGINTprimary key
name: String nameTEXTnot null
email: String emailTEXTnot null unique
okall fields mapped, 0 errors
CREATE TABLE PostgreSQL
CREATE TABLE users (id BIGSERIAL PRIMARY KEY, name TEXT NOT NULL, email TEXT NOT NULL UNIQUE)

Hover a field to lock its mapping. Click email to watch a type mismatch break the seam. Every type and every statement on the right is produced by this library, compiled to JavaScript.

The narrative

Four movements along one seam

01 — map

A struct becomes a table

Declare a model once; moonorm derives the DDL for SQLite, PostgreSQL and MySQL. The mapping is checked as you type — a field whose type has no column analogue is a compile error, not a migration surprise.
02 — compile

A query becomes SQL

A MoonBit query expression unfolds through the seam into a dialect's SQL. where_, join, group_by each emit their fragment; values are bound, never interpolated.
03 — evolve

A schema diff becomes a plan

Reflect a model against a live database, diff the two, and generate up/down migrations. Each change is labelled reversible or not, and checked against the type side.
04 — land

A session becomes work

Unit-of-work, identity map, dirty tracking and savepoints ride on the same seam. Write MoonBit; the transaction commits SQL.

The contract ledger

MoonBit type ↔ SQL type

The strong type and the weak type, reconciled. Rows that cannot map are marked — and explained.

MoonBitSQLitePostgreSQLMySQLnullable
IntINTEGERINTEGERINT·
Int64INTEGERBIGINTBIGINT·
DoubleREALDOUBLE PRECISIONDOUBLE·
StringTEXTTEXTTEXT·
BoolINTEGERBOOLEANTINYINT(1)·
BytesBLOBBYTEABLOB·
Int ↔ TEXTneeds an explicit kind=; the seam will not guessno

Side by side

What you write, what runs

MoonBit
let q = User::model()
  .select()
  .where_(fn(u) { u.name == "ada" })
  .order_by("id", Asc)
  .limit(10)
let (sql, params) = q.build()
// params: [Text("ada"), Int(10)]
SQL · dialect-neutral
SELECT id, name, email
  FROM users
 WHERE name = ?
 ORDER BY id ASC
 LIMIT ?

Placeholders are dialect tokens chosen by the driver (SQLite/MySQL ?, PostgreSQL $1). Values are bound out-of-band — injection is absent by construction.

Read the guide Open the showcase Browse the API