Skip to main content
The database client provides typed CRUD operations derived from your schema. Every query is type-checked — column names, filter values, and return types are all inferred from the model definition.

Creating the client

Each model becomes a property on 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:
Narrow included relations to specific fields:
Filter, sort, and limit included relations:
Nest includes up to 3 levels deep:
Many-to-many relations work through join tables — define a .through() relation in your model and include it like any other:

Count

Aggregation

Group by

Write operations

Create

Update

Update expressions

Use d.increment(), d.decrement(), or d.expr() to perform atomic column operations directly in the database — no read-modify-write cycle needed.
Expressions work in 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 in db.transaction() to make them atomic — either all succeed or all roll back:
The transaction client (tx) has the same model delegates as dbtx.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 return Result<T, Error> — they never throw. Error types map to specific database conditions:
ErrorHTTP equivalentCause
NotFoundError404Record not found
UniqueConstraintError409Duplicate unique value
ForeignKeyError409Referenced record doesn’t exist
NotNullError422Required field missing
CheckConstraintError422CHECK constraint violated
ConnectionError503Database unreachable
Handle errors with pattern matching:

Raw SQL

For queries that the builder can’t express, use tagged template SQL:
Parameters are automatically escaped — no SQL injection risk.

Prepared statements

All queries to PostgreSQL use prepared statements automatically — no configuration needed. The database driver sends each query with prepare: 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 and createDb() API:
DatabaseDialectConnection optionNotes
PostgreSQL'postgres' (default)url: stringFull feature support
SQLite'sqlite'path: stringDevelopment, embedded, and local use
Cloudflare D1'sqlite'd1: D1DatabaseEdge deployment
See the overview for setup examples for each dialect.