Sign in

    moonorm

    moonorm — an ORM / SQL toolkit for MoonBit (← SQLAlchemy / SQLModel): a parameterized, injection-safe query builder (select / insert / update / delete).

    sqlalchemy
    orm
    sql
    query-builder
    database
    moonbit
    Download zip
    Version
    0.11.0
    License
    Apache-2.0
    Last updated
    2 days ago
    Downloads
    94

    #moonorm

    An ORM / SQL toolkit for MoonBit — ← SQLAlchemy / SQLModel.

    Check and Test License mooncakes

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

    moonorm is the MoonBit counterpart to SQLAlchemy: the heart of SQLAlchemy Core — a parameterized, injection-safe query builder — plus a model/session execution layer. Values are never spliced into the SQL the builder produces: every bound value becomes a ? placeholder plus an entry in a params list. How far that reaches depends on the driver — SQLite and Postgres bind out-of-band, while the MySQL driver still renders parameters as escaped literals over the text protocol (see its README), so there the safety rests on the escaper matching the server's sql_mode rather than on the wire format.

    moonorm owns no driver contract of its own. It is written entirely against the moondb interface — the standard database-access seam for MoonBit — so it is pure MoonBit with zero C and compiles on every backend (wasm / wasm-gc / js / native). A concrete backend is a separate package you supply: the native SQLite driver lives in moonsqlite, and a Session drives any @moondb.Driver — including the dependency-free @moondb.MockDriver for tests.

    Imports. Bound-value constructors (Int, Text, Null, …) are moondb's: the Value type is re-exported by moonorm, but you construct values as @moondb.Int / @moondb.Text (add moon add moonbitstack/moondb). The examples below import the SQLite driver under a short alias, "moonbitstack/moonsqlite" @sqlite in moon.pkg.

    #Packages

    Each layer is its own subpackage, mirroring SQLAlchemy's: Core, table mapping, the ORM session, and Alembic's migrations. The root re-exports all of them, so @moonorm.select and @moonorm.Session work as they always did.

    PackageWhat it holdsDepends on
    moonorm/querydialects, predicates, select / insert / update / delete, Table, is_identmoondb's Value
    moonorm/schemaColumnType, Column, Field, Model, Relation, ManyToManyquery, moondb
    moonorm/sessionSession, row streams, transactions and savepoints, the unit of work, pooled sessionsschema, query, moondb
    moonorm/migrateMigration, Migrator, schema diffssession, schema, query

    A program that only renders SQL imports query and nothing else.

    Constructors are written without the package. pub using re-exports types but not their constructors, so @moonorm.Postgres does not resolve. Write the bare constructor and let the expected type pick it: stmt.build_for(Postgres). Where nothing gives the type, annotate or name the subpackage: let levels : Array[@moonorm.IsolationLevel] = [ReadCommitted, Serializable], or catch { @session.LostUpdate(msg) => ... }.

    #Quickstart

    let (sql, params) = @moonorm.select("users")
    .column("id").column("name")
    .eq("age", @moondb.Int(18))
    .where_("name", "LIKE", @moondb.Text("bob%"))
    .order_by("name", Asc)
    .limit(10)
    .build()
    // sql = "SELECT id, name FROM users WHERE age = ? AND name LIKE ? ORDER BY name ASC LIMIT 10"
    // params = [Int(18), Text("bob%")]

    let (isql, ivals) = @moonorm.insert("users")
    .set("name", @moondb.Text("bob")).set("age", @moondb.Int(30)).build()
    // "INSERT INTO users (name, age) VALUES (?, ?)"

    let (usql, uvals) = @moonorm.update("users").set("age", @moondb.Int(31)).where_("id", "=", @moondb.Int(1)).build()
    let (dsql, dvals) = @moonorm.delete("users").where_("id", "=", @moondb.Int(9)).build()

    // JOIN + GROUP BY + HAVING, with aggregates and a Table descriptor:
    let orders : @moonorm.Table = { name: "orders", columns: [] }
    let (jsql, jparams) = orders.select()
    .raw("users.name").count()
    .join("users", "users.id = orders.user_id")
    .eq("orders.status", @moondb.Text("paid"))
    .group_by("users.name")
    .having("COUNT(*)", ">", @moondb.Int(3))
    .build()
    // "SELECT users.name, COUNT(*) FROM orders JOIN users ON users.id = orders.user_id
    // WHERE orders.status = ? GROUP BY users.name HAVING COUNT(*) > ?"
    // params = [Text("paid"), Int(3)]

    #Running against a real database

    The builder is only half the story — moonorm also executes, against any @moondb.Driver. Add the native SQLite driver and a Session runs your built statements against an actual database and hands back typed rows. Every fallible operation raises @moondb.DbError (a decode or backend error surfaces at the call site, never as a silent zero value), so call them inside a function that propagates that error:

    // native target only — moonsqlite links the vendored amalgamation.
    fn demo() -> Unit raise @moondb.DbError {
    let sess = @moonorm.Session::new(@sqlite.SqliteDriver::open(":memory:"))
    sess.execute("CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT, age INTEGER)", []) |> ignore

    // INSERT through the builder — values are bound, never spliced.
    sess.add(@moonorm.insert("users").set("name", @moondb.Text("alice")).set("age", @moondb.Int(30)))
    |> ignore

    // SELECT through the builder — real rows come back, typed.
    let rows = sess.fetch(
    @moonorm.select("users").column("name").column("age").where_("age", ">", @moondb.Int(18)),
    )
    let _ = rows[0].text(0) // "alice" (raises TypeError if the column isn't Text)
    let _ = rows[0].int(1) // 30
    sess.close()
    }

    Session also has modify (UPDATE), remove (DELETE), begin / commit / rollback (delegated to the driver's transaction bracket), nested transactions via savepoint / rollback_to / release (SQLAlchemy's begin_nested), and raw execute / query. Everything travels as bound parameters as far as the driver carries them; see the note above on the MySQL text protocol.

    #Models & relationships

    A Model[T] is the explicit declarative mapping between a table and a MoonBit record — the typed columns plus the two mapping closures MoonBit cannot synthesise for want of reflection (Row -> record, record -> bound columns). From it you get CREATE TABLE DDL, typed inserts, and eager foreign-key loading. Because MoonBit has no attribute interception, parent.children cannot silently fire a SELECT the way SQLAlchemy's lazy load does; the load is an explicit call — session.load / session.load_one — exactly the shape Diesel's preload and GORM's Preload take.

    The mapping closures read cells through @moondb.Row's typed accessors, which raise on a type or index mismatch — so from_row raises too and a decode bug surfaces at the call site, not as a silent zero. Write them in arrow form so the raise effect is inferred:

    // A parent model and a child model with a foreign key back to it.
    let teams : @moonorm.Model[Team] = @moonorm.Model::new(
    "teams",
    [@moonorm.column("id", IntType, primary_key=true),
    @moonorm.column("name", TextType, nullable=false)],
    (r) => { id: r.int(0), name: r.text(1) },
    (t) => [("id", @moondb.Int(t.id)), ("name", @moondb.Text(t.name))],
    )
    let heroes : @moonorm.Model[Hero] = @moonorm.Model::new(
    "heroes",
    [@moonorm.column("id", IntType, primary_key=true),
    @moonorm.column("name", TextType, nullable=false),
    @moonorm.column("team_id", IntType, references=("teams", "id"))],
    (r) => { id: r.int(0), name: r.text(1), team_id: r.int(2) },
    (h) => [("id", @moondb.Int(h.id)), ("name", @moondb.Text(h.name)),
    ("team_id", @moondb.Int(h.team_id))],
    )

    sess.create_table(teams) |> ignore // CREATE TABLE teams (id INTEGER PRIMARY KEY, name TEXT NOT NULL)
    sess.create_table(heroes) |> ignore
    sess.insert_record(teams, { id: 1, name: "avengers" }) |> ignore
    sess.insert_record(heroes, { id: 10, name: "iron-man", team_id: 1 }) |> ignore

    // 1:N — a team's heroes, eager-loaded as mapped records.
    let children = @moonorm.has_many(heroes, "team_id", (t : Team) => @moondb.Int(t.id))
    let kids = sess.load({ id: 1, name: "avengers" }, children) // [Hero{...}, ...]

    // N:1 — a hero's team.
    let parent = @moonorm.belongs_to(teams, "id", (h : Hero) => @moondb.Int(h.team_id))
    let team = sess.load_one({ id: 10, name: "iron-man", team_id: 1 }, parent) // Some(Team{...})

    The relationship's match value is bound, never spliced, so eager loading is injection-safe like every other query.

    #Declarative models from a field list

    Writing to_columns by hand repeats what the columns already say. Model::from_fields takes that other half off you: give it a list of fields — each a column plus the one-line projection that reads it off a record — and it derives the column list, the CREATE TABLE DDL, the SELECT projection, and the INSERT binding. You still supply from_row, because turning a fetched row back into a T needs the record constructor, and MoonBit — with no reflection — cannot synthesise it; that one closure is the irreducible core of the mapping. This is exactly the shape moonctl's model generator emits from an #orm-annotated struct — a [field(...)] array and a from_row:

    let heroes : @moonorm.Model[Hero] = @moonorm.Model::from_fields(
    "heroes",
    [
    @moonorm.field("id", IntType, (h : Hero) => @moondb.Int(h.id), primary_key=true),
    @moonorm.field("name", TextType, (h : Hero) => @moondb.Text(h.name), nullable=false),
    @moonorm.field("team_id", IntType, (h : Hero) => @moondb.Int(h.team_id),
    references=("teams", "id")),
    ],
    (r) => { id: r.int(0), name: r.text(1), team_id: r.int(2) }, // the one closure that stays
    )

    #Batch loading (avoiding N+1)

    Calling load once per parent is the N+1 trap — one SELECT per row. load_batch fetches the children of many parents in a single WHERE fk IN (…) round trip and buckets them back, index-aligned with the sources you passed. load_one_batch is the N:1 counterpart. This is the explicit equivalent of SQLAlchemy's selectinload:

    let children = @moonorm.has_many(heroes, "team_id", (t : Team) => @moondb.Int(t.id))
    let teams : Array[Team] = [{ id: 1, name: "avengers" }, { id: 2, name: "x-men" }]
    let grouped = sess.load_batch(teams, children) // one query; grouped[0] = avengers' heroes, …

    #Nested transactions & isolation

    begin_nested opens a SAVEPOINT and hands back a handle that names and depth-tracks it for you (SQLAlchemy's Session.begin_nested()): finish it with release to keep the work or rollback to discard it back to the savepoint, both terminal and idempotent. For an isolation level or a read-only transaction, begin_with renders the backend's SET TRANSACTION (or SQLite PRAGMA) and orders it correctly around BEGIN per dialect:

    let sp = sess.begin_nested() // SAVEPOINT moonorm_sp_1; sess.savepoint_depth() == 1
    sess.add(insert_stmt) |> ignore
    sp.rollback() // undo just this savepoint's work; depth back to 0

    sess.begin_with({ isolation: Some(Serializable), read_only: false }, dialect=Postgres)
    // BEGIN; SET TRANSACTION ISOLATION LEVEL SERIALIZABLE

    #Design & boundaries (honest)

    • Explicit, not magic. SQLAlchemy issues SQL implicitly when you touch a mapped attribute; MoonBit has no attribute interception, so every statement is issued explicitly via Session — the same faithful trade Diesel and GORM make.
    • Pure, zero C. moonorm is written entirely against the @moondb seam, so the whole library compiles on every backend (wasm / wasm-gc / js / native) and drags no backend behind it. The C lives in one isolated place — moonsqlite — not here. A Postgres wire-protocol backend and a JS node:sqlite backend are next.
    • Tested against a real driver. The Session/Model/relationship layer is covered on every backend against @moondb.MockDriver; real SQL execution is proven end to end in moonsqlite's native integration tests, which open an actual SQLite database and are mutation-verified (neutering the C bind path turns them red).

    #Subqueries, CTEs & optimistic locking

    The builder does WITH common table expressions and IN (subquery) predicates, and both keep the injection-safety guarantee — a subquery's bound values splice into the params list in the exact left-to-right order they appear in the SQL text:

    let big = @moonorm.select("orders").column("user_id").where_("total", ">", @moondb.Int(100))
    let (sql, params) = @moonorm.select("users")
    .column("id").column("name")
    .with_cte("big_spenders", big)
    .where_("active", "=", @moondb.Bool(true))
    .where_in("id", @moonorm.select("big_spenders").column("user_id"))
    .build()
    // "WITH big_spenders AS (SELECT user_id FROM orders WHERE total > ?)
    // SELECT id, name FROM users WHERE active = ? AND id IN (SELECT user_id FROM big_spenders)"
    // params = [Int(100), Bool(true)] ← CTE value first, then the WHERE value

    Optimistic concurrency control mirrors SQLAlchemy's version_id_col. Write an UPDATE that bumps the version and guards on the one you read; modify_versioned runs it and raises LostUpdate if no row matched — the update was lost to a concurrent writer:

    let stmt = @moonorm.update("account")
    .set("balance", @moondb.Int(50))
    .set("version", @moondb.Int(current + 1))
    .where_("id", "=", @moondb.Int(1))
    .where_("version", "=", @moondb.Int(current)) // the guard
    sess.modify_versioned(stmt, what="account") |> ignore // raises LostUpdate on a stale version

    #Migrations

    Versioned schema migrations, tracked in a schema_migrations table — the Alembic / diesel-migrations counterpart. A Migration carries an integer version plus its up and down statement lists; a Migrator applies pending versions in ascending order, skips ones already applied (so re-running is a no-op), rolls back to a target version in descending order, and reports the current version.

    let migrations : Array[@moonorm.Migration] = [
    { version: 1, name: "create_users",
    up: ["CREATE TABLE users (id INTEGER PRIMARY KEY)"], down: ["DROP TABLE users"] },
    { version: 2, name: "add_email",
    up: ["ALTER TABLE users ADD COLUMN email TEXT"], down: ["ALTER TABLE users DROP COLUMN email"] },
    ]
    let m = @moonorm.Migrator::new()
    m.up(sess, migrations) |> ignore // applies 1 then 2; returns how many ran
    let _ = m.current_version(sess) // 2
    m.down_to(sess, migrations, 1) |> ignore // rolls back 2, leaving 1

    #Injection safety

    let evil = "'; DROP TABLE users; --"
    let (sql, params) = @moonorm.select("users").eq("name", @moondb.Text(evil)).build()
    // sql = "SELECT * FROM users WHERE name = ?" ← the attack string is NOT in the SQL
    // params = [Text("'; DROP TABLE users; --")] ← it's a bound parameter

    Verified across all backends (wasm, wasm-gc, js, native) in CI, 0 warnings under --deny-warn.

    #Roadmap (transliterating SQLAlchemy)

    select / insert / update / delete with WHERE / ORDER BY / LIMIT / OFFSET, inner/left JOIN, GROUP BY / HAVING, aggregate columns (count() / raw()), WITH CTEs, IN (subquery) and IN (values) predicates, window functions (OVER (PARTITION BY … ORDER BY …)), dialect-aware RETURNING and upsert (ON CONFLICT … DO UPDATE / ON DUPLICATE KEY UPDATE), and a Table descriptor are all here — and they execute against any @moondb.Driver via an explicit Session (add / fetch / modify / remove / commit / rollback, optimistic-locked updates, isolation-level and read-only transactions via begin_with, depth-tracked nested savepoints via begin_nested, plus declarative models — hand-built or from_fields from moonctl-generated metadata — eager-loaded relationships with N+1-avoiding batch loading, and versioned migrations). Connection pooling lives in moondb/pool as @pool.Pool[D], with its counting done by moonpool: idle reuse, a size ceiling, a pre_ping health probe that evicts dead connections on acquire, max_lifetime recycling, a non-blocking try_acquire, and close-all. The native SQLite backend is moonsqlite; Postgres and MySQL/MariaDB backends live in moonpostgres and moonmysql. Still to come: Alembic-style schema-diff migrations reflected from a live database, and multiple-inheritance / polymorphic mapping.

    #License

    Apache-2.0.

    AsSource

    What can stand where a table is expected. A String is the table name — so select("users") and "teams".join(…) keep reading as they always did — and a Source is anything richer (a derived table, an alias, a LATERAL).

    Column

    One declared column of a Model: its name, storage col_type, whether it is the (or part of the) primary key, whether it accepts NULL, and an optional foreign-key references target (table, column). This is the explicit stand-in for SQLAlchemy's mapped_column(...) — every fact that framework reads off the annotated attribute is stated here as data.

    ColumnDiff

    One difference between a declared schema and a reflected one (Alembic-autogenerate style): a column Added in the model but absent in the database, one Removed from the model but still present, or one whose TypeChanged. Compared by column name; ordering follows the declared columns, then the leftover reflected ones.

    ColumnType

    A column's SQL storage type. These map onto SQLite's type affinities when a Model renders CREATE TABLE DDL: IntType/BoolType -> INTEGER, TextType -> TEXT, RealType -> REAL, BlobType -> BLOB. BoolType is a distinct declared type so DDL reads intently, but binds and reads as an integer 0/1.

    Delete

    A DELETE statement builder. Generative like Select, and it shares the same WHERE clause.

    Dialect

    The SQL dialect a statement renders for. Most of the builder is dialect-neutral — it emits ? placeholders and standard clauses that every backend accepts — but a few constructs genuinely differ: upsert is ON CONFLICT ... DO UPDATE on SQLite and PostgreSQL versus ON DUPLICATE KEY UPDATE on MySQL, and RETURNING exists on SQLite and PostgreSQL but not MySQL. build_for takes a Dialect so those render correctly; the no-argument build renders the SQLite/PostgreSQL form, which is also what the ?-placeholder default targets.

    Field

    One field of a declarative model: a declared [Column] paired with the projection that reads the field's value out of a record T. A Field is the column metadata plus the "how to persist this one attribute" half of the mapping, factored to one place so a Model can be assembled from a list of fields rather than a hand-written to_columns.

    This is the shape moonctl's model generator emits: from a #orm-annotated struct it produces one field(...) per attribute (the encode closure is a trivial projection like h => Int(h.id)), and a single from_row decoder. The generated [Field] array then drives the column list, the CREATE TABLE DDL, the SELECT projection, and the INSERT binding — everything except decoding a row back into T, which needs the record constructor MoonBit cannot synthesise without reflection (see [Model::from_fields]).

    Insert

    An INSERT statement builder, with optional upsert (ON CONFLICT) and RETURNING support. Generative like Select.

    IsolationLevel

    A SQL transaction isolation level, in the four-rung ANSI ladder from weakest to strongest. Passed via TxOptions to Session::begin_with, which renders the backend's SET TRANSACTION (or SQLite PRAGMA) statement.

    LockClause

    A row-locking clause on a SELECT (SQLAlchemy with_for_update): the lock mode, the tables it is restricted to (OF …, empty = all), and whether it fails rather than waits (NOWAIT) or skips locked rows (SKIP LOCKED).

    LockMode

    The kind of row lock a FOR UPDATE / FOR SHARE clause takes.

    LostUpdate

    A lost update was detected: an optimistic-lock UPDATE matched no row because the row's version had already moved on since it was read. Raised by Session::modify_versioned.

    ManyToMany

    A many-to-many relationship from a source S to a target T through a junction (association) table — SQLAlchemy's relationship(secondary=...). The junction's source_fk column matches a key extracted from the source, and its target_fk column matches the target's target_key. Resolving the relationship JOINs the target through the junction. Load it with Session::load_many; like the 1:N / N:1 relations, MoonBit's lack of attribute interception makes the load an explicit call rather than a lazy attribute access.

    Migration

    One schema change: a monotonically increasing version, a human name, and the ordered SQL statements that apply it (up) and undo it (down). Each statement runs through Session::execute, so a backend that prepares one statement per call (SQLite) still applies a multi-statement migration.

    Migrator

    Tracks and applies migrations against a schema_migrations-style bookkeeping table (its name is configurable for coexistence with other tools). Build one with Migrator::new.

    Model

    A declarative model: the mapping between a database table and a MoonBit record type T. It bundles the table name, the ordered columns, and the two mapping closures that MoonBit cannot synthesise for want of reflection: from_row decodes a fetched Row into a T, and to_columns projects a T back into the (name, Value) pairs an INSERT binds. Build one with Model::new.

    This is the faithful explicit equivalent of a SQLAlchemy declarative class — the columns are the mapped_columns, and the two closures are the automatic attribute<->column mapping made visible. Being a plain value it stays pure and compiles on every backend.

    Nulls

    Where NULLs sort in an ORDER BY term (SQLAlchemy's nulls_first() / nulls_last()). Auto emits nothing and leaves the placement to the server, which differs: PostgreSQL sorts NULLs last ascending, SQLite first. First / Last say it explicitly; MySQL has no such syntax and rejects it.

    Order

    Sort direction for an ORDER BY term.

    Predicate

    A boolean predicate tree — the parameter-safe way to express arbitrary AND/OR/NOT groupings (SQLAlchemy's and_() / or_() / not_()), which the flat where_/eq API (always joined by AND) cannot. Every comparison binds its value as a placeholder, so a (a = ? OR b = ?) filter stays fully parameterised. Attach one to a query with Select::where_pred.

    Relation

    A foreign-key relationship from a source record S to a target model T, resolved by matching target.key_column against a Value extracted from the source. to_many records the cardinality (a 1:N parent->children link versus a N:1 child->parent link) so callers know whether to expect many rows or one.

    Because MoonBit has no attribute interception, touching hero.team cannot silently emit a SELECT the way SQLAlchemy's lazy load does. The relationship is therefore a first-class value and the load is explicit — Session::load / Session::load_one — exactly the eager, explicit shape Diesel's belonging_to / preload and GORM's Preload take.

    RowStream

    A typed streaming cursor: each next decodes one row into a record via the model. The record-level counterpart to [Session::stream], the streaming form of [fetch_as] (SQLAlchemy's yield_per).

    Savepoint

    A nested transaction opened by Session::begin_nested, wrapping one SAVEPOINT whose name and depth the session tracks for you. It is the faithful equivalent of the object SQLAlchemy's Session.begin_nested() returns: finish it by release-ing (keep the work, merging it into the enclosing transaction) or rollback-ing (discard the work since the savepoint). Both are terminal and idempotent — calling either a second time is a no-op — and both decrement the session's savepoint depth.

    Select

    A SELECT statement builder. Every method is generative — it returns a new statement and leaves the receiver alone — so a half-built query is a safe base to branch from, which is what SQLAlchemy's statement API guarantees.

    Session

    An explicit unit-of-work over a @moondb.Driver. Unlike SQLAlchemy's implicit autoflush and attribute-triggered SQL, every statement here is issued explicitly — the faithful MoonBit equivalent given the absence of attribute interception, exactly as Diesel and GORM also require. It holds the backend as a &@moondb.Driver trait object, so one Session type drives every backend.

    Source

    What a FROM or a JOIN names. A bare table is Tbl; Sub is a derived table (a whole Select, whose bound values thread into the enclosing statement's params at the position the subquery renders); Alias names either of those, which PostgreSQL and MySQL require of a derived table; Lateral lets a derived table reference the columns of the sources to its left (PostgreSQL and MySQL 8.0.14+; SQLite has no LATERAL).

    Table

    A lightweight table descriptor: a table name plus its known columns. It is the hand-written stand-in for SQLAlchemy's Table(...) metadata object — MoonBit has no reflection, so the schema is declared explicitly rather than introspected. select() turns it into a Select with every column projected.

    TxOptions

    Options for a transaction opened by Session::begin_with: an optional isolation level and a read_only flag. This is the explicit counterpart of SQLAlchemy's connection.execution_options(isolation_level=…) — a plain value the session renders into the backend's transaction-characteristics statement.

    Update

    An UPDATE statement builder. Generative like Select, and it shares the same WHERE clause, so where_pred / where_in narrow an update exactly as they narrow a query.

    Value

    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 are expected to normalise their column types down to these seven 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); dates and times ride here as ISO-8601 text until a dedicated temporal case lands (see the README roadmap).
    • Blob — an opaque byte string (BLOB / BYTEA).

    belongs_to

    fn[S, T] belongs_to(target :
    Model
    [T], target_key : String, child_key : (S) ->
    Value
    ) ->
    Relation
    [S, T]

    A N:1 relationship: child -> its single parent in target, matched by the parent's target_key column (usually its primary key) equalling the foreign key extracted from the child by child_key. Load it with Session::load_one.

    case_

    fn case_(whens : Array[(String, String)], else_? : String) -> String

    A searched CASE expression: each (condition, result) pair becomes one WHEN … THEN …, and a non-empty else_ the trailing ELSE. Identifier-level like func. With no branches there is nothing to test, so it folds to else_ (or NULL), which is what an all-branchless CASE would evaluate to anyway.

    cast

    fn cast(expr : String, ty : String) -> String

    A type conversion: cast("price", "NUMERIC(10,2)") renders CAST(price AS NUMERIC(10,2)). Identifier-level like func.

    column

    fn column(name : String, col_type :
    ColumnType
    , primary_key? : Bool, autoincrement? : Bool, nullable? : Bool, unique? : Bool, default? : String, checks? : Array[String], references? : (String, String), on_delete? : String, on_update? : String) ->
    Column

    Declare a Column. primary_key and references default off; a primary-key column is NOT NULL implicitly (SQLite treats INTEGER PRIMARY KEY as the rowid), and nullable defaults to true for every other column, matching SQLAlchemy's nullable=True default.

    delete

    fn delete(table : String) ->
    Delete

    Start a DELETE from table.

    diff_schema

    Diff a model's declared columns against the reflected ones from a live table: declared-only columns are Added, reflected-only are Removed, and a name in both with a different col_type is TypeChanged. An empty result means the table matches the model. This is the write half of autogenerate — feed the diff to DDL to synthesise the migration.

    Scope: reflection recovers only a column's storage type, so declare a model at the same granularity to avoid spurious diffs — a Bool stored as INTEGER reflects as Int, Uuid/Json stored as TEXT reflect as Text. And the diff reports add/remove/type only, not nullable / default / unique / PK / FK changes.

    field

    fn[T] field(name : String, col_type :
    ColumnType
    , encode : (T) ->
    Value
    , primary_key? : Bool, autoincrement? : Bool, nullable? : Bool, unique? : Bool, default? : String, checks? : Array[String], references? : (String, String), on_delete? : String, on_update? : String) ->
    Field
    [T]

    Declare a model field: its column metadata and the projection encode that pulls this field's [Value] out of a record. The column knobs (primary_key, nullable, references) mirror [column]; encode is the one-liner that binds the attribute, e.g. field("id", IntType, primary_key=true, h =>Int(h.id)).

    func

    fn func(name : String, args : Array[String]) -> String

    A function call as a column expression: func("coalesce", ["a", "b"]) renders coalesce(a, b). This is SQLAlchemy's func.* without the attribute magic — the arguments are identifier-level SQL, rendered verbatim like Select::raw, so bind a value by placing a ? and passing it through the surrounding clause.

    has_many

    fn[S, T] has_many(target :
    Model
    [T], foreign_key : String, parent_key : (S) ->
    Value
    ) ->
    Relation
    [S, T]

    A 1:N relationship: parent -> its children in target, matched by the child's foreign_key column equalling the parent key extracted by parent_key. Load it with Session::load, which returns every matching child record.

    index_ddl

    fn index_ddl(name : String, table : String, columns : Array[String], unique? : Bool) -> String

    Render a CREATE INDEX statement (SQLAlchemy's Index / index=True). With unique=true it is a CREATE UNIQUE INDEX; multiple columns make a composite index. Names are trusted identifiers, never bound. Pair it with a Migration's up, and a DROP INDEX in its down.

    insert

    fn insert(table : String) ->
    Insert

    Start an INSERT into table. Values are bound, never spliced, so a column holding a quote or a semicolon is a value and not syntax.

    many_to_many

    fn[S, T] many_to_many(target :
    Model
    [T], junction~ : String, source_fk~ : String, target_fk~ : String, target_key~ : String, source_key~ : (S) ->
    Value
    ) ->
    ManyToMany
    [S, T]

    Define a many-to-many relationship through junction. source_fk / target_fk are the junction columns pointing at the source key and the target_key.

    reflect_columns

    Parse PRAGMA table_info rows into Columns — the reflection core, testable without a live database. The pragma projects, in order, cid, name, type,notnull, dflt_value, pk; a non-zero pk marks the primary key and notnull its NOT NULL. This is the read half of Alembic-style autogenerate: compare the reflected columns against a Model's declared columns to diff a schema.

    reflect_columns_mysql

    Parse MySQL SHOW COLUMNS rows (Field, Type, Null, Key, Default, Extra) into Columns, recovering the primary key (Key == "PRI") and autoincrement (Extra contains auto_increment).

    reflect_columns_pg

    Parse PostgreSQL information_schema.columns rows (column_name, data_type, is_nullable) into Columns — the PG counterpart of reflect_columns.

    reflect_table

    Reflect a table's columns off a live connection: run PRAGMA table_info and parse it. The thin driver wrapper over reflect_columns.

    reflect_table_for

    Reflect a table's columns off a live connection for a specific Dialect: SQLite via PRAGMA table_info, PostgreSQL via information_schema.columns, MySQL via SHOW COLUMNS. The multi-dialect counterpart of reflect_table.

    select

    Start a SELECT over src — a table name, or a Source for a derived table, an alias or a LATERAL.

    update

    fn update(table : String) ->
    Update

    Start an UPDATE of table. Without a where_ it rewrites every row, which is what SQL does and what the caller asked for.

    Source Files