morm

A lightweight MoonBit ORM with typed queries, code generation, and multi-database engines

orm
database
sql
query builder
code generation
moonbit
moon add oboard/morm@0.4.1
Download zip
Author
Version
0.4.1
License
Apache-2.0
Last updated
last month
Downloads
488
README

#oboard/morm

morm is a lightweight ORM toolkit for MoonBit.

It is built around a simple split:

  • entities describe schema
  • mormgen generates plain MoonBit code
  • query builders describe SQL intent
  • engines render and execute real database behavior

The project deliberately avoids runtime reflection and hidden ORM state.

#What It Covers

morm provides:

  • entity-to-table metadata generation with #morm.entity
  • mapper generation from annotated traits
  • typed query builders for select, insert, upsert, update, and delete
  • page-based pagination helpers with sortable Pageable (paginate / paginate_raw)
  • multi-engine support through a shared Engine contract
  • local time-type support for PlainDate, PlainTime, PlainDateTime, and ZonedDateTime
  • direct MoonBit enum support in generated ToParam / FromParam impls and schema metadata
  • generated auto timestamp handling for created_at / updated_at and explicit timestamp annotations
  • transient entity fields via #morm.transient (kept in model, excluded from physical columns and from(entity) writes)
  • PostgreSQL schema ownership and grant support via #morm.postgres.schema, #morm.postgres.table, and #morm.postgres.grant

#Install

Add the package to your application's moon.mod.json:

{ "bin-deps": { "oboard/morm": "latest" } }

#Generate Code

A typical package imports morm and the generated-code dependencies with the aliases that mormgen emits:

import {
"oboard/morm",
"oboard/morm/engine" @morm/engine,
}

Add engine packages such as "oboard/morm/engine/sqlite3" as needed by your hand-written runtime code.

This is a breaking generated-code alias change. If your package still imports "oboard/morm/engine" as the old implicit @engine alias, update the alias before regenerating code.

A typical package uses pre-build to generate .g.mbt files:

options(
"pre-build": [
{
"command": "$mod_dir/.mooncakes/oboard/morm/morm-gen $input -o $output && moonfmt -w $output",
"input": "entities.mbt",
"output": "entities.g.mbt",
},
{
"command": "$mod_dir/.mooncakes/oboard/morm/morm-gen $input -o $output && moonfmt -w $output",
"input": "mapper.mbt",
"output": "mapper.g.mbt",
},
],
)

#Entity Example

///|
using @time {type PlainDateTime}

///|
#mormentity
pub(all) struct Class {
#mormid
#mormdefault(autoincrement())
id : Int64

#mormvarchar(length="255")
name : String

created_at : PlainDateTime
updated_at : PlainDateTime
} derive(ToJson, FromJson)

Payload-free MoonBit enums can also be used directly as entity fields. mormgen will generate enum codecs and emit native enum DDL for engines that support it (currently MySQL and PostgreSQL).

#Mapper Example

///|
#mormmapper(table="class")
pub trait ClassMapper {
async save(Self, entity : Class) -> Class
}

///|
#mormmapper(table="student")
pub trait StudentMapper {
async find_student_by_id(Self, id : Int) -> Student?
async find_student_by_name(Self, name : String) -> Student?
async find_students_by_age(Self, age : Int) -> FixedArray[Student]
}

Generated save methods can assign:

  • created_at
  • updated_at
  • fields marked with #morm.auto_create_time
  • fields marked with #morm.auto_update_time

PlainDateTime fields use @morm.current_plain_date_time_utc(), and ZonedDateTime fields use @morm.current_timestamp_utc().

#Query Builder Example

let q = @morm.select_from("class")
.where_eq("id", 1)
.order_by(@morm.desc("id"))
.limit(1)

Execute through an engine:

let res = engine.exec(q)

#Pagination Example

morm pagination is 1-based (page=1 is the first page):

let pageable = @morm.pageable_with_sort(1, 20, @morm.desc("id"))

let q = @morm.select_from("student")
.where_gte("age", 18)

let page = @morm.paginate(
engine,
q,
pageable,
)

For raw SQL:

let page = @morm.paginate_raw(
engine,
"SELECT id, name, age FROM student WHERE age >= ?",
[18],
@morm.pageable(2, 10),
)

#Migration Example

@morm.auto_migrate(engine, [Class::table()])

