db.sql) and a fluent ORM client (db.orm). Both compile your intent into immutable, hashable query plans that are verified against the contract before execution. You can use them side by side in the same project.
How plans work
Neither API executes queries directly. Instead, they compile your query into an immutable plan — a structured object containing the SQL text, bound parameters, and metadata:- Immutable — built once and reusable across requests
- Hashable — the
storageHashties the plan to a specific contract version - Auditable —
refslists every table and column the plan touches - Portable — a plan can be serialized, inspected, or passed to any compatible runtime
storageHash and profileHash match. A mismatch surfaces immediately as a structured error — not silent wrong data.
SQL DSL (db.sql)
The SQL DSL gives you explicit, composable control over the query structure. db.sql exposes one property per storage table in your contract. You chain .select(), .where(), .orderBy(), .limit(), and .build() to assemble a plan, then pass it to the runtime for execution.
.where() callback receives field references and built-in predicate functions (fns):
ORM client (db.orm)
The ORM client exposes model collections with a fluent chainable API for filtering, ordering, including relations, and projecting fields. ORM accessors use the model name from your contract (capitalized, as declared in your schema — e.g. db.orm.User, db.orm.Post). It maps storage-column rows back to model-field shapes automatically.
include calls are resolved into separate single-statement SQL plans and stitched together — no hidden JOIN explosion and no N+1 without explicit intent:
.include() is a separately compiled plan, not a cartesian JOIN. Guardrails fire at authoring time if you reference a relation or field that does not exist in the contract.
Comparing the two APIs for the same query
- SQL DSL
- ORM client
Transactions
Both APIs participate in the same transaction surface.withTransaction from @prisma-next/sql-runtime wraps a callback in a database transaction and commits on clean return or rolls back on any thrown error:
Streaming with AsyncIterable
Bothruntime.execute() and ORM .stream() return an AsyncIterable<Row>. For large result sets the runtime uses a server-side cursor (when the adapter supports it) so rows are fetched in batches rather than buffered entirely in memory:
Type inference from contract.d.ts
Result types come entirely from the contract — not from runtime inspection. The companioncontract.d.ts file contains branded TypeScript types for every model, field, and codec declared in your schema. When you write db.orm.User.select('id', 'email').all(), the return type is inferred as Array<{ id: string; email: string }> based on what contract.d.ts says about the User model’s id and email fields.
This means type errors appear at compile time, not at runtime, and the types are always in sync with the emitted contract.