moonbitstack/moonorm/schema does not have a README file

    Column

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

    Column::ddl

    fn Column::ddl(self : Column, 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.

    Column::ddl_for

    fn Column::ddl_for(self : Column, dialect :
    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.

    ColumnType

    pub(all) enum ColumnType {
    IntType
    BigIntType
    TextType
    RealType
    BlobType
    BoolType
    VarcharType(Int)
    NumericType
    DateTimeType
    DateType
    TimeType
    UuidType
    JsonType
    UserType(CustomColumnType)
    } 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.

    ColumnType::equal

    fn ColumnType::equal(ColumnType, ColumnType) -> Bool

    ColumnType::not_equal

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

    ColumnType::sql_for

    fn ColumnType::sql_for(self : ColumnType, dialect :
    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.

    CustomColumnType

    pub(all) struct CustomColumnType {
    sqlite : String
    postgres : String
    mysql : String
    } derive(Eq)

    A user-defined SQL storage type: the declared type keyword each of the three dialects emits for it. This is the extension point for a storage type the built-in ColumnType variants do not name — a citext, a tsvector, a domain type. Pair it with ToValue/FromValue on the field's type so values still bind and read, and the column stores in whatever native type each backend is told to use.

    CustomColumnType::equal

    CustomColumnType::not_equal

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

    Field

    pub struct Field[T] {
    column : Column
    encode : (T) ->
    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]).

    Field::name

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

    The name of the column this field maps to.

    Index

    pub(all) struct Index {
    name : String?
    columns : Array[String]
    unique : Bool
    } derive(Eq,
    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, as SQLAlchemy's Index does. Without a name it is called ix_<table>_<columns>, or uq_ when unique, after SQLAlchemy's naming convention.

    Index::equal

    fn Index::equal(Index, Index) -> Bool

    Index::not_equal

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

    Index::to_repr

    LoadStrategy

    pub(all) enum LoadStrategy {
    SubqueryLoad
    JoinedLoad
    RaiseLoad
    }

    How a relationship is loaded. SubqueryLoad is the default: one IN query for a whole list of sources (selectinload). JoinedLoad fetches through a JOIN against a derived table of the source keys (joinedload). RaiseLoad refuses, so a caller can assert a relation is not touched here (raiseload).

    ManyToMany

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

    }

    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.

    ManyToMany::batch_sql

    fn[S, T] ManyToMany::batch_sql(self : ManyToMany[S, T], sources : Array[S]) -> (String, Array[
    Value
    ])

    The query behind an N:M batch load: target rows joined to the junction, the junction's source_fk projected as moonorm_src so rows bucket back, filtered to junction.source_fk IN (…) with every key bound. Empty sources match nothing.

    ManyToMany::joined_sql

    fn[S, T] ManyToMany::joined_sql(self : ManyToMany[S, T], sources : Array[S]) -> (String, Array[
    Value
    ])

    The query behind a JoinedLoad of an N:M relation: target rows joined to the junction and to a derived table of the source keys, the junction's source_fk projected as moonorm_src, joined on junction.source_fk = moonorm_keys.moonorm_key.

    ManyToMany::query

    fn[S, T] ManyToMany::query(self : ManyToMany[S, T], source : S) ->
    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.

    ManyToMany::source_value

    fn[S, T] ManyToMany::source_value(self : ManyToMany[S, T], source : S) ->
    Value

    The junction-side key of a source, so an N:M batch load can bucket every fetched target row back to the source it belongs to.

    Model

    pub struct Model[T] {
    table : String
    columns : Array[Column]
    from_row : (
    Row
    ) -> T raise
    DbError

    to_columns : (T) -> Array[(String,
    Value
    )]
    indexes : Array[Index]
    filter : Array[(String,
    Value
    )]
    joins : Array[(String, String)]
    projection : Array[String]
    parent : (String, Array[String], String)?
    ident : Map[Int, 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.

    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.

    Model::column_names

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

    The declared column names, in order.

    Model::create_indexes_sql

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

    create_indexes_sql_for in the dialect create_table_sql writes.

    Model::create_indexes_sql_for

    fn[T] Model::create_indexes_sql_for(self : Model[T], dialect :
    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.

    Model::create_table_sql

    fn[T] Model::create_table_sql(self : Model[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). This is the explicit counterpart of SQLAlchemy's metadata.create_all().

    Model::create_table_sql_for

    fn[T] Model::create_table_sql_for(self : Model[T], dialect :
    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.

    Model::discriminate

    fn[T] Model::discriminate(self : Model[T], column~ : String, identity~ : String) -> Model[T]

    A single-table subclass: the table is shared with its base, and a discriminator column tells the rows apart. The returned model

    • filters every read to column = identity,
    • forces column = identity into every insert, so a subclass row cannot be written as a base row by accident, and
    • declares the discriminator column if the base already does — adding it to the column list only when it is missing.

    The base's own model stays as it was and reads every row of the table; a subclass model reads only its own. This is the mapping SQLAlchemy spells polymorphic_on plus polymorphic_identity.

    Model::from_fields

    fn[T] Model::from_fields(table : String, fields : Array[Field[T]], from_row : (
    Row
    ) -> T raise
    DbError
    , indexes? : Array[Index]) -> Model[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) }, )

    Model::insert_of

    fn[T] Model::insert_of(self : Model[T], record : T) ->
    Insert

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

    On a joined-table subclass this is the statement for the subclass's own table: the parent half is a second statement, which Session::insert_record and Session::add_record write for you, in the only order the foreign key allows.

    Model::is_joined

    fn[T] Model::is_joined(self : Model[T]) -> Bool

    Whether this model's rows are split across two tables (a joined-table subclass).

    Model::joined

    fn[T] Model::joined(self : Model[T], parent_table~ : String, parent_columns~ : Array[String], fk? : String) -> Model[T]

    A joined-table subclass: the base's columns live in parent_table, this class's own columns in the model's table, and one row is the join of the two on the primary key. The returned model

    • projects parent_table's columns qualified, then this table's, so from_row reads the parent's half before the subclass's — the order the columns were declared in,
    • joins parent_table on <parent_table>.<key> = <table>.<fk>, and
    • splits writes: an insert writes the parent row first, then the subclass row carrying the same key, which is the only order the foreign key allows.

    fk names the subclass column that references the parent key; it defaults to the parent key's own name, which is the usual id in both tables.

    Model::map_row

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

    Model::map_rows

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

    Model::new

    fn[T] Model::new(table : String, columns : Array[Column], from_row : (
    Row
    ) -> T raise
    DbError
    , to_columns : (T) -> Array[(String,
    Value
    )], indexes? : Array[Index]) -> Model[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) \}.

    Model::parent_fk

    fn[T] Model::parent_fk(self : Model[T]) -> String

    The subclass column that references the parent key, or "".

    Model::parent_key

    fn[T] Model::parent_key(self : Model[T]) -> String

    The parent key column of a joined-table subclass — the column both tables share, which the child's foreign key points at. Empty for a plain model.

    Model::parent_table

    fn[T] Model::parent_table(self : Model[T]) -> String

    The table a joined-table subclass's parent half lives in, or "".

    Model::parent_tables

    fn[T] Model::parent_tables(self : Model[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.

    Model::pk_columns

    fn[T] Model::pk_columns(self : Model[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.

    Model::pk_of

    fn[T] Model::pk_of(self : Model[T], record : T) -> Array[
    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.

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

    A subclass model — one built by discriminate or joined — carries its own predicate, joins and qualified projection, so this is also the read that returns only its rows, joined where the mapping spans two tables.

    Model::spec

    fn[T] Model::spec(self : Model[T]) -> TableSpec

    This model's table as a TableSpec — the bridge from a typed model to the schema data autogenerate compares.

    Model::split_columns

    fn[T] Model::split_columns(self : Model[T], columns : Array[(String,
    Value
    )]) -> Array[(String, Array[(String,
    Value
    )])]

    Split a record's bound columns into one group per table it is written to: one (table, columns) pair for a plain model, and parent-then-subclass for a joined mapping. The subclass group carries the foreign key even though the key column belongs to the parent, because the subclass row is what holds the reference.

    The order is the order the rows must be written in: a foreign key means the parent row has to exist first.

    Model::table_descriptor

    fn[T] Model::table_descriptor(self : Model[T]) ->
    Table

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

    Relation

    pub struct Relation[S, T] {
    target : Model[T]
    key_column : String
    source_key : (S) ->
    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, 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.

    Relation::batch_query

    fn[S, T] Relation::batch_query(self : Relation[S, T], sources : Array[S]) ->
    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.

    Relation::joined_sql

    fn[S, T] Relation::joined_sql(self : Relation[S, T], sources : Array[S]) -> (String, Array[
    Value
    ])

    The query behind a JoinedLoad of a 1:N / N:1 relation: the target rows joined to a derived table of the distinct source keys on target.key_column =moonorm_src.moonorm_key. The keys bind as ?, so the load stays injection-safe. With no sources it matches nothing (WHERE 1 = 0) instead of an empty derived table.

    Relation::query

    fn[S, T] Relation::query(self : Relation[S, T], source : S) ->
    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.

    Relation::source_value

    fn[S, T] Relation::source_value(self : Relation[S, T], source : S) ->
    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.

    Schema

    pub(all) struct Schema {
    tables : Array[TableSpec]
    }

    A set of tables — a declared metadata tree, or everything reflected off a database. The unit autogenerate compares and the unit a snapshot renders.

    Schema::names

    fn Schema::names(self : Schema) -> Array[String]

    The table names, in order.

    Schema::new

    fn Schema::new(tables : Array[TableSpec]) -> Schema

    A schema from its tables.

    Schema::snapshot

    fn Schema::snapshot(self : Schema, dialect :
    Dialect
    ) -> String

    A normalised text rendering of the schema, for comparing two snapshots of the same database. Column types are written as dialect spells them, so an Int64 and an Int both reflected as INTEGER on SQLite compare equal — the physical structure is what a round trip has to preserve, not the declared vocabulary.

    Lines are table, then one column line per column in order, then one index line per index sorted by name, so a diff points at the table and the line that changed. Tables are sorted by name for the same reason.

    Schema::table

    fn Schema::table(self : Schema, name : String) -> TableSpec?

    The table named name, or None.

    TableSpec

    pub(all) struct TableSpec {
    name : String
    columns : Array[Column]
    indexes : Array[Index]
    }

    One table described by its columns and indexes — the record-type-free view of a Model. Two TableSpecs are what a schema comparison needs: a name, the ordered columns, and the indexes on them.

    TableSpec::new

    fn TableSpec::new(name : String, columns : Array[Column], indexes? : Array[Index]) -> TableSpec

    Describe a table.

    add_column_sql

    fn add_column_sql(table : String, column : Column, dialect :
    Dialect
    ) -> String

    The statement that adds one column. SQLite, PostgreSQL and MySQL share the spelling, so the dialect is not consulted.

    belongs_to

    fn[S, T, V :
    ToValue
    ] belongs_to(target : Model[T], target_key : String, child_key : (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.

    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.

    create_index_sql

    fn create_index_sql(name : String, table : String, columns : Array[String], unique? : Bool, if_not_exists? : Bool, dialect? :
    Dialect
    ) -> String

    CREATE INDEX. if_not_exists is emitted where the dialect accepts it; MySQL has no such clause for an index, so it is dropped there rather than producing a statement the server rejects.

    create_table_sql

    fn create_table_sql(name : String, columns : Array[Column], dialect :
    Dialect
    , if_not_exists? : Bool) -> String

    CREATE TABLE for a column list, rendered for dialect. A single primary key stays inline; several become one table-level PRIMARY KEY (a, b) constraint, since SQL allows only one inline key per table. MySQL gets the ENGINE=InnoDB suffix it needs to honour foreign keys.

    custom_column_type

    fn custom_column_type(sqlite~ : String, postgres~ : String, mysql~ : String) -> CustomColumnType

    Declare a user-defined storage type from its per-dialect SQL spelling.

    drop_column_sql

    fn drop_column_sql(table : String, name : String) -> String

    The statement that drops one column.

    drop_index_sql

    fn drop_index_sql(name : String, table : String?, if_exists? : Bool, dialect? :
    Dialect
    ) -> String raise
    DbError

    DROP INDEX. SQLite and PostgreSQL name an index on its own; MySQL spells it DROP INDEX name ON table, so there the table is part of the statement and leaving it out raises rather than emitting SQL the server would reject.

    drop_table_sql

    fn drop_table_sql(name : String, if_exists? : Bool) -> String

    DROP TABLE, with the idempotent form when if_exists.

    field

    fn[T, V :
    ToValue
    ] field(name : String, col_type : ColumnType, encode : (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.

    has_many

    fn[S, T, V :
    ToValue
    ] has_many(target : Model[T], foreign_key : String, parent_key : (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.

    index

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

    index_name

    fn index_name(table : TableSpec, ix : Index) -> String

    The name of an index, applying the same default the DDL uses: ix_<table>_<cols>, or uq_ when unique.

    many_to_many

    fn[S, T, V :
    ToValue
    ] many_to_many(target : 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.

    rename_column_sql

    fn rename_column_sql(table : String, from : String, to : String) -> String

    ALTER TABLE … RENAME COLUMN a TO b (SQLite 3.25+, MySQL 8.0, PostgreSQL 8).

    rename_table_sql

    fn rename_table_sql(from : String, to : String) -> String

    ALTER TABLE … RENAME TO …. All three dialects share it.

    user_type

    fn user_type(sqlite~ : String, postgres~ : String, mysql~ : String) -> ColumnType

    A ColumnType for a user-defined storage type — the open counterpart of the built-in variants. It renders sqlite on SQLite, postgres on PostgreSQL and mysql on MySQL.