Skip to content
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -44,5 +44,8 @@ opensrc
# locally cloned prisma-next repo
prisma-next/

# locally cloned prisma-orm-messaging repo — internal, keep out of this public repo
prisma-orm-messaging/

# internal positioning doc — not user-facing, keep out of this public repo
.claude/skills/content-write-blog/assets/positioning.md
1 change: 1 addition & 0 deletions apps/docs/content/docs/(index)/meta.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
"---Prisma Next---",
"next/quickstart",
"next/add-to-existing-project",
"next/fundamentals",
"---Prisma ORM---",
"...prisma-orm",
"---Prisma Postgres---",
Expand Down
142 changes: 142 additions & 0 deletions apps/docs/content/docs/(index)/next/fundamentals/advanced-queries.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
---
title: Advanced queries
description: Drop below the ORM when a query needs shapes the ORM can't express, with the SQL builder on PostgreSQL and the aggregation pipeline builder on MongoDB.
url: /next/fundamentals/advanced-queries
metaTitle: Advanced queries in Prisma Next
metaDescription: Use the Prisma Next SQL builder for explicit joins, grouped aggregates, and RETURNING, and the typed aggregation pipeline builder on MongoDB.
---

When the ORM can't express a query, drop one level: the SQL builder on PostgreSQL, the aggregation pipeline builder on MongoDB. Both stay fully typed against your contract; neither means writing raw strings.

The ORM remains the default. Reach for these lanes for explicit joins, computed projections, grouped top-N queries, and pipeline stages. The choice is per query, not per app.

## The SQL builder (PostgreSQL)

`db.sql.public.<table>` builds a *plan*: a typed, serializable description of one SQL statement. Tables use their storage names (lowercase), and you execute the plan through the runtime.

```typescript
import { db } from "./prisma/db";

const plan = db.sql.public.post
.select("id", "title", "authorId")
.where((f, fns) => fns.eq(f.published, true))
.limit(10)
.build();

const rows = await db.runtime().execute(plan);
```

The `.where(...)` callback receives `(fields, fns)`: `fields` holds the column references, `fns` the operator namespace (`eq`, `ne`, `gt`, `lt`, `and`, `count`, and extension-provided operators).

### Explicit joins

The ORM does not express arbitrary joins. Alias each side with `.as(...)`, join on any predicate, and project columns from both:

```typescript
const plan = db.sql.public.post
.as("p")
.innerJoin(db.sql.public.user.as("u"), (f, fns) => fns.eq(f.p.authorId, f.u.id))
.select((f) => ({
postId: f.p.id,
title: f.p.title,
authorEmail: f.u.email,
}))
.where((f, fns) => fns.eq(f.p.published, true))
.limit(10)
.build();

const rows = await db.runtime().execute(plan);
```

