Filters
The where model - scalar operators, logical combinators, null handling, and dropping to raw Drizzle SQL.
where is shared by every read, count, exists, update, delete, and upsert. It is typed against your table's columns and relations.
Equality shorthand
The simplest filter is a direct value - it means "equals":
await client.users.findMany({
where: { active: true, name: 'Alice' },
});That is identical to the explicit form:
await client.users.findMany({
where: { active: { equals: true } },
});Multiple keys are combined with AND.
String filters
String columns accept equality, membership, and pattern operators:
await client.users.findMany({
where: {
name: { contains: 'ali', mode: 'insensitive' },
email: { endsWith: '@example.com' },
},
});| Operator | Meaning |
|---|---|
equals | exact match |
in / notIn | value is (not) in a list |
contains | substring match |
startsWith / endsWith | prefix / suffix match |
mode | 'insensitive' makes every string operator ignore case: contains / startsWith / endsWith use ILIKE on PostgreSQL and lower(column) LIKE lower(pattern) on SQLite and MySQL; equals, in, notIn, and a scalar not compare lower(column) with lower(value) on every dialect. A nested not filter takes its own mode. 'default' compiles to LIKE / =, whose case sensitivity follows the dialect and collation |
not | negate a value or a nested filter |
Numeric and date filters
Numbers, bigints, and dates accept range comparisons:
await client.users.findMany({
where: {
id: { gte: 100, lte: 200 },
},
});| Operator | Meaning |
|---|---|
equals | exact match |
in / notIn | value is (not) in a list |
lt / lte | less than / less than or equal |
gt / gte | greater than / greater than or equal |
not | negate a value or a nested filter |
contains and startsWith treat %, _, and the SQL escape character as literal search text. This applies to case-insensitive filters, nested not, PostgreSQL JSONB paths and array elements, and prepared params. Use a Drizzle SQL fragment when you need a wildcard pattern.
Boolean filters
await client.users.findMany({
where: { active: { equals: true } },
});Boolean columns support equals and not.
Null and negation
Nullable columns accept null directly, and any operator can be negated with not:
await client.posts.findMany({
where: {
deletedAt: null, // IS NULL
title: { not: { contains: 'draft' } },
},
});Logical operators
Combine conditions with AND, OR, and NOT. They nest arbitrarily:
await client.users.findMany({
where: {
AND: [
{ active: true },
{
OR: [
{ email: { endsWith: '@company.com' } },
{ name: { startsWith: 'Admin' } },
],
},
],
},
});Top-level keys are already AND-ed
You only need explicit AND when you want to group an OR next to other
conditions, or to express the same condition on a field twice.
PostgreSQL arrays
Native PostgreSQL array columns support exact equality, containment, overlap, emptiness, and total element-count filters. These filters are PostgreSQL-only.
const posts = await client.posts.findMany({
where: {
tags: { has: 'drizzle', length: { lte: 3 } },
roles: { hasEvery: ['admin', 'member'] },
scores: { hasSome: [10, 20], hasNone: [0] },
},
});Use equals (or pass an array directly) for exact equality, containedBy when every stored value must be in a supplied list, and isEmpty for empty/non-empty arrays. length uses PostgreSQL cardinality, including every dimension. GIN indexes help containment and overlap queries; use an expression index on cardinality(column) for frequent length filters.
See Array filters for every operator, the generated SQL, NULL semantics, and indexing.
JSONB filters
PostgreSQL only
JSONB path filters are unavailable on SQLite and MySQL.
Typed paths inside PostgreSQL JSONB columns have their own API and limitations:
Filtering by relations
where also reaches across relations with some / every / none (to-many) and is / isNot (to-one). That has its own page:
Dropping to raw Drizzle SQL
When you need an expression the structured filter does not model, pass a Drizzle SQL fragment straight into where:
import { eq } from 'drizzle-orm';
import { users } from './schema';
const user = await client.users.findFirst({
where: eq(users.id, 1),
});Anything that produces a Drizzle SQL / SQLWrapper is accepted, so you keep the full expressive power of Drizzle for the cases that need it.
Reusing a filter in raw Drizzle
Added in 0.3.0$where() goes the other way: it compiles a typed where into a Drizzle SQL condition, so the same filter works in raw queries, joins, and subqueries.
The condition includes every plugin scope of the model, so it selects the same rows as findMany: soft-delete hides deleted rows, and a tenant-scoped client adds its tenant filter. deleted and other per-call arguments do not apply. It returns undefined only when the where is empty and the model has no scope.
import { and, eq } from 'drizzle-orm';
import { users, posts } from './schema';
const rows = await db
.select()
.from(posts)
.innerJoin(users, eq(posts.authorId, users.id))
.where(
and(
client.users.$where({ OR: [{ age: { gte: 18 } }, { active: true }] }),
client.posts.$where({ published: true, score: { gt: 10 } }),
),
);- It takes the same
whereasfindMany, including logical operators and relation filters. - An empty
wherereturnsundefined, which Drizzle treats as no filter. - Static plugin filters such as the soft-delete visibility rule are not applied. Add them yourself when you need them. On a tenant-scoped client the tenant filter is applied, so the condition never reaches another tenant's rows.