Migration execution stays engine-specific by design.

#Documentation

See the VitePress docs in docs/:

#
Column

using @oboard/morm/table { type Column }

#
ColumnType

#
DbResult

type DbResult[T] = Result[T, DbError]

#
ForeignKey

#
Index

using @oboard/morm/table { type Index }

#
IndexType

#
OrderBy

#
Set

using @oboard/morm/engine { type Set }

#
Table

using @oboard/morm/table { type Table }

#
Where

using @oboard/morm/engine { type Where }

#
WhereType

#
Driver

pub(open) trait Driver {
fn open(Self, dsn : String) -> Result[Self, DbError]
fn close(Self) -> Result[Self, DbError]
fn ping(Self) -> Result[Unit, DbError]
fn exec(Self, sql : String, args : FixedArray[
Param
]) -> Result[Unit, DbError]
fn query(Self, sql : String, args : FixedArray[
Param
]) -> Result[FixedArray[Json], DbError]
}

#
Entity

pub(open) trait Entity : ToJson +
FromParam
{
fn table(Self) ->
Table

}

#
Association

pub(all) struct Association {
name : String
kind : AssociationKind
owner_key : String
target_table : String
target_key : String
join_table : String?
join_owner_key : String?
join_target_key : String?
} derive(Eq, ToJson,
FromJson
)

impl Show for Association

#
AssociationKind

pub(all) enum AssociationKind {
BelongsTo
HasOne
HasMany
ManyToMany
} derive(Eq, ToJson,
FromJson
)

#
ColumnDiff

pub(all) struct ColumnDiff {
added : FixedArray[
Column
]
removed : FixedArray[
Column
]
changed : FixedArray[(String,
Column
,
Column
)]
} derive(Eq, ToJson)

impl Show for ColumnDiff

#
DbError

pub(all) struct DbError {
message : String
} derive(ToJson)

impl Show for DbError

#
DeleteQuery

impl Show for DeleteQuery

#
DeleteQuery::from

#
DeleteQuery::where_eq

fn[V :
ToParam
] DeleteQuery::where_eq(self : DeleteQuery, col : String, value : V) -> DeleteQuery

#
DeleteQuery::where_gt

fn[V :
ToParam
] DeleteQuery::where_gt(self : DeleteQuery, col : String, value : V) -> DeleteQuery

#
DeleteQuery::where_gte

fn[V :
ToParam
] DeleteQuery::where_gte(self : DeleteQuery, col : String, value : V) -> DeleteQuery

#
DeleteQuery::where_like

fn[V :
ToParam
] DeleteQuery::where_like(self : DeleteQuery, col : String, pattern : V) -> DeleteQuery

#
DeleteQuery::where_lt

fn[V :
ToParam
] DeleteQuery::where_lt(self : DeleteQuery, col : String, value : V) -> DeleteQuery

#
DeleteQuery::where_lte

fn[V :
ToParam
] DeleteQuery::where_lte(self : DeleteQuery, col : String, value : V) -> DeleteQuery

#
DeleteQuery::where_ne

fn[V :
ToParam
] DeleteQuery::where_ne(self : DeleteQuery, col : String, value : V) -> DeleteQuery

#
ForeignKeyDiff

#
IndexDiff

pub(all) struct IndexDiff {
added : FixedArray[
Index
]
removed : FixedArray[
Index
]
changed : FixedArray[(String,
Index
,
Index
)]
} derive(Eq, ToJson,
FromJson
)

impl Show for IndexDiff

#
InsertQuery

impl Show for InsertQuery

#
InsertQuery::columns

fn InsertQuery::columns(self : InsertQuery, columns : FixedArray[String]) -> InsertQuery

#
InsertQuery::from

#
InsertQuery::from_many

fn[E : Entity + ToJson +
FromParam
] InsertQuery::from_many(self : InsertQuery, entities : FixedArray[E]) -> InsertQuery

#
InsertQuery::values

fn InsertQuery::values(self : InsertQuery, values : FixedArray[&
ToParam
]) -> InsertQuery

#
InsertQuery::values_many

fn InsertQuery::values_many(self : InsertQuery, rows : FixedArray[FixedArray[&
ToParam
]]) -> InsertQuery

#
PreloadResult

pub(all) enum PreloadResult[T] {
One(Map[String, T])
Many(Map[String, Array[T]])
}