This is also the workaround for reading through a many-to-many junction table, which [`.include(...)` does not handle yet](/next/fundamentals/relations-and-joins#limitations).

### Grouped top-N aggregates

The ORM's `.groupBy(...).aggregate(...)` cannot order or limit the grouped result at the database. The SQL builder can, so use it for "top authors by post count" shapes:

```typescript
const plan = db.sql.public.post
.select((f, fns) => ({
authorId: f.authorId,
posts: fns.count(),
}))
.groupBy((f) => f.authorId)
.orderBy((f, fns) => fns.count(), { direction: "desc" })
.limit(5)
.build();

const rows = await db.runtime().execute(plan);
// PostgreSQL returns count as a string; convert with Number(row.posts)
```

### Writes with RETURNING

SQL builder writes take an array of rows and expose `.returning(...)` explicitly:

```typescript
const plan = db.sql.public.user
.insert([{ email: "sql@prisma.io" }])
.returning("id", "email")
.build();

const [row] = await db.runtime().execute(plan);
// Contract defaults such as generated ids are applied to the inserted rows
```

There is no raw-SQL escape hatch (`db.sql.raw` does not exist) and no TypedSQL. If the builder can't express a shape you need, that is worth [reporting](https://pris.ly/discord).

## The pipeline builder (MongoDB)

MongoDB reads below the ORM are typed aggregation pipelines. Start with `db.query.from(...)`, chain stages, `.build()`, and execute through the runtime. This is also where MongoDB aggregation lives: the MongoDB ORM has no `.aggregate(...)` or `.groupBy(...)`.

```typescript
import { acc } from "@prisma-next/mongo-query-builder";
import { db } from "./prisma/db";

const runtime = await db.runtime();

// Post count per author, most prolific first
const plan = db.query
.from("posts")
.group((f) => ({
_id: f.authorId,
postCount: acc.count(),
}))
.sort({ postCount: -1 })
.build();

const byAuthor = await runtime.execute(plan);
```

`.match(...)` filters with typed field accessors, and `.lookup(...)` gives you a compile-time-checked `$lookup` join:

```typescript
const plan = db.query
.from("posts")
.match((f) => f.published.eq(true))
.lookup((from) =>
from("users")
.on((local, foreign) => ({ local: local.authorId, foreign: foreign._id }))
.as("author"),
)
.build();

const rows = await runtime.execute(plan);
```

Accumulators (`acc.count()`, `acc.max(...)`) and expression helpers import from `@prisma-next/mongo-query-builder`.

## How to choose

| You need | Use |
| --- | --- |
| CRUD, filters, relations, simple aggregates | ORM (`db.orm`) |
| Explicit join, computed projection, grouped top-N, `RETURNING` | SQL builder (`db.sql.public.<table>`) |
| `$group`, `$lookup` with reshaping, any MongoDB aggregation | Pipeline builder (`db.query.from(...)`) |

Plans execute through `db.runtime().execute(plan)` on PostgreSQL, `(await db.runtime()).execute(plan)` on MongoDB, and `tx.execute(plan)` inside a [transaction](/next/fundamentals/transactions).

## Next

- [Reading data](/next/fundamentals/reading-data): the ORM happy path these lanes back up.
- [Transactions](/next/fundamentals/transactions): run SQL builder plans atomically.
10 changes: 10 additions & 0 deletions apps/docs/content/docs/(index)/next/fundamentals/meta.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"title": "Fundamentals",
"pages": [
"reading-data",
"writing-data",
"relations-and-joins",
"transactions",
"advanced-queries"
]
}
174 changes: 174 additions & 0 deletions apps/docs/content/docs/(index)/next/fundamentals/reading-data.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
---
title: Reading data
description: Read data with the Prisma Next ORM by chaining query methods and running them with .all() or .first().
url: /next/fundamentals/reading-data
metaTitle: Reading data with Prisma Next
metaDescription: Query PostgreSQL and MongoDB with the Prisma Next ORM. Filter with where, project with select, sort with orderBy, and paginate with take and skip.
---

Read data by chaining query methods on a model collection, then running the chain with a terminal: `.all()` for many rows, `.first()` for one.

<Tabs items={["PostgreSQL", "MongoDB"]}>
<Tab value="PostgreSQL">

```typescript
import { db } from "./prisma/db";

// Every published post
const posts = await db.orm.public.Post.where({ published: true }).all();

// One user, or null
const user = await db.orm.public.User.where({ email: "alice@prisma.io" }).first();
```

</Tab>
<Tab value="MongoDB">

```typescript
import { db } from "./prisma/db";

// Every published post
const posts = await db.orm.posts.where({ published: true }).all();

// One user, or null
const user = await db.orm.users.where({ email: "alice@prisma.io" }).first();
```

</Tab>
</Tabs>

## How it works in Prisma Next

A query states what you want, the ORM runs it, and the result is typed against your [contract](/orm/next). There is no `findMany`, `findUnique`, or `findFirst`: if you are coming from Prisma 7, `.where(...).all()` replaces `findMany` and `.where(...).first()` replaces `findFirst` and `findUnique`.

The model access path differs by database:

- On PostgreSQL, models are addressed by namespace and PascalCase model name: `db.orm.public.Post`. `public` is the default PostgreSQL schema; contracts with more namespaces expose each one the same way.
- On MongoDB, collections are addressed by their lowercased plural root from the contract: `db.orm.posts`. Documents keep their raw `_id`, so filter on `_id`, not `id`.

`.first()` returns the row or `null`. On PostgreSQL it issues `LIMIT 1`, and `db.orm.public.User.first({ id })` is a shorthand for primary-key lookups. `.all()` has no implicit limit, so reserve it for the genuine many case.

