Built-in: lints
The lints middleware inspects each query's structure before it runs and blocks or warns on risky shapes.
The lints middleware inspects the structure of every query before it executes and blocks or warns on shapes that are usually mistakes, like a DELETE with no WHERE clause.
Register it on the runtime; the defaults are useful without configuration:
import postgres from '@prisma-next/postgres/runtime';
import { lints } from '@prisma-next/sql-runtime';
import type { Contract } from './contract.d';
import contractJson from './contract.json' with { type: 'json' };
export const db = postgres<Contract>({
contractJson,
url: process.env['DATABASE_URL']!,
middleware: [lints()],
});lints is SQL-family middleware (it declares familyId: 'sql'), so it registers on PostgreSQL runtimes. It ships with @prisma-next/sql-runtime, which comes in through @prisma-next/postgres.
The rules
Lints run in beforeExecute, against the query plan's AST. Each rule has a severity: error throws and blocks the query before it reaches the database, warn logs through the runtime log and lets the query run.
| Rule | Code | Default severity | Fires when |
|---|---|---|---|
| DELETE without WHERE | LINT.DELETE_WITHOUT_WHERE | error | A DELETE has no WHERE clause |
| UPDATE without WHERE | LINT.UPDATE_WITHOUT_WHERE | error | An UPDATE has no WHERE clause |
| No LIMIT | LINT.NO_LIMIT | warn | A SELECT has no LIMIT clause |
| SELECT star | LINT.SELECT_STAR | warn | A query selects all columns instead of naming them |
A warning is emitted as a structured event on the runtime's log:
warn {
code: 'LINT.NO_LIMIT',
message: 'Unbounded SELECT may return large result sets',
details: { table: 'user' }
}The postgres(...) client does not expose a way to attach a log sink yet, so warn findings are not printed anywhere by default; only error severities are observable, because they block the query. Treat warn rules as forward-looking until the client accepts a logger.
Configure severities
Raise or lower any rule per project through severities. For example, treat unbounded selects as hard failures in a service where every list endpoint must paginate:
lints({
severities: {
noLimit: 'error',
selectStar: 'warn',
},
});The severity keys are deleteWithoutWhere, updateWithoutWhere, noLimit, and selectStar, plus readOnlyMutation for the raw-SQL guardrail described below. The options type also accepts unindexedPredicate, but no rule emits that finding in v0.14.
Raw SQL
Queries built with the ORM client or the SQL query builder carry a typed AST, and the rules above inspect it structurally. Raw SQL does not have that structure, so lints falls back to text heuristics for raw plans: it flags select *, a missing LIMIT, and a mutation smuggled through a raw query marked read-only (LINT.READ_ONLY_MUTATION). The raw heuristics carry their own defaults: select * escalates to error for raw plans (stricter than the AST rule's warn), a missing LIMIT stays a warning, and a read-only-intent mutation is an error. The severities keys above override raw findings too. Control the fallback with fallbackWhenAstMissing:
lints({ fallbackWhenAstMissing: 'raw' }); // default: heuristic checks on the SQL text
lints({ fallbackWhenAstMissing: 'skip' }); // skip linting for plans without an ASTHeuristics can misread SQL that a structural check would classify correctly, so treat raw-SQL lint findings as a prompt to look at the query rather than a verdict.
Common gotchas
LINT.DELETE_WITHOUT_WHERE and LINT.UPDATE_WITHOUT_WHERE default to error, so a deliberate full-table mutation (for example a backfill) will be blocked. For a one-off script, either add a tautological but explicit WHERE clause or register lints with that rule set to warn in that script's runtime setup. Lowering the severity globally gives up the protection everywhere.
See also
- How middleware works
- Built-in: budgets for row-count and latency ceilings
- Authoring custom middleware