| outline | deep |
|---|
This page shows concrete morm usage patterns with real API names from the library.
Typical code imports look like:
using @oboard/morm as @morm
using @oboard/morm/engine as @morm/engine
using @oboard/morm/time as @timeThe exact package aliases are up to you, but these names make examples easy to read.
///|
using @time {type PlainDateTime, type ZonedDateTime}
///|
#morm.entity
pub(all) struct Class {
#morm.id
#morm.default(autoincrement())
id : Int64
#morm.varchar(length="255")
name : String
#morm.foreign_key(references="teacher.id", on_delete="CASCADE")
teacher_id : Int
created_at : PlainDateTime
updated_at : PlainDateTime
} derive(ToJson, FromJson)
///|
#morm.entity
pub(all) struct Teacher {
#morm.id
#morm.default(autoincrement())
id : Int64
name : String
major : String
age : Int
birth_date : ZonedDateTime?
} derive(ToJson, FromJson)Generated entity code gives you:
let table = Class::table()table is an @morm.Table value that can be passed to migration helpers or inspected in tests.
let q = @morm.select_from("student")This builds a select query that defaults to SELECT * FROM student.
let q = @morm.select_from("student")
.where_eq("id", 1)
.where_eq("name", "Alice")let q = @morm.select_from("student")
.where_gte("age", 18)
.where_lt("age", 30)
.where_like("name", "A%")let q = @morm.select_from("student")
.order_by(@morm.desc("id"))
.limit(20)
.offset(40)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,
)let page = @morm.paginate_raw(
engine,
"SELECT id, name, age FROM student WHERE age >= ?",
[18],
@morm.pageable(2, 10),
)let q = @morm.select_from("enrollment")
.join("LEFT JOIN student ON student.id = enrollment.student_id")
.where_eq("enrollment.id", 1)let q = @morm.insert_into("student")
.columns(["name", "age"])
.values([
@morm/engine.to_param("Alice"),
@morm/engine.to_param(18),
])let student : Student = {
id: 1,
name: "Alice",
age: 18,
birth_date: @morm.current_timestamp_utc(),
}
let q = @morm.insert_into("student").from(student)When .from(entity) is used, the entity is converted through ToJson, then fields are turned into params.
let q = @morm.upsert_into("class").from(entity)The conflict target is inferred from:
- the primary key if present
- otherwise the first unique index found in table metadata
let q = @morm.upsert_into("student")
.set("id", 1)
.set("name", "Alice")
.on_conflict(["id"])
.do_update_set("name", "Alice")Exact emitted SQL is engine-specific.
let q = @morm.update("student")
.set("name", "Alice Updated")
.set("age", 19)
.where_eq("id", 1)let q = @morm.delete_from("student")
.where_eq("id", 1)let q = @morm.delete_from("student").from(student)The entity-driven delete path uses the entity's table metadata and key fields.
If you use a soft-delete column, select_from_scoped is available:
let q = @morm.select_from_scoped("student", deleted_col="deleted")
.where_eq("id", 1)This helper applies the built-in boolean convention deleted = false before adding your extra filters.
let p1 = @morm/engine.to_param(1)
let p2 = @morm/engine.to_param(true)
let p3 = @morm/engine.to_param("Alice")let payload = {
"name": "Alice".to_json(),
"age": 18.to_json(),
}.to_json()
let p = @morm/engine.to_param(payload)let created_at = @morm.current_plain_date_time_utc()
let published_at = @morm.current_timestamp_utc()
let p1 = @morm/engine.to_param(created_at)
let p2 = @morm/engine.to_param(published_at)These become:
Param::DateTimeforPlainDateTimeParam::TimestampforZonedDateTime
The local time package supports JSON encoding and decoding directly.
let ts = @morm.current_timestamp_utc()
let json = ts.to_json()
let parsed : @time.ZonedDateTime = try! @json.from_json(json)This is useful when your entity derives ToJson and FromJson and includes time fields.
///|
#morm.entity
pub(all) struct AuditRow {
#morm.id
id : Int64
created_at : @time.PlainDateTime
updated_at : @time.PlainDateTime
} derive(ToJson, FromJson)If a generated mapper has:
async save(Self, entity : AuditRow) -> AuditRowthen mormgen injects timestamp assignment code before the upsert.
///|
#morm.entity
pub(all) struct AuditRow {
#morm.id
id : Int64
#morm.auto_create_time
inserted_on : @time.PlainDateTime
#morm.auto_update_time
touched_on : @time.ZonedDateTime
} derive(ToJson, FromJson)Generated values depend on the field type:
PlainDateTime -> @morm.current_plain_date_time_utc()ZonedDateTime -> @morm.current_timestamp_utc()
///|
#morm.mapper(table="student")
pub trait StudentMapper {
async find_student_by_id(Self, id : Int) -> Student?
async find_students_by_age(Self, age : Int) -> FixedArray[Student]
async count_students_by_age(Self, age : Int) -> Int?
async find_all(Self) -> FixedArray[Student]
}These are generated from method names without explicit SQL strings.
///|
#morm.mapper(table="enrollment")
pub trait EnrollmentMapper {
#morm.query("SELECT id, student_id, class_id, note FROM enrollment WHERE class_id = ?")
async find_by_class_raw(Self, class_id : Int) -> FixedArray[Enrollment]
}///|
#morm.mapper(table="enrollment")
pub trait EnrollmentMapper {
#morm.fetch_graph(join="LEFT JOIN student ON student.id = enrollment.student_id")
async find_with_student_by_id(Self, id : Int) -> Enrollment?
}This extends the generated query builder with .join(...).
///|
#morm.mapper(table="class")
pub trait ClassMapper {
async save(Self, entity : Class) -> Class
}This method is generated as an upsert-backed save path and can apply auto timestamp fields before building the query.
///|
#morm.mapper(table="class")
pub trait ClassMapper {
async delete(Self, entity : Class) -> Bool
}delete is treated as a recognized special method by the generator.
let mapper = StudentMapperImpl::new(engine)
let student = mapper.find_student_by_id(1)
let adults = mapper.find_students_by_age(18)The concrete engine value must satisfy the @morm/engine.Engine trait.
If you do not want the query builder path, call raw SQL directly on the engine.
let res = engine.exec_raw(
"SELECT * FROM student WHERE id = ?",
[@morm/engine.to_param(1)],
)This is useful when:
- the SQL is too specialized for name derivation
- you need engine-specific syntax
- you are debugging a database issue
@morm.auto_migrate(engine, [
Class::table(),
Student::table(),
Teacher::table(),
])The engine receives schema JSON and performs the actual migration logic. morm does not impose one universal DDL diff strategy.
You can assert on metadata in tests:
let table = Class::table()
assert_eq(table.name, "class")
assert_eq(table.columns.length(), 5)
assert_eq(table.columns[3].name, "created_at")
assert_eq(table.columns[3].column_type, DateTime)This is a good way to keep schema expectations explicit.
Prefer these patterns:
- use generated mappers for simple app-facing CRUD
- use explicit
#morm.querywhen SQL matters - use
PlainDateTimeforcreated_at/updated_atunless offset is a real domain requirement - inspect
.g.mbtoutput when behavior becomes non-trivial - keep engine-specific behavior in the engine package, not in entity code
Avoid these assumptions:
- assuming all engines render identical upsert SQL
- assuming ORM runtime will parse arbitrary date strings back into time values
- assuming auto timestamp generation preserves create-time semantics on update-only paths
- assuming relation annotations imply a full transparent object graph loader
let class_mapper = ClassMapperImpl::new(engine)
let entity : Class = {
id: 1,
name: "Databases",
teacher_id: 7,
created_at: @morm.current_plain_date_time_utc(),
updated_at: @morm.current_plain_date_time_utc(),
}
let saved = class_mapper.save(entity)
let res = @morm/engine.exec_query(
engine,
@morm.select_from("class").where_eq("id", saved.id),
)
let rows = res.rowsThis combines:
- generated mapper construction
- automatic save/upsert logic
- typed time values
- direct query builder usage for follow-up reads
Inside this repository:
moonx oboard/morm/mormgen -- example/entities.mbt -o example/entities.g.mbt
moonx oboard/morm/mormgen -- example/mapper.mbt -o example/mapper.g.mbtInside a dependent project, the packaged binary is typically called by your pre-build hook:
$mod_dir/.mooncakes/oboard/morm/morm-gen entities.mbt -o entities.g.mbtThe fastest way to understand any morm API is:
- look at your source annotations
- look at the generated
.g.mbt - look at the engine implementation that actually executes the statement
That is the intended debugging workflow.