Creating the client
db with typed query methods. The query API is identical across all dialects — only the createDb() options differ.
Read operations
Get a single record
List with filtering
Filter operators
Select specific fields
Include relations
Load related data in a single query instead of making separate API calls:.through() relation in your model and include it like any other:
Count
Aggregation
Group by
Write operations
Create
Update
Update expressions
Used.increment(), d.decrement(), or d.expr() to perform atomic column operations directly in the database — no read-modify-write cycle needed.
update, updateMany, and the update path of upsert. They compose with the sql tagged template — the col parameter in d.expr() is the column reference, automatically converted to snake_case.
Expressions override
autoUpdate() columns. If you pass d.increment() on an autoUpdate()
column like updatedAt, your expression is used instead of the automatic NOW().Upsert
Create if not found, update if exists:Delete
Bulk operations
Transactions
Wrap multiple operations indb.transaction() to make them atomic — either all succeed or all roll back:
tx) has the same model delegates as db — tx.users.create(), tx.tasks.list(), and tx.query() all work identically. The difference is that all operations share a single database transaction.
How it works
- Auto-commit: If the callback returns normally, the transaction commits.
- Auto-rollback: If the callback throws, the transaction rolls back and the error propagates to the caller.
- Return values: The callback’s return value is returned from
db.transaction().
Raw SQL in transactions
You can mix model delegates with raw SQL in the same transaction:Limitations
- No nesting: Calling
db.transaction()inside a transaction callback throws"Nested transactions are not supported.". - No D1 support: Cloudflare D1 does not support interactive transactions. Use
D1Database.batch()for atomic operations on D1.
Error handling
All operations returnResult<T, Error> — they never throw. Error types map to specific database conditions:
| Error | HTTP equivalent | Cause |
|---|---|---|
NotFoundError | 404 | Record not found |
UniqueConstraintError | 409 | Duplicate unique value |
ForeignKeyError | 409 | Referenced record doesn’t exist |
NotNullError | 422 | Required field missing |
CheckConstraintError | 422 | CHECK constraint violated |
ConnectionError | 503 | Database unreachable |
Raw SQL
For queries that the builder can’t express, use tagged template SQL:Prepared statements
All queries to PostgreSQL use prepared statements automatically — no configuration needed. The database driver sends each query withprepare: true, which enables PostgreSQL to cache the query plan and reuse it on subsequent executions of the same query shape.
This applies to all query paths: typed client operations (db.users.list()), raw SQL (db.query()), and queries inside transactions. There is no opt-out — prepared statements are always enabled for PostgreSQL connections.
SQLite and Cloudflare D1 handle statement caching internally at the driver level, so this
optimization is specific to PostgreSQL.
Database support
All databases use the same import andcreateDb() API:
| Database | Dialect | Connection option | Notes |
|---|---|---|---|
| PostgreSQL | 'postgres' (default) | url: string | Full feature support |
| SQLite | 'sqlite' | path: string | Development, embedded, and local use |
| Cloudflare D1 | 'sqlite' | d1: D1Database | Edge deployment |