Sign in

    moonbitstack/moonorm/session does not have a README file

    LostUpdate

    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.
    impl Show for LostUpdate

    LostUpdate::output

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

    LostUpdate::to_string

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

    IsolationLevel

    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.

    IsolationLevel::equal

    IsolationLevel::keyword

    fn IsolationLevel::keyword(self : IsolationLevel) -> String

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

    IsolationLevel::not_equal

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

    RowStream

    pub struct RowStream[T] {
    cursor : &
    Cursor

    model :
    Model
    [T]
    }

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

    RowStream::close

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

    Release the underlying cursor early.

    RowStream::next

    fn[T] RowStream::next(self : RowStream[T]) -> T? raise
    DbError

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

    Savepoint

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

    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.

    Savepoint::commit

    SQLAlchemy spells "keep the nested work" as commit(); this is the alias for [release].

    Savepoint::depth

    fn Savepoint::depth(self : Savepoint) -> Int

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

    Savepoint::name

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

    The generated SQL name of this savepoint.

    Savepoint::release

    fn Savepoint::release(self : Savepoint) -> Unit raise
    DbError

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

    Savepoint::rollback

    fn Savepoint::rollback(self : Savepoint) -> Unit raise
    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 — like SQLAlchemy's nested rollback() — the savepoint is terminal afterwards and the depth drops. Terminal and idempotent.

    Session

    pub struct Session {
    driver : &
    Driver

    sp_depth : Int
    sp_seq : Int
    id : Int
    pending : Array[Work]
    dirty : Array[Work]
    deleted : Array[Work]
    detach : Array[() -> Unit]
    }

    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.

    Session::add

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

    Session::add_record

    fn[T] Session::add_record(self : Session, model :
    Model
    [T], record : T) -> Unit

    Queue record for insertion — SQLAlchemy's Session.add. 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.

    Session::all

    Fetch every row of a model's table, decoded into records (`SELECT FROM

    Session::begin

    fn Session::begin(self : Session) -> Unit raise
    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.

    Session::begin_nested

    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, mirroring SQLAlchemy's begin_nested().

    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.

    Session::begin_with

    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.

    Session::close

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

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

    Session::commit

    fn Session::commit(self : Session) -> Unit raise
    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.

    Session::create_table

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

    Session::delete_record

    fn[T] Session::delete_record(self : Session, model :
    Model
    [T], record : T) -> Unit raise
    DbError

    Queue record for deletion — SQLAlchemy's Session.delete. 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.

    Session::deleted_count

    fn Session::deleted_count(self : Session) -> Int

    How many records are queued for deletion — SQLAlchemy's Session.deleted.

    Session::dirty_count

    fn Session::dirty_count(self : Session) -> Int

    How many records are queued for update — SQLAlchemy's Session.dirty.

    Session::execute

    Execute a raw statement with bound params.

    Session::expunge

    fn[T] Session::expunge(self : Session, model :
    Model
    [T], record : T) -> Unit

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

    Session::expunge_all

    fn Session::expunge_all(self : Session) -> Unit

    Empty the identity map: every model this session has loaded through forgets it. SQLAlchemy's Session.expunge_all.

    Session::fetch

    Build and run a Select, returning the matched rows.

    Session::fetch_as

    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.

    Session::flush

    fn Session::flush(self : Session) -> Unit raise
    DbError

    Emit every queued statement — SQLAlchemy's Session.flush, 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.

    Session::get

    Fetch by primary key, consulting the identity map before the database — SQLAlchemy's Session.get. 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.

    Session::get_by

    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.

    Session::insert_record

    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.

    Session::load

    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. This is the explicit stand-in for SQLAlchemy's transparent lazy load (parent.children firing a SELECT on attribute access) — MoonBit has no attribute interception, so the load is a call, exactly as Diesel and GORM require. The match value is bound, so eager loading stays injection-safe.

    Session::load_batch

    fn[S, T] Session::load_batch(self : Session, sources : Array[S], rel :
    Relation
    [S, T]) -> Array[Array[T]] raise
    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). This is the explicit equivalent of SQLAlchemy's selectinload eager strategy.

    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.

    Session::load_many

    fn[S, T] Session::load_many(self : Session, source : S, rel :
    ManyToMany
    [S, T]) -> Array[T] raise
    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.

    Session::load_one

    fn[S, T] Session::load_one(self : Session, source : S, rel :
    Relation
    [S, T]) -> T? raise
    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.

    Session::load_one_batch

    fn[S, T] Session::load_one_batch(self : Session, sources : Array[S], rel :
    Relation
    [S, T]) -> Array[T?] raise
    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.

    Session::modify

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

    Session::modify_versioned

    fn Session::modify_versioned(self : Session, stmt :
    Update
    , what? : String) ->
    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 faithful equivalent of SQLAlchemy's version_id_col StaleDataError. The whole statement is parameterised, so both the version guard and the new values stay bound.

    Session::new

    Wrap a connected driver in a session.

    Session::pending_count

    fn Session::pending_count(self : Session) -> Int

    How many records are queued for insertion — SQLAlchemy's Session.new.

    Session::query

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

    Session::query_stream

    Stream a raw query's rows through a [@moondb.Cursor] instead of materialising them — the bounded-memory counterpart to [query] (SQLAlchemy's stream_results). 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.

    Session::release

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

    Session::remove

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

    Session::rollback

    fn Session::rollback(self : Session) -> Unit raise
    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.

    Session::rollback_to

    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.

    Session::savepoint

    Open a nested transaction with SAVEPOINT <name>. Savepoints nest arbitrarily, so this is the faithful equivalent of SQLAlchemy's Session.begin_nested(): work done after the savepoint can be undone with rollback_to(name) without discarding the enclosing transaction, 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.

    Session::savepoint_depth

    fn Session::savepoint_depth(self : Session) -> Int

    How many nested savepoints opened via begin_nested are currently active.

    Session::stream

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

    Session::stream_as

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

    Session::update_record

    fn[T] Session::update_record(self : Session, model :
    Model
    [T], record : T) -> Unit raise
    DbError

    Queue record's current state as an UPDATE keyed on its primary key — the explicit stand-in for SQLAlchemy's dirty tracking, 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.

    Session::with_pool

    fn[D :
    Driver
    , T] Session::with_pool(pool :
    Pool
    [D], f : (Session) -> 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.

    TxOptions

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

    TxOptions::default

    fn TxOptions::default() -> TxOptions

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

    TxOptions::to_sql

    fn TxOptions::to_sql(self : TxOptions, dialect :
    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 SETTRANSACTION 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.

    Work

    type Work

    One queued statement: the table it writes, the tables that must be written before it, the thunk that renders it at flush time, and the identity bookkeeping to run once it has executed.