prisma-next-queries

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Prisma 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
    db.ts
    or add middleware →
    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
db
value from
src/prisma/db.ts
(which the
@prisma-next/<target>/runtime
façade returns):
  • db.orm.<Model>
    — the ORM. Model-shaped (
    db.orm.User
    ), fluent (
    .where(...).select(...).orderBy(...).all()
    ), fully typed against
    Contract
    . Default lane for CRUD with relations.
  • db.sql.<table>
    — the SQL builder. Table-shaped (
    db.sql.user
    , lowercase by storage name), produces a plan you execute through the runtime. Use when the ORM is too high-level — explicit
    JOIN
    , computed projections that aren't model fields, set operations, window functions.
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
db.sql
when the ORM is too high-level. The lane choice is local: a single query function picks one lane, not the whole app.
Lane decision table:
NeedChooseWhy
Standard CRUD with relationsORM (
db.orm.<Model>
)
Highest ergonomics; fully typed; model-shaped.
Eager-load related recordsORM
.include(...)
Composes with
.where
/
.select
/
.orderBy
/
.take
per branch.
Aggregate (count, sum, avg)ORM
.aggregate(...)
Typed result; works with grouping (
.groupBy(...).aggregate(...)
).
INSERT ... RETURNING
/
UPDATE ... RETURNING
typed result
ORM mutations (returns updated rows) or
db.sql.<t>.insert(...).returning(...)
ORM returns inserted/updated rows; SQL builder exposes
.returning(...)
explicitly.
Computed projection (e.g.
ST_DistanceSphere(location, point) AS meters
) alongside model fields
SQL builder (
db.sql.<t>
)
The ORM projects model fields; arbitrary expression projection is the SQL builder's seam.
Complex
JOIN
, set operation, window function
SQL builderThe ORM doesn't express arbitrary joins.
Postgres-specific feature (
LATERAL
,
FILTER
, custom aggregates)
SQL builder, falling back to extension operators when the extension provides themDSL first; extensions can contribute operators (
postgis
,
pgvector
,
cipherstash
).
Prisma Next 基于单个契约提供两种查询方式,均可通过
src/prisma/db.ts
中的
db
对象访问(由
@prisma-next/<target>/runtime
门面返回):
  • db.orm.<Model>
    — ORM方式。基于模型结构(如
    db.orm.User
    ),支持链式调用(
    .where(...).select(...).orderBy(...).all()
    ),完全基于
    Contract
    提供类型校验。是处理带关联关系的CRUD操作的默认方式。
  • db.sql.<table>
    — SQL构建器方式。基于数据库表结构(如
    db.sql.user
    ,采用存储时的小写表名),生成可通过运行时执行的查询计划。适用于ORM无法处理的高级场景——显式
    JOIN
    、非模型字段的计算投影、集合操作、窗口函数等。