#
Query

impl Show for Query

#
Query::join

fn Query::join(self : Query, join_sql : String) -> Query

#
Query::limit

fn Query::limit(self : Query, n : Int) -> Query

#
Query::offset

fn Query::offset(self : Query, n : Int) -> Query

#
Query::order_by

fn Query::order_by(self : Query, order_by :
OrderBy
) -> Query

#
Query::to_count_sql

fn Query::to_count_sql(self : Query) -> String

#
Query::where_eq

fn[V :
ToParam
] Query::where_eq(self : Query, col : String, value : V) -> Query

#
Query::where_gt

fn[V :
ToParam
] Query::where_gt(self : Query, col : String, value : V) -> Query

#
Query::where_gte

fn[V :
ToParam
] Query::where_gte(self : Query, col : String, value : V) -> Query

#
Query::where_like

fn[V :
ToParam
] Query::where_like(self : Query, col : String, pattern : V) -> Query

#
Query::where_lt

fn[V :
ToParam
] Query::where_lt(self : Query, col : String, value : V) -> Query

#
Query::where_lte

fn[V :
ToParam
] Query::where_lte(self : Query, col : String, value : V) -> Query

#
Query::where_ne

fn[V :
ToParam
] Query::where_ne(self : Query, col : String, value : V) -> Query

#
TableDiff

pub(all) struct TableDiff {
columns : ColumnDiff
indexes : IndexDiff
foreign_keys : ForeignKeyDiff
} derive(Eq, ToJson)

impl Show for TableDiff

#
TxBatch

pub struct TxBatch[E] {
engine : E
}

#
TxBatch::rollback_to_savepoint

async fn[E :
Engine
] TxBatch::rollback_to_savepoint(self : TxBatch[E], name : String) ->
QueryResult

#
TxBatch::savepoint

#
TxQuery

impl Show for TxQuery

#
UpdateQuery

impl Show for UpdateQuery

#
UpdateQuery::from

#
UpdateQuery::set

fn[V :
ToParam
] UpdateQuery::set(self : UpdateQuery, col : String, value : V) -> UpdateQuery

#
UpdateQuery::where_eq

fn[V :
ToParam
] UpdateQuery::where_eq(self : UpdateQuery, col : String, value : V) -> UpdateQuery

#
UpdateQuery::where_gt

fn[V :
ToParam
] UpdateQuery::where_gt(self : UpdateQuery, col : String, value : V) -> UpdateQuery

#
UpdateQuery::where_gte

fn[V :
ToParam
] UpdateQuery::where_gte(self : UpdateQuery, col : String, value : V) -> UpdateQuery

#
UpdateQuery::where_like

fn[V :
ToParam
] UpdateQuery::where_like(self : UpdateQuery, col : String, pattern : V) -> UpdateQuery

#
UpdateQuery::where_lt

fn[V :
ToParam
] UpdateQuery::where_lt(self : UpdateQuery, col : String, value : V) -> UpdateQuery

#
UpdateQuery::where_lte

fn[V :
ToParam
] UpdateQuery::where_lte(self : UpdateQuery, col : String, value : V) -> UpdateQuery

#
UpdateQuery::where_ne

fn[V :
ToParam
] UpdateQuery::where_ne(self : UpdateQuery, col : String, value : V) -> UpdateQuery

#
UpsertQuery

impl Show for UpsertQuery

#
UpsertQuery::do_update_set

fn UpsertQuery::do_update_set(self : UpsertQuery, col : String, value :
Param
) -> UpsertQuery

#
UpsertQuery::from

#
UpsertQuery::on_conflict

fn UpsertQuery::on_conflict(self : UpsertQuery, columns : FixedArray[String]) -> UpsertQuery

#
UpsertQuery::set

fn UpsertQuery::set(self : UpsertQuery, col : String, value :
Param
) -> UpsertQuery

#
add_column_engine_option

fn add_column_engine_option(col :
Column
, key : String, value : String) ->
Column

#
add_table_engine_option

fn add_table_engine_option(table :
Table
, key : String, value : String) ->
Table

#
after_create

fn[T] after_create(entity : T, callback : (T) -> T) -> T

#
after_delete

fn[T] after_delete(entity : T, callback : (T) -> T) -> T

#
after_find

fn[T] after_find(entity : T, callback : (T) -> T) -> T

