Skip to content

NormModel class

A model is your handle for a table: CRUD, query entry points, hooks, scopes, and schema.

Auto-generated

This page is generated from the source annotations by scripts/gen-api.mjs. Edit the LuaCATS doc comments in the Norm sources, not here.

🔗 Source: src/model.lua

MemberReturnsDescription
allNormRecordListPromise (async)Resolves with every record in the table.
autoincrement_pk
avgNormNumberPromise (async)AVG of a column across the whole table.
buildNormRecordBuild an unsaved record from a data table (nothing hits the database until
columns
columns_by_name
countNormNumberPromise (async)Resolves with the total row count of the table.
createNormRecordPromise (async)Build and immediately INSERT a record.
findNormRecordOrNilPromise (async)Find a single record by its primary key.
find_byNormRecordOrNilPromise (async)Find the first record matching a { column = value } filter (ANDed).
find_or_createNormRecordPromise (async)Find the first record matching attributes; if none exists, INSERT one built
find_or_newNormRecordOrNilPromise (async)Find the first record matching attributes; if none exists, return an
group_byNormQueryBuilder
havingNormQueryBuilder
hookself: NormModelRegister a lifecycle hook handler.
hooksevent -> { fn, ...
indexesIndexes emitted at sync().
insert_manyNormNumberPromise|NormRecordListPromise (async)Bulk-insert many rows in ONE statement.
joinNormQueryBuilder
left_joinNormQueryBuilder
limitNormQueryBuilder
maxNormNumberPromise (async)MAX of a column across the whole table.
minNormNumberPromise (async)MIN of a column across the whole table.
omitNormQueryBuilder
only_trashedNormQueryBuilderStart a query over ONLY soft-deleted rows (soft-delete models only).
orderNormQueryBuilderdir:
orm
paginateNormPromise (async)Paginate the whole table.
parseanyConvert a raw driver value into a Lua value for the given column (decode).
primary_key
queryNormQueryBuilderStart a chainable query against this model's table.
record_classA single row.
relations
scopeself: NormModelRegister a reusable, named query fragment.
scopesname -> fn(query, ...), reusable query fragments
selectNormQueryBuilder
select_rawNormQueryBuilder
serializeanyConvert a Lua value into something storable for the given column (encode).
soft_deletesThe soft-delete column name (nil if disabled).
sumNormNumberPromise (async)SUM of a column across the whole table.
syncNormBooleanPromise (async)Create this model's table (CREATE TABLE IF NOT EXISTS).
table
timestampsAuto-managed timestamp columns (nil if disabled).
update_or_createNormRecordPromise (async)Find the first record matching attributes and UPDATE it with values; if
upsertNormRecordOrNilPromise (async)Atomic upsert: a single `INSERT ...
whereNormQueryBuilderShortcut for :query():where(...).
where_betweenNormQueryBuilder
where_doesnt_haveNormQueryBuilder
where_hasNormQueryBuilder
where_inNormQueryBuilder
where_likeNormQueryBuilder
where_notNormQueryBuilder
where_not_inNormQueryBuilder
with_countNormQueryBuilder
with_trashedNormQueryBuilderStart a query that INCLUDES soft-deleted rows (soft-delete models only).
wrapNormRecordWrap a DB row into a persisted record (fires after_find).

all async method

lua
NormModel:all()
  -> promise: NormRecordListPromise

Resolves with every record in the table.

lua
    local users = User:all():await()
    for _, u in ipairs(users) do print(u.name) end

autoincrement_pk field

lua
boolean

avg async method

lua
NormModel:avg(column: string)
  -> promise: NormNumberPromise

AVG of a column across the whole table.

build method

lua
NormModel:build(data: table<string, any>)
  -> NormRecord

Build an unsaved record from a data table (nothing hits the database until you call :save()). Useful to prepare a record then persist it later.

lua
    local user = User:build({ name = "John" })
    user.email = "john@x.io"
    user:save():await()

columns field

lua
NormColumn[]

columns_by_name field

lua
table<string, NormColumn>

count async method

lua
NormModel:count()
  -> promise: NormNumberPromise

Resolves with the total row count of the table.

lua
    local total = User:count():await()

create async method

lua
NormModel:create(data: table<string, any>)
  -> promise: NormRecordPromise

Build and immediately INSERT a record. Resolves with the saved record, whose auto-increment primary key is populated.

lua
    local user = User:create({ name = "John", email = "john@x.io" }):await()
    print(user.id) --> 1

find async method

lua
NormModel:find(pk: any)
  -> promise: NormRecordOrNilPromise

Find a single record by its primary key. Resolves with the record or nil.

lua
    local user = User:find(1):await()
    if (user) then print(user.name) end

find_by async method

lua
NormModel:find_by(filter: table<string, any>)
  -> promise: NormRecordOrNilPromise

Find the first record matching a { column = value } filter (ANDed).

lua
    local user = User:find_by({ email = "john@x.io" }):await()

find_or_create async method

lua
NormModel:find_or_create(attributes: table<string, any>, values?: table<string, any>)
  -> promise: NormRecordPromise

Find the first record matching attributes; if none exists, INSERT one built from attributes merged with values. Resolves with the (existing or newly created) record. Not atomic: a unique constraint is the only true guard against a concurrent double-insert.

lua
    local player = Player:find_or_create({ account_id = id }, { name = "Guest" }):await()

find_or_new async method

lua
NormModel:find_or_new(attributes: table<string, any>, values?: table<string, any>)
  -> promise: NormRecordOrNilPromise

Find the first record matching attributes; if none exists, return an unsaved record built from attributes merged with values (nothing is written until you :save() it). Resolves with the record.

lua
    local u = User:find_or_new({ email = "a@b.c" }, { name = "Anon" }):await()
    if (not u.__persisted) then u:save():await() end

group_by method

lua
NormModel:group_by(...string)
  -> NormQueryBuilder

having method

lua
NormModel:having(...any)
  -> NormQueryBuilder

hook method

lua
NormModel:hook(event: string, fn: fun(record: NormRecord))
  -> self: NormModel

Register a lifecycle hook handler. Events: before_create, after_create, before_update, after_update, before_save, after_save, before_delete, after_delete, after_find. Handlers run synchronously with the record; a before_* handler that raises cancels the operation (the promise rejects). Convenience methods exist per event (e.g. Model:before_save(fn)).

lua
    User:before_save(function(u) assert(u.name ~= nil, "name required") end)
    User:after_create(function(u) print("created #" .. u.id) end)

hooks field

lua
nil

event -> { fn, ... }, created on first registration

indexes field

lua
{ name: string, columns: string[], unique: boolean }[]?

Indexes emitted at sync().

insert_many async method

lua
NormModel:insert_many(rows: table<string, any>[], opts?: { records: boolean })
  -> promise: NormNumberPromise|NormRecordListPromise

Bulk-insert many rows in ONE statement. A fast path: it does NOT fire create hooks. Timestamps are stamped; auto-increment ids are left to the DB; columns missing from a row are inserted as NULL.

By default (no per-row ids available from a multi-row insert) it resolves with the affected row count. Pass { records = true } to get the inserted rows back as records WITH their ids — this uses INSERT ... RETURNING * and therefore requires a RETURNING-capable adapter (SQLite >= 3.35 / PostgreSQL / MariaDB >= 10.5); it throws otherwise.

lua
    local n    = Log:insert_many({ { level = "info" }, { level = "warn" } }):await()
    local recs = Log:insert_many(rows, { records = true }):await()  -- recs[1].id, …

join method

lua
NormModel:join(...any)
  -> NormQueryBuilder

left_join method

lua
NormModel:left_join(...any)
  -> NormQueryBuilder

limit method

lua
NormModel:limit(...any)
  -> NormQueryBuilder

max async method

lua
NormModel:max(column: string)
  -> promise: NormNumberPromise

MAX of a column across the whole table.

min async method

lua
NormModel:min(column: string)
  -> promise: NormNumberPromise

MIN of a column across the whole table.

omit method

lua
NormModel:omit(...string|string[])
  -> NormQueryBuilder

only_trashed method

lua
NormModel:only_trashed()
  -> NormQueryBuilder

Start a query over ONLY soft-deleted rows (soft-delete models only).

order method

lua
NormModel:order(...any)
  -> NormQueryBuilder
lua
dir:
   | "ASC"
   | "DESC"

orm field

lua
NormOrm

paginate async method

lua
NormModel:paginate(page?: number, per_page?: number)
  -> NormPromise

Paginate the whole table. See NormQueryBuilder:paginate.

parse method

lua
NormModel:parse(column: NormColumn, value: any)
  -> any

Convert a raw driver value into a Lua value for the given column (decode).

primary_key field

lua
string?

query method

lua
NormModel:query()
  -> NormQueryBuilder

Start a chainable query against this model's table.

lua
    local admins = User:query():where("admin", true):order("name"):all():await()

record_class field

lua
NormRecord

A single row. Column values are plain fields (e.g. record.name).

relations field

lua
table<string, NormRelation>

scope method

lua
NormModel:scope(name: string, fn: fun(query: NormQueryBuilder, ...any))
  -> self: NormModel

Register a reusable, named query fragment. fn(query, ...) applies conditions to a query builder. The scope becomes callable as a starter (Model:active()) and chainable (query:scope("active")). The name must not collide with a built-in method.

lua
    User:scope("active", function(q) q:where("active", true) end)
    User:scope("older_than", function(q, age) q:where("age", ">", age) end)
    User:active():scope("older_than", 18):all():await()

scopes field

lua
nil

name -> fn(query, ...), reusable query fragments

select method

lua
NormModel:select(...string|string[])
  -> NormQueryBuilder

select_raw method

lua
NormModel:select_raw(expr: string)
  -> NormQueryBuilder

serialize method

lua
NormModel:serialize(column: NormColumn, value: any)
  -> any

Convert a Lua value into something storable for the given column (encode). Only json tables are transformed; a value already a string is passed through (so a pre-encoded string is never double-encoded).

soft_deletes field

lua
string?

The soft-delete column name (nil if disabled).

sum async method

lua
NormModel:sum(column: string)
  -> promise: NormNumberPromise

SUM of a column across the whole table.

lua
    local total = User:sum("coins"):await()

sync async method

lua
NormModel:sync()
  -> promise: NormBooleanPromise

Create this model's table (CREATE TABLE IF NOT EXISTS). Prefer orm:sync() to create every model at once (it also orders tables by their foreign-key dependencies). Emits this model's belongsTo foreign keys when enabled. Resolves with true.

lua
    User:sync():await()

table field

lua
string

timestamps field

lua
{ created: string, updated: string }?

Auto-managed timestamp columns (nil if disabled).

update_or_create async method

lua
NormModel:update_or_create(attributes: table<string, any>, values?: table<string, any>)
  -> promise: NormRecordPromise

Find the first record matching attributes and UPDATE it with values; if none exists, INSERT one from attributes merged with values. Resolves with the record. (Application-level upsert; not atomic.)

lua
    local p = Player:update_or_create({ account_id = id }, { last_seen = now, name = nick }):await()

upsert async method

lua
NormModel:upsert(data: table<string, any>, opts?: { conflict: string[], update: string[] })
  -> promise: NormRecordOrNilPromise

Atomic upsert: a single INSERT ... ON CONFLICT/ON DUPLICATE KEY UPDATE statement (race-safe, unlike find_or_create). data is inserted; if a row with the same opts.conflict columns exists it is updated instead. The write is one statement; the canonical row is then read back and resolved as a record.

The conflict columns MUST carry a UNIQUE (or PRIMARY KEY) constraint — that is what the database matches on. opts.conflict defaults to the primary key; opts.update defaults to every written column except the conflict columns (and created_at, preserved on an existing row).

lua
    -- create the player, or update name/last_seen if account_id already exists
    local p = Player:upsert(
        { account_id = id, name = nick, last_seen = ts },
        { conflict = { "account_id" } }
    ):await()

where method

lua
NormModel:where(...any)
  -> NormQueryBuilder

Shortcut for :query():where(...). Accepts (col, value), (col, op, value) or a { col = value } table.

lua
    local rich = User:where("coins", ">", 100):all():await()
    local john = User:where({ name = "John" }):first():await()

where_between method

lua
NormModel:where_between(column: string, min: any, max: any)
  -> NormQueryBuilder

where_doesnt_have method

lua
NormModel:where_doesnt_have(name: string, configure?: fun(q: NormQueryBuilder))
  -> NormQueryBuilder

where_has method

lua
NormModel:where_has(name: string, configure?: fun(q: NormQueryBuilder))
  -> NormQueryBuilder

where_in method

lua
NormModel:where_in(column: string, list: any[])
  -> NormQueryBuilder

where_like method

lua
NormModel:where_like(column: string, pattern: string)
  -> NormQueryBuilder

where_not method

lua
NormModel:where_not(column: string, value: any)
  -> NormQueryBuilder

where_not_in method

lua
NormModel:where_not_in(column: string, list: any[])
  -> NormQueryBuilder

with_count method

lua
NormModel:with_count(...string)
  -> NormQueryBuilder

with_trashed method

lua
NormModel:with_trashed()
  -> NormQueryBuilder

Start a query that INCLUDES soft-deleted rows (soft-delete models only).

wrap method

lua
NormModel:wrap(row: table<string, any>)
  -> NormRecord

Wrap a DB row into a persisted record (fires after_find).