Skip to content

Latest commit

 

History

History
508 lines (367 loc) · 10.3 KB

File metadata and controls

508 lines (367 loc) · 10.3 KB
outline deep

API Examples

This page shows concrete morm usage patterns with real API names from the library.

Core Imports

Typical code imports look like:

using @oboard/morm as @morm
using @oboard/morm/engine as @morm/engine
using @oboard/morm/time as @time

The exact package aliases are up to you, but these names make examples easy to read.

Entity Definition Example

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

Basic Query Builder Examples

Simple Select

let q = @morm.select_from("student")

This builds a select query that defaults to SELECT * FROM student.

Select With Filters

let q = @morm.select_from("student")
  .where_eq("id", 1)
  .where_eq("name", "Alice")

Select With Comparison Operators

let q = @morm.select_from("student")
  .where_gte("age", 18)
  .where_lt("age", 30)
  .where_like("name", "A%")

Ordering, Limit, And Offset

let q = @morm.select_from("student")
  .order_by(@morm.desc("id"))
  .limit(20)
  .offset(40)

Pageable + Paginate

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

Paginate Raw SQL

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

JOIN

let q = @morm.select_from("enrollment")
  .join("LEFT JOIN student ON student.id = enrollment.student_id")
  .where_eq("enrollment.id", 1)

Insert Examples

Insert From Individual Values

let q = @morm.insert_into("student")
  .columns(["name", "age"])
  .values([
    @morm/engine.to_param("Alice"),
    @morm/engine.to_param(18),
  ])

Insert From Entity

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.

Upsert Examples

Upsert From Entity

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

Upsert With Explicit Conflict Update

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.

Update Examples

let q = @morm.update("student")
  .set("name", "Alice Updated")
  .set("age", 19)
  .where_eq("id", 1)

Delete Examples

Delete By Condition

let q = @morm.delete_from("student")
  .where_eq("id", 1)

Delete From Entity

let q = @morm.delete_from("student").from(student)

The entity-driven delete path uses the entity's table metadata and key fields.

Scoped Select Example

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.

Parameter Conversion Examples

Primitive Params

let p1 = @morm/engine.to_param(1)
let p2 = @morm/engine.to_param(true)
let p3 = @morm/engine.to_param("Alice")

JSON Param

let payload = {
  "name": "Alice".to_json(),
  "age": 18.to_json(),
}.to_json()

let p = @morm/engine.to_param(payload)

Time Params

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::DateTime for PlainDateTime
  • Param::Timestamp for ZonedDateTime

JSON Round-Trip With Time Types

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.

Auto Timestamp Examples

Convention-Based 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) -> AuditRow

then mormgen injects timestamp assignment code before the upsert.

Custom Field Names

///|
#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()

Mapper Examples

Derived Query Methods

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

Raw SQL Mapper Method

///|
#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]
}

JOIN Mapper Method

///|
#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(...).

Special Save Method

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

Special Delete Method

///|
#morm.mapper(table="class")
pub trait ClassMapper {
  async delete(Self, entity : Class) -> Bool
}

delete is treated as a recognized special method by the generator.

Using A Generated Mapper

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.

Raw Engine Execution

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

Migration Example

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

Inspecting Generated Metadata

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.

Recommended Patterns

Prefer these patterns:

  • use generated mappers for simple app-facing CRUD
  • use explicit #morm.query when SQL matters
  • use PlainDateTime for created_at / updated_at unless offset is a real domain requirement
  • inspect .g.mbt output when behavior becomes non-trivial
  • keep engine-specific behavior in the engine package, not in entity code

Patterns To Avoid

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

End-To-End Example

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

This combines:

  • generated mapper construction
  • automatic save/upsert logic
  • typed time values
  • direct query builder usage for follow-up reads

CLI Examples

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

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

Final Note

The fastest way to understand any morm API is:

  1. look at your source annotations
  2. look at the generated .g.mbt
  3. look at the engine implementation that actually executes the statement

That is the intended debugging workflow.