#
after_update

fn[T] after_update(entity : T, callback : (T) -> T) -> T

#
asc

fn asc(property : String) ->
Sort

#
auto_migrate

async fn[E :
Engine
] auto_migrate(engine : E, tables : Array[
Table
]) -> Unit

#
before_create

fn[T] before_create(entity : T, callback : (T) -> T) -> T

Hook callbacks helpers.

#
before_delete

fn[T] before_delete(entity : T, callback : (T) -> T) -> T

#
before_update

fn[T] before_update(entity : T, callback : (T) -> T) -> T

#
belongs_to

fn belongs_to(name : String, owner_key : String, target_table : String, target_key : String) -> Association

#
current_plain_date_time_utc

fn current_plain_date_time_utc() ->
PlainDateTime

#
current_timestamp_utc

fn current_timestamp_utc() ->
ZonedDateTime

#
delete_from

fn delete_from(table : String) -> DeleteQuery

#
desc

fn desc(property : String) ->
Sort

#
diff_table

fn diff_table(old_table :
Table
, new_table :
Table
) -> TableDiff

#
has_many

fn has_many(name : String, owner_key : String, target_table : String, target_key : String) -> Association

#
has_one

fn has_one(name : String, owner_key : String, target_table : String, target_key : String) -> Association

#
infer_column_type

fn infer_column_type(value : Json) ->
ColumnType

#
insert_into

fn insert_into(table : String) -> InsertQuery

#
many_to_many

fn many_to_many(name : String, owner_key : String, target_table : String, target_key : String, join_table : String, join_owner_key : String, join_target_key : String) -> Association

#
mysql_table_compression

fn mysql_table_compression(table :
Table
, compression : String) ->
Table

#
mysql_table_key_block_size

fn mysql_table_key_block_size(table :
Table
, key_block_size : Int) ->
Table

#
mysql_table_row_format

fn mysql_table_row_format(table :
Table
, row_format : String) ->
Table

#
new_column

fn new_column(name : String, column_type :
ColumnType
) ->
Column

#
new_foreign_key

fn new_foreign_key(name : String, column : String, referenced_table : String, referenced_column : String) ->
ForeignKey

#
new_index

fn new_index(name : String, columns : FixedArray[String]) ->
Index

#
new_primary_index

fn new_primary_index(columns : FixedArray[String]) ->
Index

#
new_table

fn new_table(name : String, columns? : FixedArray[
Column
], engine? : String) ->
Table

#
new_unique_index

fn new_unique_index(name : String, columns : FixedArray[String]) ->
Index

#
oracle_table_on_commit

fn oracle_table_on_commit(table :
Table
, on_commit : String) ->
Table

#
oracle_table_organization

fn oracle_table_organization(table :
Table
, organization : String) ->
Table

#
page

fn[T] page(content : FixedArray[T], total_elements : Int, number : Int, size : Int) ->
Page
[T]

#
pageable

fn pageable(page : Int, size : Int) ->
Pageable

#
pageable_with_sort

fn pageable_with_sort(page : Int, size : Int, sort :
Sort
) ->
Pageable

#
paginate_raw

#
pg_default_sequence_grant

fn pg_default_sequence_grant(table :
Table
, role : String, privileges : String, with_grant_option : Bool, for_role : String?) ->
Table

#
pg_default_table_grant

fn pg_default_table_grant(table :
Table
, role : String, privileges : String, with_grant_option : Bool, for_role : String?) ->
Table

#
pg_schema_authorization

fn pg_schema_authorization(table :
Table
, role : String) ->
Table

#
pg_schema_grant

fn pg_schema_grant(table :
Table
, role : String, privileges : String, with_grant_option : Bool) ->
Table

#
pg_schema_sequence_grant

fn pg_schema_sequence_grant(table :
Table
, role : String, privileges : String, with_grant_option : Bool) ->
Table

#
pg_sequence_grant

fn pg_sequence_grant(table :
Table
, role : String, privileges : String, with_grant_option : Bool) ->
Table

#
pg_table_grant

fn pg_table_grant(table :
Table
, role : String, privileges : String, with_grant_option : Bool) ->
Table

#
pg_table_on_commit

fn pg_table_on_commit(table :
Table
, on_commit : String) ->
Table

#
pg_table_owner

fn pg_table_owner(table :
Table
, role : String) ->
Table