## Filter with where

`.where(...)` narrows the result. Pass an object to match fields by equality. Chained `.where(...)` calls AND-compose, which is also how you express ranges.

<Tabs items={["PostgreSQL", "MongoDB"]}>
<Tab value="PostgreSQL">

On PostgreSQL, `.where(...)` also takes a lambda over a field proxy for richer operators: `.eq`, `.neq`, `.lt`, `.lte`, `.gt`, `.gte`, `.like`, `.ilike`, `.in([...])`, `.isNull()`, `.isNotNull()`.

```typescript
// Object form: equality on named fields
const drafts = await db.orm.public.Post.where({ published: false }).all();

// Lambda form: full operator set
const matches = await db.orm.public.Post.where((p) => p.title.ilike("%prisma%")).all();

// A range is two chained where clauses
const recent = await db.orm.public.Post
.where((p) => p.createdAt.gte(start))
.where((p) => p.createdAt.lte(end))
.all();
```

There is no `.between(...)` operator. For `OR` and `NOT`, the `or`, `and`, and `not` combinators currently import from `@prisma-next/sql-orm-client`; they are planned to move to the `@prisma-next/postgres` facade.

</Tab>
<Tab value="MongoDB">

On MongoDB, use the object form. Values are compared by equality and are codec-aware, so `ObjectId` fields accept both `ObjectId` values and strings.

```typescript
const alicesPosts = await db.orm.posts
.where({ published: true })
.where({ authorId: alice._id })
.all();
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
```

Richer operators (ranges, `in`, boolean logic) are not yet exposed on the MongoDB facade. When you need them, drop to the [aggregation pipeline builder](/next/fundamentals/advanced-queries).

</Tab>
</Tabs>

## Choose fields with select

`.select(...)` limits which fields come back, and the result type narrows to match.

<Tabs items={["PostgreSQL", "MongoDB"]}>
<Tab value="PostgreSQL">

```typescript
const users = await db.orm.public.User.select("id", "email").all();
// users: Array<{ id: string; email: string }>
```

</Tab>
<Tab value="MongoDB">

```typescript
const users = await db.orm.users.select("_id", "email").all();
// users: Array<{ _id: ObjectId; email: string }>
```

</Tab>
</Tabs>

## Sort and paginate

`.orderBy(...)` sorts, `.take(n)` limits, and `.skip(n)` offsets. Chain them before the terminal.

<Tabs items={["PostgreSQL", "MongoDB"]}>
<Tab value="PostgreSQL">

```typescript
const page = await db.orm.public.Post
.orderBy((p) => p.createdAt.desc())
.take(20)
.skip(20)
.all();
```

Pass an array of lambdas for a composite sort. For stable pagination over large sets, follow `.orderBy(...)` with `.cursor({ createdAt: last.createdAt })` to resume from a known position instead of counting offsets.

</Tab>
<Tab value="MongoDB">

```typescript
const page = await db.orm.posts
.orderBy({ createdAt: -1 })
.take(20)
.skip(20)
.all();
```

MongoDB sorts use the driver's direction values: `1` ascending, `-1` descending.

</Tab>
</Tabs>

## Count rows

There is no `.count()` terminal on the collection. On PostgreSQL, count through `.aggregate(...)`:

```typescript
const result = await db.orm.public.Post
.where({ published: true })
.aggregate((a) => ({ total: a.count() }));
// result.total: number
```

The MongoDB ORM does not expose `.aggregate(...)`. Count with a `$group` stage in the [aggregation pipeline builder](/next/fundamentals/advanced-queries) instead.

## Common gotchas

:::warning

A query result is single-consumption. `await` buffers it into an array once; a second `await` on the same result throws `RUNTIME.ITERATOR_CONSUMED`. Store the array in a variable and reuse the variable. To stream rows one at a time instead of buffering, use `for await (const row of query.all())`.

:::

## Next

- [Writing data](/next/fundamentals/writing-data): create, update, delete, and bulk writes.
- [Relations and joins](/next/fundamentals/relations-and-joins): read related records with `.include(...)`.
- [Advanced queries](/next/fundamentals/advanced-queries): the SQL builder and the MongoDB pipeline builder.
Loading
Loading