moonorm

API · generated

Public surface

217 declarations across 5 packages, read out of pkg.generated.mbti and described from the /// docs in the sources.

moonbitstack/moonorm

moonorm

Functions & methods 20

pub fn[S, T, V : @moondb.ToValue] belongs_to(@schema.Model[T], String, (S) -> V) -> @schema.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.

pub fn case_(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.

pub fn cast(String, String) -> String

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

pub fn column(String, @schema.ColumnType, primary_key? : Bool, autoincrement? : Bool, nullable? : Bool, unique? : Bool, default? : String, checks? : Array[String], references? : (String, String), on_delete? : String, on_update? : String) -> @schema.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.

pub fn delete(String) -> @query.Delete

Start a DELETE from table.

pub fn diff_schema(Array[@schema.Column], Array[@schema.Column], dialect? : @query.Dialect) -> Array[@migrate.ColumnDiff]

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.

With dialect the types compare as that engine writes them, so a BigIntType declared against SQLite, where every integer is INTEGER, is no change; without it they compare as declared.

pub fn[T, V : @moondb.ToValue] field(String, @schema.ColumnType, (T) -> V, primary_key? : Bool, autoincrement? : Bool, nullable? : Bool, unique? : Bool, default? : String, checks? : Array[String], references? : (String, String), on_delete? : String, on_update? : String) -> @schema.Field[T]

Declare a model field: its column metadata and the projection encode that reads the field out of a record. The column knobs (primary_key, nullable, references) mirror [column]. encode returns anything that binds as a parameter, so it is usually the attribute itself, h => h.id, and T? binds None as NULL; a [Value] binds as itself.

pub fn func(String, Array[String]) -> String

A function call as a column expression: func("coalesce", ["a", "b"]) renders coalesce(a, b). 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.

pub fn[S, T, V : @moondb.ToValue] has_many(@schema.Model[T], String, (S) -> V) -> @schema.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, any value a column takes. Load it with Session::load, which returns every matching child record.

pub fn index(Array[String], name? : String, unique? : Bool) -> @schema.Index
pub fn index_ddl(String, String, Array[String], unique? : Bool) -> String

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.

pub fn insert(String) -> @query.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.

pub fn[S, T, V : @moondb.ToValue] many_to_many(@schema.Model[T], junction~ : String, source_fk~ : String, target_fk~ : String, target_key~ : String, source_key~ : (S) -> V) -> @schema.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.

pub fn reflect_columns(Array[@moondb.Row]) -> Array[@schema.Column] raise @moondb.DbError

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.

pub fn reflect_columns_mysql(Array[@moondb.Row]) -> Array[@schema.Column] raise @moondb.DbError

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

pub fn reflect_columns_pg(Array[@moondb.Row]) -> Array[@schema.Column] raise @moondb.DbError

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

pub fn reflect_table(&@moondb.Driver, String) -> Array[@schema.Column] raise @moondb.DbError

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

pub fn reflect_table_for(&@moondb.Driver, String, @query.Dialect) -> Array[@schema.Column] raise @moondb.DbError

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.

pub fn[S : @query.AsSource] select(S) -> @query.Select

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

pub fn update(String) -> @query.Update

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

moonbitstack/moonorm/schema

schema

Types 7

pub(all) struct 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?
}

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). Every fact that framework reads off the annotated attribute is stated here as data.

pub(all) enum ColumnType {
  IntType
  BigIntType
  TextType
  RealType
  BlobType
  BoolType
  VarcharType(Int32)
  NumericType
  DateTimeType
  DateType
  TimeType
  UuidType
  JsonType
} derive(Eq)

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.

pub struct Field[T] {
  column : Column
  encode : (T) -> @moondb.Value
}

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]).

pub(all) struct Index {
  name : String?
  columns : Array[String]
  unique : Bool
} derive(Eq, @debug.Debug)

An index on a model's table: on one column or several, and unique to enforce a key that is not the primary key. Without a name it is called ix_<table>_<columns>, or uq_ when unique.

pub struct ManyToMany[S, T] {
  target : Model[T]
  junction : String
  source_fk : String
  target_fk : String
  target_key : String
  source_key : (S) -> @moondb.Value
}

A many-to-many relationship from a source S to a target T through a junction (association) table. 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.

pub struct Model[T] {
  table : String
  columns : Array[Column]
  from_row : (@moondb.Row) -> T raise @moondb.DbError
  to_columns : (T) -> Array[(String, @moondb.Value)]
  indexes : Array[Index]
  ident : Map[Int32, Map[String, T]]
}

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

pub struct Relation[S, T] {
  target : Model[T]
  key_column : String
  source_key : (S) -> @moondb.Value
  to_many : Bool
}

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. The relationship is therefore a first-class value and the load is explicit, Session::load / Session::load_one, exactly the eager.

Functions & methods 29

pub fn[S, T, V : @moondb.ToValue] belongs_to(Model[T], String, (S) -> V) -> 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.

