prisma-next-queries
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChinesePrisma Next — Queries
Prisma Next — 查询操作
Edit your data contract. Prisma handles the rest.
Once the contract is emitted and the DB is up to date, this skill covers everything you do with the data: reading, writing, eager-loading relations, aggregating, and the choice between the ORM and the SQL builder.
编辑你的数据契约,Prisma 处理其余工作。
当契约生成且数据库更新完成后,本技能涵盖所有数据操作:读取、写入、预加载关联关系、聚合,以及在ORM和SQL构建器之间进行选择。
When to Use
适用场景
- User wants to read, write, update, or delete data.
- User wants to include / eager-load relations.
- User wants to paginate, sort, filter, project.
- User wants to wrap operations in a transaction ().
db.transaction(...) - User wants to aggregate (,
count,sum, …).avg - User asks about query lanes (ORM vs SQL builder).
- User mentions: query, select, where, orderBy, take, skip, include, eager load, first, all, count, aggregate, create, update, delete, upsert, returning, drizzle-style, kysely-style, prisma client.
- 用户需要读取、写入、更新或删除数据。
- 用户需要包含/预加载关联关系。
- 用户需要分页、排序、过滤、投影字段。
- 用户需要将操作包装在事务中()。
db.transaction(...) - 用户需要进行聚合操作(、
count、sum等)。avg - 用户询问查询方式(ORM vs SQL构建器)。
- 用户提及:query、select、where、orderBy、take、skip、include、eager load、first、all、count、aggregate、create、update、delete、upsert、returning、drizzle-style、kysely-style、prisma client。
When Not to Use
不适用场景
- User wants to add / change a model → .
prisma-next-contract - User wants to wire or add middleware →
db.ts.prisma-next-runtime - User wants to debug a query failure (structured error envelope) → .
prisma-next-debug
- 用户想要添加/修改模型 → 使用。
prisma-next-contract - 用户想要配置或添加中间件 → 使用
db.ts。prisma-next-runtime - 用户想要调试查询失败问题(结构化错误包)→ 使用。
prisma-next-debug
Key Concepts
核心概念
Prisma Next ships two query lanes on top of one contract today, both reached through the same value from (which the façade returns):
dbsrc/prisma/db.ts@prisma-next/<target>/runtime- — the ORM. Model-shaped (
db.orm.<Model>), fluent (db.orm.User), fully typed against.where(...).select(...).orderBy(...).all(). Default lane for CRUD with relations.Contract - — the SQL builder. Table-shaped (
db.sql.<table>, lowercase by storage name), produces a plan you execute through the runtime. Use when the ORM is too high-level — explicitdb.sql.user, computed projections that aren't model fields, set operations, window functions.JOIN
The two lanes share the same contract, the same connection, and the same transaction context — they compose cleanly. Reach for the ORM first; drop to when the ORM is too high-level. The lane choice is local: a single query function picks one lane, not the whole app.
db.sqlLane decision table:
| Need | Choose | Why |
|---|---|---|
| Standard CRUD with relations | ORM ( | Highest ergonomics; fully typed; model-shaped. |
| Eager-load related records | ORM | Composes with |
| Aggregate (count, sum, avg) | ORM | Typed result; works with grouping ( |
| ORM mutations (returns updated rows) or | ORM returns inserted/updated rows; SQL builder exposes |
Computed projection (e.g. | SQL builder ( | The ORM projects model fields; arbitrary expression projection is the SQL builder's seam. |
Complex | SQL builder | The ORM doesn't express arbitrary joins. |
Postgres-specific feature ( | SQL builder, falling back to extension operators when the extension provides them | DSL first; extensions can contribute operators ( |
Prisma Next 基于单个契约提供两种查询方式,均可通过中的对象访问(由门面返回):
src/prisma/db.tsdb@prisma-next/<target>/runtime- — ORM方式。基于模型结构(如
db.orm.<Model>),支持链式调用(db.orm.User),完全基于.where(...).select(...).orderBy(...).all()提供类型校验。是处理带关联关系的CRUD操作的默认方式。Contract - — SQL构建器方式。基于数据库表结构(如
db.sql.<table>,采用存储时的小写表名),生成可通过运行时执行的查询计划。适用于ORM无法处理的高级场景——显式db.sql.user、非模型字段的计算投影、集合操作、窗口函数等。JOIN
两种方式共享同一契约、同一连接和同一事务上下文,可无缝组合。优先使用ORM方式;当ORM无法满足需求时,再切换到方式。方式选择是局部性的:单个查询函数可选择一种方式,而非整个应用统一选择。
db.sql方式决策表:
| 需求 | 选择方式 | 原因 |
|---|---|---|
| 带关联关系的标准CRUD操作 | ORM( | 最高的易用性;完全类型校验;基于模型结构。 |
| 预加载关联记录 | ORM | 可与分支上的 |
| 聚合操作(count、sum、avg等) | ORM | 类型化结果;支持分组( |
带类型化结果的 | ORM 变更操作(返回更新后的行)或** | ORM返回插入/更新后的行;SQL构建器显式提供 |
计算投影(例如 | SQL构建器( | ORM仅投影模型字段;任意表达式投影是SQL构建器的核心场景。 |
复杂 | SQL构建器 | ORM无法表达任意连接逻辑。 |
Postgres专属特性( | SQL构建器,当扩展提供相关操作符时可回退到扩展操作符 | 优先使用DSL;扩展可贡献专属操作符(如 |
Workflow — ORM reads
工作流 — ORM查询
The concept: returns a collection you compose method-by-method. Each call returns a new collection (immutable chaining); the terminal verb ( / / / ) issues the query. Predicates are lambdas over a field proxy: .
db.orm.<Model>.all().first().count().aggregate(...)u.field.<op>(value)typescript
// src/queries/users.ts — one directory deep under src/, so the import is '../prisma/db'
import { db } from '../prisma/db';
// Find one record by primary key shorthand.
const user = await db.orm.User.first({ id: userId });
// Returns the full row or `null`.
// Find one matching a predicate.
const alice = await db.orm.User
.where((u) => u.email.eq('alice@example.com'))
.first();
// Find many with projection, sort, and limit.
const recentUsers = await db.orm.User
.select('id', 'email', 'createdAt')
.orderBy((u) => u.createdAt.desc())
.take(10)
.all();Predicates () come in two forms:
.where(...)typescript
// Lambda form — full expression power.
db.orm.User.where((u) => u.email.eq('alice@example.com'));
// Shorthand object form — equality on the named fields.
db.orm.User.where({ kind: 'admin' });Operators on the field proxy include , , , , , , , , , , . Extensions add target-specific operators on extension-typed columns ('s , 's / / , 's / / …).
.eq.neq.lt.lte.gt.gte.like.ilike.in([...]).isNull().isNotNull()pgvector.cosineDistance(...)postgis.within(...).intersectsBbox(...).distanceSphere(...)cipherstash.cipherstashEq(...).cipherstashGt(...)There is no operator. Express ranges either as two chained clauses (the idiomatic form — clauses AND-compose) or with the combinator inside one clause:
.between(a, b).where(...)and(...)typescript
// Chained .where() — each clause AND-composes with the previous one.
await db.orm.Sale
.where((s) => s.day.gte(start))
.where((s) => s.day.lte(end))
.all();
// Equivalent with an explicit `and(...)` inside one clause.
import { and } from '@prisma-next/sql-orm-client'; // façade re-export pending — see *What PN doesn't do yet*
await db.orm.Sale
.where((s) => and(s.day.gte(start), s.day.lte(end)))
.all();The two forms emit the same SQL. Pick chained when each clause adds a separate condition that reads as its own thought; pick when one logical predicate happens to have two parts and you want the visual grouping. Don't reach for a helper — there isn't one.
.where()and(...)betweenCombinators (, , ) compose predicates, and relation predicates (, , ) recurse into a relation. These currently come from the internal package — see What Prisma Next doesn't do yet for the façade-completeness gap:
andornot.some(...).none(...).every(...)@prisma-next/sql-orm-clienttypescript
import { and, or, not } from '@prisma-next/sql-orm-client';
await db.orm.User
.where((u) =>
and(
or(u.kind.eq('admin'), u.email.ilike('%@example.com')),
not(u.posts.none((p) => p.title.ilike('%draft%'))),
),
)
.all();Sorting and pagination. accepts a single lambda or an array of lambdas (each calling / on a field). limits; offsets.
.orderBy(...).asc().desc().take(n).skip(n)typescript
await db.orm.Post
.where((p) => p.authorId.eq(userId))
.orderBy([(p) => p.createdAt.desc(), (p) => p.id.desc()])
.take(20)
.all();
// Cursor pagination — order by an indexed unique column and filter past the cursor.
const cursor = lastPostFromPreviousPage.createdAt;
await db.orm.Post
.where((p) => p.createdAt.lt(cursor))
.orderBy((p) => p.createdAt.desc())
.take(20)
.all();.first().first({ pk }).all().first()LIMIT 1.first({ pk }).all()LIMIT核心逻辑:返回一个可通过链式调用逐步组合的集合对象。每次调用返回新的集合对象(不可变链式调用);最终调用终结方法(///)时执行查询。条件谓词是基于字段代理的lambda表达式:。
db.orm.<Model>.all().first().count().aggregate(...)u.field.<op>(value)typescript
// src/queries/users.ts — 位于src下一级目录,因此导入路径为'../prisma/db'
import { db } from '../prisma/db';
// 通过主键快速查询单条记录。
const user = await db.orm.User.first({ id: userId });
// 返回完整行数据或`null`。
// 根据条件查询单条记录。
const alice = await db.orm.User
.where((u) => u.email.eq('alice@example.com'))
.first();
// 查询多条记录,指定投影字段、排序和条数限制。
const recentUsers = await db.orm.User
.select('id', 'email', 'createdAt')
.orderBy((u) => u.createdAt.desc())
.take(10)
.all();条件谓词()有两种形式:
.where(...)typescript
// Lambda形式 — 支持完整表达式能力。
db.orm.User.where((u) => u.email.eq('alice@example.com'));
// 简写对象形式 — 对指定字段进行相等匹配。
db.orm.User.where({ kind: 'admin' });字段代理支持的操作符包括、、、、、、、、、、。扩展会为特定类型的列添加目标专属操作符(如的、的//、的/等)。
.eq.neq.lt.lte.gt.gte.like.ilike.in([...]).isNull().isNotNull()pgvector.cosineDistance(...)postgis.within(...).intersectsBbox(...).distanceSphere(...)cipherstash.cipherstashEq(...).cipherstashGt(...)不存在操作符。范围条件可通过两种方式表达:链式调用子句(惯用形式——子句之间自动AND组合)或在单个子句中使用组合器:
.between(a, b).where(...)and(...)typescript
// 链式.where() — 每个子句与前一个自动AND组合。
await db.orm.Sale
.where((s) => s.day.gte(start))
.where((s) => s.day.lte(end))
.all();
// 等价于在单个子句中显式使用`and(...)`。
import { and } from '@prisma-next/sql-orm-client'; // 门面重导出待实现 — 参见*Prisma Next 当前未支持的功能*
await db.orm.Sale
.where((s) => and(s.day.gte(start), s.day.lte(end)))
.all();两种形式生成的SQL完全相同。当每个条件是独立逻辑时,选择链式;当一个逻辑谓词包含两个部分且需要视觉分组时,选择。不要尝试使用不存在的辅助方法。
.where()and(...)between组合器(、、)用于组合谓词,关联谓词(、、)用于递归处理关联关系。这些组合器当前来自内部包——参见Prisma Next 当前未支持的功能了解门面完整性缺口:
andornot.some(...).none(...).every(...)@prisma-next/sql-orm-clienttypescript
import { and, or, not } from '@prisma-next/sql-orm-client';
await db.orm.User
.where((u) =>
and(
or(u.kind.eq('admin'), u.email.ilike('%@example.com')),
not(u.posts.none((p) => p.title.ilike('%draft%'))),
),
)
.all();排序和分页。接受单个lambda表达式或lambda表达式数组(每个表达式对字段调用/)。限制返回条数;设置偏移量。
.orderBy(...).asc().desc().take(n).skip(n)typescript
await db.orm.Post
.where((p) => p.authorId.eq(userId))
.orderBy([(p) => p.createdAt.desc(), (p) => p.id.desc()])
.take(20)
.all();
// 游标分页 — 按索引唯一列排序,并过滤掉游标之前的记录。
const cursor = lastPostFromPreviousPage.createdAt;
await db.orm.Post
.where((p) => p.createdAt.lt(cursor))
.orderBy((p) => p.createdAt.desc())
.take(20)
.all();.first().first({ pk }).all().first()LIMIT 1.first({ pk }).all()LIMITConsuming the result: await
, .toArray()
, or for await
await.toArray()for await结果消费:await
、.toArray()
或for await
await.toArray()for awaitCritical to get right early — returns an , which is both a and an . That means three consumption forms all work, and the canonical one is the shortest:
.all()AsyncIterableResult<Row>PromiseLike<Row[]>AsyncIterable<Row>typescript
const users = await db.orm.User.select('id', 'email').all();
// ^? Row[] ← the Thenable resolves to a real array. This is the default idiom.You do not need a / helper — is enough. Internally invokes the result's , which buffers the rows into an array. Two equivalent alternatives exist for the cases where they read better:
collect()toArray()awaitawaitthen(...)typescript
// Explicit buffering — same outcome as `await ... .all()`, useful when you
// want a named Promise<Row[]> to thread through downstream code.
const rows: Promise<User[]> = db.orm.User.select('id', 'email').all().toArray();
// Streaming — process rows one at a time without buffering the whole result.
// Use for genuinely large result sets (anything that wouldn't fit comfortably
// in memory) or pipelines where you can start work before all rows arrive.
for await (const user of db.orm.User.select('id', 'email').all()) {
process(user);
}Two single-row shortcuts also exist on the result, in addition to the collection-level (which issues ):
.first()LIMIT 1typescript
const user = await db.orm.User.where({ id }).all().first();
// ^? Row | null ← buffers, returns the first row or null. Issues no LIMIT.
const required = await db.orm.User.where({ id }).all().firstOrThrow();
// ^? Row ← buffers; throws `RUNTIME.NO_ROWS` if empty.For genuine single-row reads, prefer the collection-level (which adds to the SQL) over (which fetches all rows and discards the rest). The result-level helpers are for cases where you already need the full result and want the first row without an extra round-trip.
.first()LIMIT 1.all().first()The result is single-consumption. Each instance can be consumed once — by , by , or by . Trying to consume it a second time throws . The fix is almost always to store the array in a variable on first consumption and reuse the variable:
AsyncIterableResultawait.toArray()for awaitRUNTIME.ITERATOR_CONSUMEDtypescript
// Bad — second await throws RUNTIME.ITERATOR_CONSUMED.
const result = db.orm.User.select('id', 'email').all();
const a = await result;
const b = await result;
// Good — buffer once, reuse the array.
const users = await db.orm.User.select('id', 'email').all();
const a = users;
const b = users;If you've seen / helpers in a codebase wrapping , they're vestigial — does the same thing for free. Remove them when you touch the surrounding code.
collect(...)toArray(...).all()await需要尽早理解的关键特性——返回****,它既是也是。这意味着三种消费方式均有效,最规范的是最短的方式:
.all()AsyncIterableResult<Row>PromiseLike<Row[]>AsyncIterable<Row>typescript
const users = await db.orm.User.select('id', 'email').all();
// ^? Row[] ← Thenable对象解析为真实数组。这是默认惯用写法。你不需要/辅助方法——已足够。内部实现中会调用结果的方法,将行数据缓冲为数组。在某些可读性更好的场景下,有两种等价的替代方式:
collect()toArray()awaitawaitthen(...)typescript
// 显式缓冲 — 与`await ... .all()`结果相同,适用于需要将Promise<Row[]>传递给下游代码的场景。
const rows: Promise<User[]> = db.orm.User.select('id', 'email').all().toArray();
// 流式处理 — 逐行处理,无需缓冲全部结果。
// 适用于真正的大数据集(无法舒适放入内存)或可在所有行到达前开始处理的流水线场景。
for await (const user of db.orm.User.select('id', 'email').all()) {
process(user);
}除了集合级别的(生成),结果对象还提供两种单行快捷方法:
.first()LIMIT 1typescript
const user = await db.orm.User.where({ id }).all().first();
// ^? Row | null ← 缓冲结果,返回第一行或null。不生成LIMIT。
const required = await db.orm.User.where({ id }).all().firstOrThrow();
// ^? Row ← 缓冲结果;若无数据则抛出`RUNTIME.NO_ROWS`。对于真正的单行查询,优先使用集合级别的(会在SQL中添加)而非(会获取所有行并丢弃其余行)。结果级别的辅助方法适用于已需要完整结果且无需额外查询即可获取第一行的场景。
.first()LIMIT 1.all().first()结果是单消费的。每个实例只能被消费一次——通过、或。尝试第二次消费会抛出****。解决方法几乎总是在第一次消费时将数组存储到变量中,然后重用该变量:
AsyncIterableResultawait.toArray()for awaitRUNTIME.ITERATOR_CONSUMEDtypescript
// 错误 — 第二次await会抛出RUNTIME.ITERATOR_CONSUMED。
const result = db.orm.User.select('id', 'email').all();
const a = await result;
const b = await result;
// 正确 — 缓冲一次,重用数组。
const users = await db.orm.User.select('id', 'email').all();
const a = users;
const b = users;如果代码库中存在用/辅助方法包裹的情况,这些方法是多余的——可免费实现相同功能。修改相关代码时可移除这些辅助方法。
collect(...)toArray().all()awaitWorkflow — Eager-loading relations (.include
)
.include工作流 — 预加载关联关系(.include
)
.includeThe concept: adds a relation branch to the parent query. The branch is its own collection — compose / / / on it just like the parent.
.include('<relation>', (branch) => branch.<chain>).where.select.orderBy.taketypescript
await db.orm.User
.select('id', 'email')
.include('posts', (post) =>
post
.select('id', 'title', 'createdAt')
.orderBy((p) => p.createdAt.desc())
.take(5),
)
.take(10)
.all();
// → Array<{ id, email, posts: Array<{ id, title, createdAt }> }>Nested includes (e.g. ) require the contract to advertise the + capabilities for the active target. The Postgres adapter advertises both by default, so most apps get this for free; if the type system rejects a nested include with a missing capability error, route to to add the required capability declarations and use for query-shape guidance.
1:N → 1:NUser → posts → commentslateraljsonAggprisma-next-contractprisma-next-queries核心逻辑:为父查询添加关联分支。分支本身是一个集合对象——可像父查询一样组合///。
.include('<relation>', (branch) => branch.<chain>).where.select.orderBy.taketypescript
await db.orm.User
.select('id', 'email')
.include('posts', (post) =>
post
.select('id', 'title', 'createdAt')
.orderBy((p) => p.createdAt.desc())
.take(5),
)
.take(10)
.all();
// → Array<{ id, email, posts: Array<{ id, title, createdAt }> }>嵌套的关联加载(例如)要求契约声明当前目标支持+能力。Postgres适配器默认支持这两种能力,因此大多数应用可直接使用;若类型系统因缺失能力错误拒绝嵌套关联加载,需转向添加所需能力声明,并参考获取查询结构指导。
1:N → 1:NUser → posts → commentslateraljsonAggprisma-next-contractprisma-next-queriesWorkflow — ORM writes
工作流 — ORM写入操作
typescript
// Create — returns the inserted row.
const user = await db.orm.User.create({ id, email, displayName, kind, createdAt });
// Create with selected return — narrows the return shape.
const summary = await db.orm.User
.select('id', 'email', 'kind')
.create({ id, email, displayName, kind, createdAt });
// Update by predicate.
await db.orm.User.where({ id }).update({ email: newEmail });
// Update with selected return.
await db.orm.User
.where({ id })
.select('id', 'email', 'kind')
.update({ email: newEmail });
// Delete by predicate.
await db.orm.User.where({ id }).delete();
// Upsert — typed by the create branch's shape.
await db.orm.User
.select('id', 'email', 'kind', 'createdAt')
.upsert({
create: { id, email, displayName, kind, createdAt: new Date() },
update: { email, displayName, kind },
});The ORM returns inserted / updated rows by default. The selector lives on the SQL builder (next section), where you build a plan and execute it explicitly.
.returning(...)typescript
// 创建记录 — 返回插入的行。
const user = await db.orm.User.create({ id, email, displayName, kind, createdAt });
// 创建记录并指定返回字段 — 缩小返回结果结构。
const summary = await db.orm.User
.select('id', 'email', 'kind')
.create({ id, email, displayName, kind, createdAt });
// 根据条件更新记录。
await db.orm.User.where({ id }).update({ email: newEmail });
// 更新记录并指定返回字段。
await db.orm.User
.where({ id })
.select('id', 'email', 'kind')
.update({ email: newEmail });
// 根据条件删除记录。
await db.orm.User.where({ id }).delete();
// 插入更新操作 — 类型由create分支的结构决定。
await db.orm.User
.select('id', 'email', 'kind', 'createdAt')
.upsert({
create: { id, email, displayName, kind, createdAt: new Date() },
update: { email, displayName, kind },
});ORM默认返回插入/更新后的行。选择器属于SQL构建器(下一章节),在SQL构建器中需先构建查询计划再显式执行。
.returning(...)Workflow — Aggregates
工作流 — 聚合操作
typescript
const totals = await db.orm.User.aggregate((aggregate) => ({
totalUsers: aggregate.count(),
}));
const adminTotals = await db.orm.User
.where({ kind: 'admin' })
.aggregate((aggregate) => ({
adminUsers: aggregate.count(),
}));
// Group-by + aggregate.
const byKind = await db.orm.User
.groupBy('kind')
.having((having) => having.count().gte(minUsers))
.aggregate((aggregate) => ({
totalUsers: aggregate.count(),
}));aggregate.count().sum(field).avg(field).min(field).max(field)Aggregate nullability matches SQL semantics:
| Aggregate | Type | Empty result |
|---|---|---|
| | |
| | |
| | |
| | |
| | |
This isn't a typing bug — it's faithful to what the database returns. Coalesce client-side when you want zero-fill:
typescript
const revenue = await db.orm.Sale
.where((s) => s.day.gte(start))
.aggregate((a) => ({ total: a.sum('amount') }));
// revenue.total: number | null
const safe = revenue.total ?? 0; // ← apply at the consumption site, not in the aggregate spec.If is showing up on every aggregate, that's a signal you're calling (or peers) over potentially-empty filters — which is exactly when SQL returns NULL. The pattern is correct; the typing is honest.
?? 0sumtypescript
const totals = await db.orm.User.aggregate((aggregate) => ({
totalUsers: aggregate.count(),
}));
const adminTotals = await db.orm.User
.where({ kind: 'admin' })
.aggregate((aggregate) => ({
adminUsers: aggregate.count(),
}));
// 分组 + 聚合。
const byKind = await db.orm.User
.groupBy('kind')
.having((having) => having.count().gte(minUsers))
.aggregate((aggregate) => ({
totalUsers: aggregate.count(),
}));aggregate.count().sum(field).avg(field).min(field).max(field)聚合结果空值处理符合SQL语义:
| 聚合方法 | 类型 | 空结果时返回值 |
|---|---|---|
| | |
| | |
| | |
| | |
| | |
这不是类型错误——而是忠实反映数据库的返回结果。当需要零填充时,在消费端进行合并:
typescript
const revenue = await db.orm.Sale
.where((s) => s.day.gte(start))
.aggregate((a) => ({ total: a.sum('amount') }));
// revenue.total: number | null
const safe = revenue.total ?? 0; // ← 在消费端应用,而非聚合声明中。如果出现在每个聚合操作中,说明你在对可能为空的过滤结果调用(或类似方法)——这正是SQL返回NULL的场景。该模式是正确的,类型声明是准确的。
?? 0sumWorkflow — SQL builder (db.sql.<table>
)
db.sql.<table>工作流 — SQL构建器(db.sql.<table>
)
db.sql.<table>The concept: is a table-shaped builder that produces a plan. The plan is a serialisable description of the query (AST + parameters); you execute it through the runtime with . The builder gives you the lanes the ORM doesn't express — explicit , arbitrary expression projection, target-specific operations through extension helpers — without dropping to raw SQL.
db.sql.<table>db.runtime().execute(plan)JOINtypescript
// src/queries/posts.ts — adjust the relative import to match file depth.
import { db } from '../prisma/db';
// Select with predicate and limit.
const plan = db.sql.post
.select('id', 'title', 'userId', 'createdAt')
.where((f, fns) => fns.eq(f.userId, userId))
.limit(limit)
.build();
const rows = await db.runtime().execute(plan);The callback receives — is the field proxy (column references), is the operator namespace (, , , …). Extensions inject extension-shaped helpers into the same namespace (, , etc.).
.where(...)(fields, fns)fieldsfnsfns.eqfns.nefns.gtfnsfns.distanceSpherefns.cosineDistance核心逻辑:是基于表结构的构建器,生成查询计划。查询计划是查询的可序列化描述(AST + 参数);通过运行时执行。构建器提供ORM无法表达的能力——显式、任意表达式投影、通过扩展助手实现的目标专属操作——无需直接编写原生SQL。
db.sql.<table>db.runtime().execute(plan)JOINtypescript
// src/queries/posts.ts — 根据文件层级调整相对导入路径。
import { db } from '../prisma/db';
// 带条件和条数限制的查询。
const plan = db.sql.post
.select('id', 'title', 'userId', 'createdAt')
.where((f, fns) => fns.eq(f.userId, userId))
.limit(limit)
.build();
const rows = await db.runtime().execute(plan);.where(...)(fields, fns)fieldsfnsfns.eqfns.nefns.gtfnsfns.distanceSpherefns.cosineDistanceINSERT
/ UPDATE
/ DELETE
with RETURNING
INSERTUPDATEDELETERETURNING带RETURNING
的INSERT
/UPDATE
/DELETE
RETURNINGINSERTUPDATEDELETEtypescript
// Insert and return selected columns.
const plan = db.sql.user
.insert({ email })
.returning('id', 'email')
.build();
const [row] = await db.runtime().execute(plan);
// Update with predicate and returning.
const updatePlan = db.sql.user
.update({ email: newEmail })
.where((f, fns) => fns.eq(f.id, userId))
.returning('id', 'email')
.build();
const rows = await db.runtime().execute(updatePlan);
// Delete with predicate.
const deletePlan = db.sql.user
.delete()
.where((f, fns) => fns.eq(f.id, userId))
.build();
await db.runtime().execute(deletePlan);.returning(...)returningtypescript
// 插入记录并返回指定列。
const plan = db.sql.user
.insert({ email })
.returning('id', 'email')
.build();
const [row] = await db.runtime().execute(plan);
// 根据条件更新记录并返回结果。
const updatePlan = db.sql.user
.update({ email: newEmail })
.where((f, fns) => fns.eq(f.id, userId))
.returning('id', 'email')
.build();
const rows = await db.runtime().execute(updatePlan);
// 根据条件删除记录。
const deletePlan = db.sql.user
.delete()
.where((f, fns) => fns.eq(f.id, userId))
.build();
await db.runtime().execute(deletePlan);.returning(...)returningComputed projections and joins
计算投影和连接
typescript
// Project a computed expression alongside model fields.
const plan = db.sql.cafe
.select('id', 'name')
.select('meters', (f, fns) => fns.distanceSphere(f.location, point))
.orderBy((f, fns) => fns.distanceSphere(f.location, point), { direction: 'asc' })
.orderBy((f) => f.id, { direction: 'asc' })
.limit(limit)
.build();
const rows = await db.runtime().execute(plan);
// Self-join with an alias.
db.sql.post
.innerJoin(db.sql.post.as('p2'), (f, fns) => fns.ne(f.p1.userId, f.p2.userId))
// ...
.build();typescript
// 投影计算表达式与模型字段。
const plan = db.sql.cafe
.select('id', 'name')
.select('meters', (f, fns) => fns.distanceSphere(f.location, point))
.orderBy((f, fns) => fns.distanceSphere(f.location, point), { direction: 'asc' })
.orderBy((f) => f.id, { direction: 'asc' })
.limit(limit)
.build();
const rows = await db.runtime().execute(plan);
// 自连接并使用别名。
db.sql.post
.innerJoin(db.sql.post.as('p2'), (f, fns) => fns.ne(f.p1.userId, f.p2.userId))
// ...
.build();Workflow — Transactions
工作流 — 事务
The concept: opens a transaction and passes a context to the callback. and mirror / but ride the same transaction; executes a SQL-builder plan within it. The transaction commits on the callback's successful return and rolls back on any thrown error.
db.transaction(fn)txtx.ormtx.sqldb.ormdb.sqltx.execute(plan)typescript
await db.transaction(async (tx) => {
const user = await tx.orm.User.create({ id, email });
await tx.orm.Post.create({ userId: user.id, title: 'hello' });
// SQL-builder plan inside the transaction.
const plan = tx.sql.post.update({ status: 'archived' })
.where((f, fns) => fns.lt(f.createdAt, cutoff))
.build();
await tx.execute(plan);
// If anything throws, all three operations roll back.
});The callback's return value passes through . Capture inserted ids out of the callback and use them downstream after commit.
db.transaction(...)核心逻辑:开启事务,并将上下文传递给回调函数。和镜像/,但共享同一事务;在事务内执行SQL构建器计划。回调函数成功返回时提交事务,抛出错误时回滚事务。
db.transaction(fn)txtx.ormtx.sqldb.ormdb.sqltx.execute(plan)typescript
await db.transaction(async (tx) => {
const user = await tx.orm.User.create({ id, email });
await tx.orm.Post.create({ userId: user.id, title: 'hello' });
// 事务内的SQL构建器计划。
const plan = tx.sql.post.update({ status: 'archived' })
.where((f, fns) => fns.lt(f.createdAt, cutoff))
.build();
await tx.execute(plan);
// 若任何操作抛出错误,所有三个操作都会回滚。
});回调函数的返回值会通过传递出来。可在回调函数外捕获插入的ID,并在事务提交后在下游使用。
db.transaction(...)Running queries from a short script
从短脚本中运行查询
When the user is running a one-off (not a long-lived server), call at the end so the process exits cleanly — especially on Postgres, where the façade-owned pool otherwise keeps Node's event loop alive. See § Running as a script (teardown) for the full pattern including .
tsx my-script.tsawait db.close()prisma-next-runtimeawait usingtypescript
// src/scripts/seed.ts
import { db } from '../prisma/db';
for (const u of users) {
await db.orm.User.create(u);
}
console.log('Seeded.');
await db.close();当用户运行一次性脚本(而非长期运行的服务器)时,需在脚本末尾调用以确保进程正常退出——尤其是在Postgres中,门面维护的连接池会保持Node事件循环处于活跃状态。完整模式请参考章节作为脚本运行(清理),包括的使用。
tsx my-script.tsawait db.close()prisma-next-runtimeawait usingtypescript
// src/scripts/seed.ts
import { db } from '../prisma/db';
for (const u of users) {
await db.orm.User.create(u);
}
console.log('数据初始化完成。');
await db.close();Common Pitfalls
常见陷阱
- Reaching for when
db.sqlwould have done. The ORM covers most CRUD shapes; the SQL builder is the seam for the shapes it doesn't. Default to the ORM.db.orm - Using when you wanted one row.
.all()issues no implicit.all(). UseLIMIT(issues.first()) orLIMIT 1..first({ pk }) - Writing a /
collect()helper to converttoArray()to an array..all()returns an.all()which is aAsyncIterableResult<Row>—PromiseLike<Row[]>directly yieldsawait collection.all(). The helpers some codebases ship are vestigial. See Consuming the result.Row[] - Consuming an twice. Each result is single-use. The second consumer throws
AsyncIterableResult. Buffer once into a variable and reuse the variable.RUNTIME.ITERATOR_CONSUMED - Coalescing with
count()"just in case".?? 0iscount(), notnumber— the runtime already substitutesnumber | nullfor the empty case. The0belongs on?? 0/sum/avg/min, whosemaxshape is faithful to SQL semantics over empty result sets.number | null - Reaching for on a field proxy. It doesn't exist. Either chain
.between(a, b)or use.where((m) => m.field.gte(a)).where((m) => m.field.lte(b))inside oneand(m.field.gte(a), m.field.lte(b))clause..where() - Importing /
and/orfrom a façade subpath. The combinators currently live innot— an internal package. See What Prisma Next doesn't do yet.@prisma-next/sql-orm-client - Trying to . That surface does not exist. The builder is table-shaped:
db.sql.from(tables.user). There is nodb.sql.<tableName>.select(...)either.db.schema.tables - Trying to directly. Plans execute through the runtime:
db.execute(plan). Inside a transaction, usedb.runtime().execute(plan).tx.execute(plan) - Setting in
capabilities: { includeMany: true }.prisma-next.config.tsdoes not takedefineConfig. Capabilities are declared by the active adapter and become part of the emitted contract; the Postgres adapter advertisescapabilities,lateral, andjsonAggout of the box. Enable extension capabilities throughreturningin the config (seeextensions: [...]).prisma-next-contract - Confabulating a , TypedSQL, or
db.sql.raw(...)surface. None of those exist today. See What Prisma Next doesn't do yet..stream() - Mixing the ORM mutation return with . The ORM's terminal verbs (
db.execute(plan),.create,.update,.delete,.first,.all) issue the query themselves and return rows. Don't pass the builder to.aggregate— that's for SQL-builder plans.db.runtime().execute(...) - Top-N grouped queries written as in JS. That's a fallback because the grouped collection doesn't expose
groupBy(...).aggregate(...).sort().slice()/.orderBy(...)(see What PN doesn't do yet). Fine at small cardinalities; for genuinely large grouped result sets, drop to.take(...)and writedb.sql.<table>+GROUP BY+ORDER BYagainst the table directly.LIMIT
- 当ORM可满足需求时仍使用。ORM覆盖大多数CRUD场景;SQL构建器仅用于ORM无法处理的场景。默认使用ORM。
db.sql - 需要单行记录时使用。
.all()不生成隐式.all()。使用LIMIT(生成.first())或LIMIT 1。.first({ pk }) - 编写/
collect()辅助方法将toArray()转换为数组。.all()返回的.all()本身就是AsyncIterableResult<Row>——直接PromiseLike<Row[]>即可得到await collection.all()。代码库中存在的此类辅助方法是多余的。参见结果消费章节。Row[] - 多次消费。每个结果只能使用一次。第二次消费会抛出
AsyncIterableResult。缓冲一次到变量中并重用该变量。RUNTIME.ITERATOR_CONSUMED - “以防万一”使用合并
?? 0结果。count()的类型是count(),而非number——运行时已为空结果替换为number | null。0应用于?? 0/sum/avg/min,它们的max类型忠实反映了SQL对空结果集的语义。number | null - 尝试在字段代理上使用。该方法不存在。可链式调用
.between(a, b)或在单个.where((m) => m.field.gte(a)).where((m) => m.field.lte(b))子句中使用.where()。and(m.field.gte(a), m.field.lte(b)) - 从门面子路径导入/
and/or组合器。这些组合器当前位于not包——一个内部包。参见Prisma Next 当前未支持的功能。@prisma-next/sql-orm-client - 尝试使用。该接口不存在。构建器是基于表结构的:
db.sql.from(tables.user)。也不存在db.sql.<tableName>.select(...)。db.schema.tables - 直接使用。查询计划需通过运行时执行:
db.execute(plan)。在事务内使用db.runtime().execute(plan)。tx.execute(plan) - 在中设置
prisma-next.config.ts。capabilities: { includeMany: true }不接受defineConfig参数。能力由当前适配器声明并成为生成契约的一部分;Postgres适配器默认支持capabilities、lateral和jsonAgg。通过配置中的returning启用扩展能力(参见extensions: [...])。prisma-next-contract - 虚构、TypedSQL或
db.sql.raw(...)接口。这些接口当前均不存在。参见Prisma Next 当前未支持的功能。.stream() - 混合使用ORM变更返回结果与。ORM的终结方法(
db.execute(plan)、.create、.update、.delete、.first、.all)会自行执行查询并返回行数据。不要将ORM构建器传递给.aggregate——该方法用于执行SQL构建器计划。db.runtime().execute(...) - 在JS中编写Top-N分组查询为。这是一种 fallback 方案,因为分组集合未暴露
groupBy(...).aggregate(...).sort().slice()/.orderBy(...)方法(参见Prisma Next 当前未支持的功能)。在小基数场景下可行;对于真正的大数据分组结果集,应切换到.take(...)并直接针对表编写db.sql.<table>+GROUP BY+ORDER BY。LIMIT
What Prisma Next doesn't do yet
Prisma Next 当前未支持的功能
- /
and/orcombinators in the postgres façade. The combinators currently import fromnot(an internal package). Tracked alongside other façade-completeness gaps in Linear@prisma-next/sql-orm-client. Workaround today: import them fromTML-2526directly, the way the example apps do. If you want them on@prisma-next/sql-orm-client, file a feature request via@prisma-next/postgres/runtime.prisma-next-feedback - /
.orderBy(...)on grouped aggregates..take(...)materializes adb.orm.<Model>.groupBy(...).aggregate(...)and exposes neither ordering nor row limits at the DB layer. Result: a "top-N groups by SUM" query falls back to JS-side sort + slice over the full grouped result, which is fine at small cardinalities and bad at scale. Workarounds: (a) drop toPromise<Array<Group & Aggregates>>and write thedb.sql.<table>+GROUP BY+ORDER BYagainst the aggregated table directly; (b) live with the JS-side sort/slice if the grouped cardinality is bounded. File a feature request viaLIMITif this is hitting you in production.prisma-next-feedback - A raw-SQL lane. Prisma Next does not currently expose a user-facing raw-SQL surface (no ). Workaround: model the query through the SQL builder or — for shapes the builder can't yet express — file a feature request via
db.sql.raw(...)describing the shape so the team can decide whether to grow the builder or ship a raw lane.prisma-next-feedback - TypedSQL (files compiled into typed callables). Not implemented. Workaround: stick to the SQL builder; for repeated queries, extract a function that returns the built plan and call
.sqlat the call site. If you want adb.runtime().execute(plan)-file compile path, file a feature request via.sql.prisma-next-feedback - / query-plan inspection. Prisma Next does not expose an
EXPLAINmethod. Workaround: connect a.explain()you control via the runtime'spg.Poolbinding (seepg:) and issueprisma-next-runtimethrough it. If you want a first-class plan-inspection surface, file a feature request viaEXPLAIN ANALYZE.prisma-next-feedback - Streaming large result sets. No cursor today. Workaround: paginate via
.stream()for moderate sizes; for very large sets, hold a.skip(n).take(m)from the runtime'spg.Clientbinding and stream through it directly. If you want a built-in streaming surface, file a feature request viapg:.prisma-next-feedback - Multi-statement batching (Prisma-7-style ). Prisma Next runs each call sequentially. Workaround: wrap atomically-related work in
db.$transaction([call1, call2]). If you want batch-as-array semantics, file a feature request viadb.transaction(async (tx) => { ... }).prisma-next-feedback - Automatic N+1 detection. Prisma Next does not warn when an is missing. Workaround: be deliberate about
.include(...)in code review; the.include(...)middleware (seelints) catches the more common authoring slips (missingprisma-next-runtimeon aWHERE/DELETE, missingUPDATEon aLIMIT).SELECT
- postgres门面中的/
and/or组合器。这些组合器当前需从not(内部包)导入。相关门面完整性缺口在Linear@prisma-next/sql-orm-client中跟踪。当前 workaround:直接从TML-2526导入,示例应用采用此方式。若希望在@prisma-next/sql-orm-client中提供这些组合器,可通过@prisma-next/postgres/runtime提交功能请求。prisma-next-feedback - 分组聚合的/
.orderBy(...)。.take(...)生成db.orm.<Model>.groupBy(...).aggregate(...),不在数据库层暴露排序和条数限制。结果:“按SUM取Top-N分组”查询需回退到JS端对完整分组结果进行排序+切片,在小基数场景下可行,但在大规模场景下性能不佳。Workaround:(a) 切换到Promise<Array<Group & Aggregates>>并直接针对聚合表编写db.sql.<table>+GROUP BY+ORDER BY;(b) 若分组基数有限,可接受JS端排序/切片。若该问题影响生产环境,可通过LIMIT提交功能请求。prisma-next-feedback - 原生SQL方式。Prisma Next 当前未暴露面向用户的原生SQL接口(无)。Workaround:通过SQL构建器建模查询;对于构建器无法表达的场景,通过
db.sql.raw(...)提交功能请求描述场景,以便团队决定扩展构建器还是提供原生SQL方式。prisma-next-feedback - TypedSQL(编译文件为类型化可调用函数)。尚未实现。Workaround:坚持使用SQL构建器;对于重复查询,提取返回构建计划的函数,并在调用站点执行
.sql。若希望支持db.runtime().execute(plan)文件编译路径,可通过.sql提交功能请求。prisma-next-feedback - /查询计划检查。Prisma Next 未暴露
EXPLAIN方法。Workaround:通过运行时的.explain()绑定连接你自己控制的pg:(参见pg.Pool)并通过它执行prisma-next-runtime。若希望提供一流的计划检查接口,可通过EXPLAIN ANALYZE提交功能请求。prisma-next-feedback - 大数据集流式处理。当前无游标。Workaround:对于中等规模数据,通过
.stream()分页;对于超大数据集,从运行时的.skip(n).take(m)绑定获取pg:并直接流式处理。若希望提供内置流式接口,可通过pg.Client提交功能请求。prisma-next-feedback - 多语句批量处理(Prisma-7风格的)。Prisma Next 按顺序执行每个调用。Workaround:将原子相关操作包装在
db.$transaction([call1, call2])中。若希望支持数组形式的批量语义,可通过db.transaction(async (tx) => { ... })提交功能请求。prisma-next-feedback - 自动N+1检测。Prisma Next 不会在缺少时发出警告。Workaround:在代码评审中谨慎处理
.include(...);.include(...)中间件(参见lints)可捕获常见的编写疏漏(prisma-next-runtime/DELETE缺少UPDATE,WHERE缺少SELECT)。LIMIT
Reference Files
参考文件
This skill is intentionally body-only. The authoritative surfaces are:
- The example queries under and
examples/prisma-next-demo/src/orm-client/— the canonical worked references for ORM and SQL-builder shapes respectively.examples/prisma-next-demo/src/queries/ - The ORM client source under for the full collection method surface.
packages/3-extensions/sql-orm-client/src/ - The SQL builder source under for the builder method surface.
packages/2-sql/4-lanes/sql-builder/src/
本技能仅包含主体内容。权威接口如下:
- 和
examples/prisma-next-demo/src/orm-client/下的示例查询——分别是ORM和SQL构建器场景的规范参考实现。examples/prisma-next-demo/src/queries/ - 下的ORM客户端源码——包含完整的集合方法接口。
packages/3-extensions/sql-orm-client/src/ - 下的SQL构建器源码——包含完整的构建器方法接口。
packages/2-sql/4-lanes/sql-builder/src/
Checklist
检查清单
- Chose the right lane (ORM by default; SQL builder for set-builder shapes the ORM doesn't express).
- Used /
.first()for single-row reads — not.first({ pk })..all() - Consumed with plain
.all()(not aawait/collect()helper). UsedtoArray()only when streaming is actually wanted, and never iterated the same result twice.for await - Coalesced /
sum/avg/minresults withmax(or similar) at the consumption site when zero-fill is desired — did NOT coalesce?? 0, which iscount().number - Expressed ranges as chained clauses or a single
.where(...)clause — did NOT reach for a non-existentand(...)operator..between(...) - For ORM combinators, imported /
and/orfrom the (currently internal)notand noted the façade gap to the user.@prisma-next/sql-orm-client - Executed SQL-builder plans via (or
db.runtime().execute(plan)inside a transaction).tx.execute(plan) - Wrapped multi-statement work in where atomicity matters.
db.transaction(async (tx) => { ... }) - Did NOT confabulate , TypedSQL,
db.sql.raw,.stream(),db.batch, a.between(...)field oncapabilities, or adefineConfigAPI — routed to What Prisma Next doesn't do yet /db.sql.from(tables.user)instead.prisma-next-feedback - For top-N grouped aggregates at meaningful scale, dropped to rather than JS-side sort + slice over
db.sql.<table>.groupBy(...).aggregate(...) - Did NOT use the SQL builder for something the ORM cleanly expresses.
- 选择了正确的方式(默认使用ORM;SQL构建器用于ORM无法表达的集合构造场景)。
- 使用/
.first()进行单行查询——而非.first({ pk })。.all() - 使用纯消费
await(而非.all()/collect()辅助方法)。仅在确实需要流式处理时使用toArray(),且从未重复迭代同一结果。for await - 当需要零填充时,在消费端使用(或类似方式)合并
?? 0/sum/avg/min结果——未合并max,因为其类型为count()。number - 通过链式子句或单个
.where()子句表达范围条件——未尝试使用不存在的and(...)操作符。.between(...) - 对于ORM组合器,从当前内部包导入
@prisma-next/sql-orm-client/and/or,并向用户说明门面缺口。not - 通过(或事务内的
db.runtime().execute(plan))执行SQL构建器计划。tx.execute(plan) - 在需要原子性的场景下,将多语句操作包装在中。
db.transaction(async (tx) => { ... }) - 未虚构、TypedSQL、
db.sql.raw、.stream()、db.batch、.between(...)的defineConfig字段或capabilities接口——而是转向Prisma Next 当前未支持的功能 /db.sql.from(tables.user)。prisma-next-feedback - 对于大规模场景下的Top-N分组聚合,切换到而非对
db.sql.<table>进行JS端排序+切片。groupBy(...).aggregate(...) - 未使用SQL构建器处理ORM可干净表达的场景。