#
pg_table_unlogged

#
pg_table_with

fn pg_table_with(table :
Table
, with_clause : String) ->
Table

#
preload

async fn[E :
Engine
, O : ToJson, T :
FromParam
] preload(engine : E, owners : FixedArray[O], association : Association) -> PreloadResult[T]

#
preload_belongs_to

async fn[E :
Engine
, O : ToJson, T :
FromParam
] preload_belongs_to(engine : E, owners : FixedArray[O], owner_key : String, target_table : String, target_key : String) -> Map[String, T]

#
preload_has_many

async fn[E :
Engine
, O : ToJson, T :
FromParam
] preload_has_many(engine : E, owners : FixedArray[O], owner_key : String, target_table : String, target_key : String) -> Map[String, Array[T]]

#
preload_has_one

async fn[E :
Engine
, O : ToJson, T :
FromParam
] preload_has_one(engine : E, owners : FixedArray[O], owner_key : String, target_table : String, target_key : String) -> Map[String, T]

#
preload_many_to_many

async fn[E :
Engine
, O : ToJson, T :
FromParam
] preload_many_to_many(engine : E, owners : FixedArray[O], owner_key : String, target_table : String, target_key : String, join_table : String, join_owner_key : String, join_target_key : String) -> Map[String, Array[T]]

#
register_driver_scheme

fn register_driver_scheme(scheme : String, driver_name : String) -> Unit

#
resolve_driver_name

fn resolve_driver_name(dsn : String) -> String?

#
restore_by_id

fn[V :
ToParam
] restore_by_id(table : String, id_col : String, id : V, deleted_col? : String) -> UpdateQuery

#
select_from

fn select_from(table : String) -> Query

#
select_from_scoped

fn select_from_scoped(table : String, deleted_col? : String) -> Query

Soft delete default scope (boolean convention): deleted = false

#
select_raw

fn select_raw(table : String, cols_expr : String) -> Query

#
soft_delete_by_id

fn[V :
ToParam
] soft_delete_by_id(table : String, id_col : String, id : V, deleted_col? : String) -> UpdateQuery

#
sqlite_table_strict

#
sqlite_table_without_rowid

fn sqlite_table_without_rowid(table :
Table
) ->
Table

#
sqlserver_table_durability

fn sqlserver_table_durability(table :
Table
, durability : String) ->
Table

#
sqlserver_table_filegroup

fn sqlserver_table_filegroup(table :
Table
, filegroup : String) ->
Table

#
sqlserver_table_memory_optimized

fn sqlserver_table_memory_optimized(table :
Table
) ->
Table

#
sqlserver_table_textimage_on

fn sqlserver_table_textimage_on(table :
Table
, filegroup : String) ->
Table

#
table_catalog

fn table_catalog(table :
Table
, catalog : String) ->
Table

#
table_comment

fn table_comment(table :
Table
, text : String) ->
Table

#
table_create_suffix

fn table_create_suffix(table :
Table
, suffix : String) ->
Table

#
table_engine

fn table_engine(table :
Table
, engine : String) ->
Table

#
table_has_primary_key

fn table_has_primary_key(table :
Table
) -> Bool

#
table_if_not_exists

fn table_if_not_exists(table :
Table
, if_not_exists : Bool) ->
Table

#
table_primary_key

fn table_primary_key(table :
Table
) -> String?

#
table_schema

fn table_schema(table :
Table
, schema : String) ->
Table

#
table_tablespace

fn table_tablespace(table :
Table
, tablespace : String) ->
Table

#
table_temporary

fn table_temporary(table :
Table
, temporary : Bool) ->
Table

#
tx_begin

fn tx_begin() -> TxQuery

#
tx_block

async fn[E :
Engine
] tx_block(engine : E) -> TxBatch[E]

#
tx_commit

fn tx_commit() -> TxQuery

#
tx_rollback

fn tx_rollback() -> TxQuery

#
tx_rollback_to_savepoint

fn tx_rollback_to_savepoint(name : String) -> TxQuery

#
tx_savepoint

fn tx_savepoint(name : String) -> TxQuery

#
unregister_driver_scheme

fn unregister_driver_scheme(scheme : String) -> Unit

#
update

fn update(table : String) -> UpdateQuery

#
upsert_into

fn upsert_into(table : String) -> UpsertQuery