pub fn column(String, 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.

pub fn Column::ddl(Self, inline_pk? : Bool) -> String

Render this column as a single CREATE TABLE column definition, e.g. "user_id INTEGER NOT NULL REFERENCES users(id)". A PRIMARY KEY column emits that keyword; a non-nullable column emits NOT NULL; a references target emits a REFERENCES table(column) clause. Column and table names are trusted identifiers declared in code, never bound values.

pub fn Column::ddl_for(Self, @query.Dialect, inline_pk? : Bool) -> String

A column definition rendered for a specific Dialect. An autoincrement primary key takes each engine's idiom: SQLite INTEGER PRIMARY KEY AUTOINCREMENT, PostgreSQL SERIAL PRIMARY KEY (BIGSERIAL for BigIntType), MySQL INT AUTO_INCREMENT PRIMARY KEY (BIGINT for BigIntType). Otherwise it is sql_for plus the same constraint clauses as ddl.

pub fn ColumnType::sql_for(Self, @query.Dialect) -> String

The declared type keyword for a ColumnType in a given SQL Dialect. SQLite is the affinity-based default; PostgreSQL and MySQL render their native spellings (BOOLEAN/BYTEA/DOUBLE PRECISION/UUID/JSONB on PG; INT/TINYINT(1)/DOUBLE/JSON on MySQL). This is the per-dialect counterpart of ColumnType::sql.

pub fn[T, V : @moondb.ToValue] field(String, ColumnType, (T) -> V, 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 reads the field out of a record. The column knobs (primary_key, nullable, references) mirror [column]. encode returns anything that binds as a parameter, so it is usually the attribute itself, h => h.id, and T? binds None as NULL; a [Value] binds as itself.

pub fn[T] Field::name(Self[T]) -> String

The name of the column this field maps to.

pub fn[S, T, V : @moondb.ToValue] has_many(Model[T], String, (S) -> V) -> 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, any value a column takes. Load it with Session::load, which returns every matching child record.

pub fn index(Array[String], name? : String, unique? : Bool) -> Index
pub fn[S, T, V : @moondb.ToValue] many_to_many(Model[T], junction~ : String, source_fk~ : String, target_fk~ : String, target_key~ : String, source_key~ : (S) -> V) -> 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.

pub fn[S, T] ManyToMany::query(Self[S, T], S) -> @query.Select

The Select resolving this relationship for a source: the target rows joined to the junction on junction.target_fk = target.target_key, filtered to junction.source_fk = ? with the source key bound (never spliced). The target columns are projected qualified (target.col) so a same-named junction column never makes the projection ambiguous. Session::load_many runs it and maps.

pub fn[T] Model::column_names(Self[T]) -> Array[String]

The declared column names, in order.

pub fn[T] Model::create_indexes_sql(Self[T], if_not_exists? : Bool) -> Array[String]

create_indexes_sql_for in the dialect create_table_sql writes.

pub fn[T] Model::create_indexes_sql_for(Self[T], @query.Dialect, if_not_exists? : Bool) -> Array[String]

The CREATE INDEX statements for the model's indexes, in the order given. if_not_exists makes them idempotent; MySQL has no such clause for an index, so it is left out there.

pub fn[T] Model::create_table_sql(Self[T], if_not_exists? : Bool) -> String

Render CREATE TABLE DDL for this model from its declared columns. With if_not_exists=true the statement is idempotent (CREATE TABLE IF NOT EXISTS).

pub fn[T] Model::create_table_sql_for(Self[T], @query.Dialect, if_not_exists? : Bool) -> String

CREATE TABLE DDL for this model rendered for a specific Dialect — the multi-dialect counterpart of create_table_sql. Uses Column::ddl_for, keeps a composite primary key as a table-level constraint, and appends ENGINE=InnoDB on MySQL.

pub fn[T] Model::from_fields(String, Array[Field[T]], (@moondb.Row) -> T raise @moondb.DbError, indexes? : Array[Index]) -> Self[T]

Build a Model[T] from a list of [Field]s and a single row decoder — the declarative path that removes the hand-written to_columns. The columns and the binding projection are both derived from fields: columns is each field's declared column, and to_columns maps a record to (name, field.encode(record)) pairs, leaving out an autoincrement key that encodes as NULL so the database assigns it. You still supply from_row, because reconstructing a T from a fetched row needs the record constructor, and MoonBit — lacking reflection — cannot synthesise it; that one closure is the irreducible core of the mapping.

This is the constructor moonctl-generated model metadata targets: the generator emits the [Field] array and the from_row decoder, and from_fields turns them into a live Model. Written by hand it reads just as directly:

`` Model::from_fields( "hero", [ field("id", IntType, primary_key=true, h => h.id), field("name", TextType, nullable=false, h => h.name), ], r => { id: r.read(0), name: r.read(1) }, ) ``

pub fn[T] Model::insert_of(Self[T], T) -> @query.Insert

Build an Insert that persists record, binding the pairs from to_columns. Values travel as ? placeholders exactly like the rest of the builder.

pub fn[T] Model::map_row(Self[T], @moondb.Row) -> T raise @moondb.DbError

Decode a single fetched Row into a record via the model's from_row.

pub fn[T] Model::map_rows(Self[T], Array[@moondb.Row]) -> Array[T] raise @moondb.DbError

Decode a whole result set into records, preserving row order.

pub fn[T] Model::new(String, Array[Column], (@moondb.Row) -> T raise @moondb.DbError, (T) -> Array[(String, @moondb.Value)], indexes? : Array[Index]) -> Self[T]

Define a Model[T]. from_row should read the record's fields off the @moondb.Row (via Row::text / Row::int / Row::by_name …) — those accessors raise @moondb.DbError on a type or index mismatch, so from_row raises too and a decode bug surfaces at the call site. to_columns should list the column/value pairs to persist (typically every column except an autoincrement primary key). Write the closures in arrow form so the raise effect is inferred, e.g. (r) => \{ id: r.int(0), name: r.text(1) \}.

pub fn[T] Model::parent_tables(Self[T]) -> Array[String]

The distinct tables this model's foreign keys point at, itself excluded. A flush orders inserts so every table named here is written before this one, and deletes the other way round.

pub fn[T] Model::pk_columns(Self[T]) -> Array[String]

The declared primary-key column names, in declaration order. Empty when the model declares no key at all — the unit of work needs one to address a row, and Session::get / Session::delete_record say so rather than guessing.

pub fn[T] Model::pk_of(Self[T], T) -> Array[@moondb.Value]

A record's primary-key values in pk_columns order, read off the same to_columns projection an INSERT binds — the only route to them, since MoonBit cannot look up an attribute by name.

A projection that omits a key column — the usual shape for an autoincrement id the server assigns — stops the scan there, so the result is shorter than pk_columns. That length mismatch is how a session tells a record it can address from one whose key does not exist yet.

pub fn[T] Model::select(Self[T]) -> @query.Select

A Select over this model's table with every declared column projected explicitly (so the projection order is fixed and known to from_row). Add where_ / order_by / limit to it as usual, then run it with Session::fetch_as to get mapped records back.

pub fn[T] Model::table_descriptor(Self[T]) -> @query.Table

The Table descriptor for this model (name + column names), for interop with the plain builder API.

pub fn[S, T] Relation::batch_query(Self[S, T], Array[S]) -> @query.Select

One SELECT covering every source's related rows — key_column IN (…) over the sources' keys — so loading a relation for a list costs one query instead of one per row. source_value is what buckets the results back.

pub fn[S, T] Relation::query(Self[S, T], S) -> @query.Select

The Select that resolves this relationship for a given source record: the target model's projection filtered to key_column = ?, with the source-derived value bound (never spliced), so eager loading is injection-safe like every other query. Session::load runs this and maps the rows.

pub fn[S, T] Relation::source_value(Self[S, T], S) -> @moondb.Value

The single Select that resolves this relationship for *many* sources at once: the target model's projection filtered to key_column IN (k1, k2, …), where the keys are the distinct values extracted from sources. This is the query behind an N+1-avoiding batch load — one round trip fetches the related rows of every source, instead of one query(source) per source. Every key binds as its own placeholder. With no sources the result filters on the empty set (IN (NULL)), matching nothing; Session::load_batch short-circuits that case without a query. The source-side key this relationship extracts from a source record — the value the target's key_column is matched against. Exposed so a batch load can bucket fetched rows back to their sources.

moonbitstack/moonorm/query

query

Types 13

pub(open) trait AsSource {
  fn to_source(Self) -> Source
}

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

pub struct Delete {
  table : String
  filter : Where
  returning : Array[String]
}

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

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

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.

pub struct Insert {
  table : String
  cols : Array[String]
  vals : Array[@moondb.Value]
  extra_rows : Array[Array[@moondb.Value]]
  mut conflict : Conflict?
  returning : Array[String]
}

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

pub(all) struct LockClause {
  mode : LockMode
  of : Array[String]
  nowait : Bool
  skip_locked : Bool
} derive(Eq, @debug.Debug)

The tables it is restricted to (OF …, empty = all), and whether it fails rather than waits (NOWAIT) or skips locked rows (SKIP LOCKED).

pub(all) enum LockMode {
  ForUpdate
  ForShare
} derive(Eq, @debug.Debug)

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

pub(all) enum Nulls {
  Auto
  First
  Last
} derive(Eq)

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.

pub(all) enum Order {
  Asc
  Desc
} derive(Eq)

Sort direction for an ORDER BY term.

pub(all) enum Predicate {
  Cmp(col~ : String, op~ : String, val~ : @moondb.Value)
  InList(col~ : String, vals~ : Array[@moondb.Value], negated~ : Bool)
  Exists(sub~ : Select, negated~ : Bool)
  And(Array[Predicate])
  Or(Array[Predicate])
  Not(Predicate)
}

A boolean predicate tree, 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.

pub struct Select {
  from_ : Source
  ctes : Array[Cte]
  cols : Array[String]
  joins : Array[JoinClause]
  filter : Where
  groups : Array[String]
  havings : Array[Cond]
  orders : Array[OrderTerm]
  set_ops : Array[(String, Select)]
  distinct_on_ : Array[String]
  mut distinct_ : Bool
  mut limit_ : Int32?
  mut offset_ : Int32?
  mut lock_ : LockClause?
}

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.

pub(all) enum Source {
  Tbl(String)
  Sub(Select)
  Alias(Source, String)
  Lateral(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).

pub(all) struct Table {
  name : String
  columns : Array[String]
}

A lightweight table descriptor: a table name plus its known columns. MoonBit has no reflection, so the schema is declared explicitly rather than introspected. select() turns it into a Select with every column projected.

pub struct Update {
  table : String
  sets : Array[(String, Assign)]
  filter : Where
  returning : Array[String]
}

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.

Functions & methods 74

pub fn case_(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.

pub fn cast(String, String) -> String

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

pub fn delete(String) -> Delete

Start a DELETE from table.

pub fn Delete::build(Self) -> (String, Array[@moondb.Value])

Render (sql, params) for the SQLite/PostgreSQL dialect.

pub fn Delete::build_for(Self, Dialect) -> (String, Array[@moondb.Value])

Render (sql, params) for dialect. Identical everywhere but RETURNING, which MySQL does not have and so does not get.

pub fn Delete::returning(Self, String) -> Self

Return col from each deleted row (SQLite 3.35+, PostgreSQL) — the row's contents survive the delete only if you ask for them here. Repeatable; build_for(Mysql) drops it.

pub fn Delete::returning_all(Self) -> Self

Return every column of each deleted row (RETURNING *). See returning.

pub fn Delete::where_(Self, String, String, @moondb.Value) -> Self

Narrow the delete with col op ?. Conditions are ANDed together.

pub fn Delete::where_in(Self, String, Select) -> Self

Narrow the delete with col IN (subquery) — "delete the rows some other query picks out". The subquery's bound values splice in at the predicate's position.

pub fn Delete::where_not_in(Self, String, Select) -> Self

Narrow the delete with col NOT IN (subquery). See where_in.

pub fn Delete::where_pred(Self, Predicate) -> Self

Narrow the delete with a boolean Predicate tree. See Select::where_pred.

pub fn func(String, Array[String]) -> String

A function call as a column expression: func("coalesce", ["a", "b"]) renders coalesce(a, b). 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.

pub fn insert(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.

pub fn Insert::build(Self) -> (String, Array[@moondb.Value])

Render to (sql, params) for the SQLite/PostgreSQL dialect. Equivalent to build_for(Sqlite); kept as the no-argument default because plain inserts and the ?-placeholder convention target this form.

pub fn Insert::build_for(Self, Dialect) -> (String, Array[@moondb.Value])

Render to (sql, params) for dialect, with ? placeholders for every bound value. Bound values appear in params in SQL-text order: the inserted VALUES first, then any DO UPDATE SET col = ? values. Upsert renders as ON CONFLICT on SQLite/PostgreSQL and ON DUPLICATE KEY UPDATE on MySQL; RETURNING is emitted on SQLite/PostgreSQL and omitted on MySQL.

pub fn Insert::do_nothing(Self) -> Self

On a conflict, keep the existing row and change nothing (ON CONFLICT DO NOTHING). MySQL has no such form, so build_for(Mysql) renders a no-op self-assignment instead.

pub fn Insert::do_update(Self, String, @moondb.Value) -> Self

On a conflict, set col to a bound value (col = ?). The value binds as a parameter after the inserted values, so an upsert stays injection-safe.

pub fn Insert::do_update_excluded(Self, String) -> Self

On a conflict, set col to the value the failed insert tried to write (excluded.col on SQLite/PostgreSQL, VALUES(col) on MySQL). This is the "overwrite with the incoming row" upsert.

pub fn Insert::on_conflict(Self, Array[String]) -> Self

Turn this INSERT into an upsert keyed on targets. Chain do_update / do_update_excluded to say what to change on a collision, or leave it and call do_nothing. SQLite and PostgreSQL name the conflict target explicitly; build_for(Mysql) ignores it (MySQL infers the key), so pass it regardless and the right dialect uses it.

pub fn Insert::returning(Self, String) -> Self

Return col from each inserted (or upserted) row. On SQLite and PostgreSQL this appends RETURNING, so a caller reads the server-assigned id or a default/trigger-computed value back in the same round trip instead of a second SELECT. Repeatable. build_for(Mysql) drops it — MySQL has no RETURNING.

pub fn Insert::returning_all(Self) -> Self

Return every column of each inserted row (RETURNING *). See returning.

pub fn Insert::set(Self, String, @moondb.Value) -> Self

Set a column to a value.

pub fn Insert::values(Self, Array[@moondb.Value]) -> Self

Append another row to a multi-row INSERT. Every value binds as its own placeholder, so a bulk insert stays injection-safe; each row must supply one value per column.

pub fn is_ident(String) -> Bool

Whether s is a bare SQL identifier: a letter or _, then letters, digits or _. Identifiers cannot be bound as parameters, so a name that is interpolated into SQL (a table, a savepoint) is checked with this first; anything else is refused rather than quoted, which keeps the rule the same on every dialect.

pub fn Predicate::build(Self) -> (String, Array[@moondb.Value])

Render a predicate to its SQL fragment and the values it binds, in text order. A multi-child And/Or is wrapped in parentheses so nesting composes correctly.

pub fn[S : AsSource] select(S) -> Select

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

pub fn Select::build(Self) -> (String, Array[@moondb.Value])

Render to (sql, params), with ? placeholders for every bound value. Bound values appear in the params list in the same left-to-right order they occur in the SQL text: CTE subqueries first (the leading WITH), then any derived table in FROM and the joins, then the WHERE predicates, then HAVING, then the set-operation operands.

pub fn Select::column(Self, String) -> Self

Add a projected column (no columns selects *).

pub fn Select::count(Self) -> Self

Project COUNT(*) — the most common aggregate. Sugar for raw("COUNT(*)").

pub fn[S : AsSource] Select::cross_join(Self, S) -> Self

Add a CROSS JOIN src — the cartesian product, which takes no ON. Paired with a Lateral source this is how a row-generating function is joined per row.

pub fn Select::distinct(Self) -> Self

Deduplicate the result set: SELECT DISTINCT. Applies to this query's own projection (each operand of a set operation carries its own DISTINCT).

pub fn Select::distinct_on(Self, Array[String]) -> Self

Keep the first row of each distinct cols group: SELECT DISTINCT ON (a, b), PostgreSQL's extension. Which row is "first" is whatever ORDER BY says, so pair it with an order_by that leads with the same columns. Takes precedence over plain distinct when both are set; no other dialect accepts it.

pub fn Select::eq(Self, String, @moondb.Value) -> Self

Shorthand for where_(col, "=", val).

pub fn Select::except_(Self, Self) -> Self

EXCEPT — rows in this query but not in other (SQL's set difference).

pub fn Select::for_update(Self, read? : Bool, of? : Array[String], nowait? : Bool, skip_locked? : Bool) -> Self

Or FOR SHARE when read; restrict it to certain tables with of; make it fail immediately on a locked row with nowait, or skip locked rows with skip_locked. The clause is emitted after ORDER BY / LIMIT / OFFSET, the standard SQL position (SQLite, which has no row locks, ignores it).

pub fn[S : AsSource] Select::full_join(Self, S, String) -> Self

Add a FULL JOIN src ON <on> — every row of both sides. See join. MySQL has no FULL JOIN; SQLite gained it in 3.39.

pub fn Select::group_by(Self, String) -> Self

Add a GROUP BY column (repeatable; columns are emitted in call order).

pub fn Select::having(Self, String, String, @moondb.Value) -> Self

Add a col op ? predicate to the HAVING clause (ANDed with the rest). The value is bound as a ? placeholder exactly like where_, so aggregate filters stay injection-safe.

pub fn Select::intersect(Self, Self) -> Self

INTERSECT — rows present in both queries.

pub fn[S : AsSource] Select::join(Self, S, String) -> Self

Add an inner JOIN src ON <on>. src is a table name or a Source (a derived table brings its bound values with it). The on predicate is rendered verbatim (it references columns, not bound values), so pass only trusted identifiers.

pub fn[S : AsSource] Select::join_using(Self, S, Array[String]) -> Self

Add a JOIN src USING (a, b) — the equi-join on columns of the same name in both tables, which also collapses each pair into one output column. cols are identifiers, rendered verbatim.

pub fn Select::label(Self, String, String) -> Self

The name is what the result row is keyed by, so it is what from_row reads.

pub fn[S : AsSource] Select::left_join(Self, S, String) -> Self

Add a LEFT JOIN src ON <on> — every left row, matched or not. See join.

pub fn Select::limit(Self, Int32) -> Self

Cap how many rows come back.

pub fn Select::offset(Self, Int32) -> Self

Skip the first n rows. Paired with limit this is a page.

pub fn Select::order_by(Self, String, Order, nulls? : Nulls) -> Self

Order the result by a column. Repeated calls order by each in turn. nulls places NULLs explicitly (NULLS FIRST / NULLS LAST); the default leaves it to the server, which is the only portable choice — MySQL rejects the syntax.

pub fn Select::raw(Self, String) -> Self

Add a raw column expression (e.g. an aggregate like "SUM(price)" or a qualified "users.id"). Identical machinery to column; named for intent so call sites read as "this is an expression, not a plain column name".

pub fn[S : AsSource] Select::right_join(Self, S, String) -> Self

Add a RIGHT JOIN src ON <on> — every right row, matched or not. See join. SQLite gained it in 3.39; older versions need the join written the other way.

pub fn Select::union(Self, Self) -> Self

UNION this query with other — the combined rows with duplicates removed. A trailing ORDER BY / LIMIT on this query binds to the whole compound (standard SQL); one on other makes it a derived table so it keeps binding to other.

pub fn Select::union_all(Self, Self) -> Self

UNION ALL — the combined rows keeping duplicates.

pub fn Select::where_(Self, String, String, @moondb.Value) -> Self

Add a col op ? predicate (ANDed with the rest).

pub fn Select::where_exists(Self, Self) -> Self

Add an EXISTS (subquery) predicate (ANDed with the rest). The subquery's bound values splice in at its position, so it stays injection-safe.

pub fn Select::where_in(Self, String, Self) -> Self

Add a col IN (subquery) predicate (ANDed with the rest). The subquery's bound values splice in at the predicate's position, so a correlated or filtering subquery stays injection-safe exactly like a scalar where_.

pub fn Select::where_in_values(Self, String, Array[@moondb.Value]) -> Self

Add a col IN (?, ?, …) predicate over an explicit list of values (ANDed with the rest). Every value binds as its own placeholder, never spliced — this is the set-membership filter a batch load issues to fetch the related rows of many parents in one round trip (WHERE fk IN (key1, key2, …)) instead of one query per parent. An empty vals renders the always-false 1 = 0 — "in the empty set" matches nothing — so a batch load with no keys returns no rows rather than erroring.

pub fn Select::where_not_exists(Self, Self) -> Self

Add a NOT EXISTS (subquery) predicate (ANDed with the rest). See where_exists.

pub fn Select::where_not_in(Self, String, Self) -> Self

Add a col NOT IN (subquery) predicate (ANDed with the rest). See where_in.

pub fn Select::where_not_in_values(Self, String, Array[@moondb.Value]) -> Self

Add a col NOT IN (?, ?, …) predicate over an explicit list of values (ANDed with the rest). See where_in_values; an empty vals renders the always-true 1 = 1 — "not in the empty set" matches every row.

pub fn Select::where_pred(Self, Predicate) -> Self

Add a boolean Predicate tree to the WHERE — the parameter-safe AND/OR/NOT grouping the flat where_/eq cannot express, e.g. where_pred(Or([Cmp(col="a", op="=", val=Int(1)), Cmp(col="b", op="=", val=Int(2))])). Predicates are ANDed with the other clauses, so mixing eq(...) and where_pred(...) composes as one conjunction.

pub fn Select::window(Self, String, partition_by? : Array[String], order_by? : Array[(String, Order)], as_? : String) -> Self

Project a window function: <expr> OVER (PARTITION BY … ORDER BY …), optionally aliased. Window functions compute over a frame of rows without collapsing them (ROW_NUMBER(), RANK(), a running SUM(x)), so this adds one more projected column rather than grouping. expr is the function call ("ROW_NUMBER()", "SUM(amount)"), partition_by splits the rows into independent windows, and order_by ranks within each. All three are identifier-level SQL (they name columns and functions, never bound values), so they render verbatim like raw and carry no parameters. An empty partition_by and order_by yields the whole result as one unordered window, <expr> OVER ().

pub fn Select::with_cte(Self, String, Self) -> Self

Attach a common table expression: WITH name AS (sub). Repeatable — several CTEs render comma-separated in one leading WITH. The subquery is parameterised like any other statement; its bound values lead the final params list because the WITH clause leads the SQL text. name is a trusted identifier declared in code, never a bound value.

pub fn Select::with_recursive(Self, String, Self, Self, all? : Bool) -> Self

anchor is the base term and step the term that refers back to name in its own FROM — the CTE name is a trusted identifier referenceable in step and in this query's own FROM, so the usual shape is select(name).with_recursive(name, anchor, step). all picks UNION ALL (keep duplicates, the common tree/graph walk) over the default UNION. The two terms compose through the existing union / union_all builders, so their bound values thread through in text order like any CTE.

pub fn Source::to_source(Self) -> Self

AsSource's to_source, callable on the value directly.

pub fn Table::select(Self) -> Select

Start a SELECT over this table with all of its declared columns projected (an empty columns yields SELECT *, matching select(name)).

pub fn update(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.

pub fn Update::build(Self) -> (String, Array[@moondb.Value])

Render (sql, params) for the SQLite/PostgreSQL dialect. The SET values come first and the WHERE values after, in the order the placeholders appear, which is the order every driver binds in.

pub fn Update::build_for(Self, Dialect) -> (String, Array[@moondb.Value])

Render (sql, params) for dialect. Identical everywhere but RETURNING, which MySQL does not have and so does not get.

pub fn Update::returning(Self, String) -> Self

Return col from each updated row (SQLite 3.35+, PostgreSQL). Repeatable; build_for(Mysql) drops it.

pub fn Update::returning_all(Self) -> Self

Return every column of each updated row (RETURNING *). See returning.

pub fn Update::set(Self, String, @moondb.Value) -> Self

Assign one column a bound value. Repeated calls set each, in the order given.

pub fn Update::set_expr(Self, String, String, vals? : Array[@moondb.Value]) -> Self

Assign one column an expression instead of a value — set_expr("n", "n + ?", vals=[Int(1)]) renders SET n = n + ?. The expression is rendered verbatim, so it can read the row's current columns, which a bound value cannot; anything that varies belongs in vals, one per ?, and binds in the order the placeholders appear.

pub fn Update::where_(Self, String, String, @moondb.Value) -> Self

Narrow the update with col op ?. Conditions are ANDed together.

pub fn Update::where_in(Self, String, Select) -> Self

Narrow the update with col IN (subquery) — "update the rows some other query picks out". The subquery's bound values splice in at the predicate's position.

pub fn Update::where_not_in(Self, String, Select) -> Self

Narrow the update with col NOT IN (subquery). See where_in.

pub fn Update::where_pred(Self, Predicate) -> Self

Narrow the update with a boolean Predicate tree — the AND/OR/NOT grouping the flat where_ cannot express. See Select::where_pred.

moonbitstack/moonorm/session

session

Types 6

pub(all) enum IsolationLevel {
  ReadUncommitted
  ReadCommitted
  RepeatableRead
  Serializable
} derive(Eq)

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.

pub(all) suberror LostUpdate {
  LostUpdate(String)
}

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.

pub struct RowStream[T] {
  cursor : &@moondb.Cursor
  model : @schema.Model[T]
}

A typed streaming cursor: each [next](RowStream::next) decodes one row into a record via the model. The record-level counterpart to [Session::stream].

pub struct Savepoint {
  sess : Session
  name : String
  depth : Int32
  mut finished : Bool
}

A nested transaction opened by Session::begin_nested, wrapping one SAVEPOINT whose name and depth the session tracks for you. 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.

pub struct Session {
  driver : &@moondb.Driver
  mut sp_depth : Int32
  mut sp_seq : Int32
  id : Int32
  pending : Array[Work]
  dirty : Array[Work]
  deleted : Array[Work]
  detach : Array[() -> Unit]
}

An explicit unit-of-work over a @moondb.Driver. Every statement here is issued explicitly, the faithful MoonBit equivalent given the absence of attribute interception. It holds the backend as a &@moondb.Driver trait object, so one Session type drives every backend.

pub(all) struct TxOptions {
  isolation : IsolationLevel?
  read_only : Bool
}

Options for a transaction opened by Session::begin_with: an optional isolation level and a read_only flag. A plain value the session renders into the backend's transaction-characteristics statement.

Functions & methods 52

pub fn IsolationLevel::keyword(Self) -> String

The ANSI keyword for an isolation level ("READ COMMITTED", "SERIALIZABLE", …).

pub fn[T] RowStream::close(Self[T]) -> Unit

Release the underlying cursor early.

pub fn[T] RowStream::next(Self[T]) -> T? raise @moondb.DbError

The next decoded record, or None once the result is exhausted.

pub fn Savepoint::commit(Self) -> Unit raise @moondb.DbError

The same as [release]: keeps the nested work, under the name a reader coming from COMMIT looks for.

pub fn Savepoint::depth(Self) -> Int32

This savepoint's nesting depth (1 for the outermost begin_nested, 2 for one opened inside it, and so on).

pub fn Savepoint::name(Self) -> String

The generated SQL name of this savepoint.

pub fn Savepoint::release(Self) -> Unit raise @moondb.DbError

Release this savepoint (RELEASE SAVEPOINT <name>), keeping the work done since it opened and merging it into the enclosing transaction. Terminal and idempotent.

pub fn Savepoint::rollback(Self) -> Unit raise @moondb.DbError

Roll back and close this savepoint, discarding everything done since it opened while leaving the enclosing transaction intact. It issues ROLLBACK TO SAVEPOINT followed by RELEASE SAVEPOINT, so, the savepoint is terminal afterwards and the depth drops. Terminal and idempotent.

pub fn Session::add(Self, @query.Insert) -> @moondb.ExecResult raise @moondb.DbError

Build and run an Insert **now**, returning the affected-row count and new rowid. This is the statement-level form: it goes to the driver on this call and never touches the unit of work. To queue a record instead and let flush order and emit it, use add_record.

This renders the SQLite/PostgreSQL form (ON CONFLICT upserts, RETURNING); for a MySQL upsert build the dialect form explicitly — session.execute(stmt.build_for(Mysql)).

pub fn[T] Session::add_record(Self, @schema.Model[T], T) -> Unit

Queue record for insertion. Nothing reaches the database until flush, or until commit flushes on the caller's behalf; the delay is what lets the flush order parents before children and register the row in the identity map. To insert on this call instead, use insert_record.

pub fn[T] Session::all(Self, @schema.Model[T]) -> Array[T] raise @moondb.DbError

Fetch every row of a model's table, decoded into records (SELECT <cols> FROM <table>).

pub fn Session::begin(Self) -> Unit raise @moondb.DbError

Begin an explicit transaction. Delegates to the driver's transaction bracket (@moondb.Driver::begin) rather than emitting BEGIN as text, so a driver that manages transactions out-of-band (or maps them to savepoints) stays in control.

pub fn Session::begin_nested(Self) -> Savepoint raise @moondb.DbError

Open a nested transaction — SAVEPOINT <auto-name> — and return a handle that tracks its name and depth, so callers never spell (or risk mis-spelling) a savepoint name. Nesting begin_nested inside another increases the depth; each handle's release/rollback brings it back down. This is the ergonomic, depth-tracked counterpart of the raw savepoint/rollback_to/release trio. The generated name (moonorm_sp_<n>, n strictly increasing per session) is a bare identifier by construction, so it is injection-safe without a runtime check.

pub fn Session::begin_with(Self, TxOptions, dialect? : @query.Dialect) -> Unit raise @moondb.DbError

Begin a transaction with explicit isolation / read-only options. It brackets the driver's begin with the SET TRANSACTION (or PRAGMA) statements opts renders for dialect, ordered so each backend accepts them: before BEGIN for MySQL and SQLite, after BEGIN for PostgreSQL (see TxOptions::to_sql). dialect defaults to Postgres, whose SET TRANSACTION ISOLATION LEVEL spelling is the ANSI-standard one. Commit or roll back with the usual commit/rollback.

pub fn Session::close(Self) -> Unit

Close the session's underlying connection, dropping anything still queued and every record the identity map holds.

pub fn Session::commit(Self) -> Unit raise @moondb.DbError

Flush the unit of work, then commit the current transaction. Queued records reach the database here if nothing has flushed them yet, which is why a caller can add and commit without naming flush at all. With an empty queue this is the bare driver commit.

pub fn[T] Session::create_table(Self, @schema.Model[T], if_not_exists? : Bool) -> @moondb.ExecResult raise @moondb.DbError

Create the table backing model from its declared columns, then its indexes (see Model::create_table_sql and Model::create_indexes_sql). With if_not_exists=true the DDL is idempotent.

pub fn[T] Session::delete_record(Self, @schema.Model[T], T) -> Unit raise @moondb.DbError

Queue record for deletion. The DELETE is keyed on the record's primary key and runs last, in the reverse dependency order, so a child row goes before the parent it points at. Flushing it also drops the record from the identity map. Raises QueryError when the model declares no primary key or the record does not carry one. To delete on this call instead, build a Delete and use remove.

pub fn Session::deleted_count(Self) -> Int32

How many records are queued for deletion.

pub fn Session::dirty_count(Self) -> Int32

How many records are queued for update.

pub fn Session::execute(Self, String, Array[@moondb.Value]) -> @moondb.ExecResult raise @moondb.DbError

Execute a raw statement with bound params.

pub fn[T] Session::expunge(Self, @schema.Model[T], T) -> Unit

Drop record from the identity map, so the next get reads the row back from the database and returns a fresh instance. Queued work for the record is left alone.

pub fn Session::expunge_all(Self) -> Unit

Empty the identity map: every model this session has loaded through forgets it.

pub fn Session::fetch(Self, @query.Select) -> Array[@moondb.Row] raise @moondb.DbError

Build and run a Select, returning the matched rows.

pub fn[T] Session::fetch_as(Self, @schema.Model[T], @query.Select) -> Array[T] raise @moondb.DbError

Run a Select and decode every row into a T via model. Use it with a Model::select() (optionally refined with where_/order_by) to get records rather than raw Rows back.

pub fn Session::flush(Self) -> Unit raise @moondb.DbError

Emit every queued statement, the point where the unit of work becomes SQL. Inserts go first in dependency order, so a parent row precedes any child that references it; then the updates; then the deletes in the reverse order, so a child goes before its parent. Nothing here commits. A statement that has run leaves the queue even when a later one raises, so the failed flush can be rolled back without replaying what already executed.

pub fn[T] Session::get(Self, @schema.Model[T], @moondb.Value) -> T? raise @moondb.DbError

Fetch by primary key, consulting the identity map before the database. A row this session has already loaded comes back as the *same* record instance and costs no query; anything else is a SELECT … WHERE pk = ? LIMIT 1 whose result is decoded once and kept. None means there is no such row. Because the instance is shared, a record with mutable fields is shared too: two holders of the same row see each other's writes, which is the point of an identity map. expunge drops one; get_by takes a composite key. The map is scoped to this session *and* to the Model value passed in, so keep the model in a let — one rebuilt on every call is a new descriptor each time and carries no history.

pub fn[T] Session::get_by(Self, @schema.Model[T], Array[@moondb.Value]) -> T? raise @moondb.DbError

get for a composite primary key: one value per declared key column, in declaration order. Raises QueryError when the model declares no primary key, or when key is not as wide as the one it declares.

pub fn[T] Session::insert_record(Self, @schema.Model[T], T) -> @moondb.ExecResult raise @moondb.DbError

Insert a mapped record through its model **now**, binding the model's to_columns pairs. Returns the affected-row count and new rowid. Nothing is queued and the identity map is not consulted; add_record is the unit-of-work counterpart that defers to flush.

pub fn[S, T] Session::load(Self, S, @schema.Relation[S, T]) -> Array[T] raise @moondb.DbError

Eagerly load the related records of a 1:N relationship: given a source parent, run the relationship's query and decode every matching child into a record. MoonBit has no attribute interception, so the load is a call. The match value is bound, so eager loading stays injection-safe.

pub fn[S, T] Session::load_batch(Self, Array[S], @schema.Relation[S, T]) -> Array[Array[T]] raise @moondb.DbError

Eagerly load a 1:N relationship for *many* sources in a single query, avoiding the N+1 problem. Naively calling load in a loop fires one SELECT per source; this instead issues one SELECT … WHERE key_column IN (all the source keys), then buckets the fetched children back to their parents in memory. The return is index-aligned with sources: result[i] is the list of children whose foreign key matches sources[i]'s key (empty if none). Children are matched to a source by comparing the child row's key_column value against the source's extracted key, so a source with no children yields an empty list and children are shared correctly when two sources happen to hold the same key. An empty sources returns an empty array without touching the database.

pub fn[S, T] Session::load_many(Self, S, @schema.ManyToMany[S, T]) -> Array[T] raise @moondb.DbError

Eagerly load the related records of a many-to-many relationship: run the junction JOIN for source and decode every matching target row. The N:M counterpart of Session::load, and like it the source key is bound, so eager loading stays injection-safe.

pub fn[S, T] Session::load_one(Self, S, @schema.Relation[S, T]) -> T? raise @moondb.DbError

Eagerly load the single related record of a N:1 relationship (e.g. a child's parent): the first matching row decoded into a record, or None if there is no match. Like load, the match value is bound.

pub fn[S, T] Session::load_one_batch(Self, Array[S], @schema.Relation[S, T]) -> Array[T?] raise @moondb.DbError

Eagerly load a N:1 relationship for *many* sources in a single query — the to-one counterpart of load_batch. One SELECT … WHERE key_column IN (…) fetches every distinct parent; the return is index-aligned with sources, result[i] being sources[i]'s parent (Some) or None when there is no match. Avoids the N+1 that a per-source load_one loop would incur.

pub fn Session::modify(Self, @query.Update) -> @moondb.ExecResult raise @moondb.DbError

Build and run an Update, returning the affected-row count.

pub fn Session::modify_versioned(Self, @query.Update, what? : String) -> @moondb.ExecResult raise

Run a versioned UPDATE under optimistic concurrency control. stmt must carry the version predicate in its WHERE (e.g. where_(version_col, "=", expected)) and bump the version column in its SET. If the update matches no row the version has moved on since it was read, a lost update, and this raises LostUpdate instead of silently doing nothing. The whole statement is parameterised, so both the version guard and the new values stay bound.

pub fn Session::new(&@moondb.Driver) -> Self

Wrap a connected driver in a session.

pub fn Session::pending_count(Self) -> Int32

How many records are queued for insertion.

pub fn Session::query(Self, String, Array[@moondb.Value]) -> Array[@moondb.Row] raise @moondb.DbError

Run a raw query with bound params and return its rows.

pub fn Session::query_stream(Self, String, Array[@moondb.Value]) -> &@moondb.Cursor raise @moondb.DbError

Stream a raw query's rows through a [@moondb.Cursor] instead of materialising them. This seam is synchronous, so only a synchronous driver with incremental fetch streams lazily here: moonsqlite steps its prepared statement row by row. A wire driver's cursor is async and cannot come through here at all — moonpostgres implements @moondb.AsyncDriver instead, so it never sits behind a Session; MySQL's synchronous adapter falls back to a materialised cursor. For lazy streaming over the wire call PgConn/MysqlConn::query_stream directly under an event loop.

pub fn Session::release(Self, String) -> @moondb.ExecResult raise @moondb.DbError

Release a savepoint (RELEASE SAVEPOINT <name>), merging its work into the enclosing transaction (or savepoint). Rejects a non-identifier name.

pub fn Session::remove(Self, @query.Delete) -> @moondb.ExecResult raise @moondb.DbError

Build and run a Delete, returning the affected-row count.

pub fn Session::rollback(Self) -> Unit raise @moondb.DbError

Roll back the current transaction, abandoning whatever is still queued and emptying the identity map. The records held there describe rows the rollback has just undone, so keeping them would hand out state the database no longer has.

pub fn Session::rollback_to(Self, String) -> @moondb.ExecResult raise @moondb.DbError

Roll back to a savepoint (ROLLBACK TO SAVEPOINT <name>), undoing everything done since it was opened while keeping the savepoint (and the outer transaction) active. Rejects a non-identifier name with QueryError.

pub fn Session::savepoint(Self, String) -> @moondb.ExecResult raise @moondb.DbError

Open a nested transaction with SAVEPOINT <name>. Savepoints nest arbitrarily, and finalised with release(name). name must be a bare identifier (it is interpolated as an identifier, never a bound value); a non-identifier raises QueryError rather than reaching the database. The statement is issued through execute, so a backend that supports SQL savepoints (SQLite, Postgres) runs it verbatim.

pub fn Session::savepoint_depth(Self) -> Int32

How many nested savepoints opened via begin_nested are currently active.

pub fn Session::stream(Self, @query.Select) -> &@moondb.Cursor raise @moondb.DbError

Build and stream a Select, yielding raw rows through a cursor.

pub fn[T] Session::stream_as(Self, @schema.Model[T], @query.Select) -> RowStream[T] raise @moondb.DbError

Build and stream a Select, decoding each row into a T on demand — the streaming counterpart to [fetch_as].

pub fn[T] Session::update_record(Self, @schema.Model[T], T) -> Unit raise @moondb.DbError

Queue record's current state as an UPDATE keyed on its primary key, which notices a change by instrumenting attribute writes. MoonBit intercepts nothing, so a changed record is one the caller says is changed. The statement sets every non-key column, is emitted after the inserts, and refreshes the identity map. Raises QueryError when the model declares no primary key, when the record does not carry one, or when every column belongs to the key and there is nothing to set. To update on this call instead, build an Update and use modify.

pub fn[D : @moondb.Driver, T] Session::with_pool(@pool.Pool[D], (Self) -> T raise) -> T raise

Run f with a Session on a connection borrowed from pool, returning the connection to the pool when f finishes or raises. The unit of work owns its transaction: commit or roll back inside f before it returns, since the connection is reused as-is.

pub fn TxOptions::default() -> Self

Transaction options with everything defaulted off (backend default isolation, read-write). Set what you need: { ..TxOptions::default(), isolation: Some(Serializable) }.

pub fn TxOptions::to_sql(Self, @query.Dialect) -> Array[String]

Render these options into the statements that impose them for dialect. On PostgreSQL and MySQL that is SET TRANSACTION ISOLATION LEVEL <level> and/or SET TRANSACTION READ ONLY; on SQLite, which has no such statement, it is PRAGMA read_uncommitted = 1 for the one weaker level it supports (every other level is SQLite's default serializable behaviour, so nothing is emitted) and PRAGMA query_only = 1 for read-only. An empty result means the backend's defaults already satisfy the options.

moonbitstack/moonorm/migrate

migrate

Types 3

pub(all) enum ColumnDiff {
  Added(name~ : String, col_type~ : @schema.ColumnType)
  Removed(String)
  TypeChanged(name~ : String, from~ : @schema.ColumnType, to~ : @schema.ColumnType)
} derive(Eq)

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.

pub(all) struct Migration {
  version : Int32
  name : String
  up : Array[String]
  down : Array[String]
}

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.

pub struct Migrator {
  table : String
}

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.

Functions & methods 13

pub fn diff_schema(Array[@schema.Column], Array[@schema.Column], dialect? : @query.Dialect) -> Array[ColumnDiff]

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.

With dialect the types compare as that engine writes them, so a BigIntType declared against SQLite, where every integer is INTEGER, is no change; without it they compare as declared.

pub fn index_ddl(String, String, Array[String], unique? : Bool) -> String

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.

pub fn Migrator::applied_versions(Self, @session.Session) -> Array[Int32] raise @moondb.DbError

Every applied version, ascending.

pub fn Migrator::current_version(Self, @session.Session) -> Int32 raise @moondb.DbError

The highest applied version, or 0 when nothing has been applied yet.

pub fn Migrator::down_to(Self, @session.Session, Array[Migration], Int32) -> Int32 raise @moondb.DbError

Roll back every applied migration whose version is greater than target, in descending version order, running each one's down statements and removing its bookkeeping row. Returns how many were rolled back. down_to(0) unwinds everything.

pub fn Migrator::ensure_table(Self, @session.Session) -> Unit raise @moondb.DbError

Create the bookkeeping table if it is absent. Idempotent, so it is safe to call before every up/down.

pub fn Migrator::new(table? : String) -> Self raise @moondb.DbError

A migrator recording applied versions in table (default schema_migrations). The name must be a bare SQL identifier — it is interpolated as an identifier, never bound — so a non-identifier is refused up front.

pub fn Migrator::up(Self, @session.Session, Array[Migration]) -> Int32 raise @moondb.DbError

Apply every pending migration (those whose version is not yet recorded), in ascending version order, running each one's up statements and recording it. Returns how many were applied. Already-applied versions are skipped, so this is safe to run repeatedly (it converges the schema to the latest version).

pub fn reflect_columns(Array[@moondb.Row]) -> Array[@schema.Column] raise @moondb.DbError

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.

pub fn reflect_columns_mysql(Array[@moondb.Row]) -> Array[@schema.Column] raise @moondb.DbError

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

pub fn reflect_columns_pg(Array[@moondb.Row]) -> Array[@schema.Column] raise @moondb.DbError

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

pub fn reflect_table(&@moondb.Driver, String) -> Array[@schema.Column] raise @moondb.DbError

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

pub fn reflect_table_for(&@moondb.Driver, String, @query.Dialect) -> Array[@schema.Column] raise @moondb.DbError

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.