两种方式共享同一契约、同一连接和同一事务上下文,可无缝组合。优先使用ORM方式;当ORM无法满足需求时,再切换到
db.sql
方式。方式选择是局部性的:单个查询函数可选择一种方式,而非整个应用统一选择。
方式决策表:
需求选择方式原因
带关联关系的标准CRUD操作ORM(
db.orm.<Model>
最高的易用性;完全类型校验;基于模型结构。
预加载关联记录ORM
.include(...)
可与分支上的
.where
/
.select
/
.orderBy
/
.take
组合使用。
聚合操作(count、sum、avg等)ORM
.aggregate(...)
类型化结果;支持分组(
.groupBy(...).aggregate(...)
)。
带类型化结果的
INSERT ... RETURNING
/
UPDATE ... RETURNING
ORM 变更操作(返回更新后的行)或**
db.sql.<t>.insert(...).returning(...)
**
ORM返回插入/更新后的行;SQL构建器显式提供
.returning(...)
方法。
计算投影(例如
ST_DistanceSphere(location, point) AS meters
)与模型字段共存
SQL构建器(
db.sql.<t>
ORM仅投影模型字段;任意表达式投影是SQL构建器的核心场景。
复杂
JOIN
、集合操作、窗口函数
SQL构建器ORM无法表达任意连接逻辑。
Postgres专属特性(
LATERAL
FILTER
、自定义聚合)
SQL构建器,当扩展提供相关操作符时可回退到扩展操作符优先使用DSL;扩展可贡献专属操作符(如
postgis
pgvector
cipherstash
)。

Workflow — ORM reads

工作流 — ORM查询

The concept:
db.orm.<Model>
returns a collection you compose method-by-method. Each call returns a new collection (immutable chaining); the terminal verb (
.all()
/
.first()
/
.count()
/
.aggregate(...)
) issues the query. Predicates are lambdas over a field proxy:
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 (
.where(...)
) come in two forms:
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
.eq
,
.neq
,
.lt
,
.lte
,
.gt
,
.gte
,
.like
,
.ilike
,
.in([...])
,
.isNull()
,
.isNotNull()
. Extensions add target-specific operators on extension-typed columns (
pgvector
's
.cosineDistance(...)
,
postgis
's
.within(...)
/
.intersectsBbox(...)
/
.distanceSphere(...)
,
cipherstash
's
.cipherstashEq(...)
/
.cipherstashGt(...)
/ …).
There is no
.between(a, b)
operator.
Express ranges either as two chained
.where(...)
clauses (the idiomatic form — clauses AND-compose) or with the
and(...)
combinator inside one clause:
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
.where()
when each clause adds a separate condition that reads as its own thought; pick
and(...)
when one logical predicate happens to have two parts and you want the visual grouping. Don't reach for a
between
helper — there isn't one.
Combinators (
and
,
or
,
not
) compose predicates, and relation predicates (
.some(...)
,
.none(...)
,
.every(...)
) recurse into a relation. These currently come from the internal
@prisma-next/sql-orm-client
package — see What Prisma Next doesn't do yet for the façade-completeness gap:
typescript
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.
.orderBy(...)
accepts a single lambda or an array of lambdas (each calling
.asc()
/
.desc()
on a field).
.take(n)
limits;
.skip(n)
offsets.
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()
vs
.first({ pk })
vs
.all()
.
Use
.first()
for a single row (issues a
LIMIT 1
); use
.first({ pk })
for primary-key lookups; reserve
.all()
for the genuine many case (no implicit
LIMIT
).
核心逻辑:
db.orm.<Model>
返回一个可通过链式调用逐步组合的集合对象。每次调用返回新的集合对象(不可变链式调用);最终调用终结方法(
.all()
/
.first()
/
.count()
/
.aggregate(...)
)时执行查询。条件谓词是基于字段代理的lambda表达式:
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(...)
等)。
不存在
.between(a, b)
操作符
。范围条件可通过两种方式表达:链式调用
.where(...)
子句(惯用形式——子句之间自动AND组合)或在单个子句中使用
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
辅助方法。
组合器
and
or
not
)用于组合谓词,关联谓词
.some(...)
.none(...)
.every(...)
)用于递归处理关联关系。这些组合器当前来自内部包
@prisma-next/sql-orm-client
——参见Prisma Next 当前未支持的功能了解门面完整性缺口:
typescript
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();
排序和分页
.orderBy(...)
接受单个lambda表达式或lambda表达式数组(每个表达式对字段调用
.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()
vs
.first({ pk })
vs
.all()
。使用
.first()
查询单条记录(生成
LIMIT 1
);使用
.first({ pk })
进行主键查询;仅在确实需要多条记录时使用
.all()
(无隐式
LIMIT
)。

Consuming the result:
await
,
.toArray()
, or
for await

结果消费:
await
.toArray()
for await

Critical to get right early —
.all()
returns an
AsyncIterableResult<Row>
, which is both a
PromiseLike<Row[]>
and an
AsyncIterable<Row>
. That means three consumption forms all work, and the canonical one is the shortest:
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
collect()
/
toArray()
helper —
await
is enough. Internally
await
invokes the result's
then(...)
, which buffers the rows into an array. Two equivalent alternatives exist for the cases where they read better:
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
.first()
(which issues
LIMIT 1
):
typescript
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
.first()
(which adds
LIMIT 1
to the SQL) over
.all().first()
(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.
The result is single-consumption. Each
AsyncIterableResult
instance can be consumed once — by
await
, by
.toArray()
, or by
for await
. Trying to consume it a second time throws
RUNTIME.ITERATOR_CONSUMED
. The fix is almost always to store the array in a variable on first consumption and reuse the variable:
typescript
// 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
collect(...)
/
toArray(...)
helpers in a codebase wrapping
.all()
, they're vestigial —
await
does the same thing for free. Remove them when you touch the surrounding code.
需要尽早理解的关键特性——
.all()
返回**
AsyncIterableResult<Row>
**,它既是
PromiseLike<Row[]>
也是
AsyncIterable<Row>
。这意味着三种消费方式均有效,最规范的是最短的方式:
typescript
const users = await db.orm.User.select('id', 'email').all();
//    ^? Row[]   ← Thenable对象解析为真实数组。这是默认惯用写法。
不需要
collect()
/
toArray()
辅助方法——
await
已足够。内部实现中
await
会调用结果的
then(...)
方法,将行数据缓冲为数组。在某些可读性更好的场景下,有两种等价的替代方式:
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 1
),结果对象还提供两种单行快捷方法:
typescript
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`。
对于真正的单行查询,优先使用集合级别
.first()
(会在SQL中添加
LIMIT 1
)而非
.all().first()
(会获取所有行并丢弃其余行)。结果级别的辅助方法适用于已需要完整结果且无需额外查询即可获取第一行的场景。
结果是单消费的。每个
AsyncIterableResult
实例只能被消费一次——通过
await
.toArray()
for await
。尝试第二次消费会抛出**
RUNTIME.ITERATOR_CONSUMED
**。解决方法几乎总是在第一次消费时将数组存储到变量中,然后重用该变量:
typescript
// 错误 — 第二次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()
的情况,这些方法是多余的——
await
可免费实现相同功能。修改相关代码时可移除这些辅助方法。

Workflow — Eager-loading relations (
.include
)

工作流 — 预加载关联关系(
.include

The concept:
.include('<relation>', (branch) => branch.<chain>)
adds a relation branch to the parent query. The branch is its own collection — compose
.where
/
.select
/
.orderBy
/
.take
on it just like the parent.
typescript
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
1:N → 1:N
includes (e.g.
User → posts → comments
) require the contract to advertise the
lateral
+
jsonAgg
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
prisma-next-contract
to add the required capability declarations and use
prisma-next-queries
for query-shape guidance.
核心逻辑:
.include('<relation>', (branch) => branch.<chain>)
为父查询添加关联分支。分支本身是一个集合对象——可像父查询一样组合
.where
/
.select
/
.orderBy
/
.take
typescript
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 }> }>
嵌套的
1:N → 1:N
关联加载(例如
User → posts → comments
)要求契约声明当前目标支持
lateral
+
jsonAgg
能力。Postgres适配器默认支持这两种能力,因此大多数应用可直接使用;若类型系统因缺失能力错误拒绝嵌套关联加载,需转向
prisma-next-contract
添加所需能力声明,并参考
prisma-next-queries
获取查询结构指导。

Workflow — 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
.returning(...)
selector lives on the SQL builder (next section), where you build a plan and execute it explicitly.
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默认返回插入/更新后的行。
.returning(...)
选择器属于SQL构建器(下一章节),在SQL构建器中需先构建查询计划再显式执行。

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
exposes
.count()
,
.sum(field)
,
.avg(field)
,
.min(field)
,
.max(field)
. Project the aggregates into named result keys; the result type narrows accordingly.
Aggregate nullability matches SQL semantics:
AggregateTypeEmpty result
count()
number
0
sum(field)
number | null
null
(SQL
SUM
over zero rows is
NULL
)
avg(field)
number | null
null
min(field)
number | null
null
max(field)
number | null
null
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
?? 0
is showing up on every aggregate, that's a signal you're calling
sum
(or peers) over potentially-empty filters — which is exactly when SQL returns NULL. The pattern is correct; the typing is honest.
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(),
  }));

// 分组 + 聚合。
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语义:
聚合方法类型空结果时返回值
count()
number
0
sum(field)
number | null
null
(SQL中对零行执行
SUM
返回
NULL
avg(field)
number | null
null
min(field)
number | null
null
max(field)
number | null
null
这不是类型错误——而是忠实反映数据库的返回结果。当需要零填充时,在消费端进行合并:
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;   // ← 在消费端应用,而非聚合声明中。
如果
?? 0
出现在每个聚合操作中,说明你在对可能为空的过滤结果调用
sum
(或类似方法)——这正是SQL返回NULL的场景。该模式是正确的,类型声明是准确的。

Workflow — SQL builder (
db.sql.<table>
)

工作流 — SQL构建器(
db.sql.<table>

The concept:
db.sql.<table>
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
db.runtime().execute(plan)
. The builder gives you the lanes the ORM doesn't express — explicit
JOIN
, arbitrary expression projection, target-specific operations through extension helpers — without dropping to raw SQL.
typescript
// 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
.where(...)
callback receives
(fields, fns)
fields
is the field proxy (column references),
fns
is the operator namespace (
fns.eq
,
fns.ne
,
fns.gt
, …). Extensions inject extension-shaped helpers into the same
fns
namespace (
fns.distanceSphere
,
fns.cosineDistance
, etc.).
核心逻辑:
db.sql.<table>
是基于表结构的构建器,生成查询计划。查询计划是查询的可序列化描述(AST + 参数);通过运行时
db.runtime().execute(plan)
执行。构建器提供ORM无法表达的能力——显式
JOIN
、任意表达式投影、通过扩展助手实现的目标专属操作——无需直接编写原生SQL。
typescript
// 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)
——
fields
是字段代理(列引用),
fns
是操作符命名空间(
fns.eq
fns.ne
fns.gt
等)。扩展会将扩展专属助手注入同一
fns
命名空间(如
fns.distanceSphere
fns.cosineDistance
等)。

INSERT
/
UPDATE
/
DELETE
with
RETURNING

RETURNING
INSERT
/
UPDATE
/
DELETE

typescript
// 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(...)
requires the target adapter to advertise the
returning
capability. The Postgres adapter advertises it by default.
typescript
// 插入记录并返回指定列。
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(...)
要求目标适配器声明支持
returning
能力。Postgres适配器默认支持该能力。

Computed 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:
db.transaction(fn)
opens a transaction and passes a
tx
context to the callback.
tx.orm
and
tx.sql
mirror
db.orm
/
db.sql
but ride the same transaction;
tx.execute(plan)
executes a SQL-builder plan within it. The transaction commits on the callback's successful return and rolls back on any thrown error.
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
db.transaction(...)
. Capture inserted ids out of the callback and use them downstream after commit.
核心逻辑:
db.transaction(fn)
开启事务,并将
tx
上下文传递给回调函数。
tx.orm
tx.sql
镜像
db.orm
/
db.sql
,但共享同一事务;
tx.execute(plan)
在事务内执行SQL构建器计划。回调函数成功返回时提交事务,抛出错误时回滚事务。
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);

  // 若任何操作抛出错误,所有三个操作都会回滚。
});
回调函数的返回值会通过
db.transaction(...)
传递出来。可在回调函数外捕获插入的ID,并在事务提交后在下游使用。

Running queries from a short script

从短脚本中运行查询

When the user is running a one-off
tsx my-script.ts
(not a long-lived server), call
await db.close()
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
prisma-next-runtime
§ Running as a script (teardown) for the full pattern including
await using
.
typescript
// 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();
当用户运行一次性脚本
tsx my-script.ts
(而非长期运行的服务器)时,需在脚本末尾调用
await db.close()
以确保进程正常退出——尤其是在Postgres中,门面维护的连接池会保持Node事件循环处于活跃状态。完整模式请参考
prisma-next-runtime
章节作为脚本运行(清理),包括
await using
的使用。
typescript
// 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

常见陷阱

  1. Reaching for
    db.sql
    when
    db.orm
    would have done.
    The ORM covers most CRUD shapes; the SQL builder is the seam for the shapes it doesn't. Default to the ORM.
  2. Using
    .all()
    when you wanted one row.
    .all()
    issues no implicit
    LIMIT
    . Use
    .first()
    (issues
    LIMIT 1
    ) or
    .first({ pk })
    .
  3. Writing a
    collect()
    /
    toArray()
    helper to convert
    .all()
    to an array.
    .all()
    returns an
    AsyncIterableResult<Row>
    which is a
    PromiseLike<Row[]>
    await collection.all()
    directly yields
    Row[]
    . The helpers some codebases ship are vestigial. See Consuming the result.
  4. Consuming an
    AsyncIterableResult
    twice.
    Each result is single-use. The second consumer throws
    RUNTIME.ITERATOR_CONSUMED
    . Buffer once into a variable and reuse the variable.
  5. Coalescing
    count()
    with
    ?? 0
    "just in case".
    count()
    is
    number
    , not
    number | null
    — the runtime already substitutes
    0
    for the empty case. The
    ?? 0
    belongs on
    sum
    /
    avg
    /
    min
    /
    max
    , whose
    number | null
    shape is faithful to SQL semantics over empty result sets.
  6. Reaching for
    .between(a, b)
    on a field proxy.
    It doesn't exist. Either chain
    .where((m) => m.field.gte(a)).where((m) => m.field.lte(b))
    or use
    and(m.field.gte(a), m.field.lte(b))
    inside one
    .where()
    clause.
  7. Importing
    and
    /
    or
    /
    not
    from a façade subpath.
    The combinators currently live in
    @prisma-next/sql-orm-client
    — an internal package. See What Prisma Next doesn't do yet.
  8. Trying to
    db.sql.from(tables.user)
    .
    That surface does not exist. The builder is table-shaped:
    db.sql.<tableName>.select(...)
    . There is no
    db.schema.tables
    either.
  9. Trying to
    db.execute(plan)
    directly.
    Plans execute through the runtime:
    db.runtime().execute(plan)
    . Inside a transaction, use
    tx.execute(plan)
    .
  10. Setting
    capabilities: { includeMany: true }
    in
    prisma-next.config.ts
    .
    defineConfig
    does not take
    capabilities
    . Capabilities are declared by the active adapter and become part of the emitted contract; the Postgres adapter advertises
    lateral
    ,
    jsonAgg
    , and
    returning
    out of the box. Enable extension capabilities through
    extensions: [...]
    in the config (see
    prisma-next-contract
    ).
  11. Confabulating a
    db.sql.raw(...)
    , TypedSQL, or
    .stream()
    surface.
    None of those exist today. See What Prisma Next doesn't do yet.
  12. Mixing the ORM mutation return with
    db.execute(plan)
    .
    The ORM's terminal verbs (
    .create
    ,
    .update
    ,
    .delete
    ,
    .first
    ,
    .all
    ,
    .aggregate
    ) issue the query themselves and return rows. Don't pass the builder to
    db.runtime().execute(...)
    — that's for SQL-builder plans.
  13. Top-N grouped queries written as
    groupBy(...).aggregate(...).sort().slice()
    in JS.
    That's a fallback because the grouped collection doesn't expose
    .orderBy(...)
    /
    .take(...)
    (see What PN doesn't do yet). Fine at small cardinalities; for genuinely large grouped result sets, drop to
    db.sql.<table>
    and write
    GROUP BY
    +
    ORDER BY
    +
    LIMIT
    against the table directly.
  1. 当ORM可满足需求时仍使用
    db.sql
    。ORM覆盖大多数CRUD场景;SQL构建器仅用于ORM无法处理的场景。默认使用ORM。
  2. 需要单行记录时使用
    .all()
    .all()
    不生成隐式
    LIMIT
    。使用
    .first()
    (生成
    LIMIT 1
    )或
    .first({ pk })
  3. 编写
    collect()
    /
    toArray()
    辅助方法将
    .all()
    转换为数组
    .all()
    返回的
    AsyncIterableResult<Row>
    本身就是
    PromiseLike<Row[]>
    ——直接
    await collection.all()
    即可得到
    Row[]
    。代码库中存在的此类辅助方法是多余的。参见结果消费章节。
  4. 多次消费
    AsyncIterableResult
    。每个结果只能使用一次。第二次消费会抛出
    RUNTIME.ITERATOR_CONSUMED
    。缓冲一次到变量中并重用该变量。
  5. “以防万一”使用
    ?? 0
    合并
    count()
    结果
    count()
    的类型是
    number
    ,而非
    number | null
    ——运行时已为空结果替换为
    0
    ?? 0
    应用于
    sum
    /
    avg
    /
    min
    /
    max
    ,它们的
    number | null
    类型忠实反映了SQL对空结果集的语义。
  6. 尝试在字段代理上使用
    .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))
  7. 从门面子路径导入
    and
    /
    or
    /
    not
    组合器
    。这些组合器当前位于
    @prisma-next/sql-orm-client
    包——一个内部包。参见Prisma Next 当前未支持的功能
  8. 尝试使用
    db.sql.from(tables.user)
    。该接口不存在。构建器是基于表结构的:
    db.sql.<tableName>.select(...)
    。也不存在
    db.schema.tables
  9. 直接使用
    db.execute(plan)
    。查询计划需通过运行时执行:
    db.runtime().execute(plan)
    。在事务内使用
    tx.execute(plan)
  10. prisma-next.config.ts
    中设置
    capabilities: { includeMany: true }
    defineConfig
    不接受
    capabilities
    参数。能力由当前适配器声明并成为生成契约的一部分;Postgres适配器默认支持
    lateral
    jsonAgg
    returning
    。通过配置中的
    extensions: [...]
    启用扩展能力(参见
    prisma-next-contract
    )。
  11. 虚构
    db.sql.raw(...)
    、TypedSQL或
    .stream()
    接口
    。这些接口当前均不存在。参见Prisma Next 当前未支持的功能
  12. 混合使用ORM变更返回结果与
    db.execute(plan)
    。ORM的终结方法(
    .create
    .update
    .delete
    .first
    .all
    .aggregate
    )会自行执行查询并返回行数据。不要将ORM构建器传递给
    db.runtime().execute(...)
    ——该方法用于执行SQL构建器计划。
  13. 在JS中编写Top-N分组查询为
    groupBy(...).aggregate(...).sort().slice()
    。这是一种 fallback 方案,因为分组集合未暴露
    .orderBy(...)
    /
    .take(...)
    方法(参见Prisma Next 当前未支持的功能)。在小基数场景下可行;对于真正的大数据分组结果集,应切换到
    db.sql.<table>
    并直接针对表编写
    GROUP BY
    +
    ORDER BY
    +
    LIMIT

What Prisma Next doesn't do yet

Prisma Next 当前未支持的功能

  • and
    /
    or
    /
    not
    combinators in the postgres façade.
    The combinators currently import from
    @prisma-next/sql-orm-client
    (an internal package). Tracked alongside other façade-completeness gaps in Linear
    TML-2526
    . Workaround today: import them from
    @prisma-next/sql-orm-client
    directly, the way the example apps do. If you want them on
    @prisma-next/postgres/runtime
    , file a feature request via
    prisma-next-feedback
    .
  • .orderBy(...)
    /
    .take(...)
    on grouped aggregates.
    db.orm.<Model>.groupBy(...).aggregate(...)
    materializes a
    Promise<Array<Group & Aggregates>>
    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 to
    db.sql.<table>
    and write the
    GROUP BY
    +
    ORDER BY
    +
    LIMIT
    against the aggregated table directly; (b) live with the JS-side sort/slice if the grouped cardinality is bounded. File a feature request via
    prisma-next-feedback
    if this is hitting you in production.
  • A raw-SQL lane. Prisma Next does not currently expose a user-facing raw-SQL surface (no
    db.sql.raw(...)
    ). Workaround: model the query through the SQL builder or — for shapes the builder can't yet express — file a feature request via
    prisma-next-feedback
    describing the shape so the team can decide whether to grow the builder or ship a raw lane.
  • TypedSQL (
    .sql
    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
    db.runtime().execute(plan)
    at the call site. If you want a
    .sql
    -file compile path, file a feature request via
    prisma-next-feedback
    .
  • EXPLAIN
    / query-plan inspection.
    Prisma Next does not expose an
    .explain()
    method. Workaround: connect a
    pg.Pool
    you control via the runtime's
    pg:
    binding (see
    prisma-next-runtime
    ) and issue
    EXPLAIN ANALYZE
    through it. If you want a first-class plan-inspection surface, file a feature request via
    prisma-next-feedback
    .
  • Streaming large result sets. No
    .stream()
    cursor today. Workaround: paginate via
    .skip(n).take(m)
    for moderate sizes; for very large sets, hold a
    pg.Client
    from the runtime's
    pg:
    binding and stream through it directly. If you want a built-in streaming surface, file a feature request via
    prisma-next-feedback
    .
  • Multi-statement batching (Prisma-7-style
    db.$transaction([call1, call2])
    ).
    Prisma Next runs each call sequentially. Workaround: wrap atomically-related work in
    db.transaction(async (tx) => { ... })
    . If you want batch-as-array semantics, file a feature request via
    prisma-next-feedback
    .
  • Automatic N+1 detection. Prisma Next does not warn when an
    .include(...)
    is missing. Workaround: be deliberate about
    .include(...)
    in code review; the
    lints
    middleware (see
    prisma-next-runtime
    ) catches the more common authoring slips (missing
    WHERE
    on a
    DELETE
    /
    UPDATE
    , missing
    LIMIT
    on a
    SELECT
    ).
  • postgres门面中的
    and
    /
    or
    /
    not
    组合器
    。这些组合器当前需从
    @prisma-next/sql-orm-client
    (内部包)导入。相关门面完整性缺口在Linear
    TML-2526
    中跟踪。当前 workaround:直接从
    @prisma-next/sql-orm-client
    导入,示例应用采用此方式。若希望在
    @prisma-next/postgres/runtime
    中提供这些组合器,可通过
    prisma-next-feedback
    提交功能请求。
  • 分组聚合的
    .orderBy(...)
    /
    .take(...)
    db.orm.<Model>.groupBy(...).aggregate(...)
    生成
    Promise<Array<Group & Aggregates>>
    ,不在数据库层暴露排序和条数限制。结果:“按SUM取Top-N分组”查询需回退到JS端对完整分组结果进行排序+切片,在小基数场景下可行,但在大规模场景下性能不佳。Workaround:(a) 切换到
    db.sql.<table>
    并直接针对聚合表编写
    GROUP BY
    +
    ORDER BY
    +
    LIMIT
    ;(b) 若分组基数有限,可接受JS端排序/切片。若该问题影响生产环境,可通过
    prisma-next-feedback
    提交功能请求。
  • 原生SQL方式。Prisma Next 当前未暴露面向用户的原生SQL接口(无
    db.sql.raw(...)
    )。Workaround:通过SQL构建器建模查询;对于构建器无法表达的场景,通过
    prisma-next-feedback
    提交功能请求描述场景,以便团队决定扩展构建器还是提供原生SQL方式。
  • TypedSQL(编译
    .sql
    文件为类型化可调用函数)
    。尚未实现。Workaround:坚持使用SQL构建器;对于重复查询,提取返回构建计划的函数,并在调用站点执行
    db.runtime().execute(plan)
    。若希望支持
    .sql
    文件编译路径,可通过
    prisma-next-feedback
    提交功能请求。
  • EXPLAIN
    /查询计划检查
    。Prisma Next 未暴露
    .explain()
    方法。Workaround:通过运行时的
    pg:
    绑定连接你自己控制的
    pg.Pool
    (参见
    prisma-next-runtime
    )并通过它执行
    EXPLAIN ANALYZE
    。若希望提供一流的计划检查接口,可通过
    prisma-next-feedback
    提交功能请求。
  • 大数据集流式处理。当前无
    .stream()
    游标。Workaround:对于中等规模数据,通过
    .skip(n).take(m)
    分页;对于超大数据集,从运行时的
    pg:
    绑定获取
    pg.Client
    并直接流式处理。若希望提供内置流式接口,可通过
    prisma-next-feedback
    提交功能请求。
  • 多语句批量处理(Prisma-7风格的
    db.$transaction([call1, call2])
    。Prisma Next 按顺序执行每个调用。Workaround:将原子相关操作包装在
    db.transaction(async (tx) => { ... })
    中。若希望支持数组形式的批量语义,可通过
    prisma-next-feedback
    提交功能请求。
  • 自动N+1检测。Prisma Next 不会在缺少
    .include(...)
    时发出警告。Workaround:在代码评审中谨慎处理
    .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
    examples/prisma-next-demo/src/orm-client/
    and
    examples/prisma-next-demo/src/queries/
    — the canonical worked references for ORM and SQL-builder shapes respectively.
  • The ORM client source under
    packages/3-extensions/sql-orm-client/src/
    for the full collection method surface.
  • The SQL builder source under
    packages/2-sql/4-lanes/sql-builder/src/
    for the builder method surface.
本技能仅包含主体内容。权威接口如下:

Checklist

检查清单

  • Chose the right lane (ORM by default; SQL builder for set-builder shapes the ORM doesn't express).
  • Used
    .first()
    /
    .first({ pk })
    for single-row reads — not
    .all()
    .
  • Consumed
    .all()
    with plain
    await
    (not a
    collect()
    /
    toArray()
    helper). Used
    for await
    only when streaming is actually wanted, and never iterated the same result twice.
  • Coalesced
    sum
    /
    avg
    /
    min
    /
    max
    results with
    ?? 0
    (or similar) at the consumption site when zero-fill is desired — did NOT coalesce
    count()
    , which is
    number
    .
  • Expressed ranges as chained
    .where(...)
    clauses or a single
    and(...)
    clause — did NOT reach for a non-existent
    .between(...)
    operator.
  • For ORM combinators, imported
    and
    /
    or
    /
    not
    from the (currently internal)
    @prisma-next/sql-orm-client
    and noted the façade gap to the user.
  • Executed SQL-builder plans via
    db.runtime().execute(plan)
    (or
    tx.execute(plan)
    inside a transaction).
  • Wrapped multi-statement work in
    db.transaction(async (tx) => { ... })
    where atomicity matters.
  • Did NOT confabulate
    db.sql.raw
    , TypedSQL,
    .stream()
    ,
    db.batch
    ,
    .between(...)
    , a
    capabilities
    field on
    defineConfig
    , or a
    db.sql.from(tables.user)
    API — routed to What Prisma Next doesn't do yet /
    prisma-next-feedback
    instead.
  • For top-N grouped aggregates at meaningful scale, dropped to
    db.sql.<table>
    rather than JS-side sort + slice over
    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)
    (或事务内的
    tx.execute(plan)
    )执行SQL构建器计划。
  • 在需要原子性的场景下,将多语句操作包装在
    db.transaction(async (tx) => { ... })
    中。
  • 未虚构
    db.sql.raw
    、TypedSQL、
    .stream()
    db.batch
    .between(...)
    defineConfig
    capabilities
    字段或
    db.sql.from(tables.user)
    接口——而是转向Prisma Next 当前未支持的功能 /
    prisma-next-feedback
  • 对于大规模场景下的Top-N分组聚合,切换到
    db.sql.<table>
    而非对
    groupBy(...).aggregate(...)
    进行JS端排序+切片。
  • 未使用SQL构建器处理ORM可干净表达的场景。