Methods Reference
Available Methods
Section titled “Available Methods”Every data method is on the querier and on the pool, same name and arguments. The querier runs it on the connection you are holding; the pool acquires one for that call and releases it (which to use). Only the last four are the querier’s alone, because they are what owning a connection means.
| Method | Description |
|---|---|
find |
Find multiple records matching the query. |
find |
Stream records as an Async, row by row, each with the relations find would load. |
find |
Find records and return [rows, total - the page, and how many matched beyond it. |
find |
Find a single record matching the query. |
find |
Find a record by its primary key. |
count |
Count records matching the query. A $skip/$limit counts that page instead of every match. |
exists |
Whether anything matches, stopping at the first row. |
estimated |
The engine’s own row estimate, read from its statistics without scanning. Approximate, whole-table, server-side only. |
aggregate |
Run an aggregate query (GROUP BY, HAVING, etc.). |
insert |
Insert a single record and return its ID. |
insert |
Insert multiple records and return their IDs. |
update |
Update a record by its primary key. |
update |
Update multiple records matching the query. One naming no rows (no $where, no $limit) throws; pass { unfiltered: true } to mean the whole table. |
save |
Insert, or upsert on the primary key when the payload names it. Returns the ID. |
save |
Bulk insert/upsert, per row, on the same rule. Returns the IDs in payload order. |
upsert |
Insert or update on the conflict paths. Returns { id, changes, created }. |
upsert |
Bulk insert or update on the conflict paths. Returns { ids, changes }, IDs in payload order. |
delete |
Delete by primary key. Soft-deletes when the entity has a soft-delete field; pass { hard to remove permanently. |
delete |
Delete multiple records matching the query (soft by default; { hard removes permanently). Naming no rows throws, as for update. |
restore |
Restore a soft-deleted record by its primary key. |
restore |
Restore soft-deleted records matching the query. |
run |
Execute raw SQL (INSERT, UPDATE, DELETE). |
all<T> |
Execute raw SQL SELECT with generics. |
transaction |
Run a transaction within a callback. |
begin |
Start a transaction manually. |
commit |
Commit the active transaction. |
rollback |
Roll back the active transaction. |
release() |
Roll back any unfinished transaction and return the connection to the pool. The querier is finished afterwards: using it again throws. |
The trailing opts? on reads, updates, and deletes is a QueryOptions: bypass query filters for the call (e.g. withDeleted() to include soft-deleted rows, or { filters: false }), or force { hardDelete: true } on a delete.
Atomic arithmetic
Section titled “Atomic arithmetic”$inc adds to a numeric field and $mul multiplies it, inside the statement, so no read comes between and no concurrent write is lost. A NULL counts as 0, on every database, and a field takes one of them per update. Put the guard in $where and the count of changed rows tells you whether it held:
const taken = await pool.updateMany( Item, { $where: { id, stock: { $gte: quantity } } }, { stock: { $inc: -quantity } },);if (taken === 0) throw new Error('sold out');A bigint field takes a bigint operand, which keeps it exact. A fraction needs a column that holds one (precision/scale), as any written value does. Both are plain JSON, so they also work from the browser, where raw does not. JSON fields have their own update operators.
Insert IDs
Section titled “Insert IDs”Every write reports its ID in one shape: the column’s value on a single key, the key map on a composite. insertOne/insertMany return them in payload order.
IDs you provide, and IDs generated client-side via @Id({ onInsert }) (e.g. randomUUID), come back as-is on every database. Database-generated ones are exact per row wherever the statement itself reports them: PostgreSQL, CockroachDB, MariaDB, and SQLite (including LibSQL/Turso, Cloudflare D1, and Bun’s native SQL) via INSERT ... RETURNING, MSSQL via OUTPUT INSERTED, and MongoDB via insertedIds.
Only MySQL (and Bun SQL’s MySQL mode) has no RETURNING: its driver reports one generated ID per statement, and UQL infers the rest arithmetically. That inference needs every row in a statement to have left the key to the database, so a mixed batch is split into one statement per kind and both halves report (MySQL detects a clustered auto_increment_increment stride automatically). An entry is undefined only where nothing could name the row: a non-auto-increment key the caller did not supply.
const ids = await pool.insertMany(User, [ { name: 'Ada', email: 'ada@uql-orm.dev' }, { id: 5000, name: 'Alan' }, // explicit id, and omits email]);// Alan's missing email falls back to its column default.// ids on every database, MySQL included: [1, 5000]Records in one insertMany batch may provide different subsets of columns: the statement uses the union of columns, and missing cells fall back to the database default (DEFAULT keyword; NULL on SQLite, which also triggers its auto-generated keys). Batches larger than the dialect’s bind-parameter limit are split into multiple statements automatically; wrap the call in a transaction if all-or-nothing behavior matters across such splits.
saveOne / saveMany
Section titled “saveOne / saveMany”save picks a statement per row from whether the payload names its primary key, not from whether the row exists:
| The row | What runs |
|---|---|
| Names no key | INSERT |
| Names its key, and carries other columns | INSERT ... ON CONFLICT DO UPDATE on that key |
| Names its key, and nothing else | nothing: a reference, not a write |
That last row is how a relation links something it did not author: { tags: [{ id: 22 }] } writes the junction row and leaves tag 22 untouched.
A stale ID therefore writes the row instead of updating nothing. It fires @BeforeUpsert/@AfterUpsert, never the update pair (lifecycle hooks). IDs come back in payload order.
Composite keys
Section titled “Composite keys”On an entity with a composite primary key, every write reports that key as the map the by-id methods take. No column holds it, so no statement reports one; the row is named from the payload that wrote it.
await pool.insertMany(Enrolment, [ { studentId: 1, courseId: 'maths', grade: 'A' },]);// [{ studentId: 1, courseId: 'maths' }]idOf(getMeta(Enrolment), row) names a row you already hold, the same way.
MongoDB refuses composite keys outright, on reads as well as writes; see what is not supported yet.
Pool API
Section titled “Pool API”The pool manages the connection lifecycle. These are the main pool methods:
| Method | Description |
|---|---|
pool. |
Acquire a querier, run callback, and auto-release, even on errors. |
pool. |
Like with, but wraps the callback in a transaction. |
pool. |
Manually acquire a querier. Releasing it is yours: bind it with await using, or call querier. in a finally. Either way, an unfinished transaction is rolled back on release. |
pool. and every other operation |
Run a single operation on its own connection; see pool vs. querier. |
pool. / pool. |
Run one raw SQL statement on its own connection (SQL pools only). |
pool. |
Gracefully shut down the pool (close all connections). |
Upsert Operations
Section titled “Upsert Operations”Upsert (insert-or-update) resolves conflicts using conflict paths: the fields that define uniqueness. If a row with matching conflict path values already exists, it is updated; otherwise, a new row is inserted.
upsertOne
Section titled “upsertOne”await pool.upsertOne( User, { email: true }, { email: 'roger@uql-orm.dev', name: 'Roger', },);INSERT INTO "User" ("email", "name") VALUES ($1, $2)ON CONFLICT ("email") DO UPDATE SET "name" = EXCLUDED."name"upsertMany
Section titled “upsertMany”Efficiently upsert multiple records in a single statement:
await pool.upsertMany(User, { email: true }, [ { email: 'roger@uql-orm.dev', name: 'Roger' }, { email: 'ana@uql-orm.dev', name: 'Ana' }, { email: 'freddy@uql-orm.dev', name: 'Freddy' },]);INSERT INTO "User" ("email", "name") VALUES ($1, $2), ($3, $4), ($5, $6)ON CONFLICT ("email") DO UPDATE SET "name" = EXCLUDED."name"INSERT INTO `User` (`email`, `name`) VALUES (?, ?), (?, ?), (?, ?) AS `_uql_new`ON DUPLICATE KEY UPDATE `name` = `_uql_new`.`name`INSERT INTO `User` (`email`, `name`) VALUES (?, ?), (?, ?), (?, ?)ON DUPLICATE KEY UPDATE `name` = VALUE(`name`) RETURNING `id` `id`What the upsert result reports
Section titled “What the upsert result reports”id (upsertOne) and ids (upsertMany, in payload order) name every row, inserted or updated, on every database. Where the statement cannot report a row’s id (on MySQL, CockroachDB, MSSQL and MongoDB, and for mixed-shape batches everywhere), UQL reads it back by the conflict columns.
created, on upsertOne only, is true/false on Postgres and MySQL (see Raw SQL), and undefined elsewhere: CockroachDB, for one, has no equivalent of Postgres’s xmax system column.